Browse Source

Merge pull request #31 from inkhey/feature/generic_hapic

Bastien Sevajol 6 years ago
parent
commit
924de5520f
No account linked to committer's email

+ 1 - 1
.travis.yml View File

@@ -6,7 +6,7 @@ python:
6 6
 
7 7
 install:
8 8
   - python setup.py develop
9
-  - pip install pytest pytest-cov python-coveralls
9
+  - pip install -e ."[test]"
10 10
 
11 11
 script: 
12 12
   - pytest --cov=hapic tests

example.py → example/example.py View File


example_a.py → example/example_a_bottle.py View File

@@ -5,32 +5,13 @@ from http import HTTPStatus
5 5
 import bottle
6 6
 import time
7 7
 import yaml
8
-from beaker.middleware import SessionMiddleware
9 8
 
10 9
 import hapic
10
+from hapic.ext.bottle.context import BottleContext
11 11
 from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
12 12
     ErrorResponseSchema, HelloQuerySchema, HelloFileSchema
13 13
 from hapic.data import HapicData
14 14
 
15
-# hapic.global_exception_handler(UnAuthExc, StandardErrorSchema)
16
-# hapic.global_exception_handler(UnAuthExc2, StandardErrorSchema)
17
-# hapic.global_exception_handler(UnAuthExc3, StandardErrorSchema)
18
-# bottle.default_app.push(app)
19
-
20
-# session_opts = {
21
-#     'session.type': 'file',
22
-#     'session.data_dir': '/tmp',
23
-#     'session.cookie_expires': 3600,
24
-#     'session.auto': True
25
-# }
26
-# session_middleware = SessionMiddleware(
27
-#     app,
28
-#     session_opts,
29
-#     environ_key='beaker.session',
30
-#     key='beaker.session.id',
31
-# )
32
-# app = session_middleware.wrap_app
33
-
34 15
 
35 16
 def bob(f):
36 17
     def boby(*args, **kwargs):
@@ -90,7 +71,7 @@ class Controllers(object):
90 71
     # @hapic.error_schema(ErrorResponseSchema())
91 72
     @hapic.input_path(HelloPathSchema())
92 73
     @hapic.output_body(HelloResponseSchema())
