#!/usr/bin/python3 # Copyright (C) 2018 Lars Wirzenius # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import copy import json import logging import os import sys import uuid import bottle import requests class MuckError(Exception): pass class MuckAPI: _copy_headers = [ 'Authorization', 'Muck-Id', 'Muck-Revision', 'Muck-User', ] def __init__(self, url): self._url = url def _get_headers(self): h = {} for name in self._copy_headers: value = bottle.request.get_header(name) if value is not None: h[name] = value logging.debug('Copy header from request: %s: %s', name, value) return h def url(self, path): return '{}{}'.format(self._url, path) def status(self): url = self.url('/status') r = requests.get(url) return r.json() def show(self, rid): url = self.url('/res') headers = self._get_headers() r = requests.get(url, headers=headers) logging.info('Show result: %s %s', r.status_code, r.text) if not r.ok: raise MuckError('{} {}'.format(r.status_code, r.text)) return r.json() def create(self, member): url = self.url('/res') headers = self._get_headers() headers['Content-Type'] = 'application/json' r = requests.post(url, headers=headers, data=json.dumps(member)) logging.info('Show result: %s %s', r.status_code, r.text) if not r.ok: raise MuckError('{} {}'.format(r.status_code, r.text)) return r.json() def search(self, cond): url = self.url('/search') headers = self._get_headers() headers['Content-Type'] = 'application/json' r = requests.get(url, data=json.dumps(cond), headers=headers) logging.info('Search result: %s %s', r.status_code, r.text) if not r.ok: raise MuckError('{} {}'.format(r.status_code, r.text)) return r.json() class API: def __init__(self, bottleapp, config): self._add_routes(bottleapp) self._muck = MuckAPI(config['muck-url']) def _add_routes(self, bottleapp): routes = [ { 'method': 'GET', 'path': '/status', 'callback': self._call(self._show_status), }, { 'method': 'GET', 'path': '/mem', 'callback': self._call(self._show_member), }, { 'method': 'GET', 'path': '/search', 'callback': self._call(self._search), }, { 'method': 'POST', 'path': '/mem', 'callback': self._call(self._create), }, ] for route in routes: bottleapp.route(**route) def _call(self, callback): def helper(): r = bottle.request logging.info('Request: path=%s', r.path) logging.info('Request: content type: %s', r.content_type) for h in r.headers: logging.info('Request: headers: %s: %s', h, r.get_header(h)) logging.info('Request: body: %r', r.body.read()) return callback() return helper def _get_token(self): r = bottle.request authz = r.get_header('Authorization') if not authz: return None w = authz.split() if len(w) != 2: return None if w[0].lower() != 'bearer': return None return w[1] def _show_status(self): status = self._muck.status() return response(200, status) def _show_member(self): rid = bottle.request.get_header('Muck-Id') if rid is None: return response(400) try: res = self._muck.show(rid) return response(200, res) except MuckError as e: return response(404, str(e)) def _search(self): cond = bottle.request.json result = self._muck.search(cond) return response(200, result) def _create(self): r = bottle.request if r.content_type != 'application/json': return response(400) obj = bottle.request.json logging.info('CREATE %r', repr(obj)) newobj = self._muck.create(obj) return response(201, newobj) def response(status, body): headers = {} if isinstance(body, dict): headers['Content-Type'] = 'application/json' return bottle.HTTPResponse( status=status, body=json.dumps(body), headers=headers) with open(sys.argv[1]) as f: config = json.load(f) logging.basicConfig( filename=config['log'], level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s') logging.info('Effi API starts') if config.get('pid'): pid = os.getpid() with open(config['pid'], 'w') as f: f.write(str(pid)) app = bottle.default_app() api = API(app, config) app.run(host='127.0.0.1', port=8080)