Browse Source

Make doc generation more generic, see #11 + Pyramid context

Bastien Sevajol 6 years ago
parent
commit
b76b6bc3e0
7 changed files with 117 additions and 109 deletions
  1. 2 0
      .gitignore
  2. 14 63
      example_a_pyramid.py
  3. 28 5
      hapic/context.py
  4. 9 32
      hapic/doc.py
  5. 4 0
      hapic/ext/bottle/context.py
  6. 57 2
      hapic/ext/pyramid/context.py
  7. 3 7
      hapic/hapic.py

+ 2 - 0
.gitignore View File

@@ -6,3 +6,5 @@ __pycache__
6 6
 .cache
7 7
 *.egg-info
8 8
 .coverage
9
+/build
10
+/dist

+ 14 - 63
example_a_pyramid.py View File

@@ -102,73 +102,24 @@ class Controllers(object):
102 102
             'name': name,
103 103
         }
104 104
 
105
-    def bind(self, app):
106
-        app.route('/hello/{name}', callback=self.hello)
107
-        app.route('/hello/{name}', callback=self.hello2, method='POST')
108
-        app.route('/hello3/{name}', callback=self.hello3)
109
-        app.config.include('pyramid_debugtoolbar')
105
+    def bind(self, configurator: Configurator):
106
+        configurator.add_route('hello', '/hello/{name}', request_method='GET')
107
+        configurator.add_view(self.hello, route_name='hello', renderer='json')
110 108
 
109
+        configurator.add_route('hello2', '/hello/{name}', request_method='POST')  # nopep8
110
+        configurator.add_view(self.hello2, route_name='hello2', renderer='json')  # nopep8
111 111
 
112
-class PyramRoute(object):
112
+        configurator.add_route('hello3', '/hello3/{name}', request_method='GET')  # nopep8
113
+        configurator.add_view(self.hello3, route_name='hello3', renderer='json')  # nopep8
113 114
 
114
-    def __init__(self, app, rule, method, callback, name, **options):
115
-        self.app = app
116
-        self.rule = rule
117
-        self.method = method
118
-        self.callback = callback
119
-        self.name = name
120 115
 
121
-        if not self.name:
122
-            self.name = str(uuid.uuid4())
123
-
124
-        with self.app.config as config:
125
-            config.add_route(self.name, self.rule, request_method=self.method)
126
-            config.add_view(
127
-                self.callback, route_name=self.name, renderer='json')
128
-        #import pdb; pdb.set_trace()
129
-
130
-
131
-class Pyramidapp(object):
132
-
133
-    def __init__(self):
134
-        self.config = Configurator()
135
-        self.routes = []
136
-
137
-    def route(self,
138
-              rule,
139
-              callback,
140
-              method='GET',
141
-              name=None,
142
-              **options):
143
-        r = PyramRoute(self, rule, method, callback, name, **options)
144
-        self.routes.append(r)
145
-
146
-    def run(self, host, port, debug):
147
-        server = make_server('0.0.0.0', port, self.config.make_wsgi_app())
148
-        server.serve_forever()
116
+configurator = Configurator(autocommit=True)
117
+controllers = Controllers()
149 118
 
119
+controllers.bind(configurator)
150 120
 
151
-app = Pyramidapp()
121
+hapic.set_context(hapic.ext.pyramid.PyramidContext(configurator))
122
+print(json.dumps(hapic.generate_doc()))
152 123
 
153
-controllers = Controllers()
154
-controllers.bind(app)
155
-
156
-
157
-# time.sleep(1)
158
-# s = hapic.generate_doc(app)
159
-# ss = json.loads(json.dumps(s))
160
-# for path in ss['paths']:
161
-#     for method in ss['paths'][path]:
162
-#         for response_code in ss['paths'][path][method]['responses']:
163
-#             ss['paths'][path][method]['responses'][int(response_code)] = ss['paths'][path][method]['responses'][response_code]
164
-#             del ss['paths'][path][method]['responses'][int(response_code)]
165
-# print(yaml.dump(ss, default_flow_style=False))
166
-# time.sleep(1)
167
-
168
-# hapic.set_context(hapic.ext.bottle.BottleContext())
169
-hapic.set_context(hapic.ext.pyramid.PyramidContext())
170
-#import pdb; pdb.set_trace()
171
-print(json.dumps(hapic.generate_doc(app)))
172
-app.run('localhost', 8080, True)
173
-#server = make_server('0.0.0.0', 8080, app.config.make_wsgi_app())
174
-# server.serve_forever()
124
+server = make_server('0.0.0.0', 8080, configurator.make_wsgi_app())
125
+server.serve_forever()

