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

2Defines command line services to install, setup and run apps. 

3''' 

4import io 

5import os 

6import re 

7import six 

8import sys 

9import yaml 

10import stat 

11import shlex 

12import string 

13import shutil 

14import datetime 

15import requests 

16from glob import glob 

17from shutilwhich import which 

18from pathlib import Path 

19from subprocess import Popen, check_output, CalledProcessError # nosec 

20from orderedattrdict import AttrDict 

21from orderedattrdict.yamlutils import AttrDictYAMLLoader 

22from zipfile import ZipFile 

23from tornado.template import Template 

24from orderedattrdict.yamlutils import from_yaml # noqa 

25import gramex 

26import gramex.license 

27from gramex.config import ChainConfig, PathConfig, variables, app_log 

28 

29usage = ''' 

30install: | 

31 usage: 

32 gramex install <app> <url> [--target=DIR] 

33 gramex install <app> --cmd="COMMAND" [--target=DIR] 

34 

35 "app" is any name you want to locally call the application. 

36 

37 "url" can be a: 

38 - local ZIP file (/path/to/app.zip) 

39 - local directory (/path/to/directory) 

40 - URL of a ZIP file (https://github.com/user/repo/archive/master.zip) 

41 

42 target is the directory to install at (defaults to user data directory.) 

43 

44 cmd is a shell command to run. If it has the word "TARGET" in caps, it is 

45 replaced by the target directory. 

46 

47 After installation, runs "gramex setup" which runs the Makefile, setup.ps1, 

48 setup.sh, requirements.txt, setup.py, yarn/npm install and bower install. 

49 

50 Installed apps: 

51 {apps} 

52 

53setup: | 

54 usage: gramex setup <target> [<target> ...] [--all] 

55 

56 target is the directory to set up (required). This can be an absolute path, 

57 relative path, or a directory name under $GRAMEXPATH/apps/. 

58 

59 gramex setup --all sets up all apps under $GRAMEXPATH/apps/ 

60 

61 Run the following commands at that directory in sequence, if possible: 

62 - make 

63 - powershell -File setup.ps1 

64 - bash setup.sh 

65 - pip install --upgrade -r requirements.txt 

66 - python setup.py 

67 - yarn/npm install 

68 - bower install 

69 

70run: | 

71 usage: gramex run <app> [--target=DIR] [--dir=DIR] [--<options>=<value>] 

72 

73 "app" is the name of the locally installed application. 

74 

75 If "app" is not installed, specify --target=DIR to run from DIR. The next 

76 "gramex run app" will automatically run from DIR. 

77 

78 "dir" is a *sub-directory* under "target" to run from. This is useful if 

79 "app" has multiple sub-applications. 

80 

81 All Gramex command line options can be used. These are saved. For example: 

82 

83 gramex run app --target=/path/to/dir --listen.port=8899 --browser=true 

84 

85 ... will preserve the "target", "listen.port" and "browser" values. Running 

86 "gramex run app" will re-use these values. To clear the option, leave the 

87 value blank. For example "--browser=" will clear the browser option. 

88 

89 Installed apps: 

90 {apps} 

91 

92uninstall: | 

93 usage: gramex uninstall <app> [<app> ...] 

94 

95 "app" is the name of the locally installed application. You can uninstall 

96 multiple applications in one command. 

97 

98 All information about the application is lost. You cannot undo this. 

99 

100 Installed apps: 

101 {apps} 

102 

103service: | 

104 usage: gramex service <cmd> [--options] 

105 

106 Install a Gramex application as a Windows service: 

107 

108 gramex service install 

109 --cwd "C:/path/to/application/" 

110 --user "DOMAIN\\USER" # Optional user to run as 

111 --password "user-password" # Required if user is specified 

112 --startup manual|auto|disabled 

113 

114 Update: 

115 

116 gramex service update <same parameters as install> 

117 

118 Remove: 

119 

120 gramex service remove # or gramex service uninstall 

121 

122 Start / stop commands 

123 

124 gramex service start 

125 gramex service stop 

126 

127init: | 

128 usage: gramex init [--target=DIR] 

129 

130 Initializes a Gramex project at the current or target dir. Specifically, it: 

131 - Sets up a git repo 

132 - Install supporting files for a gramex project 

133 - Runs gramex setup (which runs yarn/npm install and other dependencies) 

134 

135mail: | 

136 gramex mail <key> # Send mail named <key> 

137 gramex mail --list # Lists all keys in config file 

138 gramex mail --init # Initializes config file 

139 

140 The config is a gramex.yaml file. It must have email: and alert: sections. 

141 If the current folder has a gramex.yaml, that's used. Else the default is 

142 $GRAMEXDATA/mail/gramexmail.yaml. 

143 

144 Options: 

145 --conf <path> # Specify a different conf file location 

146 

147license: | 

148 gramex license # Show Gramex license 

149 gramex license accept # Accept Gramex license 

150 gramex license reject # Reject Gramex license 

151''' 

152usage = yaml.load(usage, Loader=AttrDictYAMLLoader) # nosec 

153 

154 

155class TryAgainError(Exception): 

156 '''If shutil.rmtree fails, and we've fixed the problem, raise this to try again''' 

157 pass 

158 

159 

160try: 

161 WindowsError 

162except NameError: 

163 # On non-Windows systems, _ensure_remove just raises the exception 

164 def _ensure_remove(remove, path, exc_info): # noqa -- redefine function 

165 raise exc_info[1] 

166else: 

167 # On Windows systems, try harder 

168 def _ensure_remove(function, path, exc_info): 

169 '''onerror callback for rmtree that tries hard to delete files''' 

170 if issubclass(exc_info[0], WindowsError): 170 ↛ 199line 170 didn't jump to line 199, because the condition on line 170 was never false

171 import winerror 

172 # Delete read-only files 

173 # https://bugs.python.org/issue19643 

174 # https://bugs.python.org/msg218021 

175 if exc_info[1].winerror == winerror.ERROR_ACCESS_DENIED: 

176 os.chmod(path, stat.S_IWRITE) 

177 return os.remove(path) 

178 # Delay delete a bit if directory is used by another process. 

179 # Typically happens on uninstall immediately after bower / npm / git 

180 # (e.g. during testing.) 

181 elif exc_info[1].winerror == winerror.ERROR_SHARING_VIOLATION: 181 ↛ 192line 181 didn't jump to line 192, because the condition on line 181 was never false

182 import time 

183 delays = [0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0] 

184 for delay in delays: 

185 time.sleep(delay) 

186 try: 

187 return os.remove(path) 

188 except WindowsError: 

189 pass 

190 # npm creates windows shortcuts that shutil.rmtree cannot delete. 

191 # os.listdir failes with a PATH_NOT_FOUND. Delete these and try again 

192 elif function == os.listdir and exc_info[1].winerror == winerror.ERROR_PATH_NOT_FOUND: 

193 app_log.error('Cannot delete %s', path) 

194 from win32com.shell import shell, shellcon 

195 options = shellcon.FOF_NOCONFIRMATION | shellcon.FOF_NOERRORUI 

196 code, err = shell.SHFileOperation((0, shellcon.FO_DELETE, path, None, options)) 

197 if code == 0: 

198 raise TryAgainError() 

199 raise exc_info[1] 

200 

201 

202def safe_rmtree(target): 

203 ''' 

204 A replacement for shutil.rmtree that removes directories within $GRAMEXDATA. 

205 It tries to remove the target multiple times, recovering from errors. 

206 ''' 

207 if not os.path.exists(target): 

208 return True 

209 # TODO: check case insensitive in Windows, but case sensitive on other OS 

210 elif target.lower().startswith(variables['GRAMEXDATA'].lower()): 

211 # Try multiple times to recover from errors, since we have no way of 

212 # auto-resuming rmtree: https://bugs.python.org/issue8523 

213 for count in range(100): 213 ↛ 220line 213 didn't jump to line 220, because the loop on line 213 didn't complete

214 try: 

215 shutil.rmtree(target, onerror=_ensure_remove) 

216 except TryAgainError: 

217 pass 

218 else: 

219 break 

220 return True 

221 else: 

222 app_log.warning('Not removing directory %s (outside $GRAMEXDATA)', target) 

223 return False 

224 

225 

226def zip_prefix_filter(members, prefix): 

227 ''' 

228 Return only ZIP file members starting with the directory prefix, with the 

229 prefix stripped out. 

230 ''' 

231 if not prefix: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 return members 

233 offset = len(prefix) 

234 result = [] 

235 for zipinfo in members: 

236 if zipinfo.filename.startswith(prefix): 236 ↛ 235line 236 didn't jump to line 235, because the condition on line 236 was never false

237 zipinfo.filename = zipinfo.filename[offset:] 

238 if len(zipinfo.filename) > 0: 

239 result.append(zipinfo) 

240 return result 

241 

242 

243def run_install(config): 

244 ''' 

245 Download config.url into config.target. 

246 If config.url is a directory, copy it. 

247 If config.url is a file or a URL (http, https, ftp), unzip it. 

248 If config.contentdir is True, skip parent folders with single subfolder. 

249 If no files match, log a warning. 

250 ''' 

251 url, target = config.url, config.target 

252 

253 # If the URL is a directory, copy it 

254 if os.path.isdir(url): 

255 if os.path.exists(target): 

256 url = os.path.abspath(url).lower().rstrip(os.sep) 

257 target = os.path.abspath(target).lower().rstrip(os.sep) 

258 if url != target: 258 ↛ 261line 258 didn't jump to line 261, because the condition on line 258 was never false

259 if not safe_rmtree(target): 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true

260 return 

261 if url != target: 261 ↛ 264line 261 didn't jump to line 264, because the condition on line 261 was never false

262 shutil.copytree(url, target) 

263 app_log.info('Copied %s into %s', url, target) 

264 config.url = url 

265 return 

266 

267 # If it's a file, unzip it 

268 if os.path.exists(url): 

269 handle = url 

270 else: 

271 # Otherwise, assume that it's a URL containing a ZIP file 

272 app_log.info('Downloading: %s', url) 

273 response = requests.get(url) 

274 response.raise_for_status() 

275 handle = six.BytesIO(response.content) 

276 

277 # Identify relevant files from the ZIP file 

278 zipfile = ZipFile(handle) 

279 files = zipfile.infolist() 

280 if config.get('contentdir', True): 

281 prefix = os.path.commonprefix(zipfile.namelist()) 

282 if prefix.endswith('/'): 

283 files = zip_prefix_filter(files, prefix) 

284 

285 # Extract relevant files from ZIP file 

286 if safe_rmtree(target): 286 ↛ exitline 286 didn't return from function 'run_install', because the condition on line 286 was never false

287 zipfile.extractall(target, files) 

288 app_log.info('Extracted %d files into %s', len(files), target) 

289 

290 

291def run_command(config): 

292 ''' 

293 Run config.cmd. If the command has a TARGET, replace it with config.target. 

294 Else append config.target as an argument. 

295 ''' 

296 appcmd = config.cmd 

297 # Split the command into an array of words 

298 if isinstance(appcmd, six.string_types): 298 ↛ 301line 298 didn't jump to line 301, because the condition on line 298 was never false

299 appcmd = shlex.split(appcmd) 

300 # If the app is a Cygwin app, TARGET should be a Cygwin path too. 

301 target = config.target 

302 cygcheck, cygpath, kwargs = which('cygcheck'), which('cygpath'), {'universal_newlines': True} 

303 if cygcheck is not None and cygpath is not None: 303 ↛ 309line 303 didn't jump to line 309, because the condition on line 303 was never false

304 app_path = check_output([cygpath, '-au', which(appcmd[0])], **kwargs).strip() # nosec 

305 is_cygwin_app = check_output([cygcheck, '-f', app_path], **kwargs).strip() # nosec 

306 if is_cygwin_app: 306 ↛ 309line 306 didn't jump to line 309, because the condition on line 306 was never false

307 target = check_output([cygpath, '-au', target], **kwargs).strip() # n osec 

308 # Replace TARGET with the actual target 

309 if 'TARGET' in appcmd: 

310 appcmd = [target if arg == 'TARGET' else arg for arg in appcmd] 

311 else: 

312 appcmd.append(target) 

313 app_log.info('Running %s', ' '.join(appcmd)) 

314 if not safe_rmtree(config.target): 314 ↛ 315line 314 didn't jump to line 315, because the condition on line 314 was never true

315 app_log.error('Cannot delete target %s. Aborting installation', config.target) 

316 return 

317 proc = Popen(appcmd, bufsize=-1, stdout=sys.stdout, stderr=sys.stderr, **kwargs) # nosec 

318 proc.communicate() 

319 return proc.returncode 

320 

321 

322# Setup file configurations. 

323# Structure: {File: {exe: cmd}} 

324# If File exists, then if exe exists, run cmd. 

325# For example, if package.json exists: 

326# then if yarn exists, run yarn install 

327# else if npm exists, run npm install 

328setup_paths = ''' 

329Makefile: 

330 make: '"{EXE}"' 

331setup.ps1: 

332 powershell: '"{EXE}" -File "{FILE}"' 

333setup.sh: 

334 bash: '"{EXE}" "{FILE}"' 

335requirements.txt: 

336 pip: '"{EXE}" install -r "{FILE}"' 

337setup.py: 

338 python: '"{EXE}" "{FILE}"' 

339package.json: 

340 yarn: '"{EXE}" install --prefer-offline' 

341 npm: '"{EXE}" install' 

342bower.json: 

343 bower: '"{EXE}" --allow-root install' 

344''' 

345setup_paths = yaml.load(setup_paths, Loader=AttrDictYAMLLoader) # nosec 

346 

347 

348def run_setup(target): 

349 ''' 

350 Install any setup file in target directory. Target directory can be: 

351 

352 - An absolute path 

353 - A relative path to current directory 

354 - A relative path to the Gramex apps/ folder 

355 

356 Returns the absolute path of the final target path. 

357 

358 This supports: 

359 

360 - ``make`` (if Makefile exists) 

361 - ``powershell -File setup.ps1`` 

362 - ``bash setup.sh`` 

363 - ``pip install -r requirements.txt`` 

364 - ``python setup.py`` 

365 - ``yarn install`` else ``npm install`` 

366 - ``bower --allow-root install`` 

367 ''' 

368 if not os.path.exists(target): 368 ↛ 369line 368 didn't jump to line 369, because the condition on line 368 was never true

369 app_target = os.path.join(variables['GRAMEXPATH'], 'apps', target) 

370 if not os.path.exists(app_target): 

371 raise OSError('No directory %s' % target) 

372 target = app_target 

373 target = os.path.abspath(target) 

374 app_log.info('Setting up %s', target) 

375 for file, runners in setup_paths.items(): 

376 setup_file = os.path.join(target, file) 

377 if not os.path.exists(setup_file): 

378 continue 

379 for exe, cmd in runners.items(): 379 ↛ 387line 379 didn't jump to line 387, because the loop on line 379 didn't complete

380 exe_path = which(exe) 

381 if exe_path is not None: 381 ↛ 379line 381 didn't jump to line 379, because the condition on line 381 was never false

382 cmd = cmd.format(FILE=setup_file, EXE=exe_path) 

383 app_log.info('Running %s', cmd) 

384 _run_console(cmd, cwd=target) 

385 break 

386 else: 

387 app_log.warning('Skipping %s. No %s found', setup_file, exe) 

388 

389 

390app_dir = Path(variables.get('GRAMEXDATA')) / 'apps' 

391if not app_dir.exists(): 391 ↛ 392line 391 didn't jump to line 392, because the condition on line 391 was never true

392 app_dir.mkdir(parents=True) 

393 

394# Get app configuration by chaining apps.yaml in gramex + app_dir + command line 

395apps_config = ChainConfig() 

396apps_config['base'] = PathConfig(gramex.paths['source'] / 'apps.yaml') 

397user_conf_file = app_dir / 'apps.yaml' 

398apps_config['user'] = PathConfig(user_conf_file) if user_conf_file.exists() else AttrDict() 

399 

400app_keys = { 

401 'url': 'URL / filename of a ZIP file to install', 

402 'cmd': 'Command used to install file', 

403 'dir': 'Sub-directory under "url" to run from (optional)', 

404 'contentdir': 'Strip root directory with a single child (optional, default=True)', 

405 'target': 'Local directory where the app is installed', 

406 'installed': 'Additional installation information about the app', 

407 'run': 'Runtime keyword arguments for the app', 

408} 

409 

410 

411def save_user_config(appname, value): 

412 user_config = AttrDict() 

413 if user_conf_file.exists(): 413 ↛ 416line 413 didn't jump to line 416, because the condition on line 413 was never false

414 with user_conf_file.open(encoding='utf-8') as handle: 

415 user_config = yaml.safe_load(handle) 

416 if value is None: 

417 if appname in user_config: 417 ↛ 423line 417 didn't jump to line 423, because the condition on line 417 was never false

418 del user_config[appname] 

419 else: 

420 app_config = user_config.setdefault(appname, AttrDict()) 

421 app_config.update({key: value[key] for key in app_keys if key in value}) 

422 

423 with user_conf_file.open(mode='w', encoding='utf-8') as handle: 

424 yaml.safe_dump(user_config, handle, indent=4, default_flow_style=False) 

425 

426 

427def get_app_config(appname, args): 

428 ''' 

429 Get the stored configuration for appname, and override it with args. 

430 ``.target`` defaults to $GRAMEXDATA/apps/<appname>. 

431 ''' 

432 apps_config['cmd'] = {appname: args} 

433 app_config = AttrDict((+apps_config).get(appname, {})) 

434 app_config.setdefault('target', str(app_dir / app_config.get('target', appname))) 

435 app_config.target = os.path.abspath(app_config.target) 

436 return app_config 

437 

438 

439def flatten_config(config, base=None): 

440 'Get flattened configurations' 

441 for key, value in config.items(): 

442 keystr = key if base is None else base + '.' + key 

443 if hasattr(value, 'items'): 443 ↛ 444line 443 didn't jump to line 444, because the condition on line 443 was never true

444 for sub in flatten_config(value, keystr): 

445 yield sub 

446 else: 

447 yield keystr, value 

448 

449 

450def show_usage(command): 

451 apps = (+apps_config).keys() 

452 return 'gramex {command}\n\n{desc}'.format( 

453 command=command, 

454 desc=usage[command].strip().format( 

455 apps='\n'.join('- ' + app for app in sorted(apps)) 

456 )) 

457 

458 

459def install(cmd, args): 

460 if len(cmd) < 1: 460 ↛ 461line 460 didn't jump to line 461, because the condition on line 460 was never true

461 app_log.error(show_usage('install')) 

462 return 

463 

464 appname = cmd[0] 

465 app_log.info('Installing: %s', appname) 

466 app_config = get_app_config(appname, args) 

467 if len(cmd) == 2: 

468 app_config.url = cmd[1] 

469 run_install(app_config) 

470 elif 'url' in app_config: 

471 run_install(app_config) 

472 elif 'cmd' in app_config: 472 ↛ 478line 472 didn't jump to line 478, because the condition on line 472 was never false

473 returncode = run_command(app_config) 

474 if returncode != 0: 474 ↛ 475line 474 didn't jump to line 475, because the condition on line 474 was never true

475 app_log.error('Command failed with return code %d. Aborting installation', returncode) 

476 return 

477 else: 

478 app_log.error('Use --url=... or --cmd=... to specific source of %s', appname) 

479 return 

480 

481 # Post-installation 

482 app_config.target = run_setup(app_config.target) 

483 app_config['installed'] = {'time': datetime.datetime.utcnow()} 

484 save_user_config(appname, app_config) 

485 app_log.info('Installed. Run `gramex run %s`', appname) 

486 

487 

488def setup(cmd, args): 

489 for target in cmd: 

490 run_setup(target) 

491 return 

492 if 'all' in args: 

493 root = os.path.join(variables['GRAMEXPATH'], 'apps') 

494 for filename in os.listdir(root): 

495 target = os.path.join(root, filename) 

496 # Only run setup on directories. Ignore __pycache__, etc 

497 if os.path.isdir(target) and not filename.startswith('_'): 

498 run_setup(target) 

499 return 

500 app_log.error(show_usage('setup')) 

501 

502 

503def uninstall(cmd, args): 

504 if len(cmd) < 1: 504 ↛ 505line 504 didn't jump to line 505, because the condition on line 504 was never true

505 app_log.error(show_usage('uninstall')) 

506 return 

507 if len(cmd) > 1 and args: 507 ↛ 508line 507 didn't jump to line 508, because the condition on line 507 was never true

508 app_log.error('Arguments allowed only with single app. Ignoring %s', ', '.join(cmd[1:])) 

509 cmd = cmd[:1] 

510 

511 for appname in cmd: 

512 app_log.info('Uninstalling: %s', appname) 

513 

514 # Delete the target directory if it exists 

515 app_config = get_app_config(appname, args) 

516 if os.path.exists(app_config.target): 516 ↛ 519line 516 didn't jump to line 519, because the condition on line 516 was never false

517 safe_rmtree(app_config.target) 

518 else: 

519 app_log.error('No directory %s to remove', app_config.target) 

520 save_user_config(appname, None) 

521 

522 

523def run(cmd, args): 

524 if len(cmd) < 1: 524 ↛ 525line 524 didn't jump to line 525, because the condition on line 524 was never true

525 app_log.error(show_usage('run')) 

526 return 

527 if len(cmd) > 1: 527 ↛ 528line 527 didn't jump to line 528, because the condition on line 527 was never true

528 app_log.error('Can only run one app. Ignoring %s', ', '.join(cmd[1:])) 

529 

530 appname = cmd.pop(0) 

531 app_config = get_app_config(appname, args) 

532 

533 target = app_config.target 

534 if 'dir' in app_config: 

535 target = os.path.join(target, app_config.dir) 

536 if os.path.isdir(target): 536 ↛ 550line 536 didn't jump to line 550, because the condition on line 536 was never false

537 os.chdir(target) 

538 gramex.paths['base'] = Path('.') 

539 # If we run with updated parameters, save for next run under the .run config 

540 run_config = app_config.setdefault('run', {}) 

541 for key, val in args.items(): 

542 if key not in app_keys: 

543 run_config[key] = app_config.pop(key) 

544 save_user_config(appname, app_config) 

545 # Tell the user what configs are used 

546 cline = ' '.join('--%s=%s' % arg for arg in flatten_config(app_config.get('run', {}))) 

547 app_log.info('Gramex %s | %s %s | %s | Python %s', gramex.__version__, appname, cline, 

548 os.getcwd(), sys.version.replace('\n', ' ')) 

549 gramex.init(cmd=AttrDict(app=app_config['run'])) 

550 elif appname in apps_config['user']: 

551 # The user configuration has a wrong path. Inform user 

552 app_log.error('%s: no directory %s', appname, app_config.target) 

553 app_log.error('Run "gramex uninstall %s" and try again.', appname) 

554 else: 

555 app_log.error('%s: no directory %s', appname, app_config.target) 

556 

557 

558def service(cmd, args): 

559 try: 

560 import gramex.winservice 

561 except ImportError: 

562 app_log.error('Unable to load winservice. Is this Windows?') 

563 raise 

564 if len(cmd) < 1: 

565 app_log.error(show_usage('service')) 

566 return 

567 gramex.winservice.GramexService.setup(cmd, **args) 

568 

569 

570def _check_output(cmd, default=b'', **kwargs): 

571 '''Run cmd and return output. Return default in case the command fails''' 

572 try: 

573 return check_output(shlex.split(cmd), **kwargs).strip() # nosec 

574 # OSError is raised if the cmd is not found. 

575 # CalledProcessError is raised if the cmd returns an error. 

576 except (OSError, CalledProcessError): 

577 return default 

578 

579 

580def _run_console(cmd, **kwargs): 

581 '''Run cmd and pipe output to console (sys.stdout / sys.stderr)''' 

582 cmd = shlex.split(cmd) 

583 try: 

584 proc = Popen(cmd, bufsize=-1, stdout=sys.stdout, stderr=sys.stderr, 

585 universal_newlines=True, **kwargs) 

586 except OSError: 

587 app_log.error('Cannot find command: %s', cmd[0]) 

588 raise 

589 proc.communicate() 

590 

591 

592def _mkdir(path): 

593 '''Create directory tree up to path if path does not exist''' 

594 if not os.path.exists(path): 594 ↛ exitline 594 didn't return from function '_mkdir', because the condition on line 594 was never false

595 os.makedirs(path) 

596 

597 

598def _copy(source, target, template_data=None): 

599 ''' 

600 Copy single directory or file (as binary) from source to target. 

601 Warn if target exists, or source is not file/directory, and exit. 

602 If template_data is specified, treat source as a Tornado template. 

603 ''' 

604 if os.path.exists(target): 604 ↛ 605line 604 didn't jump to line 605, because the condition on line 604 was never true

605 app_log.warning('Skip existing %s', target) 

606 elif os.path.isdir(source): 

607 _mkdir(target) 

608 elif os.path.isfile(source): 608 ↛ 619line 608 didn't jump to line 619, because the condition on line 608 was never false

609 with io.open(source, 'rb') as handle: 

610 result = handle.read() 

611 from mimetypes import guess_type 

612 filetype = guess_type(source)[0] 

613 basetype = 'text' if filetype is None else filetype.split('/')[0] 

614 if template_data is not None and basetype in {'text', 'application'}: 

615 result = Template(result).generate(**template_data) 

616 with io.open(target, 'wb') as handle: 

617 handle.write(result) 

618 else: 

619 app_log.warning('Skip unknown file %s', source) 

620 

621 

622def init(cmd, args): 

623 '''Create Gramex scaffolding files.''' 

624 if len(cmd) > 1: 624 ↛ 625line 624 didn't jump to line 625, because the condition on line 624 was never true

625 app_log.error(show_usage('init')) 

626 return 

627 args.setdefault('target', os.getcwd()) 

628 app_log.info('Initializing Gramex project at %s', args.target) 

629 data = { 

630 'appname': os.path.basename(args.target), 

631 'author': _check_output('git config user.name', default='Author'), 

632 'email': _check_output('git config user.email', default='user@example.org'), 

633 'date': datetime.datetime.today().strftime('%Y-%m-%d'), 

634 'version': gramex.__version__, 

635 } 

636 # Ensure that appname is a valid Python module name 

637 appname = re.sub(r'[^a-z0-9_]+', '_', data['appname'].lower()) 

638 if appname[0] not in string.ascii_lowercase: 638 ↛ 639line 638 didn't jump to line 639, because the condition on line 638 was never true

639 appname = 'app' + appname 

640 

641 # Copy all directories & files (as templates) 

642 source_dir = os.path.join(variables['GRAMEXPATH'], 'apps', 'init') 

643 for root, dirs, files in os.walk(source_dir): 

644 for name in dirs + files: 

645 source = os.path.join(root, name) 

646 relpath = os.path.relpath(root, start=source_dir) 

647 target = os.path.join(args.target, relpath, name.replace('appname', appname)) 

648 _copy(source, target, template_data=data) 

649 for empty_dir in ('img', 'data'): 

650 _mkdir(os.path.join(args.target, 'assets', empty_dir)) 

651 # Copy error files as-is (not as templates) 

652 error_dir = os.path.join(args.target, 'error') 

653 _mkdir(error_dir) 

654 for source in glob(os.path.join(variables['GRAMEXPATH'], 'handlers', '?0?.html')): 

655 target = os.path.join(error_dir, os.path.basename(source)) 

656 _copy(source, target) 

657 

658 # Create a git repo if none exists. 

659 # But if git is not installed, do not stop. Continue with the rest. 

660 if not os.path.exists(os.path.join(args.target, '.git')): 660 ↛ 665line 660 didn't jump to line 665, because the condition on line 660 was never false

661 try: 

662 _run_console('git init') 

663 except OSError: 

664 pass 

665 run_setup(args.target) 

666 

667 

668default_mail_config = r'''# Gramex mail configuration at 

669# List keys with "gramex mail --list --conf={confpath}" 

670 

671# See https://learn.gramener.com/guide/email/ for help 

672email: 

673 default-email: 

674 type: gmail 

675 email: $GRAMEXMAILUSER 

676 password: $GRAMEXMAILPASSWORD 

677 # Uncomment the next line to test the application without sending mails 

678 # stub: log 

679 

680# See https://learn.gramener.com/guide/alert/ 

681alert: 

682 hello-world: 

683 to: admin@example.org 

684 subject: Alert from Gramex 

685 body: | 

686 This is a test email 

687''' 

688 

689 

690def mail(cmd, args): 

691 # Get config file location 

692 default_dir = os.path.join(variables['GRAMEXDATA'], 'mail') 

693 _mkdir(default_dir) 

694 if 'conf' in args: 

695 confpath = args.conf 

696 elif os.path.exists('gramex.yaml'): 

697 confpath = os.path.abspath('gramex.yaml') 

698 else: 

699 confpath = os.path.join(default_dir, 'gramexmail.yaml') 

700 

701 if not os.path.exists(confpath): 

702 if 'init' in args: 

703 with io.open(confpath, 'w', encoding='utf-8') as handle: 

704 handle.write(default_mail_config.format(confpath=confpath)) 

705 app_log.info('Initialized %s', confpath) 

706 elif not cmd and not args: 

707 app_log.error(show_usage('mail')) 

708 else: 

709 app_log.error('Missing config %s. Use --init to generate skeleton', confpath) 

710 return 

711 

712 conf = PathConfig(confpath) 

713 if 'list' in args: 

714 for key, alert in conf.get('alert', {}).items(): 

715 to = alert.get('to', '') 

716 if isinstance(to, list): 

717 to = ', '.join(to) 

718 gramex.console('{:15}\t"{}" to {}'.format(key, alert.get('subject'), to)) 

719 return 

720 

721 if 'init' in args: 

722 app_log.error('Config already exists at %s', confpath) 

723 return 

724 

725 if len(cmd) < 1: 

726 app_log.error(show_usage('mail')) 

727 return 

728 

729 from gramex.services import email as setup_email, create_alert 

730 alert_conf = conf.get('alert', {}) 

731 email_conf = conf.get('email', {}) 

732 setup_email(email_conf) 

733 sys.path += os.path.dirname(confpath) 

734 for key in cmd: 

735 if key not in alert_conf: 

736 app_log.error('Missing key %s in %s', key, confpath) 

737 continue 

738 alert = create_alert(key, alert_conf[key]) 

739 alert() 

740 

741 

742def license(cmd, args): 

743 if len(cmd) == 0: 

744 gramex.console(gramex.license.EULA) 

745 if gramex.license.is_accepted(): 

746 gramex.console('License already ACCEPTED. Run "gramex license reject" to reject') 

747 else: 

748 gramex.console('License NOT YET accepted. Run "gramex license accept" to accept') 

749 elif cmd[0] == 'accept': 

750 gramex.license.accept(force=True) 

751 elif cmd[0] == 'reject': 

752 gramex.license.reject() 

753 else: 

754 app_log.error('Invalid command license %s', cmd[0])