93
-    def hello3(self, name: str):
74
+    def hello3(self, name: str, hapic_data: HapicData):
94 75
         return {
95 76
             'sentence': 'Hello !',
96 77
             'name': name,
@@ -113,19 +94,11 @@ app = bottle.Bottle()
113 94
 controllers = Controllers()
114 95
 controllers.bind(app)
115 96
 
97
+hapic.set_context(BottleContext(app))
116 98
 
117
-# time.sleep(1)
118
-# s = hapic.generate_doc(app)
119
-# ss = json.loads(json.dumps(s))
120
-# for path in ss['paths']:
121
-#     for method in ss['paths'][path]:
122
-#         for response_code in ss['paths'][path][method]['responses']:
123
-#             ss['paths'][path][method]['responses'][int(response_code)] = ss['paths'][path][method]['responses'][response_code]
124
-#             del ss['paths'][path][method]['responses'][int(response_code)]
125
-# print(yaml.dump(ss, default_flow_style=False))
126
-# time.sleep(1)
127
-
128
-hapic.set_context(hapic.ext.bottle.BottleContext())
129
-print(json.dumps(hapic.generate_doc(app)))
99
+print(yaml.dump(
100
+    json.loads(json.dumps(hapic.generate_doc())),
101
+    default_flow_style=False,
102
+))
130 103
 
131 104
 app.run(host='localhost', port=8080, debug=True)

+ 91 - 0
example/example_a_flask.py View File

@@ -0,0 +1,91 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+from http import HTTPStatus
4
+
5
+from flask import Flask
6
+import hapic
7
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
8
+    ErrorResponseSchema, HelloQuerySchema
9
+from hapic.data import HapicData
10
+from hapic.ext.flask import FlaskContext
11
+
12
+
13
+def bob(f):
14
+    def boby(*args, **kwargs):
15
+        return f(*args, **kwargs)
16
+    return boby
17
+
18
+app = Flask(__name__)
19
+
20
+
21
+class Controllers(object):
22
+    @hapic.with_api_doc()
23
+    # @hapic.ext.bottle.bottle_context()
24
+    @hapic.handle_exception(ZeroDivisionError, http_code=HTTPStatus.BAD_REQUEST)
25
+    @hapic.input_path(HelloPathSchema())
26
+    @hapic.input_query(HelloQuerySchema())
27
+    @hapic.output_body(HelloResponseSchema())
28
+    def hello(self, name: str, hapic_data: HapicData):
29
+        """
30
+        my endpoint hello
31
+        ---
32
+        get:
33
+            description: my description
34
+            parameters:
35
+                - in: "path"
36
+                  description: "hello"
37
+                  name: "name"
38
+                  type: "string"
39
+            responses:
40
+                200:
41
+                    description: A pet to be returned
42
+                    schema: HelloResponseSchema
43
+        """
44
+        if name == 'zero':
45
+            raise ZeroDivisionError('Don\'t call him zero !')
46
+
47
+        return {
48
+            'sentence': 'Hello !',
49
+            'name': name,
50
+        }
51
+
52
+    @hapic.with_api_doc()
53
+    # @hapic.ext.bottle.bottle_context()
54
+    # @hapic.error_schema(ErrorResponseSchema())
55
+    @hapic.input_path(HelloPathSchema())
56
+    @hapic.input_body(HelloJsonSchema())
57
+    @hapic.output_body(HelloResponseSchema())
58
+    @bob
59
+    def hello2(self, name: str, hapic_data: HapicData):
60
+        return {
61
+            'sentence': 'Hello !',
62
+            'name': name,
63
+            'color': hapic_data.body.get('color'),
64
+        }
65
+
66
+    kwargs = {'validated_data': {'name': 'bob'}, 'name': 'bob'}
67
+
68
+    @hapic.with_api_doc()
69
+    # @hapic.ext.bottle.bottle_context()
70
+    # @hapic.error_schema(ErrorResponseSchema())
71
+    @hapic.input_path(HelloPathSchema())
72
+    @hapic.output_body(HelloResponseSchema())
73
+    def hello3(self, name: str,hapic_data: HapicData ):
74
+        return {
75
+            'sentence': 'Hello !',
76
+            'name': name,
77
+        }
78
+
79
+    def bind(self, app):
80
+        pass
81
+        app.add_url_rule('/hello/<name>', "hello", self.hello)
82
+        app.add_url_rule('/hello/<name>', "hello2",
83
+                         self.hello2, methods=['POST', ])
84
+        app.add_url_rule('/hello3/<name>', "hello3", self.hello3)
85
+
86
+controllers = Controllers()
87
+controllers.bind(app)
88
+
89
+hapic.set_context(FlaskContext(app))
90
+print(json.dumps(hapic.generate_doc()))
91
+app.run(host='localhost', port=8080, debug=True)

+ 102 - 0
example/example_a_pyramid.py View File

@@ -0,0 +1,102 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+from http import HTTPStatus
4
+
5
+from pyramid.config import Configurator
6
+from wsgiref.simple_server import make_server
7
+
8
+import hapic
9
+from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
10
+    ErrorResponseSchema, HelloQuerySchema
11
+from hapic.data import HapicData
12
+from hapic.ext.pyramid import PyramidContext
13
+
14
+
15
+def bob(f):
16
+    def boby(*args, **kwargs):
17
+        return f(*args, **kwargs)
18
+    return boby
19
+
20
+
21
+class Controllers(object):
22
+    @hapic.with_api_doc()
23
+    # @hapic.ext.bottle.bottle_context()
24
+    @hapic.handle_exception(ZeroDivisionError, http_code=HTTPStatus.BAD_REQUEST)
25
+    @hapic.input_path(HelloPathSchema())
26
+    @hapic.input_query(HelloQuerySchema())
27
+    @hapic.output_body(HelloResponseSchema())
28
+    def hello(self, context, request, hapic_data: HapicData):
29
+        """
30
+        my endpoint hello
31
+        ---
32
+        get:
33
+            description: my description
34
+            parameters:
35
+                - in: "path"
36
+                  description: "hello"
37
+                  name: "name"
38
+                  type: "string"
39
+            responses:
40
+                200:
41
+                    description: A pet to be returned
42
+                    schema: HelloResponseSchema
43
+        """
44
+        name = request.matchdict.get('name', None)
45
+        if name == 'zero':
46
+            raise ZeroDivisionError('Don\'t call him zero !')
47
+
48
+        return {
49
+            'sentence': 'Hello !',
50
+            'name': name,
51
+        }
52
+
53
+    @hapic.with_api_doc()
54
+    # @hapic.ext.bottle.bottle_context()
55
+    # @hapic.error_schema(ErrorResponseSchema())
56
+    @hapic.input_path(HelloPathSchema())
57
+    @hapic.input_body(HelloJsonSchema())
58
+    @hapic.output_body(HelloResponseSchema())
59
+    @bob
60
+    def hello2(self, context, request, hapic_data: HapicData):
61
+        name = request.matchdict.get('name', None)
62
+        return {
63
+            'sentence': 'Hello !',
64
+            'name': name,
65
+            'color': hapic_data.body.get('color'),
66
+        }
67
+
68
+    kwargs = {'validated_data': {'name': 'bob'}, 'name': 'bob'}
69
+
70
+    @hapic.with_api_doc()
71
+    # @hapic.ext.bottle.bottle_context()
72
+    # @hapic.error_schema(ErrorResponseSchema())
73
+    @hapic.input_path(HelloPathSchema())
74
+    @hapic.output_body(HelloResponseSchema())
75
+    def hello3(self, context, request, hapic_data: HapicData):
76
+        name = request.matchdict.get('name', None)
77
+        return {
78
+            'sentence': 'Hello !',
79
+            'name': name,
80
+        }
81
+
82
+    def bind(self, configurator: Configurator):
83
+        configurator.add_route('hello', '/hello/{name}', request_method='GET')
84
+        configurator.add_view(self.hello, route_name='hello', renderer='json')
85
+
86
+        configurator.add_route('hello2', '/hello/{name}', request_method='POST')  # nopep8
87
+        configurator.add_view(self.hello2, route_name='hello2', renderer='json')  # nopep8
88
+
89
+        configurator.add_route('hello3', '/hello3/{name}', request_method='GET')  # nopep8
90
+        configurator.add_view(self.hello3, route_name='hello3', renderer='json')  # nopep8
91
+
92
+
93
+configurator = Configurator(autocommit=True)
94
+controllers = Controllers()
95
+
96
+controllers.bind(configurator)
97
+
98
+hapic.set_context(PyramidContext(configurator))
99
+print(json.dumps(hapic.generate_doc()))
100
+
101
+server = make_server('0.0.0.0', 8080, configurator.make_wsgi_app())
102
+server.serve_forever()

example_b.py → example/example_b.py View File


+ 123 - 0
example/fake_api/bottle_api.py View File

@@ -0,0 +1,123 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+from http import HTTPStatus
4
+
5
+import bottle
6
+import time
7
+from datetime import datetime
8
+
9
+from example.fake_api.model import User
10
+from hapic import Hapic
11
+from example.fake_api.schema import *
12
+from hapic.data import HapicData
13
+from hapic.ext.bottle import BottleContext
14
+
15
+hapic = Hapic()
16
+
17
+
18
+class BottleController(object):
19
+    @hapic.with_api_doc()
20
+    @hapic.output_body(AboutResponseSchema())
21
+    def about(self):
22
+        """
23
+        General information about this API.
24
+        """
25
+        return {
26
+            'version': '1.2.3',
27
+            'datetime': datetime(2017, 12, 7, 10, 55, 8, 488996),
28
+        }
29
+
30
+    @hapic.with_api_doc()
31
+    @hapic.output_body(ListsUserSchema())
32
+    def get_users(self):
33
+        """
34
+        Obtain users list.
35
+        """
36
+        some_user = User(
37
+            id=4,
38
+            username='some_user',
39
+            display_name='Damien Accorsi',
40
+            company='Algoo',
41
+        )
42
+        return {
43
+            'item_nb': 1,
44
+            'items': [
45
+                some_user,
46
+            ],
47
+            'pagination': {
48
+                'first_id': 0,
49
+                'last_id': 5,
50
+                'current_id': 0,
51
+            }
52
+        }
53
+
54
+    @hapic.with_api_doc()
55
+    @hapic.input_path(UserPathSchema())
56
+    @hapic.output_body(UserSchema())
57
+    def get_user(self, id, hapic_data: HapicData):
58
+        """
59
+        Obtain one user
60
+        """
61
+        return {
62
+             'id': 4,
63
+             'username': 'some_user',
64
+             'email_address': 'some.user@hapic.com',
65
+             'first_name': 'Damien',
66
+             'last_name': 'Accorsi',
67
+             'display_name': 'Damien Accorsi',
68
+             'company': 'Algoo',
69
+        }
70
+
71
+    @hapic.with_api_doc()
72
+    # TODO - G.M - 2017-12-5 - Support input_forms ?
73
+    # TODO - G.M - 2017-12-5 - Support exclude, only ?
74
+    @hapic.input_body(UserSchema(exclude=('id',)))
75
+    @hapic.output_body(UserSchema())
76
+    def add_user(self, hapic_data: HapicData):
77
+        """
78
+        Add new user
79
+        """
80
+        return {
81
+             'id': 4,
82
+             'username': 'some_user',
83
+             'email_address': 'some.user@hapic.com',
84
+             'first_name': 'Damien',
85
+             'last_name': 'Accorsi',
86
+             'display_name': 'Damien Accorsi',
87
+             'company': 'Algoo',
88
+        }
89
+
90
+    @hapic.with_api_doc()
91
+    @hapic.output_body(NoContentSchema(),
92
+                       default_http_code=204)
93
+    @hapic.input_path(UserPathSchema())
94
+    def del_user(self, id, hapic_data: HapicData):
95
+        """
96
+        delete user
97
+        """
98
+        return NoContentSchema()
99
+
100
+    def bind(self, app:bottle.Bottle):
101
+        app.route('/about', callback=self.about)
102
+        app.route('/users', callback=self.get_users)
103
+        app.route('/users/<id>', callback=self.get_user)
104
+        app.route('/users/', callback=self.add_user,  method='POST')
105
+        app.route('/users/<id>', callback=self.del_user, method='DELETE')
106
+
107
+if __name__ == "__main__":
108
+    app = bottle.Bottle()
109
+    controllers = BottleController()
110
+    controllers.bind(app)
111
+    hapic.set_context(BottleContext(app))
112
+    time.sleep(1)
113
+    s = json.dumps(
114
+        hapic.generate_doc(
115
+            title='Fake API',
116
+            description='just an example of hapic API'
117
+        )
118
+    )
119
+    time.sleep(1)
120
+    # print swagger doc
121
+    print(s)
122
+    # Run app
123
+    app.run(host='localhost', port=8081, debug=True)

+ 128 - 0
example/fake_api/flask_api.py View File

@@ -0,0 +1,128 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+from http import HTTPStatus
4
+
5
+import flask
6
+import time
7
+from datetime import datetime
8
+from hapic import Hapic
9
+
10
+from example.fake_api.schema import *
11
+from hapic.ext.flask import FlaskContext
12
+from hapic.data import HapicData
13
+
14
+hapic = Hapic()
15
+
16
+
17
+class FlaskController(object):
18
+    @hapic.with_api_doc()
19
+    @hapic.output_body(AboutResponseSchema())
20
+    def about(self):
21
+        """
22
+        General information about this API.
23
+        """
24
+        return {
25
+            'version': '1.2.3',
26
+            'datetime': datetime(2017, 12, 7, 10, 55, 8, 488996),
27
+        }
28
+
29
+    @hapic.with_api_doc()
30
+    @hapic.output_body(ListsUserSchema())
31
+    def get_users(self):
32
+        """
33
+        Obtain users list.
34
+        """
35
+        return {
36
+            'item_nb': 1,
37
+            'items': [
38
+                {
39
+                    'id': 4,
40
+                    'username': 'some_user',
41
+                    'display_name': 'Damien Accorsi',
42
+                    'company': 'Algoo',
43
+                },
44
+            ],
45
+            'pagination': {
46
+                'first_id': 0,
47
+                'last_id': 5,
48
+                'current_id': 0,
49
+            }
50
+        }
51
+
52
+    @hapic.with_api_doc()
53
+    @hapic.input_path(UserPathSchema())
54
+    @hapic.output_body(UserSchema())
55
+    def get_user(self, id, hapic_data: HapicData):
56
+        """
57
+        Obtain one user
58
+        """
59
+        return {
60
+             'id': 4,
61
+             'username': 'some_user',
62
+             'email_address': 'some.user@hapic.com',
63
+             'first_name': 'Damien',
64
+             'last_name': 'Accorsi',
65
+             'display_name': 'Damien Accorsi',
66
+             'company': 'Algoo',
67
+        }
68
+
69
+    @hapic.with_api_doc()
70
+    # TODO - G.M - 2017-12-5 - Support input_forms ?
71
+    # TODO - G.M - 2017-12-5 - Support exclude, only ?
72
+    @hapic.input_body(UserSchema(exclude=('id',)))
73
+    @hapic.output_body(UserSchema())
74
+    def add_user(self, hapic_data: HapicData):
75
+        """
76
+        Add new user
77
+        """
78
+        return {
79
+             'id': 4,
80
+             'username': 'some_user',
81
+             'email_address': 'some.user@hapic.com',
82
+             'first_name': 'Damien',
83
+             'last_name': 'Accorsi',
84
+             'display_name': 'Damien Accorsi',
85
+             'company': 'Algoo',
86
+        }
87
+
88
+    @hapic.with_api_doc()
89
+    @hapic.output_body(NoContentSchema(),
90
+                       default_http_code=204)
91
+    @hapic.input_path(UserPathSchema())
92
+    def del_user(self, id, hapic_data: HapicData):
93
+        """
94
+        delete user
95
+        """
96
+        return NoContentSchema()
97
+
98
+    def bind(self, app: flask.Flask):
99
+        app.add_url_rule('/about',
100
+                         view_func=self.about)
101
+        app.add_url_rule('/users',
102
+                         view_func=self.get_users)
103
+        app.add_url_rule('/users/<id>',
104
+                         view_func=self.get_user)
105
+        app.add_url_rule('/users/',
106
+                         view_func=self.add_user,
107
+                         methods=['POST'])
108
+        app.add_url_rule('/users/<id>',
109
+                         view_func=self.del_user,
110
+                         methods=['DELETE'])
111
+
112
+if __name__ == "__main__":
113
+    app = flask.Flask(__name__)
114
+    controllers = FlaskController()
115
+    controllers.bind(app)
116
+    hapic.set_context(FlaskContext(app))
117
+    time.sleep(1)
118
+    s = json.dumps(
119
+        hapic.generate_doc(
120
+            title='Fake API',
121
+            description='just an example of hapic API'
122
+        )
123
+    )
124
+    time.sleep(1)
125
+    # print swagger doc
126
+    print(s)
127
+    # Run app
128
+    app.run(host='localhost', port=8082, debug=True)

+ 21 - 0
example/fake_api/model.py View File

@@ -0,0 +1,21 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+class User(object):
5
+    def __init__(
6
+        self,
7
+        id: int,
8
+        username: str,
9
+        display_name: str,
10
+        company: str,
11
+        email_address: str='',
12
+        first_name: str='',
13
+        last_name: str='',
14
+    ) -> None:
15
+        self.id = id
16
+        self.username = username
17
+        self.email_address = email_address
18
+        self.first_name = first_name
19
+        self.last_name = last_name
20
+        self.display_name = display_name
21
+        self.company = company

+ 132 - 0
example/fake_api/pyramid_api.py View File

@@ -0,0 +1,132 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+from http import HTTPStatus
4
+from pyramid.response import Response
5
+from pyramid.config import Configurator
6
+from wsgiref.simple_server import make_server
7
+import time
8
+from datetime import datetime
9
+from hapic import Hapic
10
+from example.fake_api.schema import *
11
+
12
+from hapic.data import HapicData
13
+from hapic.ext.pyramid import PyramidContext
14
+
15
+hapic = Hapic()
16
+
17
+
18
+class PyramidController(object):
19
+    @hapic.with_api_doc()
20
+    @hapic.output_body(AboutResponseSchema())
21
+    def about(self, context, request):
22
+        """
23
+        General information about this API.
24
+        """
25
+        return {
26
+            'version': '1.2.3',
27
+            'datetime': datetime(2017, 12, 7, 10, 55, 8, 488996),
28
+        }
29
+
30
+    @hapic.with_api_doc()
31
+    @hapic.output_body(ListsUserSchema())
32
+    def get_users(self, context, request):
33
+        """
34
+        Obtain users list.
35
+        """
36
+        return {
37
+            'item_nb': 1,
38
+            'items': [
39
+                {
40
+                    'id': 4,
41
+                    'username': 'some_user',
42
+                    'display_name': 'Damien Accorsi',
43
+                    'company': 'Algoo',
44
+                },
45
+            ],
46
+            'pagination': {
47
+                'first_id': 0,
48
+                'last_id': 5,
49
+                'current_id': 0,
50
+            }
51
+        }
52
+
53
+    @hapic.with_api_doc()
54
+    @hapic.input_path(UserPathSchema())
55
+    @hapic.output_body(UserSchema())
56
+    def get_user(self, context, request, hapic_data: HapicData):
57
+        """
58
+        Obtain one user
59
+        """
60
+        return {
61
+             'id': 4,
62
+             'username': 'some_user',
63
+             'email_address': 'some.user@hapic.com',
64
+             'first_name': 'Damien',
65
+             'last_name': 'Accorsi',
66
+             'display_name': 'Damien Accorsi',
67
+             'company': 'Algoo',
68
+        }
69
+
70
+    @hapic.with_api_doc()
71
+    # TODO - G.M - 2017-12-5 - Support input_forms ?
72
+    # TODO - G.M - 2017-12-5 - Support exclude, only ?
73
+    @hapic.input_body(UserSchema(exclude=('id',)))
74
+    @hapic.output_body(UserSchema())
75
+    def add_user(self, context, request, hapic_data: HapicData):
76
+        """
77
+        Add new user
78
+        """
79
+        return {
80
+             'id': 4,
81
+             'username': 'some_user',
82
+             'email_address': 'some.user@hapic.com',
83
+             'first_name': 'Damien',
84
+             'last_name': 'Accorsi',
85
+             'display_name': 'Damien Accorsi',
86
+             'company': 'Algoo',
87
+        }
88
+
89
+    @hapic.with_api_doc()
90
+    @hapic.output_body(NoContentSchema(),
91
+                       default_http_code=204)
92
+    @hapic.input_path(UserPathSchema())
93
+    def del_user(self, context, request, hapic_data: HapicData):
94
+        """
95
+        delete user
96
+        """
97
+        return NoContentSchema()
98
+
99
+    def bind(self, configurator: Configurator):
100
+        configurator.add_route('about', '/about', request_method='GET')
101
+        configurator.add_view(self.about, route_name='about', renderer='json')
102
+
103
+        configurator.add_route('get_users', '/users', request_method='GET')  # nopep8
104
+        configurator.add_view(self.get_users, route_name='get_users', renderer='json')  # nopep8
105
+
106
+        configurator.add_route('get_user', '/users/{id}', request_method='GET')  # nopep8
107
+        configurator.add_view(self.get_user, route_name='get_user', renderer='json')  # nopep8
108
+
109
+        configurator.add_route('add_user', '/users/', request_method='POST')  # nopep8
110
+        configurator.add_view(self.add_user, route_name='add_user', renderer='json')  # nopep8
111
+
112
+        configurator.add_route('del_user', '/users/{id}', request_method='DELETE')  # nopep8
113
+        configurator.add_view(self.del_user, route_name='del_user', renderer='json')  # nopep8
114
+
115
+if __name__ == "__main__":
116
+    configurator = Configurator(autocommit=True)
117
+    controllers = PyramidController()
118
+    controllers.bind(configurator)
119
+    hapic.set_context(PyramidContext(configurator))
120
+    time.sleep(1)
121
+    s = json.dumps(
122
+        hapic.generate_doc(
123
+            title='Fake API',
124
+            description='just an example of hapic API'
125
+        )
126
+    )
127
+    time.sleep(1)
128
+    # print swagger doc
129
+    print(s)
130
+    # Run app
131
+    server = make_server('0.0.0.0', 8083, configurator.make_wsgi_app())
132
+    server.serve_forever()

+ 54 - 0
example/fake_api/schema.py View File

@@ -0,0 +1,54 @@
1
+# -*- coding: utf-8 -*-
2
+import marshmallow
3
+
4
+class NoContentSchema(marshmallow.Schema):
5
+    pass
6
+
7
+class AboutResponseSchema(marshmallow.Schema):
8
+    version = marshmallow.fields.String(required=True,)
9
+    datetime = marshmallow.fields.DateTime(required=True)
10
+
11
+
12
+class UserPathSchema(marshmallow.Schema):
13
+    id = marshmallow.fields.Int(
14
+        required=True,
15
+        validate=marshmallow.validate.Range(min=1),
16
+    )
17
+
18
+
19
+class UserSchema(marshmallow.Schema):
20
+    id = marshmallow.fields.Int(required=True)
21
+    username = marshmallow.fields.String(
22
+        required=True,
23
+        validate = marshmallow.validate.Regexp(regex='[\w-]+'),
24
+    )
25
+    email_address = marshmallow.fields.Email(required=True)
26
+    first_name = marshmallow.fields.String(required=True)
27
+    last_name = marshmallow.fields.String(required=True)
28
+    display_name = marshmallow.fields.String(required=True)
29
+    company = marshmallow.fields.String(required=True)
30
+
31
+
32
+class PaginationSchema(marshmallow.Schema):
33
+    first_id = marshmallow.fields.Int(required=True)
34
+    last_id = marshmallow.fields.Int(required=True)
35
+    current_id = marshmallow.fields.Int(required=True)
36
+
37
+
38
+class ListsUserSchema(marshmallow.Schema):
39
+    item_nb = marshmallow.fields.Int(
40
+        required=True,
41
+        validate=marshmallow.validate.Range(min=0)
42
+    )
43
+    items = marshmallow.fields.Nested(
44
+        UserSchema,
45
+        many=True,
46
+        only=['id', 'username', 'display_name', 'company']
47
+    )
48
+    # TODO - G.M - 2017-12-05 - Fix nested schema import into doc !
49
+    # Can't add doc for nested Schema properly
50
+    # When schema item isn't added through their own method
51
+    # Ex : Pagination Schema doesn't work here but UserSchema is ok.
52
+    pagination = marshmallow.fields.Nested(
53
+        PaginationSchema
54
+    )

+ 0 - 1
hapic/__init__.py View File

@@ -1,7 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from hapic.hapic import Hapic
3 3
 from hapic.data import HapicData
4
-from hapic import ext
5 4
 
6 5
 _hapic_default = Hapic()
7 6
 

+ 39 - 5
hapic/context.py View File

@@ -1,13 +1,24 @@
1 1
 # -*- coding: utf-8 -*-
2
-import json
3 2
 import typing
4 3
 from http import HTTPStatus
5 4
 
6
-import bottle
5
+from hapic.processor import RequestParameters
6
+from hapic.processor import ProcessValidationError
7 7
 
8
-from hapic.exception import OutputValidationException
9
-# from hapic.hapic import _default_global_error_schema
10
-from hapic.processor import RequestParameters, ProcessValidationError
8
+if typing.TYPE_CHECKING:
9
+    from hapic.decorator import DecoratedController
10
+
11
+
12
+class RouteRepresentation(object):
13
+    def __init__(
14
+        self,
15
+        rule: str,
16
+        method: str,
17
+        original_route_object: typing.Any=None,
18
+    ) -> None:
19
+        self.rule = rule
20
+        self.method = method
21
+        self.original_route_object = original_route_object
11 22
 
12 23
 
13 24
 class ContextInterface(object):
@@ -27,3 +38,26 @@ class ContextInterface(object):
27 38
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
28 39
     ) -> typing.Any:
