encadre 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- encadre-0.1.1/PKG-INFO +19 -0
- encadre-0.1.1/encadre/__init__.py +80 -0
- encadre-0.1.1/encadre/controllers.py +135 -0
- encadre-0.1.1/encadre/framework.py +186 -0
- encadre-0.1.1/encadre.egg-info/PKG-INFO +19 -0
- encadre-0.1.1/encadre.egg-info/SOURCES.txt +13 -0
- encadre-0.1.1/encadre.egg-info/dependency_links.txt +1 -0
- encadre-0.1.1/encadre.egg-info/entry_points.txt +9 -0
- encadre-0.1.1/encadre.egg-info/requires.txt +7 -0
- encadre-0.1.1/encadre.egg-info/top_level.txt +2 -0
- encadre-0.1.1/encadre_flask/__init__.py +168 -0
- encadre-0.1.1/encadre_flask/test.py +25 -0
- encadre-0.1.1/setup.cfg +8 -0
- encadre-0.1.1/setup.py +23 -0
encadre-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: encadre
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Encadre framework
|
|
5
|
+
Author: Jean Schurger
|
|
6
|
+
Author-email: jean@schurger.org
|
|
7
|
+
License: GPLv3
|
|
8
|
+
Requires-Dist: Flask
|
|
9
|
+
Requires-Dist: fastjsonschema
|
|
10
|
+
Requires-Dist: flask-cors
|
|
11
|
+
Requires-Dist: decorator
|
|
12
|
+
Requires-Dist: nose
|
|
13
|
+
Requires-Dist: coverage
|
|
14
|
+
Requires-Dist: flask_jwt_extended
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: license
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: summary
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import configparser
|
|
3
|
+
import traceback
|
|
4
|
+
import logging
|
|
5
|
+
import logging.config
|
|
6
|
+
from encadre.controllers import Controller, load_controllers
|
|
7
|
+
from encadre.framework import Framework, DumpRoutes
|
|
8
|
+
from encadre.framework import framework_from_config, validate
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotReady:
|
|
14
|
+
def __init__(self, diagnostic):
|
|
15
|
+
self.diagnostic = diagnostic
|
|
16
|
+
|
|
17
|
+
def __getattr__(self, attribute):
|
|
18
|
+
raise AttributeError("Cannot access %r: %s" %
|
|
19
|
+
(attribute, self.diagnostic))
|
|
20
|
+
|
|
21
|
+
def __getitem__(self, item):
|
|
22
|
+
raise AttributeError("Cannot find %r: %s" % (item, self.diagnostic))
|
|
23
|
+
|
|
24
|
+
def __bool__(self):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
request = cookies = NotReady("Encadre is not setup yet.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Encadre():
|
|
32
|
+
|
|
33
|
+
config = {}
|
|
34
|
+
|
|
35
|
+
def __init__(self, config_filename):
|
|
36
|
+
self.application = getattr(self, 'application',
|
|
37
|
+
self.__class__.__name__.lower())
|
|
38
|
+
logger.debug("Starting encadre application '%s'." % self.application)
|
|
39
|
+
self.config_filename = config_filename
|
|
40
|
+
if self.config_filename:
|
|
41
|
+
self.config.update(self.read_config(self.config_filename))
|
|
42
|
+
try:
|
|
43
|
+
logging.config.fileConfig(self.config_filename)
|
|
44
|
+
except Exception:
|
|
45
|
+
print("You'd better configure logging in '%s':\n%s" %
|
|
46
|
+
(self.config_filename, traceback.format_exc()))
|
|
47
|
+
setattr(sys.modules['encadre'], 'config', self.config)
|
|
48
|
+
|
|
49
|
+
def read_config(self, filename):
|
|
50
|
+
config = {self.__class__.__name__.lower(): {}}
|
|
51
|
+
p = configparser.ConfigParser()
|
|
52
|
+
p.read(filename)
|
|
53
|
+
for n, s in p.items():
|
|
54
|
+
if s:
|
|
55
|
+
config[n] = dict(s)
|
|
56
|
+
return config
|
|
57
|
+
|
|
58
|
+
def serve(self):
|
|
59
|
+
assert self.config, "Missing config."
|
|
60
|
+
f = framework_from_config(self.__class__.__name__.lower(), self.config)
|
|
61
|
+
f.setup_application(self)
|
|
62
|
+
load_controllers(f, self.application)
|
|
63
|
+
f.serve()
|
|
64
|
+
|
|
65
|
+
def dump_routes(self):
|
|
66
|
+
assert self.config, "Missing config."
|
|
67
|
+
f = DumpRoutes(self.__class__.__name__.lower(), self.config)
|
|
68
|
+
load_controllers(f, self.application)
|
|
69
|
+
print(f)
|
|
70
|
+
|
|
71
|
+
def get_wsgi_app(self):
|
|
72
|
+
config = self.read_config(self.config_filename)
|
|
73
|
+
assert config, "Missing config."
|
|
74
|
+
f = framework_from_config(self.__class__.__name__.lower(), config)
|
|
75
|
+
f.setup_application(self)
|
|
76
|
+
load_controllers(f, self.application)
|
|
77
|
+
return f.get_wsgi_app()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ['Encadre', 'Framework', 'Controller', 'validate']
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import types
|
|
3
|
+
import logging
|
|
4
|
+
import unittest
|
|
5
|
+
import pkg_resources
|
|
6
|
+
from encadre.framework import Framework
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Controller():
|
|
12
|
+
|
|
13
|
+
version = 0
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def setup(cls, framework, root):
|
|
17
|
+
methods = []
|
|
18
|
+
cls.framework = framework
|
|
19
|
+
ctrl_name = cls.__name__.lower()
|
|
20
|
+
version = ctrl_name.split('_')[-1]
|
|
21
|
+
if version.isdigit():
|
|
22
|
+
ctrl_name = ctrl_name[:-(len(version) + 1)]
|
|
23
|
+
version = int(version)
|
|
24
|
+
else:
|
|
25
|
+
version = framework.first_version
|
|
26
|
+
|
|
27
|
+
for name in [a for a in dir(cls) if a[0] != '_']:
|
|
28
|
+
attr = getattr(cls, name)
|
|
29
|
+
if isinstance(attr, types.FunctionType) and \
|
|
30
|
+
not name.startswith('test_'):
|
|
31
|
+
verbs = ''.join([c for c in name if c.isupper() or c == '_'
|
|
32
|
+
]).lstrip('_').split('_')
|
|
33
|
+
if verbs != ['']:
|
|
34
|
+
name = name[:name.find("_%s" % verbs[0])]
|
|
35
|
+
if name == 'index__':
|
|
36
|
+
if root:
|
|
37
|
+
if name == 'index__':
|
|
38
|
+
framework.root_id = id(attr)
|
|
39
|
+
else:
|
|
40
|
+
root_id = getattr(framework, 'root_id', None)
|
|
41
|
+
# Prevent inheritance of index__
|
|
42
|
+
if root_id == id(attr):
|
|
43
|
+
continue
|
|
44
|
+
methods.append(
|
|
45
|
+
(name, attr,
|
|
46
|
+
attr.__code__.co_varnames[1:attr.__code__.co_argcount],
|
|
47
|
+
verbs if verbs != [''] else ['GET']))
|
|
48
|
+
framework.add_controller((cls, '__root__' if root else ctrl_name,
|
|
49
|
+
version), methods)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setup_controllers(cls, f, level=0):
|
|
53
|
+
if cls.__name__ != 'Controller': # exclude abstract
|
|
54
|
+
cls.setup(f, root=level < 2)
|
|
55
|
+
for sub in cls.__subclasses__():
|
|
56
|
+
setup_controllers(sub, f, level + 1)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_controllers(f, application):
|
|
60
|
+
for ep in pkg_resources.iter_entry_points('%s.controllers' % application):
|
|
61
|
+
try:
|
|
62
|
+
ep.load()
|
|
63
|
+
except ModuleNotFoundError:
|
|
64
|
+
logger.error("Error importing '%s': %s" % (
|
|
65
|
+
ep, traceback.format_exc()))
|
|
66
|
+
for ep in pkg_resources.iter_entry_points('encadre.controllers'):
|
|
67
|
+
try:
|
|
68
|
+
ep.load()
|
|
69
|
+
except ModuleNotFoundError:
|
|
70
|
+
logger.error("Error importing '%s': %s" % (
|
|
71
|
+
ep, traceback.format_exc()))
|
|
72
|
+
setup_controllers(Controller, f)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EncadreTestCase(unittest.TestCase):
|
|
76
|
+
def setUp(self):
|
|
77
|
+
self.framework._json = {}
|
|
78
|
+
self.framework._environ = {}
|
|
79
|
+
self.framework._headers = {}
|
|
80
|
+
self.framework._args = {}
|
|
81
|
+
self.framework._cookies = {}
|
|
82
|
+
|
|
83
|
+
def tearDown(self):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestFramework(Framework):
|
|
88
|
+
def request_json(self):
|
|
89
|
+
return self._json
|
|
90
|
+
|
|
91
|
+
def request_environ(self):
|
|
92
|
+
|
|
93
|
+
return self._environ
|
|
94
|
+
|
|
95
|
+
def request_headers(self):
|
|
96
|
+
return self._headers
|
|
97
|
+
|
|
98
|
+
def request_args(self):
|
|
99
|
+
return self._args
|
|
100
|
+
|
|
101
|
+
def get_cookie(self, key):
|
|
102
|
+
return self._cookies.get(key)
|
|
103
|
+
|
|
104
|
+
def set_cookie(self, key, value): # pylint: disable=W0221
|
|
105
|
+
self._cookies[key] = value
|
|
106
|
+
|
|
107
|
+
def send_bytes(self, bytes_like, **kwargs): # pylint: disable=W0613
|
|
108
|
+
return bytes_like.read()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_controllers_tsts(app):
|
|
112
|
+
f = TestFramework(app.application, None)
|
|
113
|
+
load_controllers(f, app.application)
|
|
114
|
+
|
|
115
|
+
def missing_test(self):
|
|
116
|
+
raise Exception("Missing test")
|
|
117
|
+
|
|
118
|
+
for c in f.controllers:
|
|
119
|
+
for v in f.controllers[c]:
|
|
120
|
+
cls = f.controllers[c][v]['cls']
|
|
121
|
+
attributes = {}
|
|
122
|
+
for name in [a for a in dir(cls) if a[0] != '_']:
|
|
123
|
+
attr = getattr(cls, name)
|
|
124
|
+
if not name.startswith('test_'):
|
|
125
|
+
attributes[name] = attr
|
|
126
|
+
if isinstance(attr, types.FunctionType) and \
|
|
127
|
+
not name.startswith('test_'):
|
|
128
|
+
test_name = 'test_%s' % name
|
|
129
|
+
test_long_name = 'test_%s_%s' % (cls.__name__, name)
|
|
130
|
+
test_fct = getattr(cls, test_name, missing_test)
|
|
131
|
+
attributes[test_long_name] = test_fct
|
|
132
|
+
test_cls = type("Test%s" % cls.__name__,
|
|
133
|
+
(getattr(cls, 'testcase_cls', EncadreTestCase), ),
|
|
134
|
+
attributes)
|
|
135
|
+
yield (test_cls.__name__, test_cls)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import logging
|
|
3
|
+
import pkg_resources
|
|
4
|
+
import traceback
|
|
5
|
+
from decorator import decorator
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
ENI = Exception("Not implemented.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Cookies():
|
|
12
|
+
def __init__(self, framework):
|
|
13
|
+
self.f = framework
|
|
14
|
+
|
|
15
|
+
def get(self, key):
|
|
16
|
+
return self.f.get_cookie(key)
|
|
17
|
+
|
|
18
|
+
def set(self, *args, **kwargs):
|
|
19
|
+
return self.f.set_cookie(*args, **kwargs)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Request():
|
|
23
|
+
def __init__(self, framework):
|
|
24
|
+
self.f = framework
|
|
25
|
+
self._json = None
|
|
26
|
+
self._environ = None
|
|
27
|
+
self._headers = None
|
|
28
|
+
self._args = None
|
|
29
|
+
|
|
30
|
+
def _get_json(self):
|
|
31
|
+
return self._json or self.f.request_json()
|
|
32
|
+
|
|
33
|
+
# Let unit tests set some json
|
|
34
|
+
def _set_json(self, json):
|
|
35
|
+
self._json = json
|
|
36
|
+
|
|
37
|
+
json = property(_get_json, _set_json)
|
|
38
|
+
|
|
39
|
+
def _get_environ(self):
|
|
40
|
+
return self._environ or self.f.request_environ()
|
|
41
|
+
|
|
42
|
+
# Let unit tests set environ
|
|
43
|
+
def _set_environ(self, environ):
|
|
44
|
+
self._environ = environ
|
|
45
|
+
|
|
46
|
+
environ = property(_get_environ, _set_environ)
|
|
47
|
+
|
|
48
|
+
def _get_headers(self):
|
|
49
|
+
return self._headers or self.f.request_headers()
|
|
50
|
+
|
|
51
|
+
# Let unit tests set environ
|
|
52
|
+
def _set_headers(self, headers):
|
|
53
|
+
self._headers = headers
|
|
54
|
+
|
|
55
|
+
headers = property(_get_headers, _set_headers)
|
|
56
|
+
|
|
57
|
+
def _get_args(self):
|
|
58
|
+
return self._args or self.f.request_args()
|
|
59
|
+
|
|
60
|
+
# Let unit tests set environ
|
|
61
|
+
def _set_args(self, args):
|
|
62
|
+
self._args = args
|
|
63
|
+
|
|
64
|
+
args = property(_get_args, _set_args)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate(vargs):
|
|
68
|
+
def helper(func, *args, **kwargs):
|
|
69
|
+
if hasattr(args[0].framework, 'validate_query'):
|
|
70
|
+
args[0].framework.validate_query(vargs)
|
|
71
|
+
return func(*args, **kwargs)
|
|
72
|
+
return decorator(helper)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Framework():
|
|
76
|
+
|
|
77
|
+
request_class = Request
|
|
78
|
+
cookies_class = Cookies
|
|
79
|
+
|
|
80
|
+
def __init__(self, application, config):
|
|
81
|
+
self.application = application
|
|
82
|
+
self.config = config
|
|
83
|
+
self.first_version = int((self.config or {}).get(application, {}).get(
|
|
84
|
+
'first_version', '0'))
|
|
85
|
+
if self.first_version:
|
|
86
|
+
logger.debug("Config set 'first_version' to %s." %
|
|
87
|
+
self.first_version)
|
|
88
|
+
self.versioning_use_prefix = (self.config or {}).get(
|
|
89
|
+
application, {}).get('versioning', 'headers').lower() == 'prefix'
|
|
90
|
+
if self.versioning_use_prefix:
|
|
91
|
+
logger.debug("Config set versioning to 'prefix' mode.")
|
|
92
|
+
self.routes = {}
|
|
93
|
+
self.controllers = {}
|
|
94
|
+
self.request = self.request_class(self)
|
|
95
|
+
setattr(sys.modules['encadre'], 'request', self.request)
|
|
96
|
+
self.cookies = self.cookies_class(self)
|
|
97
|
+
setattr(sys.modules['encadre'], 'cookies', self.cookies)
|
|
98
|
+
|
|
99
|
+
def add_controller(self, ctrl, methods):
|
|
100
|
+
ctrl_cls, ctrl_name, ctrl_version = ctrl
|
|
101
|
+
self.controllers.setdefault(ctrl_name, {})
|
|
102
|
+
self.controllers[ctrl_name][ctrl_version] = {
|
|
103
|
+
'doc': (ctrl_cls.__doc__ or '').strip(),
|
|
104
|
+
'cls': ctrl_cls,
|
|
105
|
+
'methods': {}}
|
|
106
|
+
for method_name, method, args, verbs in methods:
|
|
107
|
+
self.controllers[ctrl_name][ctrl_version]['methods'].setdefault(
|
|
108
|
+
method_name, {})
|
|
109
|
+
for verb in verbs:
|
|
110
|
+
self.controllers[ctrl_name][ctrl_version]['methods'][
|
|
111
|
+
method_name][verb] = {
|
|
112
|
+
'fct': method,
|
|
113
|
+
'doc': (method.__doc__ or '').strip(),
|
|
114
|
+
'args': args}
|
|
115
|
+
|
|
116
|
+
def setup_application(self, instance):
|
|
117
|
+
self.instance = instance
|
|
118
|
+
self.before_call_chain = []
|
|
119
|
+
if hasattr(instance, 'before_call'):
|
|
120
|
+
self.before_call_chain.append(self.instance_before_call)
|
|
121
|
+
self.after_call_chain = []
|
|
122
|
+
if hasattr(instance, 'after_call'):
|
|
123
|
+
self.after_call_chain.append(self.instance_after_call)
|
|
124
|
+
if hasattr(instance, 'validate_query'):
|
|
125
|
+
self.validate_query = instance.validate_query
|
|
126
|
+
|
|
127
|
+
def instance_before_call(self):
|
|
128
|
+
getattr(self.instance, 'before_call')()
|
|
129
|
+
|
|
130
|
+
def instance_after_call(self, response):
|
|
131
|
+
getattr(self.instance, 'after_call')()
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
def not_implemented(self):
|
|
135
|
+
raise ENI
|
|
136
|
+
|
|
137
|
+
request_json = request_environ = request_headers = \
|
|
138
|
+
request_args = get_cookie = set_cookie = send_bytes = not_implemented
|
|
139
|
+
|
|
140
|
+
def format_exception(self, e):
|
|
141
|
+
ret = None
|
|
142
|
+
code = 500
|
|
143
|
+
logger.exception(traceback.format_exc())
|
|
144
|
+
if hasattr(self.instance, "format_exception"):
|
|
145
|
+
ret, code = self.instance.format_exception(e)
|
|
146
|
+
else:
|
|
147
|
+
ha = (self.config or {}).get(self.application, {}).get(
|
|
148
|
+
'on_exception', '').lower()
|
|
149
|
+
if ha == 'traceback':
|
|
150
|
+
ret = traceback.format_exc()
|
|
151
|
+
if ha == 'str':
|
|
152
|
+
ret = str(e)
|
|
153
|
+
if ret is None:
|
|
154
|
+
raise e
|
|
155
|
+
return ret, code
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class DumpRoutes(Framework):
|
|
159
|
+
|
|
160
|
+
def __repr__(self):
|
|
161
|
+
ret = []
|
|
162
|
+
for c in sorted(self.controllers):
|
|
163
|
+
ret.append("* Controller: %s" % (c))
|
|
164
|
+
for v in self.controllers[c]:
|
|
165
|
+
ret.append("** Version: %s: %s" %
|
|
166
|
+
(v, self.controllers[c][v]['doc']))
|
|
167
|
+
for m in self.controllers[c][v]['methods']:
|
|
168
|
+
for _v in self.controllers[c][v]['methods'][m]:
|
|
169
|
+
ret.append(
|
|
170
|
+
"- [%s] %s%s: %s" %
|
|
171
|
+
(_v, m,
|
|
172
|
+
self.controllers[c][v]['methods'][m][_v]['args'],
|
|
173
|
+
self.controllers[c][v]['methods'][m][_v]['doc']))
|
|
174
|
+
return "\n".join(ret)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def framework_from_config(application, config):
|
|
178
|
+
fw_name = (config.get(application) or {}).get('framework')
|
|
179
|
+
assert fw_name, "No framework configured."
|
|
180
|
+
for ep in pkg_resources.iter_entry_points('%s.frameworks' % application):
|
|
181
|
+
if ep.name == fw_name:
|
|
182
|
+
return ep.load()(application, config)
|
|
183
|
+
for ep in pkg_resources.iter_entry_points('encadre.frameworks'):
|
|
184
|
+
if ep.name == fw_name:
|
|
185
|
+
return ep.load()(application, config)
|
|
186
|
+
raise Exception("No framework available")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: encadre
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Encadre framework
|
|
5
|
+
Author: Jean Schurger
|
|
6
|
+
Author-email: jean@schurger.org
|
|
7
|
+
License: GPLv3
|
|
8
|
+
Requires-Dist: Flask
|
|
9
|
+
Requires-Dist: fastjsonschema
|
|
10
|
+
Requires-Dist: flask-cors
|
|
11
|
+
Requires-Dist: decorator
|
|
12
|
+
Requires-Dist: nose
|
|
13
|
+
Requires-Dist: coverage
|
|
14
|
+
Requires-Dist: flask_jwt_extended
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: license
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: summary
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
setup.cfg
|
|
2
|
+
setup.py
|
|
3
|
+
encadre/__init__.py
|
|
4
|
+
encadre/controllers.py
|
|
5
|
+
encadre/framework.py
|
|
6
|
+
encadre.egg-info/PKG-INFO
|
|
7
|
+
encadre.egg-info/SOURCES.txt
|
|
8
|
+
encadre.egg-info/dependency_links.txt
|
|
9
|
+
encadre.egg-info/entry_points.txt
|
|
10
|
+
encadre.egg-info/requires.txt
|
|
11
|
+
encadre.egg-info/top_level.txt
|
|
12
|
+
encadre_flask/__init__.py
|
|
13
|
+
encadre_flask/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
encadre-test-app-dump-routes = encadre_test_app:dump_routes
|
|
3
|
+
encadre-test-app-serve = encadre_test_app:serve
|
|
4
|
+
|
|
5
|
+
[encadre.frameworks]
|
|
6
|
+
flask = encadre_flask:FlaskFramework
|
|
7
|
+
|
|
8
|
+
[encadre_test_app.controllers]
|
|
9
|
+
test_controllers = encadre_test_controllers
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
from flask import Flask, request, send_file, abort
|
|
5
|
+
from flask_cors import CORS
|
|
6
|
+
from flask_jwt_extended import JWTManager
|
|
7
|
+
from encadre import Framework
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlaskFramework(Framework):
|
|
13
|
+
def __init__(self, application, config):
|
|
14
|
+
Framework.__init__(self, application, config)
|
|
15
|
+
self.versions = {}
|
|
16
|
+
self._endpoints = set()
|
|
17
|
+
self.application = application
|
|
18
|
+
self.flask_app = Flask(application)
|
|
19
|
+
self.flask_app.url_map.strict_slashes = False
|
|
20
|
+
self.flask_app.register_error_handler(404, self.handle_exception)
|
|
21
|
+
for k, v in (self.config.get('flask') or {}).items():
|
|
22
|
+
_v = v
|
|
23
|
+
if v.lower() == 'false':
|
|
24
|
+
_v = False
|
|
25
|
+
if v.lower() == 'true':
|
|
26
|
+
_v = True
|
|
27
|
+
if v.isdigit():
|
|
28
|
+
_v = int(v)
|
|
29
|
+
self.flask_app.config[k.upper()] = _v
|
|
30
|
+
if not (self.config.get('flask') or {}).get('external_cors'):
|
|
31
|
+
logger.debug("We handle CORS.")
|
|
32
|
+
CORS(self.flask_app)
|
|
33
|
+
else:
|
|
34
|
+
logger.debug("CORS must be handheld upstream.")
|
|
35
|
+
if (self.config.get('flask') or {}).get('JWT_SECRET_KEY'.lower()):
|
|
36
|
+
self.jwt_manager = JWTManager(self.flask_app)
|
|
37
|
+
|
|
38
|
+
def setup_application(self, instance):
|
|
39
|
+
Framework.setup_application(self, instance)
|
|
40
|
+
for fct in self.before_call_chain:
|
|
41
|
+
self.flask_app.before_request(fct)
|
|
42
|
+
for fct in self.after_call_chain:
|
|
43
|
+
self.flask_app.after_request(fct)
|
|
44
|
+
self.flask_app.after_request(self._set_cookies)
|
|
45
|
+
|
|
46
|
+
def _add_method(self, ctrl_name, method_name):
|
|
47
|
+
def accessor(**args):
|
|
48
|
+
import flask
|
|
49
|
+
v = None
|
|
50
|
+
if self.versioning_use_prefix:
|
|
51
|
+
v = flask.request.environ.get('PATH_INFO').lstrip('/').split(
|
|
52
|
+
'/')[0]
|
|
53
|
+
logger.debug("Got version '%s' from URL." % v)
|
|
54
|
+
else:
|
|
55
|
+
# flask handle case and underscores
|
|
56
|
+
h = 'X-%s-Version' % self.application
|
|
57
|
+
v = flask.request.headers.get(h)
|
|
58
|
+
logger.debug("Got version '%s' from header '%s'." % (v, h))
|
|
59
|
+
if v and not v.isdigit():
|
|
60
|
+
flask.abort(405)
|
|
61
|
+
v = int(v) if v else sys.maxsize
|
|
62
|
+
v = v if v in self.controllers[ctrl_name] else max(
|
|
63
|
+
self.controllers[ctrl_name].keys())
|
|
64
|
+
verb = flask.request.environ['REQUEST_METHOD']
|
|
65
|
+
cls = self.controllers[ctrl_name][v]['cls']
|
|
66
|
+
try:
|
|
67
|
+
m = self.controllers[ctrl_name][v]['methods'][method_name][
|
|
68
|
+
verb]['fct'].__name__
|
|
69
|
+
except KeyError:
|
|
70
|
+
logger.error("Cannot found method for: controller: '%s', "
|
|
71
|
+
"method: '%s', version: '%s', verb: '%s'" %
|
|
72
|
+
(ctrl_name, method_name, v, verb))
|
|
73
|
+
flask.abort(405)
|
|
74
|
+
self._cookies = []
|
|
75
|
+
try:
|
|
76
|
+
ret = getattr(cls(), m)(**args)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
ret = self.handle_exception(e)
|
|
79
|
+
return ret
|
|
80
|
+
|
|
81
|
+
setattr(self, '_m_%s_%s' % (ctrl_name, method_name), accessor)
|
|
82
|
+
|
|
83
|
+
def handle_exception(self, e):
|
|
84
|
+
message, code = self.format_exception(e)
|
|
85
|
+
if not message:
|
|
86
|
+
abort(code)
|
|
87
|
+
return message, code
|
|
88
|
+
|
|
89
|
+
def _mk_endpoint(self, ctrl_name, method_name, args, version):
|
|
90
|
+
ep = "/%s/%s%s" % (ctrl_name.lower(),
|
|
91
|
+
method_name if method_name != 'index__' else '',
|
|
92
|
+
('/' + '/'.join(['<%s>' % a
|
|
93
|
+
for a in args]) if args else ''))
|
|
94
|
+
return '/%s%s' % (version, ep) if version else ep
|
|
95
|
+
|
|
96
|
+
def _setup_routes(self):
|
|
97
|
+
""" Add routes to flask """
|
|
98
|
+
endpoints = {}
|
|
99
|
+
methods = {}
|
|
100
|
+
for c in sorted(self.controllers):
|
|
101
|
+
for v in self.controllers[c]:
|
|
102
|
+
for m in self.controllers[c][v]['methods']:
|
|
103
|
+
for _v in self.controllers[c][v]['methods'][m]:
|
|
104
|
+
fct = self.controllers[c][v]['methods'][m][_v]['fct']
|
|
105
|
+
args = self.controllers[c][v]['methods'][m][_v]['args']
|
|
106
|
+
e = self._mk_endpoint(
|
|
107
|
+
c,
|
|
108
|
+
m,
|
|
109
|
+
args,
|
|
110
|
+
version=v if self.versioning_use_prefix else None)
|
|
111
|
+
endpoints.setdefault(e, set())
|
|
112
|
+
if isinstance(fct.__defaults__, tuple):
|
|
113
|
+
url = re.search(r'^(.*[\\\/])', e).group()
|
|
114
|
+
endpoints.setdefault(url, set())
|
|
115
|
+
endpoints[url].add(_v)
|
|
116
|
+
methods[url] = (c, m)
|
|
117
|
+
endpoints[e].add(_v)
|
|
118
|
+
methods[e] = (c, m)
|
|
119
|
+
for e, verbs in endpoints.items():
|
|
120
|
+
self._add_method(*methods[e])
|
|
121
|
+
logger.debug("Adding endpoint: '%s' allowing with verbs: '%s'." %
|
|
122
|
+
(e, verbs))
|
|
123
|
+
route = e
|
|
124
|
+
if route.startswith('/__root__'):
|
|
125
|
+
route = e.replace('/__root__', '', 1)
|
|
126
|
+
self.flask_app.add_url_rule(route,
|
|
127
|
+
route,
|
|
128
|
+
getattr(self, '_m_%s_%s' % methods[e]),
|
|
129
|
+
methods=verbs)
|
|
130
|
+
|
|
131
|
+
def serve(self):
|
|
132
|
+
self._setup_routes()
|
|
133
|
+
self.flask_app.run(
|
|
134
|
+
host=self.flask_app.config.get('HOST', '0.0.0.0'),
|
|
135
|
+
port=self.flask_app.config.get('PORT', 5001),
|
|
136
|
+
debug=self.flask_app.config.get('DEBUG', True),
|
|
137
|
+
threaded=self.flask_app.config.get('THREADED', False),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def get_wsgi_app(self):
|
|
141
|
+
self._setup_routes()
|
|
142
|
+
return self.flask_app
|
|
143
|
+
|
|
144
|
+
def _set_cookies(self, response):
|
|
145
|
+
for args, kwargs in getattr(self, '_cookies', {}):
|
|
146
|
+
response.set_cookie(*args, **kwargs)
|
|
147
|
+
return response
|
|
148
|
+
|
|
149
|
+
def request_json(self):
|
|
150
|
+
return request.json
|
|
151
|
+
|
|
152
|
+
def request_environ(self):
|
|
153
|
+
return request.environ
|
|
154
|
+
|
|
155
|
+
def request_headers(self):
|
|
156
|
+
return request.headers
|
|
157
|
+
|
|
158
|
+
def request_args(self):
|
|
159
|
+
return request.args
|
|
160
|
+
|
|
161
|
+
def get_cookie(self, key):
|
|
162
|
+
return request.cookies.get(key)
|
|
163
|
+
|
|
164
|
+
def set_cookie(self, *args, **kwargs):
|
|
165
|
+
self._cookies.append((args, kwargs))
|
|
166
|
+
|
|
167
|
+
def send_bytes(self, bytes_like, **kwargs):
|
|
168
|
+
return send_file(bytes_like, **kwargs)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from encadre import Encadre
|
|
3
|
+
from encadre.framework import framework_from_config
|
|
4
|
+
from encadre.controllers import load_controllers
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EncadreFlask(Encadre):
|
|
8
|
+
|
|
9
|
+
application = 'encadre_test_app'
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
Encadre.__init__(self, None)
|
|
13
|
+
self.framework = framework_from_config(self.application,
|
|
14
|
+
{self.application:
|
|
15
|
+
{'framework': 'flask'}})
|
|
16
|
+
load_controllers(self.framework, self.application)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestEncadreFlask(unittest.TestCase):
|
|
20
|
+
|
|
21
|
+
def setUp(self):
|
|
22
|
+
self.encadre = EncadreFlask()
|
|
23
|
+
|
|
24
|
+
def test_setup_routes(self):
|
|
25
|
+
self.encadre.framework._setup_routes()
|
encadre-0.1.1/setup.cfg
ADDED
encadre-0.1.1/setup.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup
|
|
2
|
+
|
|
3
|
+
console_scripts = """
|
|
4
|
+
encadre-test-app-serve = encadre_test_app:serve
|
|
5
|
+
encadre-test-app-dump-routes = encadre_test_app:dump_routes
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
setup(name='encadre',
|
|
9
|
+
version='0.1.1',
|
|
10
|
+
description="Encadre framework",
|
|
11
|
+
author="Jean Schurger",
|
|
12
|
+
author_email='jean@schurger.org',
|
|
13
|
+
packages=['encadre', 'encadre_flask'],
|
|
14
|
+
install_requires=['Flask', 'fastjsonschema', 'flask-cors',
|
|
15
|
+
'decorator', 'nose', 'coverage', 'flask_jwt_extended'],
|
|
16
|
+
entry_points={
|
|
17
|
+
'console_scripts': console_scripts,
|
|
18
|
+
'encadre.frameworks':
|
|
19
|
+
['flask = encadre_flask:FlaskFramework'],
|
|
20
|
+
'encadre_test_app.controllers':
|
|
21
|
+
['test_controllers = encadre_test_controllers']
|
|
22
|
+
},
|
|
23
|
+
license='GPLv3')
|