commit a9d80d185f5de3686b450e9a3e4d466e55b68f0b
parent 19716fbc6735b0183ae7c8dfc4400bb8c65f7e03
Author: Jean Schurger <jean@schurger.org>
Date: Thu, 20 Feb 2025 11:31:34 -0500
Renamed.
Diffstat:
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)))