Coverage for gramex\__init__.py : 79%

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'''
2Gramex {__version__} Copyright (c) 2017 by Gramener
4Start the Gramex server on port 9988 at the current directory.
5If no gramex.yaml exists, show the guide (https://learn.gramener.com/guide/)
7Options
8 --listen.port=9090 Starts Gramex at port 9090
9 --browser Open the browser after startup
10 --settings.debug Enable serving tracebacks and autoreload
11 --settings.xsrf_cookies=false Disable XSRF cookies (only for testing)
12 --settings.cookie_secret=... Change cookie encryption key
14Helper applications
15 gramex init Add Gramex project scaffolding to current dir
16 gramex service Windows service setup
17 gramex mail Send email from command line
18 gramex license See Gramex license, accept or reject it
20Installation commands. Run without arguments to see help
21 gramex install Install an app
22 gramex update Update an app
23 gramex setup Run make, npm install, bower install etc on app
24 gramex run Run an installed app
25 gramex uninstall Uninstall an app
26'''
28import os
29import sys
30import json
31import yaml
32import logging
33import logging.config
34import tornado.ioloop
35from pathlib import Path
36from copy import deepcopy
37from orderedattrdict import AttrDict
38from gramex.config import ChainConfig, PathConfig, app_log, variables, setup_variables
39from gramex.config import ioloop_running
41paths = AttrDict() # Paths where configurations are stored
42conf = AttrDict() # Final merged configurations
43config_layers = ChainConfig() # Loads all configurations. init() updates it
45paths['source'] = Path(__file__).absolute().parent # Where gramex source code is
46paths['base'] = Path('.') # Where gramex is run from
48callbacks = {} # Services callbacks
50# Populate __version__ from release.json
51with (paths['source'] / 'release.json').open() as _release_file:
52 release = json.load(_release_file, object_pairs_hook=AttrDict)
53 __version__ = release.version
55_sys_path = list(sys.path) # Preserve original sys.path
58# List of URLs to warn about in case of duplicates
59PathConfig.duplicate_warn = [
60 'url.*',
61 'cache.*',
62 'schedule.*',
63 'watch.*',
64 'email.*',
65 'alert.*',
66 'sms.*',
67 'log.loggers.*', 'log.handlers.*', 'log.formatters.*',
68]
71def parse_command_line(commands):
72 '''
73 Parse command line arguments. For example:
75 gramex cmd1 cmd2 --a=1 2 -b x --c --p.q=4
77 returns:
79 {"_": ["cmd1", "cmd2"], "a": [1, 2], "b": "x", "c": True, "p": {"q": [4]}}
81 Values are parsed as YAML. Arguments with '.' are split into subgroups. For
82 example, ``gramex --listen.port 80`` returns ``{"listen": {"port": 80}}``.
83 '''
84 group = '_'
85 args = AttrDict({group: []})
86 for arg in commands:
87 if arg.startswith('-'):
88 group, value = arg.lstrip('-'), 'True'
89 if '=' in group:
90 group, value = group.split('=', 1)
91 else:
92 value = arg
94 value = yaml.safe_load(value)
95 base = args
96 keys = group.split('.')
97 for key in keys[:-1]:
98 base = base.setdefault(key, AttrDict())
100 # Add the key to the base.
101 # If it's already there, make it a list.
102 # If it's already a list, append to it.
103 if keys[-1] not in base or base[keys[-1]] is True:
104 base[keys[-1]] = value
105 elif not isinstance(base[keys[-1]], list):
106 base[keys[-1]] = [base[keys[-1]], value]
107 else:
108 base[keys[-1]].append(value)
110 return args
113def callback_commandline(commands):
114 '''
115 Find what method should be run based on the command line programs. This
116 refactoring allows us to test gramex.commandline() to see if it processes
117 the command line correctly, without actually running the commands.
119 Returns a callback method and kwargs for the callback method.
120 '''
121 # Set logging config at startup. (Services may override this.)
122 log_config = (+PathConfig(paths['source'] / 'gramex.yaml')).get('log', AttrDict())
123 log_config.root.level = logging.INFO
124 from . import services
125 services.log(log_config)
127 # args has all optional command line args as a dict of values / lists.
128 # cmd has all positional arguments as a list.
129 args = parse_command_line(commands)
130 cmd = args.pop('_')
132 # If --help or -V --version is specified, print a message and end
133 if args.get('V') is True or args.get('version') is True:
134 return console, {'msg': 'Gramex %s' % __version__}
135 if args.get('help') is True:
136 return console, {'msg': __doc__.strip().format(**globals())}
138 # Any positional argument is treated as a gramex command
139 if len(cmd) > 0:
140 kwargs = {'cmd': cmd, 'args': args}
141 base_command = cmd.pop(0).lower()
142 method = 'install' if base_command == 'update' else base_command
143 if method in {
144 'install', 'uninstall', 'setup', 'run', 'service', 'init',
145 'mail', 'license',
146 }:
147 import gramex.install
148 return getattr(gramex.install, method), kwargs
149 raise NotImplementedError('Unknown gramex command: %s' % base_command)
151 # Use current dir as base (where gramex is run from) if there's a gramex.yaml.
152 if not os.path.isfile('gramex.yaml'):
153 return console, {'msg': 'No gramex.yaml. See https://learn.gramener.com/guide/'}
155 app_log.info('Gramex %s | %s | Python %s', __version__, os.getcwd(),
156 sys.version.replace('\n', ' '))
157 return init, {'cmd': AttrDict(app=args)}
160def commandline(args=None):
161 '''
162 Run Gramex from the command line. Called via:
164 - setup.py console_scripts when running gramex
165 - __main__.py when running python -m gramex
166 '''
167 callback, kwargs = callback_commandline(sys.argv[1:] if args is None else args)
168 callback(**kwargs)
171def gramex_update(url):
172 '''If a newer version of gramex is available, logs a warning'''
173 import time
174 import requests
175 import platform
176 from . import services
178 if not services.info.eventlog: 178 ↛ 179line 178 didn't jump to line 179, because the condition on line 178 was never true
179 return app_log.error('eventlog: service is not running. So Gramex update is disabled')
181 query = services.info.eventlog.query
182 update = query('SELECT * FROM events WHERE event="update" ORDER BY time DESC LIMIT 1')
183 delay = 24 * 60 * 60 # Wait for one day before updates
184 if update and time.time() < update[0]['time'] + delay:
185 return app_log.debug('Gramex update ran recently. Deferring check.')
187 meta = {
188 'dir': variables.get('GRAMEXDATA'),
189 'uname': platform.uname(),
190 }
191 if update:
192 events = query('SELECT * FROM events WHERE time > ? ORDER BY time',
193 (update[0]['time'], ))
194 else:
195 events = query('SELECT * FROM events')
196 logs = [dict(log, **meta) for log in events]
198 r = requests.post(url, data=json.dumps(logs))
199 r.raise_for_status()
200 update = r.json()
201 version = update['version']
202 if version > __version__: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true
203 app_log.error('Gramex %s is available. See https://learn.gramener.com/guide/', version)
204 elif version < __version__:
205 app_log.warning('Gramex update: your version %s is ahead of the stable %s',
206 __version__, version)
207 else:
208 app_log.debug('Gramex version %s is up to date', __version__)
209 services.info.eventlog.add('update', update)
210 return {'logs': logs, 'response': update}
213def console(msg):
214 '''Write message to console'''
215 print(msg) # noqa
218def init(force_reload=False, **kwargs):
219 '''
220 Update Gramex configurations and start / restart the instance.
222 ``gramex.init()`` can be called any time to refresh configuration files.
223 ``gramex.init(key=val)`` adds ``val`` as a configuration layer named
224 ``key``. If ``val`` is a Path, it is converted into a PathConfig. (If it is
225 Path directory, use ``gramex.yaml``.)
227 Services are re-initialised if their configurations have changed. Service
228 callbacks are always re-run (even if the configuration hasn't changed.)
229 '''
230 # Reset variables
231 variables.clear()
232 variables.update(setup_variables())
234 # Initialise configuration layers with provided configurations
235 # AttrDicts are updated as-is. Paths are converted to PathConfig
236 paths.update(kwargs)
237 for key, val in paths.items():
238 if isinstance(val, Path):
239 if val.is_dir(): 239 ↛ 241line 239 didn't jump to line 241, because the condition on line 239 was never false
240 val = val / 'gramex.yaml'
241 val = PathConfig(val)
242 config_layers[key] = val
244 # Locate all config files
245 config_files = set()
246 for path_config in config_layers.values():
247 if hasattr(path_config, '__info__'):
248 for pathinfo in path_config.__info__.imports:
249 config_files.add(pathinfo.path)
250 config_files = list(config_files)
252 # Add config file folders to sys.path
253 sys.path[:] = _sys_path + [str(path.absolute().parent) for path in config_files]
255 from . import services
256 globals()['service'] = services.info # gramex.service = gramex.services.info
258 # Override final configurations
259 final_config = +config_layers
260 # --settings.debug => log.root.level = True
261 if final_config.app.get('settings', {}).get('debug', False): 261 ↛ 262line 261 didn't jump to line 262, because the condition on line 261 was never true
262 final_config.log.root.level = logging.DEBUG
264 # Set up a watch on config files (including imported files)
265 if final_config.app.get('watch', True): 265 ↛ 271line 265 didn't jump to line 271, because the condition on line 265 was never false
266 from services import watcher
267 watcher.watch('gramex-reconfig', paths=config_files, on_modified=lambda event: init()) 267 ↛ exitline 267 didn't run the lambda on line 267
269 # Run all valid services. (The "+" before config_chain merges the chain)
270 # Services may return callbacks to be run at the end
271 for key, val in final_config.items():
272 if key not in conf or conf[key] != val or force_reload:
273 if hasattr(services, key):
274 app_log.debug('Loading service: %s', key)
275 conf[key] = deepcopy(val)
276 callback = getattr(services, key)(conf[key])
277 if callable(callback):
278 callbacks[key] = callback
279 else:
280 app_log.error('No service named %s', key)
282 # Run the callbacks. Specifically, the app service starts the Tornado ioloop
283 for key in (+config_layers).keys():
284 if key in callbacks:
285 app_log.debug('Running callback: %s', key)
286 callbacks[key]()
289def shutdown():
290 '''Shut down this instance'''
291 ioloop = tornado.ioloop.IOLoop.current()
292 if ioloop_running(ioloop): 292 ↛ exitline 292 didn't return from function 'shutdown', because the condition on line 292 was never false
293 app_log.info('Shutting down Gramex...')
294 ioloop.stop()