encadre

Encadre, petit framework web Python (Flask) : contrôleurs en greffons, validation, tests
git clone https://git.schurger.org/encadre.git
Log | Files | Refs

commit a9d80d185f5de3686b450e9a3e4d466e55b68f0b
parent 19716fbc6735b0183ae7c8dfc4400bb8c65f7e03
Author: Jean Schurger <jean@schurger.org>
Date:   Thu, 20 Feb 2025 11:31:34 -0500

Renamed.

Diffstat:
MREADME.org | 74+++++++++++++++++++++++++++++++++++++-------------------------------------
Dcadre/__init__.py | 80-------------------------------------------------------------------------------
Dcadre/controllers.py | 135-------------------------------------------------------------------------------
Dcadre/framework.py | 186-------------------------------------------------------------------------------
Dcadre_flask/__init__.py | 168-------------------------------------------------------------------------------
Dcadre_flask/test.py | 25-------------------------
Dcadre_test_app/__init__.py | 25-------------------------
Dcadre_test_controllers/__init__.py | 191-------------------------------------------------------------------------------
Mconfig.ini | 14+++++++-------
Aencadre/__init__.py | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencadre/controllers.py | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencadre/framework.py | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencadre_flask/__init__.py | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aencadre_flask/test.py | 25+++++++++++++++++++++++++
Rcadre_test_app.wsgi -> encadre_test_app.wsgi | 0
Aencadre_test_app/__init__.py | 25+++++++++++++++++++++++++
Aencadre_test_controllers/__init__.py | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Droutes.html | 171-------------------------------------------------------------------------------
Droutes.org | 0
Msetup.py | 18+++++++++---------
Mtest/__init__.py | 6+++---
21 files changed, 866 insertions(+), 1037 deletions(-)