+ 28 - 5
hapic/context.py View File

@@ -1,13 +1,22 @@
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
+    ) -> None:
18
+        self.rule = rule
19
+        self.method = method
11 20
 
12 21
 
13 22
 class ContextInterface(object):
@@ -27,3 +36,17 @@ class ContextInterface(object):
27 36
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
28 37
     ) -> typing.Any:
29 38
         raise NotImplementedError()
39
+
40
+    def find_route(
41
+        self,
42
+        decorated_controller: 'DecoratedController',
43
+    ) -> RouteRepresentation:
44
+        raise NotImplementedError()
45
+
46
+    def get_swagger_path(self, contextualised_rule: str) -> str:
47
+        """
48
+        Return OpenAPI path with context path
49
+        :param contextualised_rule: path of original context
50
+        :return: OpenAPI path
51
+        """
52
+        raise NotImplementedError()

+ 9 - 32
hapic/doc.py View File

@@ -1,5 +1,4 @@
1 1
 # -*- coding: utf-8 -*-
2
-import re
3 2
 import typing
4 3
 
5 4
 import bottle
@@ -7,15 +6,13 @@ from apispec import APISpec
7 6
 from apispec import Path
8 7
 from apispec.ext.marshmallow.swagger import schema2jsonschema
9 8
 
10
-from hapic.decorator import DecoratedController
9
+from hapic.context import ContextInterface, RouteRepresentation
11 10
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
11
+from hapic.decorator import DecoratedController
12 12
 from hapic.description import ControllerDescription
13 13
 from hapic.exception import NoRoutesException
14 14
 from hapic.exception import RouteNotFound
15 15
 
16
-# Bottle regular expression to locate url parameters
17
-BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
18
-
19 16
 
20 17
 def find_bottle_route(
21 18
     decorated_controller: DecoratedController,
@@ -48,7 +45,7 @@ def find_bottle_route(
48 45
 
49 46
 def bottle_generate_operations(
50 47
     spec,
51
-    bottle_route: bottle.Route,
48
+    route: RouteRepresentation,
52 49
     description: ControllerDescription,
53 50
 ):
54 51
     method_operations = dict()
@@ -110,7 +107,7 @@ def bottle_generate_operations(
110 107
             })
111 108
 
112 109
     operations = {
113
-        bottle_route.method.lower(): method_operations,
110
+        route.method.lower(): method_operations,
114 111
     }
115 112
 
116 113
     return operations
@@ -120,21 +117,13 @@ class DocGenerator(object):
120 117
     def get_doc(
121 118
         self,
122 119
         controllers: typing.List[DecoratedController],
123
-        app,
120
+        context: ContextInterface,
124 121
     ) -> dict:
125
-        # TODO: Découper, see #11
126
-        # TODO: bottle specific code !, see #11
127
-        if not app:
128
-            app = bottle.default_app()
129
-        else:
130
-            bottle.default_app.push(app)
131
-        flatten = lambda l: [item for sublist in l for item in sublist]
132
-
133 122
         spec = APISpec(
134 123
             title='Swagger Petstore',
135 124
             version='1.0.0',
136 125
             plugins=[
137
-                'apispec.ext.bottle',
126
+                # 'apispec.ext.bottle',
138 127
                 'apispec.ext.marshmallow',
139 128
             ],
140 129
         )
@@ -170,12 +159,12 @@ class DocGenerator(object):
170 159
         # with app.test_request_context():
171 160
         paths = {}
172 161
         for controller in controllers:
173
-            bottle_route = find_bottle_route(controller, app)
174
-            swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
162
+            route = context.find_route(controller)
163
+            swagger_path = context.get_swagger_path(route.rule)
175 164
 
176 165
             operations = bottle_generate_operations(
177 166
                 spec,
178
-                bottle_route,
167
+                route,
179 168
                 controller.description,
180 169
             )
181 170
 
@@ -189,15 +178,3 @@ class DocGenerator(object):
189 178
             spec.add_path(path)
190 179
 
191 180
         return spec.to_dict()
192
-
193
-        # route_by_callbacks = []
194
-        # routes = flatten(app.router.dyna_routes.values())
195
-        # for path, path_regex, route, func_ in routes:
196
-        #     route_by_callbacks.append(route.callback)
197
-        #
198
-        # for description in self._controllers:
199
-        #     for path, path_regex, route, func_ in routes:
200
-        #         if route.callback == description.reference:
201
-        #             # TODO: use description to feed apispec
202
-        #             print(route.method, path, description)
203
-        #             continue

+ 4 - 0
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
 
@@ -9,6 +10,9 @@ from hapic.context import ContextInterface
9 10
 from hapic.exception import OutputValidationException
10 11
 from hapic.processor import RequestParameters, ProcessValidationError
11 12
 
13
+# Bottle regular expression to locate url parameters
14
+BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
15
+
12 16
 
13 17
 class BottleContext(ContextInterface):
14 18
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:

+ 57 - 2
hapic/ext/pyramid/context.py View File

@@ -1,18 +1,30 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import json
3
+import re
3 4
 import typing
4 5
 from http import HTTPStatus
5 6
 
6 7
 from pyramid.request import Request
7 8
 from pyramid.response import Response
8
-
9
+from pyramid.config import Configurator
9 10
 
10 11
 from hapic.context import ContextInterface
12
+from hapic.context import RouteRepresentation
13
+from hapic.decorator import DecoratedController
14
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
15
+from hapic.ext.bottle.context import BOTTLE_RE_PATH_URL
11 16
 from hapic.exception import OutputValidationException
12
-from hapic.processor import RequestParameters, ProcessValidationError
17
+from hapic.processor import RequestParameters
18
+from hapic.processor import ProcessValidationError
19
+
20
+# Bottle regular expression to locate url parameters
21
+PYRAMID_RE_PATH_URL = re.compile(r'')
13 22
 
14 23
 
15 24
 class PyramidContext(ContextInterface):
25
+    def __init__(self, configurator: Configurator):
26
+        self.configurator = configurator
27
+
16 28
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
17 29
         req = args[-1]  # TODO : Check
18 30
         assert isinstance(req, Request)
@@ -67,3 +79,46 @@ class PyramidContext(ContextInterface):
67 79
             ],
