Hide keyboard shortcuts

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 

10 

11import click 

12from flask import current_app 

13 

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 

28 

29 

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. 

35 

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 

43 

44 Returns 

45 ------- 

46 None 

47 ... 

48 

49 """ 

50 click.echo("Cleaning...") 

51 click.echo(SEP_CHAR * SEP_NUM) 

52 db = current_app.extensions["sqlalchemy"].db 

53 

54 if clear_db: 

55 db.drop_all() 

56 db.engine.execute("DROP TABLE IF EXISTS alembic_version;") 

57 

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") 

64 

65 tryrmcache(os.getcwd(), verbose=verbose) 

66 

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") 

72 

73 

74def _collectstatic(target_module="modules", verbose=False): 

75 

76 """ 

77 Copies ``module/static`` into ``/static/modules/module``. 

78 In static it becomes like 

79 

80 :: 

81 

82 static/ 

83 modules/ 

84 box_something/ 

85 modulename 

86 modulename2 

87 

88 

89 Parameters 

90 ---------- 

91 target_module: str 

92 name of module, in alphanumeric-underscore, 

93 supports ``module`` or ``box__name/module`` 

94 

95 Returns 

96 ------- 

97 None 

98 

99 """ 

100 click.echo("Collecting static...") 

101 click.echo(SEP_CHAR * SEP_NUM) 

102 

103 root_path = os.getcwd() 

104 static_path = os.path.join(root_path, "static") 

105 

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": 

109 

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) 

114 

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) 

119 

120 # get the full path for modules (the src). Defaults to ./modules 

121 modules_path = os.path.join(root_path, target_module) 

122 

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") 

126 

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) 

131 

132 # clear ./static/modules before coping to it 

133 tryrmtree(modules_path_in_static, verbose=verbose) 

134 

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 ) 

147 

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 = "" 

153 

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] 

159 

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) 

166 

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) 

172 

173 click.echo("") 

174 

175 

176def _upload_data(verbose=False): 

177 click.echo("Uploading initial data to db...") 

178 click.echo(SEP_CHAR * SEP_NUM) 

179 

180 root_path = os.getcwd() 

181 

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 

192 

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}") 

209 

210 click.echo("") 

211 

212 

213def _create_box(boxname, verbose=False): 

214 

215 base_path = os.path.join("modules", boxname) 

216 trymkdir(base_path, verbose=verbose) 

217 

218 info_json = { 

219 "display_string": boxname.capitalize(), 

220 "box_name": boxname, 

221 "author": {"name": "", "website": "", "mail": ""}, 

222 } 

223 

224 box_info = os.path.join(base_path, "box_info.json") 

225 

226 with open(box_info, "w", encoding="utf-8") as f: 

227 json.dump(info_json, f, indent=4, sort_keys=True) 

228 

229 if verbose: 

230 click.echo("'box_info.json' content:") 

231 click.echo(json.dumps(info_json, indent=4, sort_keys=True)) 

232 

233 click.echo(f"{boxname} created!") 

234 

235 

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 

240 

241 Parameters 

242 ---------- 

243 modulename: str 

244 name of module, in alphanumeric-underscore 

245 

246 Returns 

247 ------- 

248 None 

249 

250 """ 

251 

252 click.echo(f"creating module: {modulename}") 

253 click.echo(SEP_CHAR * SEP_NUM) 

254 

255 if base_path is None: 

256 base_path = os.path.join("modules", modulename) 

257 

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) 

264 

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) 

274 

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) 

281 

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) 

294 

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)) 

298 

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) 

303 

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 ) 

310 

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 ) 

315 

316 

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") 

320 

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) 

324 

325 os.environ["FLASK_APP"] = f"app:create_app('{mode}')" 

326 os.environ["FLASK_ENV"] = mode 

327 run(["flask", "run"]) 

328 

329 

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() 

337 

338 

339def _verify_app(app_path, box_name=None): 

340 app_folder = last_part_of_path(app_path) 

341 

342 audit_info = {"path": app_path, "issues": []} 

343 

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) 

350 

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) 

358 

359 if ("module_name" not in not_found) and ("url_prefix" not in not_found): 

360 

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) 

369 

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) 

376 

377 # verify components 

378 

379 if not path_exists(os.path.join(app_path, "templates")): 

380 audit_info["issues"].append("warning: templates folder not found") 

381 

382 if not path_exists(os.path.join(app_path, "view.py")): 

383 audit_info["issues"].append("severe: view.py not found") 

384 

385 if not path_exists(os.path.join(app_path, "forms.py")): 

386 audit_info["issues"].append("info: forms.py not found") 

387 

388 if not path_exists(os.path.join(app_path, "models.py")): 

389 audit_info["issues"].append("info: models.py not found") 

390 

391 if not path_exists(os.path.join(app_path, "global.py")): 

392 audit_info["issues"].append("info: global.py not found") 

393 

394 return audit_info 

395 

396 

397def _verify_box(box_path): 

398 audit_info = {"issues": []} 

399 

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) 

406 

407 to_check_keys = ["box_name"] 

408 

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) 

413 

414 return audit_info 

415 

416 

417def _check_apps(root_path): 

418 issues_found = [] 

419 

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__")] 

423 

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) 

428 

429 return issues_found 

430 

431 

432def _check_boxes(root_path): 

433 box_issues = [] 

434 

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__")] 

438 

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"] 

442 

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) 

447 

448 return box_issues 

449 

450 

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) 

459 

460 click.echo("Running audit ...") 

461 

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("") 

468 

469 for box_issue in boxes_issues: 

470 click.echo(box_issue["path"]) 

471 for issue in box_issue["issues"]: 

472 click.echo(" " + issue) 

473 

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("") 

479 

480 click.echo("Audit finished!") 

481 

482 

483def _verify_app_name(app_name): 

484 if (app_name.startswith("box__")) and (app_name.count("/") != 1): 

485 return False 

486 

487 if app_name.endswith("/"): 

488 return False 

489 

490 return True 

491 

492 

493def name_is_box(app_name): 

494 if app_name.startswith("box__"): 

495 return True 

496 

497 return False 

498 

499 

500def _rename_app(old_app_name, new_app_name): 

501 

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() 

509 

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() 

513 

514 root_path = os.getcwd() 

515 modules_path = os.path.join(root_path, "modules") 

516 

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] 

522 

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() 

526 

527 try: 

528 os.rename( 

529 os.path.join(modules_path, old_app_name), 

530 os.path.join(modules_path, new_app_name), 

531 ) 

532 

533 with open(os.path.join(modules_path, new_app_name, "info.json")) as f: 

534 json_data = json.load(f) 

535 

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) 

543 

544 click.echo(f"Renamed app {old_app_name} to {new_app_name}") 

545 except Exception as e: 

546 raise e