25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

845 satır
25 KiB

  1. import json
  2. import os
  3. import subprocess
  4. import sys
  5. from distutils.spawn import find_executable
  6. import click
  7. import frappe
  8. from frappe.commands import get_site, pass_context
  9. from frappe.exceptions import SiteNotSpecifiedError
  10. from frappe.utils import get_bench_path, update_progress_bar, cint
  11. DATA_IMPORT_DEPRECATION = click.style(
  12. "[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
  13. "Use `data-import` command instead to import data via 'Data Import'.",
  14. fg="yellow"
  15. )
  16. @click.command('build')
  17. @click.option('--app', help='Build assets for app')
  18. @click.option('--apps', help='Build assets for specific apps')
  19. @click.option('--hard-link', is_flag=True, default=False, help='Copy the files instead of symlinking')
  20. @click.option('--make-copy', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking')
  21. @click.option('--restore', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking with force')
  22. @click.option('--production', is_flag=True, default=False, help='Build assets in production mode')
  23. @click.option('--verbose', is_flag=True, default=False, help='Verbose')
  24. @click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
  25. def build(app=None, apps=None, hard_link=False, make_copy=False, restore=False, production=False, verbose=False, force=False):
  26. "Compile JS and CSS source files"
  27. from frappe.build import bundle, download_frappe_assets
  28. frappe.init('')
  29. if not apps and app:
  30. apps = app
  31. # dont try downloading assets if force used, app specified or running via CI
  32. if not (force or apps or os.environ.get('CI')):
  33. # skip building frappe if assets exist remotely
  34. skip_frappe = download_frappe_assets(verbose=verbose)
  35. else:
  36. skip_frappe = False
  37. # don't minify in developer_mode for faster builds
  38. development = frappe.local.conf.developer_mode or frappe.local.dev_server
  39. mode = "development" if development else "production"
  40. if production:
  41. mode = "production"
  42. if make_copy or restore:
  43. hard_link = make_copy or restore
  44. click.secho(
  45. "bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
  46. fg="yellow",
  47. )
  48. bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
  49. @click.command('watch')
  50. @click.option('--apps', help='Watch assets for specific apps')
  51. def watch(apps=None):
  52. "Watch and compile JS and CSS files as and when they change"
  53. from frappe.build import watch
  54. frappe.init('')
  55. watch(apps)
  56. @click.command('clear-cache')
  57. @pass_context
  58. def clear_cache(context):
  59. "Clear cache, doctype cache and defaults"
  60. import frappe.sessions
  61. from frappe.website.utils import clear_website_cache
  62. from frappe.desk.notifications import clear_notifications
  63. for site in context.sites:
  64. try:
  65. frappe.connect(site)
  66. frappe.clear_cache()
  67. clear_notifications()
  68. clear_website_cache()
  69. finally:
  70. frappe.destroy()
  71. if not context.sites:
  72. raise SiteNotSpecifiedError
  73. @click.command('clear-website-cache')
  74. @pass_context
  75. def clear_website_cache(context):
  76. "Clear website cache"
  77. from frappe.website.utils import clear_website_cache
  78. for site in context.sites:
  79. try:
  80. frappe.init(site=site)
  81. frappe.connect()
  82. clear_website_cache()
  83. finally:
  84. frappe.destroy()
  85. if not context.sites:
  86. raise SiteNotSpecifiedError
  87. @click.command('destroy-all-sessions')
  88. @click.option('--reason')
  89. @pass_context
  90. def destroy_all_sessions(context, reason=None):
  91. "Clear sessions of all users (logs them out)"
  92. import frappe.sessions
  93. for site in context.sites:
  94. try:
  95. frappe.init(site=site)
  96. frappe.connect()
  97. frappe.sessions.clear_all_sessions(reason)
  98. frappe.db.commit()
  99. finally:
  100. frappe.destroy()
  101. if not context.sites:
  102. raise SiteNotSpecifiedError
  103. @click.command('show-config')
  104. @click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
  105. @pass_context
  106. def show_config(context, format):
  107. "Print configuration file to STDOUT in speified format"
  108. if not context.sites:
  109. raise SiteNotSpecifiedError
  110. sites_config = {}
  111. sites_path = os.getcwd()
  112. from frappe.utils.commands import render_table
  113. def transform_config(config, prefix=None):
  114. prefix = f"{prefix}." if prefix else ""
  115. site_config = []
  116. for conf, value in config.items():
  117. if isinstance(value, dict):
  118. site_config += transform_config(value, prefix=f"{prefix}{conf}")
  119. else:
  120. log_value = json.dumps(value) if isinstance(value, list) else value
  121. site_config += [[f"{prefix}{conf}", log_value]]
  122. return site_config
  123. for site in context.sites:
  124. frappe.init(site)
  125. if len(context.sites) != 1 and format == "text":
  126. if context.sites.index(site) != 0:
  127. click.echo()
  128. click.secho(f"Site {site}", fg="yellow")
  129. configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
  130. if format == "text":
  131. data = transform_config(configuration)
  132. data.insert(0, ['Config','Value'])
  133. render_table(data)
  134. if format == "json":
  135. sites_config[site] = configuration
  136. frappe.destroy()
  137. if format == "json":
  138. click.echo(frappe.as_json(sites_config))
  139. @click.command('reset-perms')
  140. @pass_context
  141. def reset_perms(context):
  142. "Reset permissions for all doctypes"
  143. from frappe.permissions import reset_perms
  144. for site in context.sites:
  145. try:
  146. frappe.init(site=site)
  147. frappe.connect()
  148. for d in frappe.db.sql_list("""select name from `tabDocType`
  149. where istable=0 and custom=0"""):
  150. frappe.clear_cache(doctype=d)
  151. reset_perms(d)
  152. finally:
  153. frappe.destroy()
  154. if not context.sites:
  155. raise SiteNotSpecifiedError
  156. @click.command('execute')
  157. @click.argument('method')
  158. @click.option('--args')
  159. @click.option('--kwargs')
  160. @click.option('--profile', is_flag=True, default=False)
  161. @pass_context
  162. def execute(context, method, args=None, kwargs=None, profile=False):
  163. "Execute a function"
  164. for site in context.sites:
  165. ret = ""
  166. try:
  167. frappe.init(site=site)
  168. frappe.connect()
  169. if args:
  170. try:
  171. args = eval(args)
  172. except NameError:
  173. args = [args]
  174. else:
  175. args = ()
  176. if kwargs:
  177. kwargs = eval(kwargs)
  178. else:
  179. kwargs = {}
  180. if profile:
  181. import cProfile
  182. pr = cProfile.Profile()
  183. pr.enable()
  184. try:
  185. ret = frappe.get_attr(method)(*args, **kwargs)
  186. except Exception:
  187. ret = frappe.safe_eval(method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals())
  188. if profile:
  189. import pstats
  190. from io import StringIO
  191. pr.disable()
  192. s = StringIO()
  193. pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(.5)
  194. print(s.getvalue())
  195. if frappe.db:
  196. frappe.db.commit()
  197. finally:
  198. frappe.destroy()
  199. if ret:
  200. from frappe.utils.response import json_handler
  201. print(json.dumps(ret, default=json_handler))
  202. if not context.sites:
  203. raise SiteNotSpecifiedError
  204. @click.command('add-to-email-queue')
  205. @click.argument('email-path')
  206. @pass_context
  207. def add_to_email_queue(context, email_path):
  208. "Add an email to the Email Queue"
  209. site = get_site(context)
  210. if os.path.isdir(email_path):
  211. with frappe.init_site(site):
  212. frappe.connect()
  213. for email in os.listdir(email_path):
  214. with open(os.path.join(email_path, email)) as email_data:
  215. kwargs = json.load(email_data)
  216. kwargs['delayed'] = True
  217. frappe.sendmail(**kwargs)
  218. frappe.db.commit()
  219. @click.command('export-doc')
  220. @click.argument('doctype')
  221. @click.argument('docname')
  222. @pass_context
  223. def export_doc(context, doctype, docname):
  224. "Export a single document to csv"
  225. import frappe.modules
  226. for site in context.sites:
  227. try:
  228. frappe.init(site=site)
  229. frappe.connect()
  230. frappe.modules.export_doc(doctype, docname)
  231. finally:
  232. frappe.destroy()
  233. if not context.sites:
  234. raise SiteNotSpecifiedError
  235. @click.command('export-json')
  236. @click.argument('doctype')
  237. @click.argument('path')
  238. @click.option('--name', help='Export only one document')
  239. @pass_context
  240. def export_json(context, doctype, path, name=None):
  241. "Export doclist as json to the given path, use '-' as name for Singles."
  242. from frappe.core.doctype.data_import.data_import import export_json
  243. for site in context.sites:
  244. try:
  245. frappe.init(site=site)
  246. frappe.connect()
  247. export_json(doctype, path, name=name)
  248. finally:
  249. frappe.destroy()
  250. if not context.sites:
  251. raise SiteNotSpecifiedError
  252. @click.command('export-csv')
  253. @click.argument('doctype')
  254. @click.argument('path')
  255. @pass_context
  256. def export_csv(context, doctype, path):
  257. "Export data import template with data for DocType"
  258. from frappe.core.doctype.data_import.data_import import export_csv
  259. for site in context.sites:
  260. try:
  261. frappe.init(site=site)
  262. frappe.connect()
  263. export_csv(doctype, path)
  264. finally:
  265. frappe.destroy()
  266. if not context.sites:
  267. raise SiteNotSpecifiedError
  268. @click.command('export-fixtures')
  269. @click.option('--app', default=None, help='Export fixtures of a specific app')
  270. @pass_context
  271. def export_fixtures(context, app=None):
  272. "Export fixtures"
  273. from frappe.utils.fixtures import export_fixtures
  274. for site in context.sites:
  275. try:
  276. frappe.init(site=site)
  277. frappe.connect()
  278. export_fixtures(app=app)
  279. finally:
  280. frappe.destroy()
  281. if not context.sites:
  282. raise SiteNotSpecifiedError
  283. @click.command('import-doc')
  284. @click.argument('path')
  285. @pass_context
  286. def import_doc(context, path, force=False):
  287. "Import (insert/update) doclist. If the argument is a directory, all files ending with .json are imported"
  288. from frappe.core.doctype.data_import.data_import import import_doc
  289. if not os.path.exists(path):
  290. path = os.path.join('..', path)
  291. if not os.path.exists(path):
  292. print('Invalid path {0}'.format(path))
  293. sys.exit(1)
  294. for site in context.sites:
  295. try:
  296. frappe.init(site=site)
  297. frappe.connect()
  298. import_doc(path)
  299. finally:
  300. frappe.destroy()
  301. if not context.sites:
  302. raise SiteNotSpecifiedError
  303. @click.command('import-csv', help=DATA_IMPORT_DEPRECATION)
  304. @click.argument('path')
  305. @click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records')
  306. @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
  307. @click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
  308. @click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
  309. @pass_context
  310. def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
  311. click.secho(DATA_IMPORT_DEPRECATION)
  312. sys.exit(1)
  313. @click.command('data-import')
  314. @click.option('--file', 'file_path', type=click.Path(), required=True, help="Path to import file (.csv, .xlsx)")
  315. @click.option('--doctype', type=str, required=True)
  316. @click.option('--type', 'import_type', type=click.Choice(['Insert', 'Update'], case_sensitive=False), default='Insert', help="Insert New Records or Update Existing Records")
  317. @click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
  318. @click.option('--mute-emails', default=True, is_flag=True, help='Mute emails during import')
  319. @pass_context
  320. def data_import(context, file_path, doctype, import_type=None, submit_after_import=False, mute_emails=True):
  321. "Import documents in bulk from CSV or XLSX using data import"
  322. from frappe.core.doctype.data_import.data_import import import_file
  323. site = get_site(context)
  324. frappe.init(site=site)
  325. frappe.connect()
  326. import_file(doctype, file_path, import_type, submit_after_import, console=True)
  327. frappe.destroy()
  328. @click.command('bulk-rename')
  329. @click.argument('doctype')
  330. @click.argument('path')
  331. @pass_context
  332. def bulk_rename(context, doctype, path):
  333. "Rename multiple records via CSV file"
  334. from frappe.model.rename_doc import bulk_rename
  335. from frappe.utils.csvutils import read_csv_content
  336. site = get_site(context)
  337. with open(path, 'r') as csvfile:
  338. rows = read_csv_content(csvfile.read())
  339. frappe.init(site=site)
  340. frappe.connect()
  341. bulk_rename(doctype, rows, via_console = True)
  342. frappe.destroy()
  343. @click.command('mariadb')
  344. @pass_context
  345. def mariadb(context):
  346. """
  347. Enter into mariadb console for a given site.
  348. """
  349. import os
  350. site = get_site(context)
  351. if not site:
  352. raise SiteNotSpecifiedError
  353. frappe.init(site=site)
  354. # This is assuming you're within the bench instance.
  355. mysql = find_executable('mysql')
  356. os.execv(mysql, [
  357. mysql,
  358. '-u', frappe.conf.db_name,
  359. '-p'+frappe.conf.db_password,
  360. frappe.conf.db_name,
  361. '-h', frappe.conf.db_host or "localhost",
  362. '--pager=less -SFX',
  363. '--safe-updates',
  364. "-A"])
  365. @click.command('postgres')
  366. @pass_context
  367. def postgres(context):
  368. """
  369. Enter into postgres console for a given site.
  370. """
  371. site = get_site(context)
  372. frappe.init(site=site)
  373. # This is assuming you're within the bench instance.
  374. psql = find_executable('psql')
  375. subprocess.run([ psql, '-d', frappe.conf.db_name])
  376. @click.command('jupyter')
  377. @pass_context
  378. def jupyter(context):
  379. installed_packages = (r.split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze'], encoding='utf8'))
  380. if 'jupyter' not in installed_packages:
  381. subprocess.check_output([sys.executable, '-m', 'pip', 'install', 'jupyter'])
  382. site = get_site(context)
  383. frappe.init(site=site)
  384. jupyter_notebooks_path = os.path.abspath(frappe.get_site_path('jupyter_notebooks'))
  385. sites_path = os.path.abspath(frappe.get_site_path('..'))
  386. try:
  387. os.stat(jupyter_notebooks_path)
  388. except OSError:
  389. print('Creating folder to keep jupyter notebooks at {}'.format(jupyter_notebooks_path))
  390. os.mkdir(jupyter_notebooks_path)
  391. bin_path = os.path.abspath('../env/bin')
  392. print('''
  393. Starting Jupyter notebook
  394. Run the following in your first cell to connect notebook to frappe
  395. ```
  396. import frappe
  397. frappe.init(site='{site}', sites_path='{sites_path}')
  398. frappe.connect()
  399. frappe.local.lang = frappe.db.get_default('lang')
  400. frappe.db.connect()
  401. ```
  402. '''.format(site=site, sites_path=sites_path))
  403. os.execv('{0}/jupyter'.format(bin_path), [
  404. '{0}/jupyter'.format(bin_path),
  405. 'notebook',
  406. jupyter_notebooks_path,
  407. ])
  408. @click.command('console')
  409. @pass_context
  410. def console(context):
  411. "Start ipython console for a site"
  412. site = get_site(context)
  413. frappe.init(site=site)
  414. frappe.connect()
  415. frappe.local.lang = frappe.db.get_default("lang")
  416. import IPython
  417. all_apps = frappe.get_installed_apps()
  418. failed_to_import = []
  419. for app in all_apps:
  420. try:
  421. locals()[app] = __import__(app)
  422. except ModuleNotFoundError:
  423. failed_to_import.append(app)
  424. all_apps.remove(app)
  425. print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
  426. if failed_to_import:
  427. print("\nFailed to import:\n{}".format(", ".join(failed_to_import)))
  428. IPython.embed(display_banner="", header="", colors="neutral")
  429. @click.command('run-tests')
  430. @click.option('--app', help="For App")
  431. @click.option('--doctype', help="For DocType")
  432. @click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt")
  433. @click.option('--test', multiple=True, help="Specific test")
  434. @click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
  435. @click.option('--module', help="Run tests in a module")
  436. @click.option('--profile', is_flag=True, default=False)
  437. @click.option('--coverage', is_flag=True, default=False)
  438. @click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records")
  439. @click.option('--skip-before-tests', is_flag=True, default=False, help="Don't run before tests hook")
  440. @click.option('--junit-xml-output', help="Destination file path for junit xml report")
  441. @click.option('--failfast', is_flag=True, default=False)
  442. @pass_context
  443. def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
  444. coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
  445. skip_test_records=False, skip_before_tests=False, failfast=False):
  446. "Run tests"
  447. import frappe.test_runner
  448. tests = test
  449. site = get_site(context)
  450. allow_tests = frappe.get_conf(site).allow_tests
  451. if not (allow_tests or os.environ.get('CI')):
  452. click.secho('Testing is disabled for the site!', bold=True)
  453. click.secho('You can enable tests by entering following command:')
  454. click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
  455. return
  456. frappe.init(site=site)
  457. frappe.flags.skip_before_tests = skip_before_tests
  458. frappe.flags.skip_test_records = skip_test_records
  459. if coverage:
  460. from coverage import Coverage
  461. from frappe.coverage import STANDARD_INCLUSIONS, STANDARD_EXCLUSIONS, FRAPPE_EXCLUSIONS
  462. # Generate coverage report only for app that is being tested
  463. source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
  464. omit = STANDARD_EXCLUSIONS[:]
  465. if not app or app == 'frappe':
  466. omit.extend(FRAPPE_EXCLUSIONS)
  467. cov = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
  468. cov.start()
  469. ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
  470. force=context.force, profile=profile, junit_xml_output=junit_xml_output,
  471. ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
  472. if coverage:
  473. cov.stop()
  474. cov.save()
  475. if len(ret.failures) == 0 and len(ret.errors) == 0:
  476. ret = 0
  477. if os.environ.get('CI'):
  478. sys.exit(ret)
  479. @click.command('run-parallel-tests')
  480. @click.option('--app', help="For App", default='frappe')
  481. @click.option('--build-number', help="Build number", default=1)
  482. @click.option('--total-builds', help="Total number of builds", default=1)
  483. @click.option('--with-coverage', is_flag=True, help="Build coverage file")
  484. @click.option('--use-orchestrator', is_flag=True, help="Use orchestrator to run parallel tests")
  485. @pass_context
  486. def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False):
  487. site = get_site(context)
  488. if use_orchestrator:
  489. from frappe.parallel_test_runner import ParallelTestWithOrchestrator
  490. ParallelTestWithOrchestrator(app, site=site, with_coverage=with_coverage)
  491. else:
  492. from frappe.parallel_test_runner import ParallelTestRunner
  493. ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds, with_coverage=with_coverage)
  494. @click.command('run-ui-tests')
  495. @click.argument('app')
  496. @click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
  497. @click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
  498. @click.option('--ci-build-id')
  499. @pass_context
  500. def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
  501. "Run UI tests"
  502. site = get_site(context)
  503. app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
  504. site_url = frappe.utils.get_site_url(site)
  505. admin_password = frappe.get_conf(site).admin_password
  506. # override baseUrl using env variable
  507. site_env = 'CYPRESS_baseUrl={}'.format(site_url)
  508. password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
  509. os.chdir(app_base_path)
  510. node_bin = subprocess.getoutput("npm bin")
  511. cypress_path = "{0}/cypress".format(node_bin)
  512. plugin_path = "{0}/../cypress-file-upload".format(node_bin)
  513. # check if cypress in path...if not, install it.
  514. if not (
  515. os.path.exists(cypress_path)
  516. and os.path.exists(plugin_path)
  517. and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
  518. ):
  519. # install cypress
  520. click.secho("Installing Cypress...", fg="yellow")
  521. frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
  522. # run for headless mode
  523. run_or_open = 'run --browser firefox --record' if headless else 'open'
  524. command = '{site_env} {password_env} {cypress} {run_or_open}'
  525. formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
  526. if parallel:
  527. formatted_command += ' --parallel'
  528. if ci_build_id:
  529. formatted_command += ' --ci-build-id {}'.format(ci_build_id)
  530. click.secho("Running Cypress...", fg="yellow")
  531. frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
  532. @click.command('serve')
  533. @click.option('--port', default=8000)
  534. @click.option('--profile', is_flag=True, default=False)
  535. @click.option('--noreload', "no_reload", is_flag=True, default=False)
  536. @click.option('--nothreading', "no_threading", is_flag=True, default=False)
  537. @pass_context
  538. def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None):
  539. "Start development web server"
  540. import frappe.app
  541. if not context.sites:
  542. site = None
  543. else:
  544. site = context.sites[0]
  545. frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
  546. @click.command('request')
  547. @click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`')
  548. @click.option('--path', help='path to request JSON')
  549. @pass_context
  550. def request(context, args=None, path=None):
  551. "Run a request as an admin"
  552. import frappe.handler
  553. import frappe.api
  554. for site in context.sites:
  555. try:
  556. frappe.init(site=site)
  557. frappe.connect()
  558. if args:
  559. if "?" in args:
  560. frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")])
  561. else:
  562. frappe.local.form_dict = frappe._dict()
  563. if args.startswith("/api/method"):
  564. frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1]
  565. elif path:
  566. with open(os.path.join('..', path), 'r') as f:
  567. args = json.loads(f.read())
  568. frappe.local.form_dict = frappe._dict(args)
  569. frappe.handler.execute_cmd(frappe.form_dict.cmd)
  570. print(frappe.response)
  571. finally:
  572. frappe.destroy()
  573. if not context.sites:
  574. raise SiteNotSpecifiedError
  575. @click.command('make-app')
  576. @click.argument('destination')
  577. @click.argument('app_name')
  578. def make_app(destination, app_name):
  579. "Creates a boilerplate app"
  580. from frappe.utils.boilerplate import make_boilerplate
  581. make_boilerplate(destination, app_name)
  582. @click.command('set-config')
  583. @click.argument('key')
  584. @click.argument('value')
  585. @click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
  586. @click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
  587. @click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
  588. @pass_context
  589. def set_config(context, key, value, global_=False, parse=False, as_dict=False):
  590. "Insert/Update a value in site_config.json"
  591. from frappe.installer import update_site_config
  592. if as_dict:
  593. from frappe.utils.commands import warn
  594. warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
  595. parse = as_dict
  596. if parse:
  597. import ast
  598. value = ast.literal_eval(value)
  599. if global_:
  600. sites_path = os.getcwd()
  601. common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
  602. update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
  603. else:
  604. for site in context.sites:
  605. frappe.init(site=site)
  606. update_site_config(key, value, validate=False)
  607. frappe.destroy()
  608. @click.command("version")
  609. @click.option("-f", "--format", "output",
  610. type=click.Choice(["plain", "table", "json", "legacy"]), help="Output format", default="legacy")
  611. def get_version(output):
  612. """Show the versions of all the installed apps."""
  613. from git import Repo
  614. from frappe.utils.commands import render_table
  615. from frappe.utils.change_log import get_app_branch
  616. frappe.init("")
  617. data = []
  618. for app in sorted(frappe.get_all_apps()):
  619. module = frappe.get_module(app)
  620. app_hooks = frappe.get_module(app + ".hooks")
  621. repo = Repo(frappe.get_app_path(app, ".."))
  622. app_info = frappe._dict()
  623. app_info.app = app
  624. app_info.branch = get_app_branch(app)
  625. app_info.commit = repo.head.object.hexsha[:7]
  626. app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
  627. data.append(app_info)
  628. {
  629. "legacy": lambda: [
  630. click.echo(f"{app_info.app} {app_info.version}")
  631. for app_info in data
  632. ],
  633. "plain": lambda: [
  634. click.echo(f"{app_info.app} {app_info.version} {app_info.branch} ({app_info.commit})")
  635. for app_info in data
  636. ],
  637. "table": lambda: render_table(
  638. [["App", "Version", "Branch", "Commit"]] +
  639. [
  640. [app_info.app, app_info.version, app_info.branch, app_info.commit]
  641. for app_info in data
  642. ]
  643. ),
  644. "json": lambda: click.echo(json.dumps(data, indent=4)),
  645. }[output]()
  646. @click.command('rebuild-global-search')
  647. @click.option('--static-pages', is_flag=True, default=False, help='Rebuild global search for static pages')
  648. @pass_context
  649. def rebuild_global_search(context, static_pages=False):
  650. '''Setup help table in the current site (called after migrate)'''
  651. from frappe.utils.global_search import (get_doctypes_with_global_search, rebuild_for_doctype,
  652. get_routes_to_index, add_route_to_global_search, sync_global_search)
  653. for site in context.sites:
  654. try:
  655. frappe.init(site)
  656. frappe.connect()
  657. if static_pages:
  658. routes = get_routes_to_index()
  659. for i, route in enumerate(routes):
  660. add_route_to_global_search(route)
  661. frappe.local.request = None
  662. update_progress_bar('Rebuilding Global Search', i, len(routes))
  663. sync_global_search()
  664. else:
  665. doctypes = get_doctypes_with_global_search()
  666. for i, doctype in enumerate(doctypes):
  667. rebuild_for_doctype(doctype)
  668. update_progress_bar('Rebuilding Global Search', i, len(doctypes))
  669. finally:
  670. frappe.destroy()
  671. if not context.sites:
  672. raise SiteNotSpecifiedError
  673. commands = [
  674. build,
  675. clear_cache,
  676. clear_website_cache,
  677. jupyter,
  678. console,
  679. destroy_all_sessions,
  680. execute,
  681. export_csv,
  682. export_doc,
  683. export_fixtures,
  684. export_json,
  685. get_version,
  686. import_csv,
  687. data_import,
  688. import_doc,
  689. make_app,
  690. mariadb,
  691. postgres,
  692. request,
  693. reset_perms,
  694. run_tests,
  695. run_ui_tests,
  696. serve,
  697. set_config,
  698. show_config,
  699. watch,
  700. bulk_rename,
  701. add_to_email_queue,
  702. rebuild_global_search,
  703. run_parallel_tests
  704. ]