seitime-frappe/frappe/commands/utils.py
18alantom 9ece71bc56 feat: add using-cached flag to bench build
- add using-cache flag to node esbuild call

The flag is to be used by bench get-app if its is
using cache.

Flag causes building of assets to not run and
instead updates assets.json files from built assets.

Linking of assets in the sites folder also occurs.
2024-01-17 13:20:57 +05:30

1183 lines
30 KiB
Python

import json
import os
import subprocess
import sys
import click
import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.coverage import CodeCoverage
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint, update_progress_bar
EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@click.command("build")
@click.option("--app", help="Build assets for app")
@click.option("--apps", help="Build assets for specific apps")
@click.option(
"--hard-link",
is_flag=True,
default=False,
help="Copy the files instead of symlinking",
envvar="FRAPPE_HARD_LINK_ASSETS",
)
@click.option("--production", is_flag=True, default=False, help="Build assets in production mode")
@click.option("--verbose", is_flag=True, default=False, help="Verbose")
@click.option(
"--force", is_flag=True, default=False, help="Force build assets instead of downloading available"
)
@click.option(
"--save-metafiles",
is_flag=True,
default=False,
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
)
@click.option(
"--using-cached",
is_flag=True,
default=False,
help="Skips build and uses cached build artifacts (cache is set by Bench). Ignored if developer_mode enabled.",
)
def build(
app=None,
apps=None,
hard_link=False,
production=False,
verbose=False,
force=False,
save_metafiles=False,
using_cached=False,
):
"Compile JS and CSS source files"
from frappe.build import bundle, download_frappe_assets
from frappe.gettext.translate import compile_translations
from frappe.utils.synchronization import filelock
frappe.init("")
if not apps and app:
apps = app
with filelock("bench_build", is_global=True, timeout=10):
# dont try downloading assets if force used, app specified or running via CI
if not (force or apps or os.environ.get("CI")):
# skip building frappe if assets exist remotely
skip_frappe = download_frappe_assets(verbose=verbose)
else:
skip_frappe = False
# don't minify in developer_mode for faster builds
development = frappe.local.conf.developer_mode or frappe.local.dev_server
mode = "development" if development else "production"
if production:
mode = "production"
if development:
using_cached = False
bundle(
mode,
apps=apps,
hard_link=hard_link,
verbose=verbose,
skip_frappe=skip_frappe,
save_metafiles=save_metafiles,
using_cached=using_cached,
)
if apps and isinstance(apps, str):
apps = apps.split(",")
if not apps:
apps = frappe.get_all_apps()
for app in apps:
print("Compiling translations for", app)
compile_translations(app, force=force)
@click.command("watch")
@click.option("--apps", help="Watch assets for specific apps")
def watch(apps=None):
"Watch and compile JS and CSS files as and when they change"
from frappe.build import watch
frappe.init("")
watch(apps)
@click.command("clear-cache")
@pass_context
def clear_cache(context):
"Clear cache, doctype cache and defaults"
import frappe.sessions
from frappe.website.utils import clear_website_cache
for site in context.sites:
try:
frappe.connect(site)
frappe.clear_cache()
clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("clear-website-cache")
@pass_context
def clear_website_cache(context):
"Clear website cache"
from frappe.website.utils import clear_website_cache
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
clear_website_cache()
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("destroy-all-sessions")
@click.option("--reason")
@pass_context
def destroy_all_sessions(context, reason=None):
"Clear sessions of all users (logs them out)"
import frappe.sessions
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
frappe.sessions.clear_all_sessions(reason)
frappe.db.commit()
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("show-config")
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
def show_config(context, format):
"Print configuration file to STDOUT in speified format"
if not context.sites:
raise SiteNotSpecifiedError
sites_config = {}
sites_path = os.getcwd()
from frappe.utils.commands import render_table
def transform_config(config, prefix=None):
prefix = f"{prefix}." if prefix else ""
site_config = []
for conf, value in config.items():
if isinstance(value, dict):
site_config += transform_config(value, prefix=f"{prefix}{conf}")
else:
log_value = json.dumps(value) if isinstance(value, list) else value
site_config += [[f"{prefix}{conf}", log_value]]
return site_config
for site in context.sites:
frappe.init(site)
if len(context.sites) != 1 and format == "text":
if context.sites.index(site) != 0:
click.echo()
click.secho(f"Site {site}", fg="yellow")
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
if format == "text":
data = transform_config(configuration)
data.insert(0, ["Config", "Value"])
render_table(data)
if format == "json":
sites_config[site] = configuration
frappe.destroy()
if format == "json":
click.echo(frappe.as_json(sites_config))
@click.command("reset-perms")
@pass_context
def reset_perms(context):
"Reset permissions for all doctypes"
from frappe.permissions import reset_perms
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
for d in frappe.db.sql_list(
"""select name from `tabDocType`
where istable=0 and custom=0"""
):
frappe.clear_cache(doctype=d)
reset_perms(d)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("execute")
@click.argument("method")
@click.option("--args")
@click.option("--kwargs")
@click.option("--profile", is_flag=True, default=False)
@pass_context
def execute(context, method, args=None, kwargs=None, profile=False):
"Execute a function"
for site in context.sites:
ret = ""
try:
frappe.init(site=site)
frappe.connect()
if args:
try:
args = eval(args)
except NameError:
args = [args]
else:
args = ()
if kwargs:
kwargs = eval(kwargs)
else:
kwargs = {}
if profile:
import cProfile
pr = cProfile.Profile()
pr.enable()
try:
ret = frappe.get_attr(method)(*args, **kwargs)
except Exception:
# eval is safe here because input is from console
ret = eval(method + "(*args, **kwargs)", globals(), locals()) # nosemgrep
if profile:
import pstats
from io import StringIO
pr.disable()
s = StringIO()
pstats.Stats(pr, stream=s).sort_stats("cumulative").print_stats(0.5)
print(s.getvalue())
if frappe.db:
frappe.db.commit()
finally:
frappe.destroy()
if ret:
from frappe.utils.response import json_handler
print(json.dumps(ret, default=json_handler))
if not context.sites:
raise SiteNotSpecifiedError
@click.command("add-to-email-queue")
@click.argument("email-path")
@pass_context
def add_to_email_queue(context, email_path):
"Add an email to the Email Queue"
site = get_site(context)
if os.path.isdir(email_path):
with frappe.init_site(site):
frappe.connect()
for email in os.listdir(email_path):
with open(os.path.join(email_path, email)) as email_data:
kwargs = json.load(email_data)
kwargs["delayed"] = True
frappe.sendmail(**kwargs)
frappe.db.commit()
@click.command("export-doc")
@click.argument("doctype")
@click.argument("docname")
@pass_context
def export_doc(context, doctype, docname):
"Export a single document to csv"
import frappe.modules
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
frappe.modules.export_doc(doctype, docname)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("export-json")
@click.argument("doctype")
@click.argument("path")
@click.option("--name", help="Export only one document")
@pass_context
def export_json(context, doctype, path, name=None):
"Export doclist as json to the given path, use '-' as name for Singles."
from frappe.core.doctype.data_import.data_import import export_json
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
export_json(doctype, path, name=name)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("export-csv")
@click.argument("doctype")
@click.argument("path")
@pass_context
def export_csv(context, doctype, path):
"Export data import template with data for DocType"
from frappe.core.doctype.data_import.data_import import export_csv
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
export_csv(doctype, path)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("export-fixtures")
@click.option("--app", default=None, help="Export fixtures of a specific app")
@pass_context
def export_fixtures(context, app=None):
"Export fixtures"
from frappe.utils.fixtures import export_fixtures
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
export_fixtures(app=app)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("import-doc")
@click.argument("path")
@pass_context
def import_doc(context, path, force=False):
"Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported"
from frappe.core.doctype.data_import.data_import import import_doc
if not os.path.exists(path):
path = os.path.join("..", path)
if not os.path.exists(path):
print(f"Invalid path {path}")
sys.exit(1)
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
import_doc(path)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("data-import")
@click.option(
"--file",
"file_path",
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
required=True,
help=(
"Path to import file (.csv, .xlsx)."
"Consider that relative paths will resolve from 'sites' directory"
),
)
@click.option("--doctype", type=str, required=True)
@click.option(
"--type",
"import_type",
type=click.Choice(["Insert", "Update"], case_sensitive=False),
default="Insert",
help="Insert New Records or Update Existing Records",
)
@click.option(
"--submit-after-import", default=False, is_flag=True, help="Submit document after importing it"
)
@click.option("--mute-emails", default=True, is_flag=True, help="Mute emails during import")
@pass_context
def data_import(
context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True
):
"Import documents in bulk from CSV or XLSX using data import"
from frappe.core.doctype.data_import.data_import import import_file
site = get_site(context)
frappe.init(site=site)
frappe.connect()
import_file(doctype, file_path, import_type, submit_after_import, console=True)
frappe.destroy()
@click.command("bulk-rename")
@click.argument("doctype")
@click.argument("path")
@pass_context
def bulk_rename(context, doctype, path):
"Rename multiple records via CSV file"
from frappe.model.rename_doc import bulk_rename
from frappe.utils.csvutils import read_csv_content
site = get_site(context)
with open(path) as csvfile:
rows = read_csv_content(csvfile.read())
frappe.init(site=site)
frappe.connect()
bulk_rename(doctype, rows, via_console=True)
frappe.destroy()
@click.command("db-console", context_settings=EXTRA_ARGS_CTX)
@click.argument("extra_args", nargs=-1)
@pass_context
def database(context, extra_args):
"""
Enter into the Database console for given site.
"""
site = get_site(context)
frappe.init(site=site)
_enter_console(extra_args=extra_args)
@click.command("mariadb", context_settings=EXTRA_ARGS_CTX)
@click.argument("extra_args", nargs=-1)
@pass_context
def mariadb(context, extra_args):
"""
Enter into mariadb console for a given site.
"""
site = get_site(context)
frappe.init(site=site)
frappe.conf.db_type = "mariadb"
_enter_console(extra_args=extra_args)
@click.command("postgres", context_settings=EXTRA_ARGS_CTX)
@click.argument("extra_args", nargs=-1)
@pass_context
def postgres(context, extra_args):
"""
Enter into postgres console for a given site.
"""
site = get_site(context)
frappe.init(site=site)
frappe.conf.db_type = "postgres"
_enter_console(extra_args=extra_args)
def _enter_console(extra_args=None):
from frappe.database import get_command
bin, args, bin_name = get_command(
host=frappe.conf.db_host,
port=frappe.conf.db_port,
user=frappe.conf.db_name,
password=frappe.conf.db_password,
db_name=frappe.conf.db_name,
extra=list(extra_args) if extra_args else [],
)
if not bin:
frappe.throw(
_("{} not found in PATH! This is required to access the console.").format(bin_name),
exc=frappe.ExecutableNotFound,
)
os.execv(bin, [bin] + args)
@click.command("jupyter")
@pass_context
def jupyter(context):
"""Start an interactive jupyter notebook"""
installed_packages = (
r.split("==", 1)[0]
for r in subprocess.check_output([sys.executable, "-m", "pip", "freeze"], encoding="utf8")
)
if "jupyter" not in installed_packages:
subprocess.check_output([sys.executable, "-m", "pip", "install", "jupyter"])
site = get_site(context)
frappe.init(site=site)
jupyter_notebooks_path = os.path.abspath(frappe.get_site_path("jupyter_notebooks"))
sites_path = os.path.abspath(frappe.get_site_path(".."))
try:
os.stat(jupyter_notebooks_path)
except OSError:
print(f"Creating folder to keep jupyter notebooks at {jupyter_notebooks_path}")
os.mkdir(jupyter_notebooks_path)
bin_path = os.path.abspath("../env/bin")
print(
"""
Starting Jupyter notebook
Run the following in your first cell to connect notebook to frappe
```
import frappe
frappe.init(site='{site}', sites_path='{sites_path}')
frappe.connect()
frappe.local.lang = frappe.db.get_default('lang')
frappe.db.connect()
```
""".format(
site=site, sites_path=sites_path
)
)
os.execv(
f"{bin_path}/jupyter",
[
f"{bin_path}/jupyter",
"notebook",
jupyter_notebooks_path,
],
)
def _console_cleanup():
# Execute after_rollback on console close
frappe.db.rollback()
frappe.destroy()
@click.command("console")
@click.option("--autoreload", is_flag=True, help="Reload changes to code automatically")
@pass_context
def console(context, autoreload=False):
"Start ipython console for a site"
site = get_site(context)
frappe.init(site=site)
frappe.connect()
frappe.local.lang = frappe.db.get_default("lang")
from atexit import register
from IPython.terminal.embed import InteractiveShellEmbed
register(_console_cleanup)
terminal = InteractiveShellEmbed.instance()
if autoreload:
terminal.extension_manager.load_extension("autoreload")
terminal.run_line_magic("autoreload", "2")
all_apps = frappe.get_installed_apps()
failed_to_import = []
for app in all_apps:
try:
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
all_apps.remove(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
terminal.colors = "neutral"
terminal.display_banner = False
terminal()
@click.command(
"transform-database", help="Change tables' internal settings changing engine and row formats"
)
@click.option(
"--table",
required=True,
help="Comma separated name of tables to convert. To convert all tables, pass 'all'",
)
@click.option(
"--engine",
default=None,
type=click.Choice(["InnoDB", "MyISAM"]),
help="Choice of storage engine for said table(s)",
)
@click.option(
"--row_format",
default=None,
type=click.Choice(["DYNAMIC", "COMPACT", "REDUNDANT", "COMPRESSED"]),
help="Set ROW_FORMAT parameter for said table(s)",
)
@click.option("--failfast", is_flag=True, default=False, help="Exit on first failure occurred")
@pass_context
def transform_database(context, table, engine, row_format, failfast):
"Transform site database through given parameters"
site = get_site(context)
check_table = []
add_line = False
skipped = 0
frappe.init(site=site)
if frappe.conf.db_type != "mariadb":
click.secho("This command only has support for MariaDB databases at this point", fg="yellow")
sys.exit(1)
if not (engine or row_format):
click.secho("Values for `--engine` or `--row_format` must be set")
sys.exit(1)
frappe.connect()
if table == "all":
information_schema = frappe.qb.Schema("information_schema")
queried_tables = (
frappe.qb.from_(information_schema.tables)
.select("table_name")
.where(
(information_schema.tables.row_format != row_format)
& (information_schema.tables.table_schema == frappe.conf.db_name)
)
.run()
)
tables = [x[0] for x in queried_tables]
else:
tables = [x.strip() for x in table.split(",")]
total = len(tables)
for current, table in enumerate(tables):
values_to_set = ""
if engine:
values_to_set += f" ENGINE={engine}"
if row_format:
values_to_set += f" ROW_FORMAT={row_format}"
try:
frappe.db.sql(f"ALTER TABLE `{table}`{values_to_set}")
update_progress_bar("Updating table schema", current - skipped, total)
add_line = True
except Exception as e:
check_table.append([table, e.args])
skipped += 1
if failfast:
break
if add_line:
print()
for errored_table in check_table:
table, err = errored_table
err_msg = f"{table}: ERROR {err[0]}: {err[1]}"
click.secho(err_msg, fg="yellow")
frappe.destroy()
@click.command("run-tests")
@click.option("--app", help="For App")
@click.option("--doctype", help="For DocType")
@click.option("--module-def", help="For all Doctypes in Module Def")
@click.option("--case", help="Select particular TestCase")
@click.option(
"--doctype-list-path",
help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt",
)
@click.option("--test", multiple=True, help="Specific test")
@click.option("--module", help="Run tests in a module")
@click.option("--profile", is_flag=True, default=False)
@click.option("--coverage", is_flag=True, default=False)
@click.option("--skip-test-records", is_flag=True, default=False, help="Don't create test records")
@click.option(
"--skip-before-tests", is_flag=True, default=False, help="Don't run before tests hook"
)
@click.option("--junit-xml-output", help="Destination file path for junit xml report")
@click.option(
"--failfast", is_flag=True, default=False, help="Stop the test run on the first error or failure"
)
@pass_context
def run_tests(
context,
app=None,
module=None,
doctype=None,
module_def=None,
test=(),
profile=False,
coverage=False,
junit_xml_output=False,
doctype_list_path=None,
skip_test_records=False,
skip_before_tests=False,
failfast=False,
case=None,
):
"""Run python unit-tests"""
with CodeCoverage(coverage, app):
import frappe
import frappe.test_runner
tests = test
site = get_site(context)
allow_tests = frappe.get_conf(site).allow_tests
if not (allow_tests or os.environ.get("CI")):
click.secho("Testing is disabled for the site!", bold=True)
click.secho("You can enable tests by entering following command:")
click.secho(f"bench --site {site} set-config allow_tests true", fg="green")
return
frappe.init(site=site)
frappe.flags.skip_before_tests = skip_before_tests
frappe.flags.skip_test_records = skip_test_records
ret = frappe.test_runner.main(
app,
module,
doctype,
module_def,
context.verbose,
tests=tests,
force=context.force,
profile=profile,
junit_xml_output=junit_xml_output,
doctype_list_path=doctype_list_path,
failfast=failfast,
case=case,
)
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
if os.environ.get("CI"):
sys.exit(ret)
@click.command("run-parallel-tests")
@click.option("--app", help="For App", default="frappe")
@click.option("--build-number", help="Build number", default=1)
@click.option("--total-builds", help="Total number of builds", default=1)
@click.option("--with-coverage", is_flag=True, help="Build coverage file")
@click.option("--use-orchestrator", is_flag=True, help="Use orchestrator to run parallel tests")
@click.option("--dry-run", is_flag=True, default=False, help="Dont actually run tests")
@pass_context
def run_parallel_tests(
context,
app,
build_number,
total_builds,
with_coverage=False,
use_orchestrator=False,
dry_run=False,
):
from traceback_with_variables import activate_by_import
with CodeCoverage(with_coverage, app):
site = get_site(context)
if use_orchestrator:
from frappe.parallel_test_runner import ParallelTestWithOrchestrator
ParallelTestWithOrchestrator(app, site=site)
else:
from frappe.parallel_test_runner import ParallelTestRunner
ParallelTestRunner(
app,
site=site,
build_number=build_number,
total_builds=total_builds,
dry_run=dry_run,
)
@click.command(
"run-ui-tests",
context_settings=dict(
ignore_unknown_options=True,
),
)
@click.argument("app")
@click.argument("cypressargs", nargs=-1, type=click.UNPROCESSED)
@click.option("--headless", is_flag=True, help="Run UI Test in headless mode")
@click.option("--parallel", is_flag=True, help="Run UI Test in parallel mode")
@click.option("--with-coverage", is_flag=True, help="Generate coverage report")
@click.option("--browser", default="chrome", help="Browser to run tests in")
@click.option("--ci-build-id")
@pass_context
def run_ui_tests(
context,
app,
headless=False,
parallel=True,
with_coverage=False,
browser="chrome",
ci_build_id=None,
cypressargs=None,
):
"Run UI tests"
site = get_site(context)
frappe.init(site)
app_base_path = frappe.get_app_source_path(app)
site_url = frappe.utils.get_site_url(site)
admin_password = frappe.get_conf(site).admin_password
# override baseUrl using env variable
site_env = f"CYPRESS_baseUrl={site_url}"
password_env = f"CYPRESS_adminPassword={admin_password}" if admin_password else ""
coverage_env = f"CYPRESS_coverage={str(with_coverage).lower()}"
os.chdir(app_base_path)
node_bin = subprocess.getoutput("(cd ../frappe && yarn bin)")
cypress_path = f"{node_bin}/cypress"
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
testing_library_path = f"{node_bin}/../@testing-library"
coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage"
# check if cypress in path...if not, install it.
if not (
os.path.exists(cypress_path)
and os.path.exists(drag_drop_plugin_path)
and os.path.exists(real_events_plugin_path)
and os.path.exists(testing_library_path)
and os.path.exists(coverage_plugin_path)
):
# install cypress & dependent plugins
click.secho("Installing Cypress...", fg="yellow")
packages = " ".join(
[
"cypress@^13",
"@4tw/cypress-drag-drop@^2",
"cypress-real-events",
"@testing-library/cypress@^10",
"@testing-library/dom@8.17.1",
"@cypress/code-coverage@^3",
]
)
frappe.commands.popen(f"(cd ../frappe && yarn add {packages} --no-lockfile)")
# run for headless mode
run_or_open = f"run --browser {browser}" if headless else "open"
formatted_command = f"{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}"
if os.environ.get("CYPRESS_RECORD_KEY"):
formatted_command += " --record"
if parallel:
formatted_command += " --parallel"
if ci_build_id:
formatted_command += f" --ci-build-id {ci_build_id}"
if cypressargs:
formatted_command += " " + " ".join(cypressargs)
click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@click.command("serve")
@click.option("--port", default=8000)
@click.option("--profile", is_flag=True, default=False)
@click.option(
"--proxy",
is_flag=True,
default=False,
help="The development server may be run behind a proxy, e.g. ngrok / localtunnel",
)
@click.option("--noreload", "no_reload", is_flag=True, default=False)
@click.option("--nothreading", "no_threading", is_flag=True, default=False)
@click.option("--with-coverage", is_flag=True, default=False)
@pass_context
def serve(
context,
port=None,
profile=False,
proxy=False,
no_reload=False,
no_threading=False,
sites_path=".",
site=None,
with_coverage=False,
):
"Start development web server"
import frappe.app
if not context.sites:
site = None
else:
site = context.sites[0]
with CodeCoverage(with_coverage, "frappe"):
if with_coverage:
# unable to track coverage with threading enabled
no_threading = True
no_reload = True
frappe.app.serve(
port=port,
profile=profile,
proxy=proxy,
no_reload=no_reload,
no_threading=no_threading,
site=site,
sites_path=".",
)
@click.command("request")
@click.option("--args", help="arguments like `?cmd=test&key=value` or `/api/request/method?..`")
@click.option("--path", help="path to request JSON")
@pass_context
def request(context, args=None, path=None):
"Run a request as an admin"
import frappe.api
import frappe.handler
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
if args:
if "?" in args:
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")])
else:
frappe.local.form_dict = frappe._dict()
if args.startswith("/api/method"):
frappe.local.form_dict.cmd = args.split("?", 1)[0].split("/")[-1]
elif path:
with open(os.path.join("..", path)) as f:
args = json.loads(f.read())
frappe.local.form_dict = frappe._dict(args)
frappe.handler.execute_cmd(frappe.form_dict.cmd)
print(frappe.response)
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
@click.command("make-app")
@click.argument("destination")
@click.argument("app_name")
@click.option(
"--no-git", is_flag=True, default=False, help="Do not initialize git repository for the app"
)
def make_app(destination, app_name, no_git=False):
"Creates a boilerplate app"
from frappe.utils.boilerplate import make_boilerplate
make_boilerplate(destination, app_name, no_git=no_git)
@click.command("create-patch")
def create_patch():
"Creates a new patch interactively"
from frappe.utils.boilerplate import PatchCreator
pc = PatchCreator()
pc.fetch_user_inputs()
pc.create_patch_file()
@click.command("set-config")
@click.argument("key")
@click.argument("value")
@click.option(
"-g", "--global", "global_", is_flag=True, default=False, help="Set value in bench config"
)
@click.option("-p", "--parse", is_flag=True, default=False, help="Evaluate as Python Object")
@pass_context
def set_config(context, key, value, global_=False, parse=False):
"Insert/Update a value in site_config.json"
from frappe.installer import update_site_config
if parse:
import ast
value = ast.literal_eval(value)
if global_:
sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, "common_site_config.json")
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
else:
if not context.sites:
raise SiteNotSpecifiedError
for site in context.sites:
frappe.init(site=site)
update_site_config(key, value, validate=False)
frappe.destroy()
@click.command("version")
@click.option(
"-f",
"--format",
"output",
type=click.Choice(["plain", "table", "json", "legacy"]),
help="Output format",
default="legacy",
)
def get_version(output):
"""Show the versions of all the installed apps."""
from git import Repo
from git.exc import InvalidGitRepositoryError
from frappe.utils.change_log import get_app_branch
from frappe.utils.commands import render_table
frappe.init("")
data = []
for app in sorted(frappe.get_all_apps()):
module = frappe.get_module(app)
app_hooks = frappe.get_module(app + ".hooks")
app_info = frappe._dict()
try:
app_info.commit = Repo(frappe.get_app_source_path(app)).head.object.hexsha[:7]
except InvalidGitRepositoryError:
app_info.commit = ""
app_info.app = app
app_info.branch = get_app_branch(app)
app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
data.append(app_info)
{
"legacy": lambda: [click.echo(f"{app_info.app} {app_info.version}") for app_info in data],
"plain": lambda: [
click.echo(f"{app_info.app} {app_info.version} {app_info.branch} ({app_info.commit})")
for app_info in data
],
"table": lambda: render_table(
[["App", "Version", "Branch", "Commit"]]
+ [[app_info.app, app_info.version, app_info.branch, app_info.commit] for app_info in data]
),
"json": lambda: click.echo(json.dumps(data, indent=4)),
}[output]()
@click.command("rebuild-global-search")
@click.option(
"--static-pages", is_flag=True, default=False, help="Rebuild global search for static pages"
)
@pass_context
def rebuild_global_search(context, static_pages=False):
"""Setup help table in the current site (called after migrate)"""
from frappe.utils.global_search import (
add_route_to_global_search,
get_doctypes_with_global_search,
get_routes_to_index,
rebuild_for_doctype,
sync_global_search,
)
for site in context.sites:
try:
frappe.init(site)
frappe.connect()
if static_pages:
routes = get_routes_to_index()
for i, route in enumerate(routes):
add_route_to_global_search(route)
frappe.local.request = None
update_progress_bar("Rebuilding Global Search", i, len(routes))
sync_global_search()
else:
doctypes = get_doctypes_with_global_search()
for i, doctype in enumerate(doctypes):
rebuild_for_doctype(doctype)
update_progress_bar("Rebuilding Global Search", i, len(doctypes))
finally:
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError
commands = [
build,
clear_cache,
clear_website_cache,
database,
transform_database,
jupyter,
console,
destroy_all_sessions,
execute,
export_csv,
export_doc,
export_fixtures,
export_json,
get_version,
data_import,
import_doc,
make_app,
create_patch,
mariadb,
postgres,
request,
reset_perms,
run_tests,
run_ui_tests,
serve,
set_config,
show_config,
watch,
bulk_rename,
add_to_email_queue,
rebuild_global_search,
run_parallel_tests,
]