def main(): #### become cooperative import gevent.monkey gevent.monkey.patch_all() #### import from apiphant.paths import init_paths from gevent import sleep import logging, sys, time from traceback import format_exc #### command-line config usage = 'Usage: apiphant-background path/to/myproduct' try: _, product_path = sys.argv except ValueError: exit(usage) #### paths api_path, product_name = init_paths(product_path) #### logging logging.basicConfig(level=logging.DEBUG, format='%(levelname)s at %(module)s.%(funcName)s:%(lineno)d [%(asctime)s] %(message)s') # To sys.stderr by default. #### import myproduct.api.background to init tasks[] with @seconds product_module_name = '{product_name}.api.background'.format(product_name=product_name) product_module = __import__(product_module_name, globals(), locals()) on_error = getattr(product_module.api.background, 'on_error', None) #### loop logging.info('\nApiphant is scheduling {product_module_name}\n'.format(product_module_name=product_module_name)) while True: #### Select tasks ready to run. now = time.time() ready_tasks = [ task for task in tasks if now - task.last >= task.seconds ] #### Sort to respect desired "seconds" as possible with non-overlapping mode. ready_tasks.sort(key=lambda task: task.seconds) #### Run them one by one, non-overlapping, safe, logging. for task in ready_tasks: task.last = now # Even if it fails, we don't want to retry before the next time it should run. try: task.task() except: error = 'Task {task} failed:\n{traceback}'.format(task=task.name, traceback=format_exc()) logging.error(error) #### on_error if on_error: # E.g. send_email_message(to=email_config['user'], subject='Error', text=error, **email_config) try: on_error(error) except: logging.error('on_error failed:\n{traceback}'.format(traceback=format_exc())) else: logging.info('on_error: OK.') else: logging.info('Task {task}: OK.'.format(task=task.name)) #### Minimal 1-second sleep to not make the loop too tight. sleep(1)
def serve(product_path, host, port): #### become cooperative import gevent.monkey gevent.monkey.patch_all() #### import from adict import adict from apiphant.paths import init_paths from apiphant.validation import ApiError, status_by_code import gevent.wsgi import json, logging, os, re, sys from traceback import print_exc #### paths api_path, product_name = init_paths(product_path) #### routes def routes(): '''Creates map of routes from PATH_INFO to action().''' routes = dict() version_re = re.compile('^v\d+$') py_extension = '.py' for version in os.listdir(api_path): version_path = os.path.join(api_path, version) if not os.path.isdir(version_path) or not version_re.match(version): continue for target_file_name in os.listdir(version_path): if not target_file_name.endswith(py_extension): continue target_name = target_file_name[:-len(py_extension)] # TODO: Add support for nested t/a/r/g/e/t-s. target_module = __import__( '{product_name}.api.{version}.{target_name}'.format( product_name=product_name, version=version, target_name=target_name, ), globals(), locals(), ) for action_name in 'create', 'read', 'update', 'delete': action = getattr(getattr(getattr(target_module.api, version), target_name), action_name, None) if not action: continue routes['/api/{version}/{target_name}/{action_name}'.format(**locals())] = action return routes routes = routes() #### app def app(environ, start_response): status = response = raw_response = '' try: action = routes.get(environ['PATH_INFO']) if not action: raise ApiError(404) raw_response = hasattr(action, 'raw_response') #### raw environ if hasattr(action, 'raw_environ'): if raw_response: response = action(environ, start_response) else: response = action(environ) #### normal request else: if environ['REQUEST_METHOD'] != 'POST': # See README.md. raise ApiError(501) request = environ['wsgi.input'].read() try: request = json.loads(request) if request else {} if not isinstance(request, dict): raise ValueError except ValueError: raise ApiError(400, 'Request content should be JSON Object') # See README.md. request = adict(request) if raw_response: response = action(request, start_response) else: response = action(request) #### normal response if not raw_response: if response is None: response = {} if not isinstance(response, dict): raise Exception('Response content should be JSON Object') # See README.md. status = status_by_code[200] #### Expected error, no logging. except ApiError as e: status = status_by_code[e.status_code] response = dict(error=(e.error or status)) #### Unexpected error, traceback is logged. except: print_exc() # To sys.stderr. status = status_by_code[500] response = dict(error=status) # Server error details are saved to log and not disclosed to a client. #### Response. finally: #### raw response if raw_response: return response #### normal response start_response(status, headers=[ ('Content-Type', 'application/json'), ]) return [ json.dumps(response), ] #### logging logging.basicConfig(level=logging.DEBUG, format='%(levelname)s at %(module)s.%(funcName)s:%(lineno)d [%(asctime)s] %(message)s') # To sys.stderr by default. logging.info('\nApiphant is serving {api_path} at http://{host}:{port}/api\n'.format(**locals())) #### gevent.serve gevent.wsgi.WSGIServer((host, port), app).serve_forever()