# -*- coding: utf-8 -*- # vi:si:et:sw=4:sts=4:ts=4 from contextlib import contextmanager import inspect import sys import json import datetime import tornado.ioloop import tornado.web import tornado.gen import tornado.concurrent from threading import Thread from functools import wraps import logging logger = logging.getLogger(__name__) def json_response(data=None, status=200, text='ok'): if not data: data = {} return {'status': {'code': status, 'text': text}, 'data': data} def _to_json(python_object): if isinstance(python_object, datetime.datetime): if python_object.year < 1900: tt = python_object.timetuple() return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6]) return python_object.strftime('%Y-%m-%dT%H:%M:%SZ') raise TypeError('%s %s is not JSON serializable' % (repr(python_object), type(python_object))) def json_dumps(obj): indent = 2 return json.dumps(obj, indent=indent, default=_to_json, ensure_ascii=False).encode() def run_async(func): @wraps(func) def async_func(*args, **kwargs): func_hl = Thread(target=func, args=args, kwargs=kwargs) func_hl.start() return func_hl return async_func def trim(docstring): if not docstring: return '' # Convert tabs to spaces (following the normal Python rules) # and split into a list of lines: lines = docstring.expandtabs().splitlines() # Determine minimum indentation (first line doesn't count): indent = sys.maxsize for line in lines[1:]: stripped = line.lstrip() if stripped: indent = min(indent, len(line) - len(stripped)) # Remove indentation (first line is special): trimmed = [lines[0].strip()] if indent < sys.maxsize: for line in lines[1:]: trimmed.append(line[indent:].rstrip()) # Strip off trailing and leading blank lines: while trimmed and not trimmed[-1]: trimmed.pop() while trimmed and not trimmed[0]: trimmed.pop(0) # Return a single string: return '\n'.join(trimmed) @contextmanager def defaultcontext(): yield @run_async def api_task(context, request, callback): import settings if context == None: context = defaultcontext action = request.arguments.get('action', [None])[0].decode('utf-8') data = request.arguments.get('data', [b'{}'])[0] data = json.loads(data.decode('utf-8')) if data else {} if not action: methods = list(actions.keys()) api = [] for f in sorted(methods): api.append({'name': f, 'doc': actions.doc(f).replace('\n', '
\n')}) response = json_response(api) else: if settings.DEBUG_API: logger.debug('API %s %s', action, data) f = actions.get(action) if f: with context(): try: response = f(data) except: logger.debug('FAILED %s %s', action, data, exc_info=True) response = json_response(status=500, text='%s failed' % action) else: response = json_response(status=400, text='Unknown action %s' % action) callback(response) class ApiHandler(tornado.web.RequestHandler): def initialize(self, context=None): self._context = context def get(self): self.write('use POST') @tornado.web.asynchronous @tornado.gen.coroutine def post(self): if 'origin' in self.request.headers and self.request.host not in self.request.headers['origin']: logger.debug('reject cross site attempt to access api %s', self.request) self.set_status(403) self.write('') return response = yield tornado.gen.Task(api_task, self._context, self.request) if not 'status' in response: response = json_response(response) response = json_dumps(response) self.set_header('Content-Type', 'application/json') self.write(response) class ApiActions(dict): properties = {} versions = {} def __init__(self): def api(data): ''' returns list of all known api actions takes { docs: bool } if docs is true, action properties contain docstrings returns { actions: { 'api': { cache: true, doc: 'recursion' }, 'hello': { cache: true, .. } ... } } ''' data = data or {} docs = data.get('docs', False) code = data.get('code', False) _actions = list(self.keys()) _actions.sort() actions = {} for a in _actions: actions[a] = self.properties[a] if docs: actions[a]['doc'] = self.doc(a) if code: actions[a]['code'] = self.code(a) return {'actions': actions} self.register(api) def doc(self, name): f = self[name] return trim(f.__doc__) def code(self, name, version=None): f = self[name] if name != 'api' and hasattr(f, 'func_closure') and f.__closure__: fc = [c for c in f.__closure__ if hasattr(c.cell_contents, '__call__')] f = fc[len(fc)-1].cell_contents info = f.__code__.co_filename info = '%s:%s' % (info, f.__code__.co_firstlineno) return info, trim(inspect.getsource(f)) def register(self, method, action=None, cache=True, version=None): if not action: action = method.__name__ if version: if not version in self.versions: self.versions[version] = {} self.versions[version][action] = method else: self[action] = method self.properties[action] = {'cache': cache} def unregister(self, action): if action in self: del self[action] actions = ApiActions()