diff --git a/README.org b/README.org @@ -1,6 +1,6 @@ -#+TITLE: This is the 'cadre' framework ! +#+TITLE: This is the 'encadre' framework ! -This is the cadre framework engine. +This is the encadre framework engine. * Introduction @@ -25,54 +25,54 @@ Your app *MUST* be packaged ! (also your controllers). from setuptools import setup console_scripts = """ -cadre-test-app-serve = cadre_test_app:serve -cadre-test-app-dump-routes = cadre_test_app:dump_routes +encadre-test-app-serve = encadre_test_app:serve +encadre-test-app-dump-routes = encadre_test_app:dump_routes """ -setup(name='cadre_test_app', +setup(name='encadre_test_app', version='0.1.0', - description="Cadre_Test_App framework", + description="Encadre_Test_App framework", author="Ubity Awesome Dev Team", author_email='dev@ubity.com', - packages=['cadre_test_app'], - install_requires=["Cadre"], + packages=['encadre_test_app'], + install_requires=["Encadre"], entry_points={'console_scripts': console_scripts, - 'cadre_test_app.controllers': [ - 'hello = cadre_test_app_hello']}, + 'encadre_test_app.controllers': [ + 'hello = encadre_test_app_hello']}, license='GPLv3') #+end_example python -*** main (cadre_test_app/__init__.py) +*** main (encadre_test_app/__init__.py) #+begin_example python import sys -from cadre import Cadre +from encadre import Encadre -class Cadre_Test_App(Cadre): +class Encadre_Test_App(Encadre): pass def serve(): if len(sys.argv) != 2: print("Usage: %s config.ini") sys.exit(1) - Cadre_Test_App(sys.argv[1]).serve() + Encadre_Test_App(sys.argv[1]).serve() # tool to dump routes def dump_routes(): if len(sys.argv) != 2: print("Usage: %s config.ini") sys.exit(1) - Cadre_Test_App(sys.argv[1]).dump_routes() + Encadre_Test_App(sys.argv[1]).dump_routes() #+end_example *** config.ini Should contains at least a section telling that we are using /flask/ (as this is the only sub framework implemented. -The section name is you app name (ie: *cadre_test_app*). +The section name is you app name (ie: *encadre_test_app*). #+begin_example -[cadre_test_app] +[encadre_test_app] framework = flask #+end_example @@ -88,20 +88,20 @@ HOST = 0.0.0.0 Optionnaly the *loggers* section. #+begin_example [loggers] -keys = root, cadre, cadre_flask +keys = root, encadre, encadre_flask [logger_root] handlers = -[logger_cadre] +[logger_encadre] level = DEBUG handlers = console -qualname = cadre +qualname = encadre -[logger_cadre_flask] +[logger_encadre_flask] level = DEBUG handlers = console -qualname = cadre_flask +qualname = encadre_flask [handlers] keys = console @@ -116,10 +116,10 @@ class = StreamHandler args = (sys.stderr,) #+end_example -*** a controller (cadre_test_app_hello/__init__.py) +*** a controller (encadre_test_app_hello/__init__.py) #+begin_example python -from cadre import Controller +from encadre import Controller class Hello(Controller): @@ -161,7 +161,7 @@ class Hello_2(Controller): #+begin_example python setup.py develop -cadre_test_app-serve config.ini +encadre_test_app-serve config.ini #+end_example ** Query it @@ -176,7 +176,7 @@ curl http://localhost:5001/hello/ **** Using headers #+begin_example -curl http://localhost:5001/hello/ -H X-cadre_test_app-version:0 +curl http://localhost:5001/hello/ -H X-encadre_test_app-version:0 #+end_example **** Using URL prefix @@ -225,7 +225,7 @@ They are available in requests.args. class Foo(Controller): def bar(self): - from cadre import request + from encadre import request request.args.get('qwe') #+end_example @@ -236,7 +236,7 @@ class Foo(Controller): class Foo(Controller): def bar(self): - from cadre import request + from encadre import request request.headers.get('X-Magic-Header') #+end_example @@ -246,11 +246,11 @@ class Foo(Controller): class Foo(Controller): def bar_needs_cookie(self): - from cadre import request + from encadre import request request.cookies.get('my_great_cookie') def baz_set_cookie(self): - from cadre import request + from encadre import request request.cookies.set('my_great_cookie', 'my_great_value') #+end_example @@ -261,7 +261,7 @@ class Foo(Controller): class Foo(Controller): def bar(self): - from cadre import request + from encadre import request request.json # containt an object, # mostlikely a dict of the received JSON #+end_example @@ -270,7 +270,7 @@ class Foo(Controller): #+begin_example python setup.py develop # if not done already -cadre_test_app-dump-routes +encadre_test_app-dump-routes #+end_example ** Run with UWSGI @@ -285,13 +285,13 @@ Can be added in /config.ini/ #+begin_example [uwsgi] http = localhost:5001 -file = cadre_test_app.wsgi +file = encadre_test_app.wsgi #+end_example -*** loader cadre_test_app.wsgi +*** loader encadre_test_app.wsgi #+begin_example python -from cadre_test_app import Cadre_Test_App -application = Cadre_Test_App("config.ini").get_wsgi_app() +from encadre_test_app import Encadre_Test_App +application = Encadre_Test_App("config.ini").get_wsgi_app() #+end_example *** start it @@ -301,7 +301,7 @@ uwsgi config.ini #+end_example -* Running 'cadre' tests +* Running 'encadre' tests #+begin_example shell pip install nose coverage diff --git a/cadre/__init__.py b/cadre/__init__.py @@ -1,80 +0,0 @@ -import sys -import configparser -import traceback -import logging -import logging.config -from cadre.controllers import Controller, load_controllers -from cadre.framework import Framework, DumpRoutes -from cadre.framework import framework_from_config, validate - -logger = logging.getLogger(__name__) - - -class NotReady: - def __init__(self, diagnostic): - self.diagnostic = diagnostic - - def __getattr__(self, attribute): - raise AttributeError("Cannot access %r: %s" % - (attribute, self.diagnostic)) - - def __getitem__(self, item): - raise AttributeError("Cannot find %r: %s" % (item, self.diagnostic)) - - def __bool__(self): - return False - - -request = cookies = NotReady("Cadre is not setup yet.") - - -class Cadre(): - - config = {} - - def __init__(self, config_filename): - self.application = getattr(self, 'application', - self.__class__.__name__.lower()) - logger.debug("Starting cadre application '%s'." % self.application) - self.config_filename = config_filename - if self.config_filename: - self.config.update(self.read_config(self.config_filename)) - try: - logging.config.fileConfig(self.config_filename) - except Exception: - print("You'd better configure logging in '%s':\n%s" % - (self.config_filename, traceback.format_exc())) - setattr(sys.modules['cadre'], 'config', self.config) - - def read_config(self, filename): - config = {self.__class__.__name__.lower(): {}} - p = configparser.ConfigParser() - p.read(filename) - for n, s in p.items(): - if s: - config[n] = dict(s) - return config - - def serve(self): - assert self.config, "Missing config." - f = framework_from_config(self.__class__.__name__.lower(), self.config) - f.setup_application(self) - load_controllers(f, self.application) - f.serve() - - def dump_routes(self): - assert self.config, "Missing config." - f = DumpRoutes(self.__class__.__name__.lower(), self.config) - load_controllers(f, self.application) - print(f) - - def get_wsgi_app(self): - config = self.read_config(self.config_filename) - assert config, "Missing config." - f = framework_from_config(self.__class__.__name__.lower(), config) - f.setup_application(self) - load_controllers(f, self.application) - return f.get_wsgi_app() - - -__all__ = ['Cadre', 'Framework', 'Controller', 'validate'] diff --git a/cadre/controllers.py b/cadre/controllers.py @@ -1,135 +0,0 @@ -import traceback -import types -import logging -import unittest -import pkg_resources -from cadre.framework import Framework - -logger = logging.getLogger(__name__) - - -class Controller(): - - version = 0 - - @classmethod - def setup(cls, framework, root): - methods = [] - cls.framework = framework - ctrl_name = cls.__name__.lower() - version = ctrl_name.split('_')[-1] - if version.isdigit(): - ctrl_name = ctrl_name[:-(len(version) + 1)] - version = int(version) - else: - version = framework.first_version - - for name in [a for a in dir(cls) if a[0] != '_']: - attr = getattr(cls, name) - if isinstance(attr, types.FunctionType) and \ - not name.startswith('test_'): - verbs = ''.join([c for c in name if c.isupper() or c == '_' - ]).lstrip('_').split('_') - if verbs != ['']: - name = name[:name.find("_%s" % verbs[0])] - if name == 'index__': - if root: - if name == 'index__': - framework.root_id = id(attr) - else: - root_id = getattr(framework, 'root_id', None) - # Prevent inheritance of index__ - if root_id == id(attr): - continue - methods.append( - (name, attr, - attr.__code__.co_varnames[1:attr.__code__.co_argcount], - verbs if verbs != [''] else ['GET'])) - framework.add_controller((cls, '__root__' if root else ctrl_name, - version), methods) - - -def setup_controllers(cls, f, level=0): - if cls.__name__ != 'Controller': # exclude abstract - cls.setup(f, root=level < 2) - for sub in cls.__subclasses__(): - setup_controllers(sub, f, level + 1) - - -def load_controllers(f, application): - for ep in pkg_resources.iter_entry_points('%s.controllers' % application): - try: - ep.load() - except ModuleNotFoundError: - logger.error("Error importing '%s': %s" % ( - ep, traceback.format_exc())) - for ep in pkg_resources.iter_entry_points('cadre.controllers'): - try: - ep.load() - except ModuleNotFoundError: - logger.error("Error importing '%s': %s" % ( - ep, traceback.format_exc())) - setup_controllers(Controller, f) - - -class CadreTestCase(unittest.TestCase): - def setUp(self): - self.framework._json = {} - self.framework._environ = {} - self.framework._headers = {} - self.framework._args = {} - self.framework._cookies = {} - - def tearDown(self): - pass - - -class TestFramework(Framework): - def request_json(self): - return self._json - - def request_environ(self): - - return self._environ - - def request_headers(self): - return self._headers - - def request_args(self): - return self._args - - def get_cookie(self, key): - return self._cookies.get(key) - - def set_cookie(self, key, value): # pylint: disable=W0221 - self._cookies[key] = value - - def send_bytes(self, bytes_like, **kwargs): # pylint: disable=W0613 - return bytes_like.read() - - -def get_controllers_tsts(app): - f = TestFramework(app.application, None) - load_controllers(f, app.application) - - def missing_test(self): - raise Exception("Missing test") - - for c in f.controllers: - for v in f.controllers[c]: - cls = f.controllers[c][v]['cls'] - attributes = {} - for name in [a for a in dir(cls) if a[0] != '_']: - attr = getattr(cls, name) - if not name.startswith('test_'): - attributes[name] = attr - if isinstance(attr, types.FunctionType) and \ - not name.startswith('test_'): - test_name = 'test_%s' % name - test_long_name = 'test_%s_%s' % (cls.__name__, name) - test_fct = getattr(cls, test_name, missing_test) - attributes[test_long_name] = test_fct - test_cls = type("Test%s" % cls.__name__, - (getattr(cls, 'testcase_cls', CadreTestCase), ), - attributes) - yield (test_cls.__name__, test_cls) diff --git a/cadre/framework.py b/cadre/framework.py @@ -1,186 +0,0 @@ -import sys -import logging -import pkg_resources -import traceback -from decorator import decorator - -logger = logging.getLogger(__name__) -ENI = Exception("Not implemented.") - - -class Cookies(): - def __init__(self, framework): - self.f = framework - - def get(self, key): - return self.f.get_cookie(key) - - def set(self, *args, **kwargs): - return self.f.set_cookie(*args, **kwargs) - - -class Request(): - def __init__(self, framework): - self.f = framework - self._json = None - self._environ = None - self._headers = None - self._args = None - - def _get_json(self): - return self._json or self.f.request_json() - - # Let unit tests set some json - def _set_json(self, json): - self._json = json - - json = property(_get_json, _set_json) - - def _get_environ(self): - return self._environ or self.f.request_environ() - - # Let unit tests set environ - def _set_environ(self, environ): - self._environ = environ - - environ = property(_get_environ, _set_environ) - - def _get_headers(self): - return self._headers or self.f.request_headers() - - # Let unit tests set environ - def _set_headers(self, headers): - self._headers = headers - - headers = property(_get_headers, _set_headers) - - def _get_args(self): - return self._args or self.f.request_args() - - # Let unit tests set environ - def _set_args(self, args): - self._args = args - - args = property(_get_args, _set_args) - - -def validate(vargs): - def helper(func, *args, **kwargs): - if hasattr(args[0].framework, 'validate_query'): - args[0].framework.validate_query(vargs) - return func(*args, **kwargs) - return decorator(helper) - - -class Framework(): - - request_class = Request - cookies_class = Cookies - - def __init__(self, application, config): - self.application = application - self.config = config - self.first_version = int((self.config or {}).get(application, {}).get( - 'first_version', '0')) - if self.first_version: - logger.debug("Config set 'first_version' to %s." % - self.first_version) - self.versioning_use_prefix = (self.config or {}).get( - application, {}).get('versioning', 'headers').lower() == 'prefix' - if self.versioning_use_prefix: - logger.debug("Config set versioning to 'prefix' mode.") - self.routes = {} - self.controllers = {} - self.request = self.request_class(self) - setattr(sys.modules['cadre'], 'request', self.request) - self.cookies = self.cookies_class(self) - setattr(sys.modules['cadre'], 'cookies', self.cookies) - - def add_controller(self, ctrl, methods): - ctrl_cls, ctrl_name, ctrl_version = ctrl - self.controllers.setdefault(ctrl_name, {}) - self.controllers[ctrl_name][ctrl_version] = { - 'doc': (ctrl_cls.__doc__ or '').strip(), - 'cls': ctrl_cls, - 'methods': {}} - for method_name, method, args, verbs in methods: - self.controllers[ctrl_name][ctrl_version]['methods'].setdefault( - method_name, {}) - for verb in verbs: - self.controllers[ctrl_name][ctrl_version]['methods'][ - method_name][verb] = { - 'fct': method, - 'doc': (method.__doc__ or '').strip(), - 'args': args} - - def setup_application(self, instance): - self.instance = instance - self.before_call_chain = [] - if hasattr(instance, 'before_call'): - self.before_call_chain.append(self.instance_before_call) - self.after_call_chain = [] - if hasattr(instance, 'after_call'): - self.after_call_chain.append(self.instance_after_call) - if hasattr(instance, 'validate_query'): - self.validate_query = instance.validate_query - - def instance_before_call(self): - getattr(self.instance, 'before_call')() - - def instance_after_call(self, response): - getattr(self.instance, 'after_call')() - return response - - def not_implemented(self): - raise ENI - - request_json = request_environ = request_headers = \ - request_args = get_cookie = set_cookie = send_bytes = not_implemented - - def format_exception(self, e): - ret = None - code = 500 - logger.exception(traceback.format_exc()) - if hasattr(self.instance, "format_exception"): - ret, code = self.instance.format_exception(e) - else: - ha = (self.config or {}).get(self.application, {}).get( - 'on_exception', '').lower() - if ha == 'traceback': - ret = traceback.format_exc() - if ha == 'str': - ret = str(e) - if ret is None: - raise e - return ret, code - - -class DumpRoutes(Framework): - - def __repr__(self): - ret = [] - for c in sorted(self.controllers): - ret.append("* Controller: %s" % (c)) - for v in self.controllers[c]: - ret.append("** Version: %s: %s" % - (v, self.controllers[c][v]['doc'])) - for m in self.controllers[c][v]['methods']: - for _v in self.controllers[c][v]['methods'][m]: - ret.append( - "- [%s] %s%s: %s" % - (_v, m, - self.controllers[c][v]['methods'][m][_v]['args'], - self.controllers[c][v]['methods'][m][_v]['doc'])) - return "\n".join(ret) - - -def framework_from_config(application, config): - fw_name = (config.get(application) or {}).get('framework') - assert fw_name, "No framework configured." - for ep in pkg_resources.iter_entry_points('%s.frameworks' % application): - if ep.name == fw_name: - return ep.load()(application, config) - for ep in pkg_resources.iter_entry_points('cadre.frameworks'): - if ep.name == fw_name: - return ep.load()(application, config) - raise Exception("No framework available") diff --git a/cadre_flask/__init__.py b/cadre_flask/__init__.py @@ -1,168 +0,0 @@ -import sys -import re -import logging -from flask import Flask, request, send_file, abort -from flask_cors import CORS -from flask_jwt_extended import JWTManager -from cadre import Framework - -logger = logging.getLogger(__name__) - - -class FlaskFramework(Framework): - def __init__(self, application, config): - Framework.__init__(self, application, config) - self.versions = {} - self._endpoints = set() - self.application = application - self.flask_app = Flask(application) - self.flask_app.url_map.strict_slashes = False - self.flask_app.register_error_handler(404, self.handle_exception) - for k, v in (self.config.get('flask') or {}).items(): - _v = v - if v.lower() == 'false': - _v = False - if v.lower() == 'true': - _v = True - if v.isdigit(): - _v = int(v) - self.flask_app.config[k.upper()] = _v - if not (self.config.get('flask') or {}).get('external_cors'): - logger.debug("We handle CORS.") - CORS(self.flask_app) - else: - logger.debug("CORS must be handheld upstream.") - if (self.config.get('flask') or {}).get('JWT_SECRET_KEY'.lower()): - self.jwt_manager = JWTManager(self.flask_app) - - def setup_application(self, instance): - Framework.setup_application(self, instance) - for fct in self.before_call_chain: - self.flask_app.before_request(fct) - for fct in self.after_call_chain: - self.flask_app.after_request(fct) - self.flask_app.after_request(self._set_cookies) - - def _add_method(self, ctrl_name, method_name): - def accessor(**args): - import flask - v = None - if self.versioning_use_prefix: - v = flask.request.environ.get('PATH_INFO').lstrip('/').split( - '/')[0] - logger.debug("Got version '%s' from URL." % v) - else: - # flask handle case and underscores - h = 'X-%s-Version' % self.application - v = flask.request.headers.get(h) - logger.debug("Got version '%s' from header '%s'." % (v, h)) - if v and not v.isdigit(): - flask.abort(405) - v = int(v) if v else sys.maxsize - v = v if v in self.controllers[ctrl_name] else max( - self.controllers[ctrl_name].keys()) - verb = flask.request.environ['REQUEST_METHOD'] - cls = self.controllers[ctrl_name][v]['cls'] - try: - m = self.controllers[ctrl_name][v]['methods'][method_name][ - verb]['fct'].__name__ - except KeyError: - logger.error("Cannot found method for: controller: '%s', " - "method: '%s', version: '%s', verb: '%s'" % - (ctrl_name, method_name, v, verb)) - flask.abort(405) - self._cookies = [] - try: - ret = getattr(cls(), m)(**args) - except Exception as e: - ret = self.handle_exception(e) - return ret - - setattr(self, '_m_%s_%s' % (ctrl_name, method_name), accessor) - - def handle_exception(self, e): - message, code = self.format_exception(e) - if not message: - abort(code) - return message, code - - def _mk_endpoint(self, ctrl_name, method_name, args, version): - ep = "/%s/%s%s" % (ctrl_name.lower(), - method_name if method_name != 'index__' else '', - ('/' + '/'.join(['<%s>' % a - for a in args]) if args else '')) - return '/%s%s' % (version, ep) if version else ep - - def _setup_routes(self): - """ Add routes to flask """ - endpoints = {} - methods = {} - for c in sorted(self.controllers): - for v in self.controllers[c]: - for m in self.controllers[c][v]['methods']: - for _v in self.controllers[c][v]['methods'][m]: - fct = self.controllers[c][v]['methods'][m][_v]['fct'] - args = self.controllers[c][v]['methods'][m][_v]['args'] - e = self._mk_endpoint( - c, - m, - args, - version=v if self.versioning_use_prefix else None) - endpoints.setdefault(e, set()) - if isinstance(fct.__defaults__, tuple): - url = re.search(r'^(.*[\\\/])', e).group() - endpoints.setdefault(url, set()) - endpoints[url].add(_v) - methods[url] = (c, m) - endpoints[e].add(_v) - methods[e] = (c, m) - for e, verbs in endpoints.items(): - self._add_method(*methods[e]) - logger.debug("Adding endpoint: '%s' allowing with verbs: '%s'." % - (e, verbs)) - route = e - if route.startswith('/__root__'): - route = e.replace('/__root__', '', 1) - self.flask_app.add_url_rule(route, - route, - getattr(self, '_m_%s_%s' % methods[e]), - methods=verbs) - - def serve(self): - self._setup_routes() - self.flask_app.run( - host=self.flask_app.config.get('HOST', '0.0.0.0'), - port=self.flask_app.config.get('PORT', 5001), - debug=self.flask_app.config.get('DEBUG', True), - threaded=self.flask_app.config.get('THREADED', False), - ) - - def get_wsgi_app(self): - self._setup_routes() - return self.flask_app - - def _set_cookies(self, response): - for args, kwargs in getattr(self, '_cookies', {}): - response.set_cookie(*args, **kwargs) - return response - - def request_json(self): - return request.json - - def request_environ(self): - return request.environ - - def request_headers(self): - return request.headers - - def request_args(self): - return request.args - - def get_cookie(self, key): - return request.cookies.get(key) - - def set_cookie(self, *args, **kwargs): - self._cookies.append((args, kwargs)) - - def send_bytes(self, bytes_like, **kwargs): - return send_file(bytes_like, **kwargs) diff --git a/cadre_flask/test.py b/cadre_flask/test.py @@ -1,25 +0,0 @@ -import unittest -from cadre import Cadre -from cadre.framework import framework_from_config -from cadre.controllers import load_controllers - - -class CadreFlask(Cadre): - - application = 'cadre_test_app' - - def __init__(self): - Cadre.__init__(self, None) - self.framework = framework_from_config(self.application, - {self.application: - {'framework': 'flask'}}) - load_controllers(self.framework, self.application) - - -class TestCadreFlask(unittest.TestCase): - - def setUp(self): - self.cadre = CadreFlask() - - def test_setup_routes(self): - self.cadre.framework._setup_routes() diff --git a/cadre_test_app/__init__.py b/cadre_test_app/__init__.py @@ -1,25 +0,0 @@ -import sys - -import fastjsonschema -from cadre import Cadre - - -class Cadre_Test_App(Cadre): - - def validate_query(self, schema): - from cadre import request - fastjsonschema.compile(schema)(request.json) - - -def serve(): - if len(sys.argv) != 2: - print("Usage: %s config.ini") - sys.exit(1) - Cadre_Test_App(sys.argv[1]).serve() - - -def dump_routes(): - if len(sys.argv) != 2: - print("Usage: %s config.ini") - sys.exit(1) - Cadre_Test_App(sys.argv[1]).dump_routes() diff --git a/cadre_test_controllers/__init__.py b/cadre_test_controllers/__init__.py @@ -1,191 +0,0 @@ -import unittest -import random -from cadre import Controller, validate - - -class Hello(Controller): - """ A very polite controller """ - - def index__(self): - """ Says hello ! """ - return "Hello world" - - def test_index__(self): - self.assertTrue(self.index__() == "Hello world") - - def config(self): - """ Dump the config """ - from cadre import config - return config - - def test_config(self): - from cadre import config - k = 'test_%s' % random.randint(0, 1000) - v = random.randint(0, 1000) - config.update({k: v}) - self.assertTrue(self.config()[k] == v) - - @validate({ - 'title': 'Dummy test', - 'type': 'object', - 'properties': { - 'numbers': { - 'description': "We want an array of numbers", - 'type': 'array', - 'items': { - 'type': 'integer' - }, - 'minItems': 1 - } - }, - 'required': ['numbers'] - }) - def i_am_validated_POST(self): - from cadre import request - return request.json - - def test_i_am_validated_POST(self): - from cadre import request - request.json = {'numbers': [1, 2]} - self.assertTrue(self.i_am_validated_POST() == { - 'numbers': [1, 2]}) - - @validate({ - 'title': 'Dummy test', - 'type': 'object', - 'properties': { - 'numbers': { - 'description': "We want an array of numbers", - 'type': 'array', - 'items': { - 'type': 'integer' - }, - 'minItems': 1 - } - }, - 'required': ['numbers'] - }) - def i_will_not_pass_validation_POST(self): - from cadre import request - return request.json - - def test_i_will_not_pass_validation_POST(self): - from cadre import request - request.json = {'xnumbers': [1, 2]} - self.i_am_validated_POST() - self.assertRaises(Exception) - - -class Hello_2(Controller): - """ A very polite controller (version 2) """ - - def index__(self): - """ Says hello ! """ - return "Hello world (2)" - - def test_index__(self): - self.assertTrue(self.index__() == "Hello world (2)") - - def to(self, who): - """ Says hello to whom ask for """ - return "Hello %s !" % who - - def test_to(self): - assert True - - def foo(self): - return "Foo" - - def test_foo(self): - assert True - - def foo_(self): - return "Foo with an '_' at the end !" - - def test_foo_(self): - assert True - - def bar_GET(self): - return "Bar" - - def test_bar_GET(self): - assert True - - def baz_POST(self): - from cadre import request - return request.json - - def test_baz_POST(self): - from cadre import request - k = 'test_%s' % random.randint(0, 1000) - v = random.randint(0, 1000) - request.json = {k: v} - self.assertTrue(self.baz_POST()[k] == v) - - def qux_GET_POST(self): - return "Qux" - - def test_qux_GET_POST(self): - assert True - - def args(self): - from cadre import request - return request.args.get('qwe', "no 'qwe' arg") - - def test_args(self): - from cadre import request - v = 'test_%s' % random.randint(0, 1000) - request.args['qwe'] = v - self.assertTrue(self.args() == v) - - -class Hello_3(Hello_2): - """ A very polite controller """ - - def index__(self): - """ Says hello ! """ - return "Hello world (3)" - - def test_index__(self): - self.assertTrue(self.index__() == "Hello world (3)") - - def bar_GET(self): - """ Better 'bar' """ - return "Better Bar" - - def test_bar_GET(self): - assert True - - def bar_POST(self): - """ That one handle POST too ! """ - return "Handle POST too." - - def test_bar_POST(self): - assert True - - def send_bytes(self): - return self.framework.send_bytes( - open("/etc/passwd", "rb"), - as_attachment=True, - attachment_filename='etc-passwd.txt', - mimetype='text/plain') - - def test_send_bytes(self): - self.assertTrue(b'root' in self.send_bytes()) - - def dividebyzero(self): - return 1 / 0 - - -class StrangeTestCase(unittest.TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - -class Hello_4(Hello_3): - - testcase_cls = StrangeTestCase diff --git a/config.ini b/config.ini @@ -1,4 +1,4 @@ -[cadre_test_app] +[encadre_test_app] framework = flask # versioning = headers # can be set to prefix # first_version = 0 @@ -17,23 +17,23 @@ HOST = 0.0.0.0 [uwsgi] http = localhost:5001 -file = cadre_test_app.wsgi +file = encadre_test_app.wsgi [loggers] -keys = root, cadre, cadre_flask +keys = root, encadre, encadre_flask [logger_root] handlers = -[logger_cadre] +[logger_encadre] level = DEBUG handlers = console -qualname = cadre +qualname = encadre -[logger_cadre_flask] +[logger_encadre_flask] level = DEBUG handlers = console -qualname = cadre_flask +qualname = encadre_flask [handlers] keys = console diff --git a/encadre/__init__.py b/encadre/__init__.py @@ -0,0 +1,80 @@ +import sys +import configparser +import traceback +import logging +import logging.config +from encadre.controllers import Controller, load_controllers +from encadre.framework import Framework, DumpRoutes +from encadre.framework import framework_from_config, validate + +logger = logging.getLogger(__name__) + + +class NotReady: + def __init__(self, diagnostic): + self.diagnostic = diagnostic + + def __getattr__(self, attribute): + raise AttributeError("Cannot access %r: %s" % + (attribute, self.diagnostic)) + + def __getitem__(self, item): + raise AttributeError("Cannot find %r: %s" % (item, self.diagnostic)) + + def __bool__(self): + return False + + +request = cookies = NotReady("Encadre is not setup yet.") + + +class Encadre(): + + config = {} + + def __init__(self, config_filename): + self.application = getattr(self, 'application', + self.__class__.__name__.lower()) + logger.debug("Starting encadre application '%s'." % self.application) + self.config_filename = config_filename + if self.config_filename: + self.config.update(self.read_config(self.config_filename)) + try: + logging.config.fileConfig(self.config_filename) + except Exception: + print("You'd better configure logging in '%s':\n%s" % + (self.config_filename, traceback.format_exc())) + setattr(sys.modules['encadre'], 'config', self.config) + + def read_config(self, filename): + config = {self.__class__.__name__.lower(): {}} + p = configparser.ConfigParser() + p.read(filename) + for n, s in p.items(): + if s: + config[n] = dict(s) + return config + + def serve(self): + assert self.config, "Missing config." + f = framework_from_config(self.__class__.__name__.lower(), self.config) + f.setup_application(self) + load_controllers(f, self.application) + f.serve() + + def dump_routes(self): + assert self.config, "Missing config." + f = DumpRoutes(self.__class__.__name__.lower(), self.config) + load_controllers(f, self.application) + print(f) + + def get_wsgi_app(self): + config = self.read_config(self.config_filename) + assert config, "Missing config." + f = framework_from_config(self.__class__.__name__.lower(), config) + f.setup_application(self) + load_controllers(f, self.application) + return f.get_wsgi_app() + + +__all__ = ['Encadre', 'Framework', 'Controller', 'validate'] diff --git a/encadre/controllers.py b/encadre/controllers.py @@ -0,0 +1,135 @@ +import traceback +import types +import logging +import unittest +import pkg_resources +from encadre.framework import Framework + +logger = logging.getLogger(__name__) + + +class Controller(): + + version = 0 + + @classmethod + def setup(cls, framework, root): + methods = [] + cls.framework = framework + ctrl_name = cls.__name__.lower() + version = ctrl_name.split('_')[-1] + if version.isdigit(): + ctrl_name = ctrl_name[:-(len(version) + 1)] + version = int(version) + else: + version = framework.first_version + + for name in [a for a in dir(cls) if a[0] != '_']: + attr = getattr(cls, name) + if isinstance(attr, types.FunctionType) and \ + not name.startswith('test_'): + verbs = ''.join([c for c in name if c.isupper() or c == '_' + ]).lstrip('_').split('_') + if verbs != ['']: + name = name[:name.find("_%s" % verbs[0])] + if name == 'index__': + if root: + if name == 'index__': + framework.root_id = id(attr) + else: + root_id = getattr(framework, 'root_id', None) + # Prevent inheritance of index__ + if root_id == id(attr): + continue + methods.append( + (name, attr, + attr.__code__.co_varnames[1:attr.__code__.co_argcount], + verbs if verbs != [''] else ['GET'])) + framework.add_controller((cls, '__root__' if root else ctrl_name, + version), methods) + + +def setup_controllers(cls, f, level=0): + if cls.__name__ != 'Controller': # exclude abstract + cls.setup(f, root=level < 2) + for sub in cls.__subclasses__(): + setup_controllers(sub, f, level + 1) + + +def load_controllers(f, application): + for ep in pkg_resources.iter_entry_points('%s.controllers' % application): + try: + ep.load() + except ModuleNotFoundError: + logger.error("Error importing '%s': %s" % ( + ep, traceback.format_exc())) + for ep in pkg_resources.iter_entry_points('encadre.controllers'): + try: + ep.load() + except ModuleNotFoundError: + logger.error("Error importing '%s': %s" % ( + ep, traceback.format_exc())) + setup_controllers(Controller, f) + + +class EncadreTestCase(unittest.TestCase): + def setUp(self): + self.framework._json = {} + self.framework._environ = {} + self.framework._headers = {} + self.framework._args = {} + self.framework._cookies = {} + + def tearDown(self): + pass + + +class TestFramework(Framework): + def request_json(self): + return self._json + + def request_environ(self): + + return self._environ + + def request_headers(self): + return self._headers + + def request_args(self): + return self._args + + def get_cookie(self, key): + return self._cookies.get(key) + + def set_cookie(self, key, value): # pylint: disable=W0221 + self._cookies[key] = value + + def send_bytes(self, bytes_like, **kwargs): # pylint: disable=W0613 + return bytes_like.read() + + +def get_controllers_tsts(app): + f = TestFramework(app.application, None) + load_controllers(f, app.application) + + def missing_test(self): + raise Exception("Missing test") + + for c in f.controllers: + for v in f.controllers[c]: + cls = f.controllers[c][v]['cls'] + attributes = {} + for name in [a for a in dir(cls) if a[0] != '_']: + attr = getattr(cls, name) + if not name.startswith('test_'): + attributes[name] = attr + if isinstance(attr, types.FunctionType) and \ + not name.startswith('test_'): + test_name = 'test_%s' % name + test_long_name = 'test_%s_%s' % (cls.__name__, name) + test_fct = getattr(cls, test_name, missing_test) + attributes[test_long_name] = test_fct + test_cls = type("Test%s" % cls.__name__, + (getattr(cls, 'testcase_cls', EncadreTestCase), ), + attributes) + yield (test_cls.__name__, test_cls) diff --git a/encadre/framework.py b/encadre/framework.py @@ -0,0 +1,186 @@ +import sys +import logging +import pkg_resources +import traceback +from decorator import decorator + +logger = logging.getLogger(__name__) +ENI = Exception("Not implemented.") + + +class Cookies(): + def __init__(self, framework): + self.f = framework + + def get(self, key): + return self.f.get_cookie(key) + + def set(self, *args, **kwargs): + return self.f.set_cookie(*args, **kwargs) + + +class Request(): + def __init__(self, framework): + self.f = framework + self._json = None + self._environ = None + self._headers = None + self._args = None + + def _get_json(self): + return self._json or self.f.request_json() + + # Let unit tests set some json + def _set_json(self, json): + self._json = json + + json = property(_get_json, _set_json) + + def _get_environ(self): + return self._environ or self.f.request_environ() + + # Let unit tests set environ + def _set_environ(self, environ): + self._environ = environ + + environ = property(_get_environ, _set_environ) + + def _get_headers(self): + return self._headers or self.f.request_headers() + + # Let unit tests set environ + def _set_headers(self, headers): + self._headers = headers + + headers = property(_get_headers, _set_headers) + + def _get_args(self): + return self._args or self.f.request_args() + + # Let unit tests set environ + def _set_args(self, args): + self._args = args + + args = property(_get_args, _set_args) + + +def validate(vargs): + def helper(func, *args, **kwargs): + if hasattr(args[0].framework, 'validate_query'): + args[0].framework.validate_query(vargs) + return func(*args, **kwargs) + return decorator(helper) + + +class Framework(): + + request_class = Request + cookies_class = Cookies + + def __init__(self, application, config): + self.application = application + self.config = config + self.first_version = int((self.config or {}).get(application, {}).get( + 'first_version', '0')) + if self.first_version: + logger.debug("Config set 'first_version' to %s." % + self.first_version) + self.versioning_use_prefix = (self.config or {}).get( + application, {}).get('versioning', 'headers').lower() == 'prefix' + if self.versioning_use_prefix: + logger.debug("Config set versioning to 'prefix' mode.") + self.routes = {} + self.controllers = {} + self.request = self.request_class(self) + setattr(sys.modules['encadre'], 'request', self.request) + self.cookies = self.cookies_class(self) + setattr(sys.modules['encadre'], 'cookies', self.cookies) + + def add_controller(self, ctrl, methods): + ctrl_cls, ctrl_name, ctrl_version = ctrl + self.controllers.setdefault(ctrl_name, {}) + self.controllers[ctrl_name][ctrl_version] = { + 'doc': (ctrl_cls.__doc__ or '').strip(), + 'cls': ctrl_cls, + 'methods': {}} + for method_name, method, args, verbs in methods: + self.controllers[ctrl_name][ctrl_version]['methods'].setdefault( + method_name, {}) + for verb in verbs: + self.controllers[ctrl_name][ctrl_version]['methods'][ + method_name][verb] = { + 'fct': method, + 'doc': (method.__doc__ or '').strip(), + 'args': args} + + def setup_application(self, instance): + self.instance = instance + self.before_call_chain = [] + if hasattr(instance, 'before_call'): + self.before_call_chain.append(self.instance_before_call) + self.after_call_chain = [] + if hasattr(instance, 'after_call'): + self.after_call_chain.append(self.instance_after_call) + if hasattr(instance, 'validate_query'): + self.validate_query = instance.validate_query + + def instance_before_call(self): + getattr(self.instance, 'before_call')() + + def instance_after_call(self, response): + getattr(self.instance, 'after_call')() + return response + + def not_implemented(self): + raise ENI + + request_json = request_environ = request_headers = \ + request_args = get_cookie = set_cookie = send_bytes = not_implemented + + def format_exception(self, e): + ret = None + code = 500 + logger.exception(traceback.format_exc()) + if hasattr(self.instance, "format_exception"): + ret, code = self.instance.format_exception(e) + else: + ha = (self.config or {}).get(self.application, {}).get( + 'on_exception', '').lower() + if ha == 'traceback': + ret = traceback.format_exc() + if ha == 'str': + ret = str(e) + if ret is None: + raise e + return ret, code + + +class DumpRoutes(Framework): + + def __repr__(self): + ret = [] + for c in sorted(self.controllers): + ret.append("* Controller: %s" % (c)) + for v in self.controllers[c]: + ret.append("** Version: %s: %s" % + (v, self.controllers[c][v]['doc'])) + for m in self.controllers[c][v]['methods']: + for _v in self.controllers[c][v]['methods'][m]: + ret.append( + "- [%s] %s%s: %s" % + (_v, m, + self.controllers[c][v]['methods'][m][_v]['args'], + self.controllers[c][v]['methods'][m][_v]['doc'])) + return "\n".join(ret) + + +def framework_from_config(application, config): + fw_name = (config.get(application) or {}).get('framework') + assert fw_name, "No framework configured." + for ep in pkg_resources.iter_entry_points('%s.frameworks' % application): + if ep.name == fw_name: + return ep.load()(application, config) + for ep in pkg_resources.iter_entry_points('encadre.frameworks'): + if ep.name == fw_name: + return ep.load()(application, config) + raise Exception("No framework available") diff --git a/encadre_flask/__init__.py b/encadre_flask/__init__.py @@ -0,0 +1,168 @@ +import sys +import re +import logging +from flask import Flask, request, send_file, abort +from flask_cors import CORS +from flask_jwt_extended import JWTManager +from encadre import Framework + +logger = logging.getLogger(__name__) + + +class FlaskFramework(Framework): + def __init__(self, application, config): + Framework.__init__(self, application, config) + self.versions = {} + self._endpoints = set() + self.application = application + self.flask_app = Flask(application) + self.flask_app.url_map.strict_slashes = False + self.flask_app.register_error_handler(404, self.handle_exception) + for k, v in (self.config.get('flask') or {}).items(): + _v = v + if v.lower() == 'false': + _v = False + if v.lower() == 'true': + _v = True + if v.isdigit(): + _v = int(v) + self.flask_app.config[k.upper()] = _v + if not (self.config.get('flask') or {}).get('external_cors'): + logger.debug("We handle CORS.") + CORS(self.flask_app) + else: + logger.debug("CORS must be handheld upstream.") + if (self.config.get('flask') or {}).get('JWT_SECRET_KEY'.lower()): + self.jwt_manager = JWTManager(self.flask_app) + + def setup_application(self, instance): + Framework.setup_application(self, instance) + for fct in self.before_call_chain: + self.flask_app.before_request(fct) + for fct in self.after_call_chain: + self.flask_app.after_request(fct) + self.flask_app.after_request(self._set_cookies) + + def _add_method(self, ctrl_name, method_name): + def accessor(**args): + import flask + v = None + if self.versioning_use_prefix: + v = flask.request.environ.get('PATH_INFO').lstrip('/').split( + '/')[0] + logger.debug("Got version '%s' from URL." % v) + else: + # flask handle case and underscores + h = 'X-%s-Version' % self.application + v = flask.request.headers.get(h) + logger.debug("Got version '%s' from header '%s'." % (v, h)) + if v and not v.isdigit(): + flask.abort(405) + v = int(v) if v else sys.maxsize + v = v if v in self.controllers[ctrl_name] else max( + self.controllers[ctrl_name].keys()) + verb = flask.request.environ['REQUEST_METHOD'] + cls = self.controllers[ctrl_name][v]['cls'] + try: + m = self.controllers[ctrl_name][v]['methods'][method_name][ + verb]['fct'].__name__ + except KeyError: + logger.error("Cannot found method for: controller: '%s', " + "method: '%s', version: '%s', verb: '%s'" % + (ctrl_name, method_name, v, verb)) + flask.abort(405) + self._cookies = [] + try: + ret = getattr(cls(), m)(**args) + except Exception as e: + ret = self.handle_exception(e) + return ret + + setattr(self, '_m_%s_%s' % (ctrl_name, method_name), accessor) + + def handle_exception(self, e): + message, code = self.format_exception(e) + if not message: + abort(code) + return message, code + + def _mk_endpoint(self, ctrl_name, method_name, args, version): + ep = "/%s/%s%s" % (ctrl_name.lower(), + method_name if method_name != 'index__' else '', + ('/' + '/'.join(['<%s>' % a + for a in args]) if args else '')) + return '/%s%s' % (version, ep) if version else ep + + def _setup_routes(self): + """ Add routes to flask """ + endpoints = {} + methods = {} + for c in sorted(self.controllers): + for v in self.controllers[c]: + for m in self.controllers[c][v]['methods']: + for _v in self.controllers[c][v]['methods'][m]: + fct = self.controllers[c][v]['methods'][m][_v]['fct'] + args = self.controllers[c][v]['methods'][m][_v]['args'] + e = self._mk_endpoint( + c, + m, + args, + version=v if self.versioning_use_prefix else None) + endpoints.setdefault(e, set()) + if isinstance(fct.__defaults__, tuple): + url = re.search(r'^(.*[\\\/])', e).group() + endpoints.setdefault(url, set()) + endpoints[url].add(_v) + methods[url] = (c, m) + endpoints[e].add(_v) + methods[e] = (c, m) + for e, verbs in endpoints.items(): + self._add_method(*methods[e]) + logger.debug("Adding endpoint: '%s' allowing with verbs: '%s'." % + (e, verbs)) + route = e + if route.startswith('/__root__'): + route = e.replace('/__root__', '', 1) + self.flask_app.add_url_rule(route, + route, + getattr(self, '_m_%s_%s' % methods[e]), + methods=verbs) + + def serve(self): + self._setup_routes() + self.flask_app.run( + host=self.flask_app.config.get('HOST', '0.0.0.0'), + port=self.flask_app.config.get('PORT', 5001), + debug=self.flask_app.config.get('DEBUG', True), + threaded=self.flask_app.config.get('THREADED', False), + ) + + def get_wsgi_app(self): + self._setup_routes() + return self.flask_app + + def _set_cookies(self, response): + for args, kwargs in getattr(self, '_cookies', {}): + response.set_cookie(*args, **kwargs) + return response + + def request_json(self): + return request.json + + def request_environ(self): + return request.environ + + def request_headers(self): + return request.headers + + def request_args(self): + return request.args + + def get_cookie(self, key): + return request.cookies.get(key) + + def set_cookie(self, *args, **kwargs): + self._cookies.append((args, kwargs)) + + def send_bytes(self, bytes_like, **kwargs): + return send_file(bytes_like, **kwargs) diff --git a/encadre_flask/test.py b/encadre_flask/test.py @@ -0,0 +1,25 @@ +import unittest +from encadre import Encadre +from encadre.framework import framework_from_config +from encadre.controllers import load_controllers + + +class EncadreFlask(Encadre): + + application = 'encadre_test_app' + + def __init__(self): + Encadre.__init__(self, None) + self.framework = framework_from_config(self.application, + {self.application: + {'framework': 'flask'}}) + load_controllers(self.framework, self.application) + + +class TestEncadreFlask(unittest.TestCase): + + def setUp(self): + self.encadre = EncadreFlask() + + def test_setup_routes(self): + self.encadre.framework._setup_routes() diff --git a/cadre_test_app.wsgi b/encadre_test_app.wsgi diff --git a/encadre_test_app/__init__.py b/encadre_test_app/__init__.py @@ -0,0 +1,25 @@ +import sys + +import fastjsonschema +from encadre import Encadre + + +class Encadre_Test_App(Encadre): + + def validate_query(self, schema): + from encadre import request + fastjsonschema.compile(schema)(request.json) + + +def serve(): + if len(sys.argv) != 2: + print("Usage: %s config.ini") + sys.exit(1) + Encadre_Test_App(sys.argv[1]).serve() + + +def dump_routes(): + if len(sys.argv) != 2: + print("Usage: %s config.ini") + sys.exit(1) + Encadre_Test_App(sys.argv[1]).dump_routes() diff --git a/encadre_test_controllers/__init__.py b/encadre_test_controllers/__init__.py @@ -0,0 +1,191 @@ +import unittest +import random +from encadre import Controller, validate + + +class Hello(Controller): + """ A very polite controller """ + + def index__(self): + """ Says hello ! """ + return "Hello world" + + def test_index__(self): + self.assertTrue(self.index__() == "Hello world") + + def config(self): + """ Dump the config """ + from encadre import config + return config + + def test_config(self): + from encadre import config + k = 'test_%s' % random.randint(0, 1000) + v = random.randint(0, 1000) + config.update({k: v}) + self.assertTrue(self.config()[k] == v) + + @validate({ + 'title': 'Dummy test', + 'type': 'object', + 'properties': { + 'numbers': { + 'description': "We want an array of numbers", + 'type': 'array', + 'items': { + 'type': 'integer' + }, + 'minItems': 1 + } + }, + 'required': ['numbers'] + }) + def i_am_validated_POST(self): + from encadre import request + return request.json + + def test_i_am_validated_POST(self): + from encadre import request + request.json = {'numbers': [1, 2]} + self.assertTrue(self.i_am_validated_POST() == { + 'numbers': [1, 2]}) + + @validate({ + 'title': 'Dummy test', + 'type': 'object', + 'properties': { + 'numbers': { + 'description': "We want an array of numbers", + 'type': 'array', + 'items': { + 'type': 'integer' + }, + 'minItems': 1 + } + }, + 'required': ['numbers'] + }) + def i_will_not_pass_validation_POST(self): + from encadre import request + return request.json + + def test_i_will_not_pass_validation_POST(self): + from encadre import request + request.json = {'xnumbers': [1, 2]} + self.i_am_validated_POST() + self.assertRaises(Exception) + + +class Hello_2(Controller): + """ A very polite controller (version 2) """ + + def index__(self): + """ Says hello ! """ + return "Hello world (2)" + + def test_index__(self): + self.assertTrue(self.index__() == "Hello world (2)") + + def to(self, who): + """ Says hello to whom ask for """ + return "Hello %s !" % who + + def test_to(self): + assert True + + def foo(self): + return "Foo" + + def test_foo(self): + assert True + + def foo_(self): + return "Foo with an '_' at the end !" + + def test_foo_(self): + assert True + + def bar_GET(self): + return "Bar" + + def test_bar_GET(self): + assert True + + def baz_POST(self): + from encadre import request + return request.json + + def test_baz_POST(self): + from encadre import request + k = 'test_%s' % random.randint(0, 1000) + v = random.randint(0, 1000) + request.json = {k: v} + self.assertTrue(self.baz_POST()[k] == v) + + def qux_GET_POST(self): + return "Qux" + + def test_qux_GET_POST(self): + assert True + + def args(self): + from encadre import request + return request.args.get('qwe', "no 'qwe' arg") + + def test_args(self): + from encadre import request + v = 'test_%s' % random.randint(0, 1000) + request.args['qwe'] = v + self.assertTrue(self.args() == v) + + +class Hello_3(Hello_2): + """ A very polite controller """ + + def index__(self): + """ Says hello ! """ + return "Hello world (3)" + + def test_index__(self): + self.assertTrue(self.index__() == "Hello world (3)") + + def bar_GET(self): + """ Better 'bar' """ + return "Better Bar" + + def test_bar_GET(self): + assert True + + def bar_POST(self): + """ That one handle POST too ! """ + return "Handle POST too." + + def test_bar_POST(self): + assert True + + def send_bytes(self): + return self.framework.send_bytes( + open("/etc/passwd", "rb"), + as_attachment=True, + attachment_filename='etc-passwd.txt', + mimetype='text/plain') + + def test_send_bytes(self): + self.assertTrue(b'root' in self.send_bytes()) + + def dividebyzero(self): + return 1 / 0 + + +class StrangeTestCase(unittest.TestCase): + + def setUp(self): + pass + + def tearDown(self): + pass + + +class Hello_4(Hello_3): + + testcase_cls = StrangeTestCase diff --git a/routes.html b/routes.html @@ -1,171 +0,0 @@ -<!doctype html> -<html lang="fr"> - <head> - <meta charset="utf-8" /> - <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8" /> - <style>@media print { - .noprint { - display: none; - } -} -@media screen { - .noscreen { - display: none; - } -} -html { - font-family: arial; - font-size: 12pt; -} -#body > h1 { - margin-top: 64px; - text-align: center; - margin-bottom: 64px; -} -.logo { - position: absolute; - right: 32px; - top: 8px; - margin-bottom: 100px; -} -li { - list-style-type: none; -} -.section-number { - color: #9d80b3; - padding-right: 10pt; -} -h1 { - color: #2c0449; - font-size: 24pt; -} -h2 { - color: #2c0449; - font-size: 20pt; -} -h3 { - color: #2c0449; - font-size: 16pt; -} -.side-title { - color: #2c0449; - font-size: 16pt; - font-weight: bold; -} -:link { - border-bottom: 1px dotted #65427e; - color: #2c0449; - text-decoration: none; -} -:visited { - border-bottom: 1px dotted #65427e; - color: #9d80b3; - text-decoration: none; -} -:link:hover, -:visited:hover { - color: #65427e; -} -b { - color: #2c0449; -} -code { - background-color: #f4e7fd; - border: 1px solid #6a4d7f; - border-radius: 1pt; - font-size: 90%; -} -pre code { - background-color: none; - border: none; -} -pre { - background-color: #f4e7fd; - font-family: courier, monospace; - font-size: 90%; - overflow: auto; - border: 1px solid #6a4d7f; - border-radius: 4pt; - box-shadow: 2px 2px 3px #9d80b3; - margin-left: auto; - margin-right: auto; - margin-bottom: 10pt; - margin-top: 15pt; - padding: 5pt; -} -textarea { - overflow-x: auto; -} -img { - border: 1px solid #6a4d7f; - border-radius: 4pt; - box-shadow: 2px 2px 3px #9d80b3; - margin-left: auto; - margin-right: auto; - margin-bottom: 10pt; - margin-top: 15pt; - padding: 5pt; -} -.noshadow { - border: none; - box-shadow: none; -} -dt { - font-weight: bold; -} -table { - background: #ebfaf9; - border: 1px solid #6a4d7f; - border-radius: 4pt; - box-shadow: 2px 2px 3px #9d80b3; - margin-left: auto; - margin-right: auto; - margin-bottom: 10pt; - margin-top: 15pt; - padding: 5pt; -} -.root { - border-bottom: none; -} -th { - background: #d1faf7; - border: 1px solid #75f0e8; - border-radius: 1pt; - padding: 5px; - vertical-align: top; -} -td { - background: #ebfaf9; - border: 1px solid #99e5e0; - border-radius: 1pt; - font-family: courier, monospace; - padding: 2px; - vertical-align: top; -} -#src_file { - padding: 8px; - float: right; - font-size: 10px; - color: #ccc; -} -.task-status { - margin-right: 4px; - padding-left: 4px; - padding-right: 4px; -} -.task-status .todo { - background: red; - color: white; -} -.task-status .done { - background: green; - color: white; -} - </style> - </head> - <body><h1>Untitled</h1> -<ul></ul> - - - </body> -</html> diff --git a/routes.org b/routes.org diff --git a/setup.py b/setup.py @@ -1,23 +1,23 @@ from setuptools import setup console_scripts = """ -cadre-test-app-serve = cadre_test_app:serve -cadre-test-app-dump-routes = cadre_test_app:dump_routes +encadre-test-app-serve = encadre_test_app:serve +encadre-test-app-dump-routes = encadre_test_app:dump_routes """ -setup(name='cadre', +setup(name='encadre', version='0.1.1', - description="Cadre framework", + description="Encadre framework", author="Jean Schurger", author_email='jean@schurger.org', - packages=['cadre', 'cadre_flask'], + packages=['encadre', 'encadre_flask'], install_requires=['Flask', 'fastjsonschema', 'flask-cors', 'decorator', 'nose', 'coverage', 'flask_jwt_extended'], entry_points={ 'console_scripts': console_scripts, - 'cadre.frameworks': - ['flask = cadre_flask:FlaskFramework'], - 'cadre_test_app.controllers': - ['test_controllers = cadre_test_controllers'] + 'encadre.frameworks': + ['flask = encadre_flask:FlaskFramework'], + 'encadre_test_app.controllers': + ['test_controllers = encadre_test_controllers'] }, license='GPLv3') diff --git a/test/__init__.py b/test/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from cadre_test_app import Cadre_Test_App -from cadre.controllers import get_controllers_tsts +from encadre_test_app import Encadre_Test_App +from encadre.controllers import get_controllers_tsts -locals().update(get_controllers_tsts(Cadre_Test_App(None))) +locals().update(get_controllers_tsts(Encadre_Test_App(None)))