68 80
             status=int(http_code),
69 81
         )
82
+
83
+    def find_route(
84
+        self,
85
+        decorated_controller: DecoratedController,
86
+    ) -> RouteRepresentation:
87
+        for category in self.configurator.introspector.get_category('views'):
88
+            view_intr = category['introspectable']
89
+            route_intr = category['related']
90
+
91
+            reference = decorated_controller.reference
92
+            route_token = getattr(
93
+                view_intr.get('callable'),
94
+                DECORATION_ATTRIBUTE_NAME,
95
+                None,
96
+            )
97
+
98
+            match_with_wrapper = view_intr.get('callable') == reference.wrapper
99
+            match_with_wrapped = view_intr.get('callable') == reference.wrapped
100
+            match_with_token = route_token == reference.token
101
+
102
+            if match_with_wrapper or match_with_wrapped or match_with_token:
103
+                # TODO BS 20171107: C'est une liste de route sous pyramid !!!
104
+                # Mais de toute maniere les framework womme pyramid, flask
105
+                # peuvent avoir un controlleur pour plusieurs routes doc
106
+                # .find_route doit retourner une liste au lieu d'une seule
107
+                # route
108
+                route_pattern = route_intr[0].get('pattern')
109
+                route_method = route_intr[0].get('request_methods')[0]
110
+
111
+                return RouteRepresentation(
112
+                    # TODO BS 20171107: ce code n'es pas du tout finis
113
+                    # (import bottle)
114
+                    rule=BOTTLE_RE_PATH_URL.sub(
115
+                        r'{\1}',
116
+                        route_pattern,
117
+                    ),
118
+                    method=route_method,
119
+                )
120
+
121
+    def get_swagger_path(self, contextualised_rule: str) -> str:
122
+        # TODO BS 20171110: Pyramid allow route like '/{foo:\d+}', so adapt
123
+        # and USE regular expression (see https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#custom-route-predicates)  # nopep8
124
+        return contextualised_rule

+ 3 - 7
hapic/hapic.py View File

@@ -50,6 +50,7 @@ class Hapic(object):
50 50
         self._buffer = DecorationBuffer()
51 51
         self._controllers = []  # type: typing.List[DecoratedController]
52 52
         self._context = None  # type: ContextInterface
53
+        self.doc_generator = DocGenerator()
53 54
 
54 55
         # This local function will be pass to different components
55 56
         # who will need context but declared (like with decorator)
@@ -289,10 +290,5 @@ class Hapic(object):
289 290
             return decoration.get_wrapper(func)
290 291
         return decorator
291 292
 
292
-    def generate_doc(self, app):
293
-        # FIXME: j'ai du tricher avec app, see #11
294
-        # FIXME @Damien bottle specific code ! see #11
295
-        # rendre ca generique
296
-        app = app or self._context.get_app()
297
-        doc_generator = DocGenerator()
298
-        return doc_generator.get_doc(self._controllers, app)
293
+    def generate_doc(self):
294
+        return self.doc_generator.get_doc(self._controllers, self.context)