import json import os import subprocess import sys from distutils.spawn import find_executable import click import frappe from frappe.commands import get_site, pass_context from frappe.exceptions import SiteNotSpecifiedError from frappe.utils import update_progress_bar, cint from frappe.coverage import CodeCoverage DATA_IMPORT_DEPRECATION = ( "[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n" "Use `data-import` command instead to import data via 'Data Import'." ) @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') @click.option('--make-copy', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking') @click.option('--restore', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking with force') @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') def build(app=None, apps=None, hard_link=False, make_copy=False, restore=False, production=False, verbose=False, force=False): "Compile JS and CSS source files" from frappe.build import bundle, download_frappe_assets frappe.init('') if not apps and app: apps = app # 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 make_copy or restore: hard_link = make_copy or restore click.secho( "bench build: --make-copy and --restore options are deprecated in favour of --hard-link", fg="yellow", ) bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe) @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 from frappe.desk.notifications import clear_notifications for site in context.sites: try: frappe.connect(site) frappe.clear_cache() clear_notifications() 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: ret = frappe.safe_eval(method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals()) if profile: import pstats from io import StringIO pr.disable() s = StringIO() pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(.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('Invalid path {0}'.format(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('import-csv', help=DATA_IMPORT_DEPRECATION) @click.argument('path') @click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records') @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it') @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode') @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable') @pass_context def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True): click.secho(DATA_IMPORT_DEPRECATION, fg="yellow") sys.exit(1) @click.command('data-import') @click.option('--file', 'file_path', type=click.Path(), required=True, help="Path to import file (.csv, .xlsx)") @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, 'r') 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') @pass_context def database(context): """ Enter into the Database console for given site. """ site = get_site(context) if not site: raise SiteNotSpecifiedError frappe.init(site=site) if not frappe.conf.db_type or frappe.conf.db_type == "mariadb": _mariadb() elif frappe.conf.db_type == "postgres": _psql() @click.command('mariadb') @pass_context def mariadb(context): """ Enter into mariadb console for a given site. """ site = get_site(context) if not site: raise SiteNotSpecifiedError frappe.init(site=site) _mariadb() @click.command('postgres') @pass_context def postgres(context): """ Enter into postgres console for a given site. """ site = get_site(context) frappe.init(site=site) _psql() def _mariadb(): mysql = find_executable('mysql') os.execv(mysql, [ mysql, '-u', frappe.conf.db_name, '-p'+frappe.conf.db_password, frappe.conf.db_name, '-h', frappe.conf.db_host or "localhost", '--pager=less -SFX', '--safe-updates', "-A"]) def _psql(): psql = find_executable('psql') subprocess.run([ psql, '-d', frappe.conf.db_name]) @click.command('jupyter') @pass_context def jupyter(context): installed_packages = (r.split('==')[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('Creating folder to keep jupyter notebooks at {}'.format(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('{0}/jupyter'.format(bin_path), [ '{0}/jupyter'.format(bin_path), 'notebook', jupyter_notebooks_path, ]) def _console_cleanup(): # Execute rollback_observers 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 IPython.terminal.embed import InteractiveShellEmbed from atexit import register register(_console_cleanup) terminal = InteractiveShellEmbed() 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 and 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('--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('--ui-tests', is_flag=True, default=False, help="Run UI Tests") @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, test=(), profile=False, coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None, skip_test_records=False, skip_before_tests=False, failfast=False): with CodeCoverage(coverage, app): 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('bench --site {0} set-config allow_tests true'.format(site), 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, context.verbose, tests=tests, force=context.force, profile=profile, junit_xml_output=junit_xml_output, ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast) 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") @pass_context def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False): 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) @click.command('run-ui-tests') @click.argument('app') @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('--ci-build-id') @pass_context def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=False, ci_build_id=None): "Run UI tests" site = get_site(context) app_base_path = os.path.abspath(os.path.join(frappe.get_app_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("npm bin") cypress_path = f"{node_bin}/cypress" plugin_path = f"{node_bin}/../cypress-file-upload" 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(plugin_path) and os.path.exists(testing_library_path) and os.path.exists(coverage_plugin_path) and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6 ): # install cypress click.secho("Installing Cypress...", fg="yellow") frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile") # run for headless mode run_or_open = 'run --browser chrome --record' if headless else 'open' formatted_command = f'{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}' if parallel: formatted_command += ' --parallel' if ci_build_id: formatted_command += f' --ci-build-id {ci_build_id}' 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('--noreload', "no_reload", is_flag=True, default=False) @click.option('--nothreading', "no_threading", is_flag=True, default=False) @pass_context def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None): "Start development web server" import frappe.app if not context.sites: site = None else: site = context.sites[0] frappe.app.serve(port=port, profile=profile, 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.handler import frappe.api 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("?")[0].split("/")[-1] elif path: with open(os.path.join('..', path), 'r') 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('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') @click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object') @pass_context def set_config(context, key, value, global_=False, parse=False, as_dict=False): "Insert/Update a value in site_config.json" from frappe.installer import update_site_config if as_dict: from frappe.utils.commands import warn warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning) parse = as_dict 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: 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 frappe.utils.commands import render_table from frappe.utils.change_log import get_app_branch frappe.init("") data = [] for app in sorted(frappe.get_all_apps()): module = frappe.get_module(app) app_hooks = frappe.get_module(app + ".hooks") repo = Repo(frappe.get_app_path(app, "..")) app_info = frappe._dict() app_info.app = app app_info.branch = get_app_branch(app) app_info.commit = repo.head.object.hexsha[:7] 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 (get_doctypes_with_global_search, rebuild_for_doctype, get_routes_to_index, add_route_to_global_search, 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, import_csv, data_import, import_doc, make_app, 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 ]