Coverage for api/cmd_helper.py : 0%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Helper utility functions for commandline api
3"""
4import importlib
5import json
6import os
7import re
8import sys
9from subprocess import run
11import click
12from flask import current_app
14from shopyo.api.cli_content import get_dashboard_html_content
15from shopyo.api.cli_content import get_global_py_content
16from shopyo.api.cli_content import get_module_view_content
17from shopyo.api.constants import SEP_CHAR
18from shopyo.api.constants import SEP_NUM
19from shopyo.api.file import get_folders
20from shopyo.api.file import last_part_of_path
21from shopyo.api.file import path_exists
22from shopyo.api.file import trycopytree
23from shopyo.api.file import trymkdir
24from shopyo.api.file import trymkfile
25from shopyo.api.file import tryrmcache
26from shopyo.api.file import tryrmfile
27from shopyo.api.file import tryrmtree
30def _clean(verbose=False, clear_migration=True, clear_db=True):
31 """
32 Deletes shopyo.db and migrations/ if present in current working directory.
33 Deletes all __pycache__ folders starting from current working directory
34 all the way to leaf directory.
36 Parameters
37 ----------
38 - verbose: flag to indicate whether to print to result of clean to
39 stdout or not.
40 - clear_migration: flag to indicate if migration folder is to be deleted or not
41 - clear_db: flag indicating if db is to be cleared or not
42 - db: db to be cleaned
44 Returns
45 -------
46 None
47 ...
49 """
50 click.echo("Cleaning...")
51 click.echo(SEP_CHAR * SEP_NUM)
52 db = current_app.extensions["sqlalchemy"].db
54 if clear_db:
55 db.drop_all()
56 db.engine.execute("DROP TABLE IF EXISTS alembic_version;")
58 tryrmfile(os.path.join(os.getcwd(), "shopyo.db"), verbose=verbose)
59 if verbose:
60 click.echo("[x] all tables dropped")
61 elif clear_db is False:
62 if verbose:
63 click.echo("[ ] db clearing skipped")
65 tryrmcache(os.getcwd(), verbose=verbose)
67 if clear_migration:
68 tryrmtree(os.path.join(os.getcwd(), "migrations"), verbose=verbose)
69 elif clear_migration is False:
70 if verbose:
71 click.echo("[ ] migration folder delete skipped")
74def _collectstatic(target_module="modules", verbose=False):
76 """
77 Copies ``module/static`` into ``/static/modules/module``.
78 In static it becomes like
80 ::
82 static/
83 modules/
84 box_something/
85 modulename
86 modulename2
89 Parameters
90 ----------
91 target_module: str
92 name of module, in alphanumeric-underscore,
93 supports ``module`` or ``box__name/module``
95 Returns
96 -------
97 None
99 """
100 click.echo("Collecting static...")
101 click.echo(SEP_CHAR * SEP_NUM)
103 root_path = os.getcwd()
104 static_path = os.path.join(root_path, "static")
106 # if target_module path does not start with 'modules\' add it to as a
107 # prefix to the target_module path
108 if target_module != "modules":
110 # normalize the target_module path to be same as that of OS
111 target_module = re.split(r"[/|\\]+", target_module)
112 target_module_start = target_module[0]
113 target_module = os.path.join(*target_module)
115 # add the modules folder to start of target_module incase it is not
116 # already present in the path
117 if target_module_start != "modules":
118 target_module = os.path.join("modules", target_module)
120 # get the full path for modules (the src). Defaults to ./modules
121 modules_path = os.path.join(root_path, target_module)
123 # get the full path of static folder to copy to (the dest).
124 # always ./static/modules
125 modules_path_in_static = os.path.join(static_path, "modules")
127 # terminate if modules_path (i.e. src to copy static from) does not exist
128 if not os.path.exists(modules_path):
129 click.echo(f"[ ] path: {modules_path} does not exist")
130 sys.exit(1)
132 # clear ./static/modules before coping to it
133 tryrmtree(modules_path_in_static, verbose=verbose)
135 # look for static folders in all project
136 for folder in get_folders(modules_path):
137 if folder.startswith("box__"):
138 box_path = os.path.join(modules_path, folder)
139 for subfolder in get_folders(box_path):
140 module_name = subfolder
141 module_static_folder = os.path.join(box_path, subfolder, "static")
142 if not os.path.exists(module_static_folder):
143 continue
144 module_in_static_dir = os.path.join(
145 modules_path_in_static, folder, module_name
146 )
148 # copy from ./modules/<box__name>/<submodule> to
149 # ./static/modules
150 trycopytree(module_static_folder, module_in_static_dir, verbose=verbose)
151 else:
152 path_split = ""
154 # split the target module if default target_module path name is
155 # not used
156 if target_module != "modules":
157 path_split = re.split(r"[/|\\]", target_module, maxsplit=1)
158 path_split = path_split[1]
160 if folder.lower() == "static":
161 module_static_folder = os.path.join(modules_path, folder)
162 module_name = path_split
163 else:
164 module_static_folder = os.path.join(modules_path, folder, "static")
165 module_name = os.path.join(path_split, folder)
167 if not os.path.exists(module_static_folder):
168 continue
169 module_in_static_dir = os.path.join(modules_path_in_static, module_name)
170 tryrmtree(module_in_static_dir, verbose=verbose)
171 trycopytree(module_static_folder, module_in_static_dir, verbose=verbose)
173 click.echo("")
176def _upload_data(verbose=False):
177 click.echo("Uploading initial data to db...")
178 click.echo(SEP_CHAR * SEP_NUM)
180 root_path = os.getcwd()
182 for folder in os.listdir(os.path.join(root_path, "modules")):
183 if folder.startswith("__"): # ignore __pycache__
184 continue
185 if folder.startswith("box__"):
186 # boxes
187 for sub_folder in os.listdir(os.path.join(root_path, "modules", folder)):
188 if sub_folder.startswith("__"): # ignore __pycache__
189 continue
190 elif sub_folder.endswith(".json"): # box_info.json
191 continue
193 try:
194 upload = importlib.import_module(
195 f"modules.{folder}.{sub_folder}.upload"
196 )
197 upload.upload(verbose=verbose)
198 except ImportError as e:
199 if verbose:
200 click.echo(f"[ ] {e}")
201 else:
202 # apps
203 try:
204 upload = importlib.import_module(f"modules.{folder}.upload")
205 upload.upload(verbose=verbose)
206 except ImportError as e:
207 if verbose:
208 click.echo(f"[ ] {e}")
210 click.echo("")
213def _create_box(boxname, verbose=False):
215 base_path = os.path.join("modules", boxname)
216 trymkdir(base_path, verbose=verbose)
218 info_json = {
219 "display_string": boxname.capitalize(),
220 "box_name": boxname,
221 "author": {"name": "", "website": "", "mail": ""},
222 }
224 box_info = os.path.join(base_path, "box_info.json")
226 with open(box_info, "w", encoding="utf-8") as f:
227 json.dump(info_json, f, indent=4, sort_keys=True)
229 if verbose:
230 click.echo("'box_info.json' content:")
231 click.echo(json.dumps(info_json, indent=4, sort_keys=True))
233 click.echo(f"{boxname} created!")
236def _create_module(modulename, base_path=None, verbose=False):
237 """creates module with the structure defined in the modules section in docs
238 Assume valid modulename i.e modulename does not start with ``box__`` and
239 modulename consist only of alphanumeric characters or underscore
241 Parameters
242 ----------
243 modulename: str
244 name of module, in alphanumeric-underscore
246 Returns
247 -------
248 None
250 """
252 click.echo(f"creating module: {modulename}")
253 click.echo(SEP_CHAR * SEP_NUM)
255 if base_path is None:
256 base_path = os.path.join("modules", modulename)
258 # create the module with directories templates, tests, static
259 trymkdir(base_path, verbose=verbose)
260 trymkdir(os.path.join(base_path, "templates"), verbose=verbose)
261 trymkdir(os.path.join(base_path, "templates", modulename), verbose=verbose)
262 trymkdir(os.path.join(base_path, "tests"), verbose=verbose)
263 trymkdir(os.path.join(base_path, "static"), verbose=verbose)
265 # create functional test and unit test files for the module
266 test_func_path = os.path.join(
267 base_path, "tests", f"test_{modulename}_functional.py"
268 )
269 test_models_path = os.path.join(base_path, "tests", f"test_{modulename}_models.py")
270 test_func_content = "# Please add your functional tests to this file.\n"
271 test_model_content = "# Please add your models tests to this file.\n"
272 trymkfile(test_func_path, test_func_content, verbose=verbose)
273 trymkfile(test_models_path, test_model_content, verbose=verbose)
275 # create view.py, forms.py and model.py files inside the module
276 trymkfile(
277 os.path.join(base_path, "view.py"), get_module_view_content(), verbose=verbose
278 )
279 trymkfile(os.path.join(base_path, "forms.py"), "", verbose=verbose)
280 trymkfile(os.path.join(base_path, "models.py"), "", verbose=verbose)
282 # create info.json file inside the module
283 info_json = {
284 "display_string": modulename.capitalize(),
285 "module_name": modulename,
286 "type": "show",
287 "fa-icon": "fa fa-store",
288 "url_prefix": f"/{modulename}",
289 "author": {"name": "", "website": "", "mail": ""},
290 }
291 info_json_path = os.path.join(base_path, "info.json")
292 with open(info_json_path, "w", encoding="utf-8") as f:
293 json.dump(info_json, f, indent=4, sort_keys=True)
295 if verbose:
296 click.echo(f"[x] file created at '{info_json_path}' with content: ")
297 click.echo(json.dumps(info_json, indent=4, sort_keys=True))
299 # create the sidebar.html inside templates/blocks
300 blocks_path = os.path.join(base_path, "templates", modulename, "blocks")
301 trymkdir(blocks_path, verbose=verbose)
302 trymkfile(os.path.join(blocks_path, "sidebar.html"), "", verbose=verbose)
304 # create the dashboard.html inside templates/MODULENAME
305 trymkfile(
306 os.path.join(base_path, "templates", modulename, "dashboard.html"),
307 get_dashboard_html_content(),
308 verbose=verbose,
309 )
311 # create the global.py files inside the module
312 trymkfile(
313 os.path.join(base_path, "global.py"), get_global_py_content(), verbose=verbose
314 )
317def _run_app(mode):
318 """helper command for running shopyo flask app in debug/production mode"""
319 app_path = os.path.join(os.getcwd(), "app.py")
321 if not os.path.exists(app_path):
322 click.secho(f"Unable to find `app.py` in {os.getcwd()}", fg="red")
323 sys.exit(1)
325 os.environ["FLASK_APP"] = f"app:create_app('{mode}')"
326 os.environ["FLASK_ENV"] = mode
327 run(["flask", "run"])
330def _check_modules_path(root_path):
331 modules_path = os.path.join(root_path, "modules")
332 if path_exists(modules_path):
333 pass
334 else:
335 click.echo("Modules folder not found!")
336 sys.exit()
339def _verify_app(app_path, box_name=None):
340 app_folder = last_part_of_path(app_path)
342 audit_info = {"path": app_path, "issues": []}
344 # verify info.json
345 if not path_exists(os.path.join(app_path, "info.json")):
346 audit_info["issues"].append("severe: info.json not found")
347 else:
348 with open(os.path.join(app_path, "info.json")) as f:
349 json_data = json.load(f)
351 to_check_keys = ["module_name", "url_prefix"]
352 not_found = []
353 for key in to_check_keys:
354 if key not in json_data:
355 not_found.append(key)
356 msg = "severe: key {key} not found in info.json".format(key)
357 audit_info["issues"].append(msg)
359 if ("module_name" not in not_found) and ("url_prefix" not in not_found):
361 if (json_data["module_name"].strip() == "") or (
362 json_data["url_prefix"].strip() == ""
363 ):
364 msg = (
365 "sus: module_name and url_prefix in info.json must not be empty"
366 " ideally"
367 )
368 audit_info["issues"].append(msg)
370 if "module_name" not in not_found:
371 if {json_data["module_name"], app_folder} != {app_folder}:
372 msg = """severe: currently module_name "{}" in info.json and app folder "{}" must have the same value""".format(
373 json_data["module_name"], app_folder
374 )
375 audit_info["issues"].append(msg)
377 # verify components
379 if not path_exists(os.path.join(app_path, "templates")):
380 audit_info["issues"].append("warning: templates folder not found")
382 if not path_exists(os.path.join(app_path, "view.py")):
383 audit_info["issues"].append("severe: view.py not found")
385 if not path_exists(os.path.join(app_path, "forms.py")):
386 audit_info["issues"].append("info: forms.py not found")
388 if not path_exists(os.path.join(app_path, "models.py")):
389 audit_info["issues"].append("info: models.py not found")
391 if not path_exists(os.path.join(app_path, "global.py")):
392 audit_info["issues"].append("info: global.py not found")
394 return audit_info
397def _verify_box(box_path):
398 audit_info = {"issues": []}
400 # verify box_info.json
401 if not path_exists(os.path.join(box_path, "box_info.json")):
402 audit_info["issues"].append("warning: box_info.json not found")
403 else:
404 with open(os.path.join(box_path, "box_info.json")) as f:
405 json_data = json.load(f)
407 to_check_keys = ["box_name"]
409 for key in to_check_keys:
410 if key not in json_data:
411 msg = f"severe: key {key} not found in box_info.json"
412 audit_info["issues"].append(msg)
414 return audit_info
417def _check_apps(root_path):
418 issues_found = []
420 modules_path = os.path.join(root_path, "modules")
421 apps = get_folders(modules_path)
422 apps = [a for a in apps if not a.startswith("__") and not a.startswith("box__")]
424 for app in apps:
425 app_path = os.path.join(modules_path, app)
426 app_issues = _verify_app(app_path)
427 issues_found.append(app_issues)
429 return issues_found
432def _check_boxes(root_path):
433 box_issues = []
435 modules_path = os.path.join(root_path, "modules")
436 boxes = get_folders(modules_path)
437 boxes = [b for b in boxes if b.startswith("box__")]
439 for b in boxes:
440 box_info = {"path": os.path.join(modules_path, b), "apps_issues": []}
441 box_info["issues"] = _verify_box(os.path.join(modules_path, b))["issues"]
443 for app in get_folders(os.path.join(modules_path, b)):
444 app_issues = _verify_app(os.path.join(modules_path, b, app), box_name=b)
445 box_info["apps_issues"].append(app_issues)
446 box_issues.append(box_info)
448 return box_issues
451def _audit():
452 """
453 checks if modules are corrupted
454 """
455 root_path = os.getcwd()
456 _check_modules_path(root_path)
457 apps_issues = _check_apps(root_path)
458 boxes_issues = _check_boxes(root_path)
460 click.echo("Running audit ...")
462 click.echo("Checking apps ...")
463 for app_issue in apps_issues:
464 click.echo(app_issue["path"])
465 for issue in app_issue["issues"]:
466 click.echo(" " + issue)
467 click.echo("")
469 for box_issue in boxes_issues:
470 click.echo(box_issue["path"])
471 for issue in box_issue["issues"]:
472 click.echo(" " + issue)
474 for app_issue in box_issue["apps_issues"]:
475 click.echo(" " + app_issue["path"])
476 for issue in app_issue["issues"]:
477 click.echo(" " + issue)
478 click.echo("")
480 click.echo("Audit finished!")
483def _verify_app_name(app_name):
484 if (app_name.startswith("box__")) and (app_name.count("/") != 1):
485 return False
487 if app_name.endswith("/"):
488 return False
490 return True
493def name_is_box(app_name):
494 if app_name.startswith("box__"):
495 return True
497 return False
500def _rename_app(old_app_name, new_app_name):
502 # box_
503 if old_app_name.startswith("box") and not old_app_name.startswith("box__"):
504 click.echo('Box names start with two __, example: "box__default"')
505 sys.exit()
506 if new_app_name.startswith("box") and not new_app_name.startswith("box__"):
507 click.echo('Box names start with two __, example: "box__default"')
508 sys.exit()
510 if (not _verify_app_name(old_app_name)) and (not _verify_app_name(new_app_name)):
511 click.echo('App names should be in the format "app" or "box_name/app"')
512 sys.exit()
514 root_path = os.getcwd()
515 modules_path = os.path.join(root_path, "modules")
517 if name_is_box(old_app_name):
518 box_name = old_app_name.split("/")[0]
519 if name_is_box(new_app_name):
520 app_part = old_app_name.split("/")[1]
521 app_name = new_app_name.split("/")[1]
523 if not path_exists(os.path.join(modules_path, box_name, app_part)):
524 click.echo(f"App {old_app_name} does not exist")
525 sys.exit()
527 try:
528 os.rename(
529 os.path.join(modules_path, old_app_name),
530 os.path.join(modules_path, new_app_name),
531 )
533 with open(os.path.join(modules_path, new_app_name, "info.json")) as f:
534 json_data = json.load(f)
536 with open(os.path.join(modules_path, new_app_name, "info.json"), "w+") as f:
537 if name_is_box(new_app_name):
538 module_name = new_app_name.split("/")[1]
539 else:
540 module_name = new_app_name
541 json_data["module_name"] = module_name
542 json.dump(json_data, f, indent=4)
544 click.echo(f"Renamed app {old_app_name} to {new_app_name}")
545 except Exception as e:
546 raise e