29 40
         raise NotImplementedError()
41
+
42
+    def find_route(
43
+        self,
44
+        decorated_controller: 'DecoratedController',
45
+    ) -> RouteRepresentation:
46
+        raise NotImplementedError()
47
+
48
+    def get_swagger_path(self, contextualised_rule: str) -> str:
49
+        """
50
+        Return OpenAPI path with context path
51
+        :param contextualised_rule: path of original context
52
+        :return: OpenAPI path
53
+        """
54
+        raise NotImplementedError()
55
+
56
+    def by_pass_output_wrapping(self, response: typing.Any) -> bool:
57
+        """
58
+        Return True if the controller response is in final state: we do not
59
+        have to apply output wrapper on it.
60
+        :param response: the original response of controller
61
+        :return:
62
+        """
63
+        raise NotImplementedError()

+ 1 - 2
hapic/decorator.py View File

@@ -5,7 +5,6 @@ from http import HTTPStatus
5 5
 
6 6
 # TODO BS 20171010: bottle specific !  # see #5
7 7
 import marshmallow
8
-from bottle import HTTPResponse
9 8
 from multidict import MultiDict
10 9
 
11 10
 from hapic.data import HapicData
@@ -215,7 +214,7 @@ class OutputControllerWrapper(InputOutputControllerWrapper):
215 214
 
216 215
     def after_wrapped_function(self, response: typing.Any) -> typing.Any:
217 216
         try:
218
-            if isinstance(response, HTTPResponse):
217
+            if self.context.by_pass_output_wrapping(response):
219 218
                 return response
220 219
 
221 220
             processed_response = self.processor.process(response)

+ 8 - 63
hapic/doc.py View File

@@ -1,54 +1,19 @@
1 1
 # -*- coding: utf-8 -*-
2
-import re
3 2
 import typing
4 3
 
5
-import bottle
6 4
 from apispec import APISpec
7 5
 from apispec import Path
8 6
 from apispec.ext.marshmallow.swagger import schema2jsonschema
9 7
 
8
+from hapic.context import ContextInterface
9
+from hapic.context import RouteRepresentation
10 10
 from hapic.decorator import DecoratedController
11
-from hapic.decorator import DECORATION_ATTRIBUTE_NAME
12 11
 from hapic.description import ControllerDescription
13
-from hapic.exception import NoRoutesException
14
-from hapic.exception import RouteNotFound
15
-
16
-# Bottle regular expression to locate url parameters
17
-BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
18
-
19
-
20
-def find_bottle_route(
21
-    decorated_controller: DecoratedController,
22
-    app: bottle.Bottle,
23
-):
24
-    if not app.routes:
25
-        raise NoRoutesException('There is no routes in your bottle app')
26
-
27
-    reference = decorated_controller.reference
28
-    for route in app.routes:
29
-        route_token = getattr(
30
-            route.callback,
31
-            DECORATION_ATTRIBUTE_NAME,
32
-            None,
33
-        )
34
-
35
-        match_with_wrapper = route.callback == reference.wrapper
36
-        match_with_wrapped = route.callback == reference.wrapped
37
-        match_with_token = route_token == reference.token
38
-
39
-        if match_with_wrapper or match_with_wrapped or match_with_token:
40
-            return route
41
-    # TODO BS 20171010: Raise exception or print error ? see #10
42
-    raise RouteNotFound(
43
-        'Decorated route "{}" was not found in bottle routes'.format(
44
-            decorated_controller.name,
45
-        )
46
-    )
47 12
 
48 13
 
49 14
 def bottle_generate_operations(
50 15
     spec,
51
-    bottle_route: bottle.Route,
16
+    route: RouteRepresentation,
52 17
     description: ControllerDescription,
53 18
 ):
54 19
     method_operations = dict()
@@ -129,7 +94,7 @@ def bottle_generate_operations(
129 94
             })
130 95
 
131 96
     operations = {
132
-        bottle_route.method.lower(): method_operations,
97
+        route.method.lower(): method_operations,
133 98
     }
134 99
 
135 100
     return operations
@@ -139,23 +104,15 @@ class DocGenerator(object):
139 104
     def get_doc(
140 105
         self,
141 106
         controllers: typing.List[DecoratedController],
142
-        app,
107
+        context: ContextInterface,
143 108
         title: str='',
144 109
         description: str='',
145 110
     ) -> dict:
146
-        # TODO: Découper, see #11
147
-        # TODO: bottle specific code !, see #11
148
-        if not app:
149
-            app = bottle.default_app()
150
-        else:
151
-            bottle.default_app.push(app)
152
-
153 111
         spec = APISpec(
154 112
             title=title,
155 113
             info=dict(description=description),
156 114
             version='1.0.0',
157 115
             plugins=(
158
-                'apispec.ext.bottle',
159 116
                 'apispec.ext.marshmallow',
160 117
             ),
161 118
             schema_name_resolver=generate_schema_name,
@@ -192,12 +149,12 @@ class DocGenerator(object):
192 149
         # with app.test_request_context():
193 150
         paths = {}
194 151
         for controller in controllers:
195
-            bottle_route = find_bottle_route(controller, app)
196
-            swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
152
+            route = context.find_route(controller)
153
+            swagger_path = context.get_swagger_path(route.rule)
197 154
 
198 155
             operations = bottle_generate_operations(
199 156
                 spec,
200
-                bottle_route,
157
+                route,
201 158
                 controller.description,
202 159
             )
203 160
 
@@ -218,18 +175,6 @@ class DocGenerator(object):
218 175
 
219 176
         return spec.to_dict()
220 177
 
221
-        # route_by_callbacks = []
222
-        # routes = flatten(app.router.dyna_routes.values())
223
-        # for path, path_regex, route, func_ in routes:
224
-        #     route_by_callbacks.append(route.callback)
225
-        #
226
-        # for description in self._controllers:
227
-        #     for path, path_regex, route, func_ in routes:
228
-        #         if route.callback == description.reference:
229
-        #             # TODO: use description to feed apispec
230
-        #             print(route.method, path, description)
231
-        #             continue
232
-
233 178
 
234 179
 # TODO BS 20171109: Must take care of already existing definition names
235 180
 def generate_schema_name(schema):

+ 0 - 1
hapic/ext/__init__.py View File

@@ -1,2 +1 @@
1 1
 # -*- coding: utf-8 -*-
2
-from hapic.ext import bottle

+ 54 - 1
hapic/ext/bottle/context.py View File

@@ -1,5 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import json
3
+import re
3 4
 import typing
4 5
 from http import HTTPStatus
5 6
 
@@ -7,11 +8,23 @@ import bottle
7 8
 from multidict import MultiDict
8 9
 
9 10
 from hapic.context import ContextInterface
11
+from hapic.context import RouteRepresentation
12
+from hapic.decorator import DecoratedController
13
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
10 14
 from hapic.exception import OutputValidationException
11
-from hapic.processor import RequestParameters, ProcessValidationError
15
+from hapic.exception import NoRoutesException
16
+from hapic.exception import RouteNotFound
17
+from hapic.processor import RequestParameters
18
+from hapic.processor import ProcessValidationError
19
+
20
+# Bottle regular expression to locate url parameters
21
+BOTTLE_RE_PATH_URL = re.compile(r'<([^:<>]+)(?::[^<>]+)?>')
12 22
 
13 23
 
14 24
 class BottleContext(ContextInterface):
25
+    def __init__(self, app: bottle.Bottle):
26
+        self.app = app
27
+
15 28
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
16 29
         path_parameters = dict(bottle.request.url_args)
17 30
         query_parameters = MultiDict(bottle.request.query.allitems())
@@ -64,3 +77,43 @@ class BottleContext(ContextInterface):
64 77
             ],
65 78
             status=int(http_code),
66 79
         )
80
+
81
+    def find_route(
82
+        self,
83
+        decorated_controller: DecoratedController,
84
+    ) -> RouteRepresentation:
85
+        if not self.app.routes:
86
+            raise NoRoutesException('There is no routes in your bottle app')
87
+
88
+        reference = decorated_controller.reference
89
+        for route in self.app.routes:
90
+            route_token = getattr(
91
+                route.callback,
92
+                DECORATION_ATTRIBUTE_NAME,
93
+                None,
94
+            )
95
+
96
+            match_with_wrapper = route.callback == reference.wrapper
97
+            match_with_wrapped = route.callback == reference.wrapped
98
+            match_with_token = route_token == reference.token
99
+
100
+            if match_with_wrapper or match_with_wrapped or match_with_token:
101
+                return RouteRepresentation(
102
+                    rule=self.get_swagger_path(route.rule),
103
+                    method=route.method.lower(),
104
+                    original_route_object=route,
105
+                )
106
+        # TODO BS 20171010: Raise exception or print error ? see #10
107
+        raise RouteNotFound(
108
+            'Decorated route "{}" was not found in bottle routes'.format(
109
+                decorated_controller.name,
110
+            )
111
+        )
112
+
113
+    def get_swagger_path(self, contextualised_rule: str) -> str:
114
+        return BOTTLE_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
115
+
116
+    def by_pass_output_wrapping(self, response: typing.Any) -> bool:
117
+        if isinstance(response, bottle.HTTPResponse):
118
+            return True
119
+        return False

+ 2 - 0
hapic/ext/flask/__init__.py View File

@@ -0,0 +1,2 @@
1
+# -*- coding: utf-8 -*-
2
+from hapic.ext.flask.context import FlaskContext

+ 106 - 0
hapic/ext/flask/context.py View File

@@ -0,0 +1,106 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+import re
4
+import typing
5
+from http import HTTPStatus
6
+
7
+from hapic.context import ContextInterface
8
+from hapic.context import RouteRepresentation
9
+from hapic.decorator import DecoratedController
10
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
11
+from hapic.exception import OutputValidationException
12
+from hapic.processor import RequestParameters, ProcessValidationError
13
+from flask import Flask
14
+
15
+if typing.TYPE_CHECKING:
16
+    from flask import Response
17
+
18
+# flask regular expression to locate url parameters
19
+FLASK_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
20
+
21
+
22
+class FlaskContext(ContextInterface):
23
+    def __init__(self, app: Flask):
24
+        self.app = app
25
+
26
+    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
27
+        from flask import request
28
+        return RequestParameters(
29
+            path_parameters=request.view_args,
30
+            query_parameters=request.args,  # TODO: Check
31
+            body_parameters=request.get_json(),  # TODO: Check
32
+            form_parameters=request.form,
33
+            header_parameters=request.headers,
34
+            files_parameters={},  # TODO: BS 20171115: Code it
35
+        )
36
+
37
+    def get_response(
38
+        self,
39
+        response: dict,
40
+        http_code: int,
41
+    ) -> 'Response':
42
+        from flask import Response
43
+        return Response(
44
+            response=json.dumps(response),
45
+            mimetype='application/json',
46
+            status=http_code,
47
+        )
48
+
49
+    def get_validation_error_response(
50
+        self,
51
+        error: ProcessValidationError,
52
+        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
53
+    ) -> typing.Any:
54
+        # TODO BS 20171010: Manage error schemas, see #4
55
+        from hapic.hapic import _default_global_error_schema
56
+        unmarshall = _default_global_error_schema.dump(error)
57
+        if unmarshall.errors:
58
+            raise OutputValidationException(
59
+                'Validation error during dump of error response: {}'.format(
60
+                    str(unmarshall.errors)
61
+                )
62
+            )
63
+        from flask import Response
64
+        return Response(
65
+            response=json.dumps(unmarshall.data),
66
+            mimetype='application/json',
67
+            status=int(http_code),
68
+        )
69
+
70
+    def find_route(
71
+        self,
72
+        decorated_controller: 'DecoratedController',
73
+    ):
74
+        reference = decorated_controller.reference
75
+        for route in self.app.url_map.iter_rules():
76
+            if route.endpoint not in self.app.view_functions:
77
+                continue
78
+            route_callback = self.app.view_functions[route.endpoint]
79
+            route_token = getattr(
80
+                route_callback,
81
+                DECORATION_ATTRIBUTE_NAME,
82
+                None,
83
+            )
84
+            match_with_wrapper = route_callback == reference.wrapper
85
+            match_with_wrapped = route_callback == reference.wrapped
86
+            match_with_token = route_token == reference.token
87
+
88
+            # FIXME - G.M - 2017-12-04 - return list instead of one method
89
+            # This fix, return only 1 allowed method, change this when
90
+            # RouteRepresentation is adapted to return multiples methods.
91
+            method = [x for x in route.methods
92
+                      if x not in ['OPTIONS', 'HEAD']][0]
93
+
94
+            if match_with_wrapper or match_with_wrapped or match_with_token:
95
+                return RouteRepresentation(
96
+                    rule=self.get_swagger_path(route.rule),
97
+                    method=method,
98
+                    original_route_object=route,
99
+                )
100
+
101
+    def get_swagger_path(self, contextualised_rule: str) -> str:
102
+        # TODO - G.M - 2017-12-05 Check if all route path are handled correctly
103
+        return FLASK_RE_PATH_URL.sub(r'{\1}', contextualised_rule)
104
+
105
+    def by_pass_output_wrapping(self, response: typing.Any) -> bool:
106
+        return False

+ 2 - 0
hapic/ext/pyramid/__init__.py View File

@@ -0,0 +1,2 @@
1
+# -*- coding: utf-8 -*-
2
+from hapic.ext.pyramid.context import PyramidContext

+ 125 - 0
hapic/ext/pyramid/context.py View File

@@ -0,0 +1,125 @@
1
+# -*- coding: utf-8 -*-
2
+import json
3
+import re
4
+import typing
5
+from http import HTTPStatus
6
+
7
+from hapic.context import ContextInterface
8
+from hapic.context import RouteRepresentation
9
+from hapic.decorator import DecoratedController
10
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
11
+from hapic.ext.bottle.context import BOTTLE_RE_PATH_URL
12
+from hapic.exception import OutputValidationException
13
+from hapic.processor import RequestParameters
14
+from hapic.processor import ProcessValidationError
15
+
16
+if typing.TYPE_CHECKING:
17
+    from pyramid.response import Response
18
+    from pyramid.config import Configurator
19
+
20
+# Bottle regular expression to locate url parameters
21
+PYRAMID_RE_PATH_URL = re.compile(r'')
22
+
23
+
24
+class PyramidContext(ContextInterface):
25
+    def __init__(self, configurator: 'Configurator'):
26
+        self.configurator = configurator
27
+
28
+    def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
29
+        req = args[-1]  # TODO : Check
30
+        # TODO : move this code to check_json
31
+        # same idea as in : https://bottlepy.org/docs/dev/_modules/bottle.html#BaseRequest.json
32
+        if req.body and req.content_type in ('application/json', 'application/json-rpc'):
33
+            json_body = req.json_body
34
+            # TODO : raise exception if not correct , return 400 if uncorrect instead ?
35
+        else:
36
+            json_body = {}
37
+
38
+        return RequestParameters(
39
+            path_parameters=req.matchdict,
40
+            query_parameters=req.GET,
41
+            body_parameters=json_body,
42
+            form_parameters=req.POST,
43
+            header_parameters=req.headers,
44
+            files_parameters={},  # TODO - G.M - 2017-11-05 - Code it
45
+        )
46
+
47
+    def get_response(
48
+        self,
49
+        response: dict,
50
+        http_code: int,
51
+    ) -> 'Response':
52
+        from pyramid.response import Response
53
+        return Response(
54
+            body=json.dumps(response),
55
+            headers=[
56
+                ('Content-Type', 'application/json'),
57
+            ],
58
+            status=http_code,
59
+        )
60
+
61
+    def get_validation_error_response(
62
+        self,
63
+        error: ProcessValidationError,
64
+        http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
65
+    ) -> typing.Any:
66
+        # TODO BS 20171010: Manage error schemas, see #4
67
+        from pyramid.response import Response
68
+        from hapic.hapic import _default_global_error_schema
69
+        unmarshall = _default_global_error_schema.dump(error)
70
+        if unmarshall.errors:
71
+            raise OutputValidationException(
72
+                'Validation error during dump of error response: {}'.format(
73
+                    str(unmarshall.errors)
74
+                )
75
+            )
76
+
77
+        return Response(
78
+            body=json.dumps(unmarshall.data),
79
+            headers=[
80
+                ('Content-Type', 'application/json'),
81
+            ],
82
+            status=int(http_code),
83
+        )
84
+
85
+    def find_route(
86
+        self,
87
+        decorated_controller: DecoratedController,
88
+    ) -> RouteRepresentation:
89
+        for category in self.configurator.introspector.get_category('views'):
90
+            view_intr = category['introspectable']
91
+            route_intr = category['related']
92
+
93
+            reference = decorated_controller.reference
94
+            route_token = getattr(
95
+                view_intr.get('callable'),
96
+                DECORATION_ATTRIBUTE_NAME,
97
+                None,
98
+            )
99
+
100
+            match_with_wrapper = view_intr.get('callable') == reference.wrapper
101
+            match_with_wrapped = view_intr.get('callable') == reference.wrapped
102
+            match_with_token = route_token == reference.token
103
+
104
+            if match_with_wrapper or match_with_wrapped or match_with_token:
105
+                # TODO BS 20171107: C'est une liste de route sous pyramid !!!
106
+                # Mais de toute maniere les framework womme pyramid, flask
107
+                # peuvent avoir un controlleur pour plusieurs routes doc
108
+                # .find_route doit retourner une liste au lieu d'une seule
109
+                # route
110
+                route_pattern = route_intr[0].get('pattern')
111
+                route_method = route_intr[0].get('request_methods')[0]
112
+
113
+                return RouteRepresentation(
114
+                    rule=self.get_swagger_path(route_pattern),
115
+                    method=route_method,
116
+                    original_route_object=route_intr[0],
117
+                )
118
+
119
+    def get_swagger_path(self, contextualised_rule: str) -> str:
120
+        # TODO BS 20171110: Pyramid allow route like '/{foo:\d+}', so adapt
121
+        # and USE regular expression (see https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#custom-route-predicates)  # nopep8
122
+        return contextualised_rule
123
+
124
+    def by_pass_output_wrapping(self, response: typing.Any) -> bool:
125
+        return False

+ 5 - 10
hapic/hapic.py View File

@@ -55,6 +55,7 @@ class Hapic(object):
55 55
         self._buffer = DecorationBuffer()
56 56
         self._controllers = []  # type: typing.List[DecoratedController]
57 57
         self._context = None  # type: ContextInterface
58
+        self.doc_generator = DocGenerator()
58 59
 
59 60
         # This local function will be pass to different components
60 61
         # who will need context but declared (like with decorator)
@@ -340,15 +341,9 @@ class Hapic(object):
340 341
             return decoration.get_wrapper(func)
341 342
         return decorator
342 343
 
343
-    def generate_doc(self, app, title: str='', description: str=''):
344
-        # FIXME: j'ai du tricher avec app, see #11
345
-        # FIXME @Damien bottle specific code ! see #11
346
-        # rendre ca generique
347
-        app = app or self._context.get_app()
348
-        doc_generator = DocGenerator()
349
-        return doc_generator.get_doc(
344
+    def generate_doc(self, title: str='', description: str=''):
345
+        return self.doc_generator.get_doc(
350 346
             self._controllers,
351
-            app,
347
+            self.context,
352 348
             title=title,
353
-            description=description,
354
-        )
349
+            description=description)

+ 6 - 5
setup.py View File

@@ -7,9 +7,6 @@ from os import path
7 7
 here = path.abspath(path.dirname(__file__))
8 8
 
9 9
 install_requires = [
10
-    # TODO: Bottle will be an extension in future, see #1
11
-    # TODO: marshmallow an extension too ? see #2
12
-    'bottle',
13 10
     'marshmallow',
14 11
     'apispec==0.27.1-algoo',
15 12
     'multidict'
@@ -19,10 +16,14 @@ dependency_links = [
19 16
 ]
20 17
 tests_require = [
21 18
     'pytest',
19
+    'pytest-cov',
20
+    'bottle',
21
+    'flask',
22
+    'pyramid',
23
+    'webtest',
22 24
 ]
23 25
 dev_require = [
24
-    'requests',
25
-]
26
+] + tests_require
26 27
 
27 28
 setup(
28 29
     name='hapic',

+ 5 - 2
tests/base.py View File

@@ -4,7 +4,7 @@ from http import HTTPStatus
4 4
 
5 5
 from multidict import MultiDict
6 6
 
7
-from hapic.context import ContextInterface
7
+from hapic.ext.bottle import BottleContext
8 8
 from hapic.processor import RequestParameters
9 9
 from hapic.processor import ProcessValidationError
10 10
 
@@ -13,9 +13,11 @@ class Base(object):
13 13
     pass
14 14
 
15 15
 
16
-class MyContext(ContextInterface):
16
+# TODO BS 20171105: Make this bottle agnostic !
17
+class MyContext(BottleContext):
17 18
     def __init__(
18 19
         self,
20
+        app,
19 21
         fake_path_parameters=None,
20 22
         fake_query_parameters=None,
21 23
         fake_body_parameters=None,
@@ -23,6 +25,7 @@ class MyContext(ContextInterface):
23 25
         fake_header_parameters=None,
24 26
         fake_files_parameters=None,
25 27
     ) -> None:
28
+        super().__init__(app=app)
26 29
         self.fake_path_parameters = fake_path_parameters or {}
27 30
         self.fake_query_parameters = fake_query_parameters or MultiDict()
28 31
         self.fake_body_parameters = fake_body_parameters or {}

+ 18 - 19
tests/ext/unit/test_bottle.py View File

@@ -2,16 +2,15 @@
2 2
 import bottle
3 3
 
4 4
 import hapic
5
-from hapic.doc import find_bottle_route
6 5
 from tests.base import Base
7 6
 
8 7
 
9 8
 class TestBottleExt(Base):
10 9
     def test_unit__map_binding__ok__decorated_function(self):
11 10
         hapic_ = hapic.Hapic()
12
-        hapic_.set_context(hapic.ext.bottle.BottleContext())
13
-
14 11
         app = bottle.Bottle()
12
+        context = hapic.ext.bottle.BottleContext(app=app)
13
+        hapic_.set_context(context)
15 14
 
16 15
         @hapic_.with_api_doc()
17 16
         @app.route('/')
@@ -20,18 +19,18 @@ class TestBottleExt(Base):
20 19
 
21 20
         assert hapic_.controllers
22 21
         decoration = hapic_.controllers[0]
23
-        route = find_bottle_route(decoration, app)
22
+        route = context.find_route(decoration)
24 23
 
25 24
         assert route
26
-        assert route.callback != controller_a
27
-        assert route.callback == decoration.reference.wrapped
28
-        assert route.callback != decoration.reference.wrapper
25
+        assert route.original_route_object.callback != controller_a
26
+        assert route.original_route_object.callback == decoration.reference.wrapped  # nopep8
27
+        assert route.original_route_object.callback != decoration.reference.wrapper  # nopep8
29 28
 
30 29
     def test_unit__map_binding__ok__mapped_function(self):
31 30
         hapic_ = hapic.Hapic()
32
-        hapic_.set_context(hapic.ext.bottle.BottleContext())
33
-
34 31
         app = bottle.Bottle()
32
+        context = hapic.ext.bottle.BottleContext(app=app)
33
+        hapic_.set_context(context)
35 34
 
36 35
         @hapic_.with_api_doc()
37 36
         def controller_a():
@@ -41,18 +40,18 @@ class TestBottleExt(Base):
41 40
 
42 41
         assert hapic_.controllers
43 42
         decoration = hapic_.controllers[0]
44
-        route = find_bottle_route(decoration, app)
43
+        route = context.find_route(decoration)
45 44
 
46 45
         assert route
47
-        assert route.callback == controller_a
48
-        assert route.callback == decoration.reference.wrapper
49
-        assert route.callback != decoration.reference.wrapped
46
+        assert route.original_route_object.callback == controller_a
47
+        assert route.original_route_object.callback == decoration.reference.wrapper  # nopep8
48
+        assert route.original_route_object.callback != decoration.reference.wrapped  # nopep8
50 49
 
51 50
     def test_unit__map_binding__ok__mapped_method(self):
52 51
         hapic_ = hapic.Hapic()
53
-        hapic_.set_context(hapic.ext.bottle.BottleContext())
54
-
55 52
         app = bottle.Bottle()
53
+        context = hapic.ext.bottle.BottleContext(app=app)
54
+        hapic_.set_context(context)
56 55
 
57 56
         class MyControllers(object):
58 57
             def bind(self, app):
@@ -67,11 +66,11 @@ class TestBottleExt(Base):
67 66
 
68 67
         assert hapic_.controllers
69 68
         decoration = hapic_.controllers[0]
70
-        route = find_bottle_route(decoration, app)
69
+        route = context.find_route(decoration)
71 70
 
72 71
         assert route
73 72
         # Important note: instance controller_a method is
74 73
         # not class controller_a, so no matches with callbacks
75
-        assert route.callback != MyControllers.controller_a
76
-        assert route.callback != decoration.reference.wrapped
77
-        assert route.callback != decoration.reference.wrapper
74
+        assert route.original_route_object.callback != MyControllers.controller_a  # nopep8
75
+        assert route.original_route_object.callback != decoration.reference.wrapped  # nopep8
76
+        assert route.original_route_object.callback != decoration.reference.wrapper  # nopep8

+ 0 - 0
tests/func/fake_api/__init__.py View File


+ 92 - 0
tests/func/fake_api/common.py View File

@@ -0,0 +1,92 @@
1
+from collections import OrderedDict
2
+SWAGGER_DOC_API = {
3
+ 'definitions': {
4
+     'AboutResponseSchema': {'properties': {
5
+                                'datetime': {'format': 'date-time',
6
+                                             'type': 'string'},
7
+                                'version': {'type': 'string'}
8
+                                },
9
+                             'required': ['datetime', 'version'],
10
+                             'type': 'object'},
11
+     'ListsUserSchema': {'properties':  {
12
+                            'item_nb': {'format': 'int32',
13
+                                        'minimum': 0,
14
+                                        'type': 'integer'},
15
+                            'items': {
16
+                                'items': {'$ref': '#/definitions/UserSchema'},
17
+                                'type': 'array'},
18
+                            'pagination': {'$ref': '#/definitions/PaginationSchema'}},
19
+                         'required': ['item_nb'],
20
+                         'type': 'object'},
21
+     'NoContentSchema': {'properties': {},
22
+                         'type': 'object'},
23
+     'PaginationSchema': {'properties': {
24
+                            'current_id': {'format': 'int32', 'type': 'integer'},
25
+                            'first_id': {'format': 'int32', 'type': 'integer'},
26
+                            'last_id': {'format': 'int32', 'type': 'integer'}},
27
+                          'required': ['current_id', 'first_id', 'last_id'],
28
+                          'type': 'object'},
29
+     'UserSchema': {'properties': {
30
+                        'company': {'type': 'string'},
31
+                        'display_name': {'type': 'string'},
32
+                        'email_address': {'format': 'email', 'type': 'string'},
33
+                        'first_name': {'type': 'string'},
34
+                        'id': {'format': 'int32', 'type': 'integer'},
35
+                        'last_name': {'type': 'string'},
36
+                        'username': {'type': 'string'}},
37
+                    'required': ['company',
38
+                                 'display_name',
39
+                                 'email_address',
40
+                                 'first_name',
41
+                                 'id',
42
+                                 'last_name',
43
+                                 'username'],
44
+                    'type': 'object'}},
45
+ 'info': {'description': 'just an example of hapic API',
46
+          'title': 'Fake API',
47
+          'version': '1.0.0'},
48
+ 'parameters': {},
49
+ 'paths': OrderedDict(
50
+     [('/about', {
51
+         'get': {
52
+           'description': 'General information about this API.',
53
+           'responses': {200: {
54
+               'description': 'HTTPStatus.OK',
55
+               'schema': {'$ref': '#/definitions/AboutResponseSchema'}}}}}),
56
+      ('/users', {
57
+         'get': {
58
+           'description': 'Obtain users list.',
59
+           'responses': {200: {
60
+               'description': 'HTTPStatus.OK',
61
+               'schema': {'$ref': '#/definitions/ListsUserSchema'}}}}}),
62
+      ('/users/{id}', {
63
+         'delete': {
64
+           'description': 'delete user',
65
+           'parameters': [{'in': 'path',
66
+                           'name': 'id',
67
+                           'required': True,
68
+                           'type': 'integer'}],
69
+           'responses': {204: {
70
+               'description': '204',
71
+               'schema': {'$ref': '#/definitions/NoContentSchema'}}}},
72
+         'get': {
73
+             'description': 'Obtain one user',
74
+             'parameters': [{'in': 'path',
75
+                             'name': 'id',
76
+                             'required': True,
77
+                             'type': 'integer'}],
78
+             'responses': {200: {
79
+                 'description': 'HTTPStatus.OK',
80
+                 'schema': {'$ref': '#/definitions/UserSchema'}}}}}),
81
+      ('/users/', {
82
+         'post': {
83
+             'description': 'Add new user',
84
+             'parameters': [{'in': 'body',
85
+                             'name': 'body',
86
+                             'schema': {'$ref': '#/definitions/UserSchema'}}],
87
+             'responses': {200: {
88
+                 'description': 'HTTPStatus.OK',
89
+                 'schema': {'$ref': '#/definitions/UserSchema'}}}}})]),
90
+ 'swagger': '2.0',
91
+ 'tags': []
92
+}

+ 94 - 0
tests/func/fake_api/test_bottle.py View File

@@ -0,0 +1,94 @@
1
+from webtest import TestApp
2
+from bottle import Bottle
3
+
4
+from hapic.ext.bottle import BottleContext
5
+from example.fake_api.bottle_api import BottleController
6
+from tests.func.fake_api.common import SWAGGER_DOC_API
7
+from example.fake_api.bottle_api import hapic
8
+
9
+
10
+def test_func_bottle_fake_api():
11
+    bottle_app = Bottle()
12
+    controllers = BottleController()
13
+    controllers.bind(bottle_app)
14
+
15
+    hapic.set_context(BottleContext(bottle_app))
16
+    app = TestApp(bottle_app)
17
+    doc = hapic.generate_doc(
18
+        title='Fake API',
19
+        description='just an example of hapic API'
20
+    )
21
+
22
+    assert doc == SWAGGER_DOC_API
23
+    resp = app.get('/about')
24
+    assert resp.status_int == 200
25
+    assert resp.json == {'datetime': '2017-12-07T10:55:08.488996+00:00',
26
+                         'version': '1.2.3'}
27
+
28
+    resp = app.get('/users')
29
+    assert resp.status_int == 200
30
+    assert resp.json == {
31
+        'items':
32
+        [
33
+            {
34
+                'username': 'some_user',
35
+                'display_name': 'Damien Accorsi',
36
+                'company': 'Algoo', 'id': 4
37
+            }
38
+        ],
39
+        'pagination': {
40
+            'first_id': 0,
41
+            'last_id': 5,
42
+            'current_id': 0,
43
+        },
44
+        'item_nb': 1,
45
+    }
46
+
47
+    resp = app.get('/users/1')
48
+    assert resp.status_int == 200
49
+    assert resp.json == {
50
+        'last_name': 'Accorsi',
51
+        'username': 'some_user',
52
+        'first_name': 'Damien',
53
+        'id': 4,
54
+        'display_name': 'Damien Accorsi',
55
+        'email_address': 'some.user@hapic.com',
56
+        'company': 'Algoo'
57
+    }
58
+
59
+    resp = app.post('/users/', status='*')
60
+    assert resp.status_int == 400
61
+    assert resp.json == {
62
+        'details': {
63
+            'email_address': ['Missing data for required field.'],
64
+            'username': ['Missing data for required field.'],
65
+            'display_name': ['Missing data for required field.'],
66
+            'last_name': ['Missing data for required field.'],
67
+            'first_name': ['Missing data for required field.'],
68
+            'company': ['Missing data for required field.']},
69
+        'message': 'Validation error of input data'}
70
+
71
+    user = {
72
+        'email_address': 'some.user@hapic.com',
73
+        'username': 'some_user',
74
+        'display_name': 'Damien Accorsi',
75
+        'last_name': 'Accorsi',
76
+        'first_name': 'Damien',
77
+        'company': 'Algoo',
78
+    }
79
+
80
+    resp = app.post_json('/users/', user)
81
+    assert resp.status_int == 200
82
+    assert resp.json == {
83
+        'last_name': 'Accorsi',
84
+        'username': 'some_user',
85
+        'first_name': 'Damien',
86
+        'id': 4,
87
+        'display_name': 'Damien Accorsi',
88
+        'email_address': 'some.user@hapic.com',
89
+        'company': 'Algoo',
90
+    }
91
+
92
+    resp = app.delete('/users/1', status='*')
93
+    assert resp.status_int == 204
94
+

+ 94 - 0
tests/func/fake_api/test_flask.py View File

@@ -0,0 +1,94 @@
1
+from webtest import TestApp
2
+from hapic.ext.flask import FlaskContext
3
+from flask import Flask
4
+from example.fake_api.flask_api import FlaskController
5
+from example.fake_api.flask_api import hapic
6
+from tests.func.fake_api.common import SWAGGER_DOC_API
7
+
8
+
9
+def test_func_flask_fake_api():
10
+    flask_app = Flask(__name__)
11
+    controllers = FlaskController()
12
+    controllers.bind(flask_app)
13
+    hapic.set_context(FlaskContext(flask_app))
14
+    app = TestApp(flask_app)
15
+    doc =  hapic.generate_doc(
16
+        title='Fake API',
17
+        description='just an example of hapic API'
18
+    )
19
+
20
+    assert doc == SWAGGER_DOC_API
21
+    resp = app.get('/about')
22
+    assert resp.status_int == 200
23
+    assert resp.json == {'datetime': '2017-12-07T10:55:08.488996+00:00',
24
+                         'version': '1.2.3'}
25
+
26
+    resp = app.get('/users')
27
+    assert resp.status_int == 200
28
+    assert resp.json == {
29
+        'items':
30
+        [
31
+            {
32
+                'username': 'some_user',
33
+                'display_name': 'Damien Accorsi',
34
+                'company': 'Algoo', 'id': 4
35
+            }
36
+        ],
37
+        'pagination': {
38
+            'first_id': 0,
39
+            'last_id': 5,
40
+            'current_id': 0,
41
+        },
42
+        'item_nb': 1,
43
+    }
44
+
45
+    resp = app.get('/users/1')
46
+    assert resp.status_int == 200
47
+    assert resp.json == {
48
+        'last_name': 'Accorsi',
49
+        'username': 'some_user',
50
+        'first_name': 'Damien',
51
+        'id': 4,
52
+        'display_name': 'Damien Accorsi',
53
+        'email_address': 'some.user@hapic.com',
54
+        'company': 'Algoo'
55
+    }
56
+
57
+    resp = app.post('/users/', status='*')
58
+    assert resp.status_int == 400
59
+    assert resp.json == {
60
+        'details': {
61
+            'email_address': ['Missing data for required field.'],
62
+            'username': ['Missing data for required field.'],
63
+            'display_name': ['Missing data for required field.'],
64
+            'last_name': ['Missing data for required field.'],
65
+            'first_name': ['Missing data for required field.'],
66
+            'company': ['Missing data for required field.']},
67
+        'message': 'Validation error of input data'}
68
+
69
+    user = {
70
+        'email_address': 'some.user@hapic.com',
71
+        'username': 'some_user',
72
+        'display_name': 'Damien Accorsi',
73
+        'last_name': 'Accorsi',
74
+        'first_name': 'Damien',
75
+        'company': 'Algoo',
76
+    }
77
+
78
+    resp = app.post_json('/users/', user)
79
+    assert resp.status_int == 200
80
+    assert resp.json == {
81
+        'last_name': 'Accorsi',
82
+        'username': 'some_user',
83
+        'first_name': 'Damien',
84
+        'id': 4,
85
+        'display_name': 'Damien Accorsi',
86
+        'email_address': 'some.user@hapic.com',
87
+        'company': 'Algoo',
88
+    }
89
+    # INFO - G.M - 2017-12-07 - Warning due to Webtest check
90
+    # Webtest check content_type. Up to know flask_api return json as
91
+    # content_type with 204 NO CONTENT status code which return a warning in
92
+    # WebTest check.
93
+    resp = app.delete('/users/1', status='*')
94
+    assert resp.status_int == 204

+ 96 - 0
tests/func/fake_api/test_pyramid.py View File

@@ -0,0 +1,96 @@
1
+from webtest import TestApp
2
+from pyramid.config import Configurator
3
+import hapic
4
+from hapic.ext.pyramid import PyramidContext
5
+from example.fake_api.pyramid_api import hapic
6
+from example.fake_api.pyramid_api import PyramidController
7
+from tests.func.fake_api.common import SWAGGER_DOC_API
8
+
9
+
10
+def test_func_pyramid_fake_api_doc():
11
+    configurator = Configurator(autocommit=True)
12
+    controllers = PyramidController()
13
+    controllers.bind(configurator)
14
+    hapic.set_context(PyramidContext(configurator))
15
+    app = TestApp(configurator.make_wsgi_app())
16
+    doc = hapic.generate_doc(
17
+        title='Fake API',
18
+        description='just an example of hapic API'
19
+    )
20
+
21
+    assert doc == SWAGGER_DOC_API
22
+    resp = app.get('/about')
23
+    assert resp.status_int == 200
24
+    assert resp.json == {'datetime': '2017-12-07T10:55:08.488996+00:00',
25
+                         'version': '1.2.3'}
26
+
27
+    resp = app.get('/users')
28
+    assert resp.status_int == 200
29
+    assert resp.json == {
30
+        'items':
31
+        [
32
+            {
33
+                'username': 'some_user',
34
+                'display_name': 'Damien Accorsi',
35
+                'company': 'Algoo', 'id': 4
36
+            }
37
+        ],
38
+        'pagination': {
39
+            'first_id': 0,
40
+            'last_id': 5,
41
+            'current_id': 0,
42
+        },
43
+        'item_nb': 1,
44
+    }
45
+
46
+    resp = app.get('/users/1')
47
+    assert resp.status_int == 200
48
+    assert resp.json == {
49
+        'last_name': 'Accorsi',
50
+        'username': 'some_user',
51
+        'first_name': 'Damien',
52
+        'id': 4,
53
+        'display_name': 'Damien Accorsi',
54
+        'email_address': 'some.user@hapic.com',
55
+        'company': 'Algoo'
56
+    }
57
+
58
+    resp = app.post('/users/', status='*')
59
+    assert resp.status_int == 400
60
+    assert resp.json == {
61
+        'details': {
62
+            'email_address': ['Missing data for required field.'],
63
+            'username': ['Missing data for required field.'],
64
+            'display_name': ['Missing data for required field.'],
65
+            'last_name': ['Missing data for required field.'],
66
+            'first_name': ['Missing data for required field.'],
67
+            'company': ['Missing data for required field.']},
68
+        'message': 'Validation error of input data'}
69
+
70
+    user = {
71
+        'email_address': 'some.user@hapic.com',
72
+        'username': 'some_user',
73
+        'display_name': 'Damien Accorsi',
74
+        'last_name': 'Accorsi',
75
+        'first_name': 'Damien',
76
+        'company': 'Algoo',
77
+    }
78
+
79
+    resp = app.post_json('/users/', user)
80
+    assert resp.status_int == 200
81
+    assert resp.json == {
82
+        'last_name': 'Accorsi',
83
+        'username': 'some_user',
84
+        'first_name': 'Damien',
85
+        'id': 4,
86
+        'display_name': 'Damien Accorsi',
87
+        'email_address': 'some.user@hapic.com',
88
+        'company': 'Algoo',
89
+    }
90
+
91
+    # INFO - G.M - 2017-12-07 - Test for delete desactivated(Webtest check fail)
92
+    # Webtest check content_type. Up to know pyramid_api return json as
93
+    # content_type with 204 NO CONTENT status code which return an error in
94
+    # WebTest check.
95
+    # resp = app.delete('/users/1', status='*')
96
+    # assert resp.status_int == 204

+ 10 - 10
tests/func/test_doc.py View File

@@ -10,8 +10,8 @@ from tests.base import MyContext
10 10
 class TestDocGeneration(Base):
11 11
     def test_func__input_files_doc__ok__one_file(self):
12 12
         hapic = Hapic()
13
-        hapic.set_context(MyContext())
14 13
         app = bottle.Bottle()
14
+        hapic.set_context(MyContext(app=app))
15 15
 
16 16
         class MySchema(marshmallow.Schema):
17 17
             file_abc = marshmallow.fields.Raw(required=True)
@@ -23,7 +23,7 @@ class TestDocGeneration(Base):
23 23
             assert hapic_data.files
24 24
 
25 25
         app.route('/upload', method='POST', callback=my_controller)
26
-        doc = hapic.generate_doc(app)
26
+        doc = hapic.generate_doc()
27 27
 
28 28
         assert doc
29 29
         assert '/upload' in doc['paths']
@@ -39,8 +39,8 @@ class TestDocGeneration(Base):
39 39
 
40 40
     def test_func__input_files_doc__ok__two_file(self):
41 41
         hapic = Hapic()
42
-        hapic.set_context(MyContext())
43 42
         app = bottle.Bottle()
43
+        hapic.set_context(MyContext(app=app))
44 44
 
45 45
         class MySchema(marshmallow.Schema):
46 46
             file_abc = marshmallow.fields.Raw(required=True)
@@ -53,7 +53,7 @@ class TestDocGeneration(Base):
53 53
             assert hapic_data.files
54 54
 
55 55
         app.route('/upload', method='POST', callback=my_controller)
56
-        doc = hapic.generate_doc(app)
56
+        doc = hapic.generate_doc()
57 57
 
58 58
         assert doc
59 59
         assert '/upload' in doc['paths']
@@ -75,8 +75,8 @@ class TestDocGeneration(Base):
75 75
 
76 76
     def test_func__output_file_doc__ok__nominal_case(self):
77 77
         hapic = Hapic()
78
-        hapic.set_context(MyContext())
79 78
         app = bottle.Bottle()
79
+        hapic.set_context(MyContext(app=app))
80 80
 
81 81
         @hapic.with_api_doc()
82 82
         @hapic.output_file(['image/jpeg'])
@@ -84,7 +84,7 @@ class TestDocGeneration(Base):
84 84
             return b'101010100101'
85 85
 
86 86
         app.route('/avatar', method='GET', callback=my_controller)
87
-        doc = hapic.generate_doc(app)
87
+        doc = hapic.generate_doc()
88 88
 
89 89
         assert doc
90 90
         assert '/avatar' in doc['paths']
@@ -94,8 +94,8 @@ class TestDocGeneration(Base):
94 94
 
95 95
     def test_func__input_files_doc__ok__one_file_and_text(self):
96 96
         hapic = Hapic()
97
-        hapic.set_context(MyContext())
98 97
         app = bottle.Bottle()
98
+        hapic.set_context(MyContext(app=app))
99 99
 
100 100
         class MySchema(marshmallow.Schema):
101 101
             name = marshmallow.fields.String(required=True)
@@ -111,7 +111,7 @@ class TestDocGeneration(Base):
111 111
             assert hapic_data.files
112 112
 
113 113
         app.route('/upload', method='POST', callback=my_controller)
114
-        doc = hapic.generate_doc(app)
114
+        doc = hapic.generate_doc()
115 115
 
116 116
         assert doc
117 117
         assert '/upload' in doc['paths']
@@ -127,8 +127,8 @@ class TestDocGeneration(Base):
127 127
 
128 128
     def test_func__docstring__ok__simple_case(self):
129 129
         hapic = Hapic()
130
-        hapic.set_context(MyContext())
131 130
         app = bottle.Bottle()
131
+        hapic.set_context(MyContext(app=app))
132 132
 
133 133
         # TODO BS 20171113: Make this test non-bottle
134 134
         @hapic.with_api_doc()
@@ -140,7 +140,7 @@ class TestDocGeneration(Base):
140 140
             assert hapic_data.files
141 141
 
142 142
         app.route('/upload', method='POST', callback=my_controller)
143
-        doc = hapic.generate_doc(app)
143
+        doc = hapic.generate_doc()
144 144
 
145 145
         assert doc.get('paths')
146 146
         assert '/upload' in doc['paths']

+ 18 - 9
tests/func/test_marshmallow_decoration.py View File

@@ -11,9 +11,12 @@ from tests.base import MyContext
11 11
 class TestMarshmallowDecoration(Base):
12 12
     def test_unit__input_files__ok__file_is_present(self):
13 13
         hapic = Hapic()
14
-        hapic.set_context(MyContext(fake_files_parameters={
15
-            'file_abc': '10101010101',
16
-        }))
14
+        hapic.set_context(MyContext(
15
+            app=None,
16
+            fake_files_parameters={
17
+                'file_abc': '10101010101',
18
+            }
19
+        ))
17 20
 
18 21
         class MySchema(marshmallow.Schema):
19 22
             file_abc = marshmallow.fields.Raw(required=True)
@@ -30,9 +33,12 @@ class TestMarshmallowDecoration(Base):
30 33
 
31 34
     def test_unit__input_files__ok__file_is_not_present(self):
32 35
         hapic = Hapic()
33
-        hapic.set_context(MyContext(fake_files_parameters={
34
-            # No file here
35
-        }))
36
+        hapic.set_context(MyContext(
37
+            app=None,
38
+            fake_files_parameters={
39
+                # No file here
40
+            }
41
+        ))
36 42
 
37 43
         class MySchema(marshmallow.Schema):
38 44
             file_abc = marshmallow.fields.Raw(required=True)
@@ -53,9 +59,12 @@ class TestMarshmallowDecoration(Base):
53 59
 
54 60
     def test_unit__input_files__ok__file_is_empty_string(self):
55 61
         hapic = Hapic()
56
-        hapic.set_context(MyContext(fake_files_parameters={
57
-            'file_abc': '',
58
-        }))
62
+        hapic.set_context(MyContext(
63
+            app=None,
64
+            fake_files_parameters={
65
+                'file_abc': '',
66
+            }
67
+        ))
59 68
 
60 69
         class MySchema(marshmallow.Schema):
61 70
             file_abc = marshmallow.fields.Raw(required=True)

+ 32 - 23
tests/unit/test_decorator.py View File

@@ -90,7 +90,7 @@ class MySchema(marshmallow.Schema):
90 90
 
91 91
 class TestControllerWrapper(Base):
92 92
     def test_unit__base_controller_wrapper__ok__no_behaviour(self):
93
-        context = MyContext()
93
+        context = MyContext(app=None)
94 94
         processor = MyProcessor()
95 95
         wrapper = InputOutputControllerWrapper(context, processor)
96 96
 
@@ -102,7 +102,7 @@ class TestControllerWrapper(Base):
102 102
         assert result == 42
103 103
 
104 104
     def test_unit__base_controller__ok__replaced_response(self):
105
-        context = MyContext()
105
+        context = MyContext(app=None)
106 106
         processor = MyProcessor()
107 107
         wrapper = MyControllerWrapper(context, processor)
108 108
 
@@ -116,7 +116,7 @@ class TestControllerWrapper(Base):
116 116
         assert {'error_response': 'we are testing'} == result
117 117
 
118 118
     def test_unit__controller_wrapper__ok__overload_input(self):
119
-        context = MyContext()
119
+        context = MyContext(app=None)
120 120
         processor = MyProcessor()
121 121
         wrapper = MyControllerWrapper(context, processor)
122 122
 
@@ -133,11 +133,14 @@ class TestControllerWrapper(Base):
133 133
 
134 134
 class TestInputControllerWrapper(Base):
135 135
     def test_unit__input_data_wrapping__ok__nominal_case(self):
136
-        context = MyContext(fake_query_parameters=MultiDict(
137
-            (
138
-                ('foo', 'bar',),
136
+        context = MyContext(
137
+            app=None,
138
+            fake_query_parameters=MultiDict(
139
+                (
140
+                    ('foo', 'bar',),
141
+                )
139 142
             )
140
-        ))
143
+        )
141 144
         processor = MyProcessor()
142 145
         wrapper = MyInputQueryControllerWrapper(context, processor)
143 146
 
@@ -153,12 +156,15 @@ class TestInputControllerWrapper(Base):
153 156
         assert result == 42
154 157
 
155 158
     def test_unit__multi_query_param_values__ok__use_as_list(self):
156
-        context = MyContext(fake_query_parameters=MultiDict(
157
-            (
158
-                ('user_id', 'abc'),
159
-                ('user_id', 'def'),
160
-            ),
161
-        ))
159
+        context = MyContext(
160
+            app=None,
161
+            fake_query_parameters=MultiDict(
162
+                (
163
+                    ('user_id', 'abc'),
164
+                    ('user_id', 'def'),
165
+                ),
166
+            )
167
+        )
162 168
         processor = MySimpleProcessor()
163 169
         wrapper = InputQueryControllerWrapper(
164 170
             context,
@@ -178,12 +184,15 @@ class TestInputControllerWrapper(Base):
178 184
         assert result == ['abc', 'def']
179 185
 
180 186
     def test_unit__multi_query_param_values__ok__without_as_list(self):
181
-        context = MyContext(fake_query_parameters=MultiDict(
182
-            (
183
-                ('user_id', 'abc'),
184
-                ('user_id', 'def'),
185
-            ),
186
-        ))
187
+        context = MyContext(
188
+            app=None,
189
+            fake_query_parameters=MultiDict(
190
+                (
191
+                    ('user_id', 'abc'),
192
+                    ('user_id', 'def'),
193
+                ),
194
+            )
195
+        )
187 196
         processor = MySimpleProcessor()
188 197
         wrapper = InputQueryControllerWrapper(
189 198
             context,
@@ -204,7 +213,7 @@ class TestInputControllerWrapper(Base):
204 213
 
205 214
 class TestOutputControllerWrapper(Base):
206 215
     def test_unit__output_data_wrapping__ok__nominal_case(self):
207
-        context = MyContext()
216
+        context = MyContext(app=None)
208 217
         processor = MyProcessor()
209 218
         wrapper = OutputControllerWrapper(context, processor)
210 219
 
@@ -222,7 +231,7 @@ class TestOutputControllerWrapper(Base):
222 231
                } == result
223 232
 
224 233
     def test_unit__output_data_wrapping__fail__error_response(self):
225
-        context = MyContext()
234
+        context = MyContext(app=None)
226 235
         processor = MarshmallowOutputProcessor()
227 236
         processor.schema = MySchema()
228 237
         wrapper = OutputControllerWrapper(context, processor)
@@ -244,7 +253,7 @@ class TestOutputControllerWrapper(Base):
244 253
 
245 254
 class TestExceptionHandlerControllerWrapper(Base):
246 255
     def test_unit__exception_handled__ok__nominal_case(self):
247
-        context = MyContext()
256
+        context = MyContext(app=None)
248 257
         wrapper = ExceptionHandlerControllerWrapper(
249 258
             ZeroDivisionError,
250 259
             context,
@@ -272,7 +281,7 @@ class TestExceptionHandlerControllerWrapper(Base):
272 281
                 super().__init__(*args, **kwargs)
273 282
                 self.error_dict = {}
274 283
 
275
-        context = MyContext()
284
+        context = MyContext(app=None)
276 285
         wrapper = ExceptionHandlerControllerWrapper(
277 286
             MyException,
278 287
             context,