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
 .cache
6
 .cache
7
 *.egg-info
7
 *.egg-info
8
 .coverage
8
 .coverage
9
+/build
10
+/dist

+ 14 - 63
example_a_pyramid.py View File

102
             'name': name,
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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import json
3
 import typing
2
 import typing
4
 from http import HTTPStatus
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
 class ContextInterface(object):
22
 class ContextInterface(object):
27
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
36
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
28
     ) -> typing.Any:
37
     ) -> typing.Any:
29
         raise NotImplementedError()
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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-import re
3
 import typing
2
 import typing
4
 
3
 
5
 import bottle
4
 import bottle
7
 from apispec import Path
6
 from apispec import Path
8
 from apispec.ext.marshmallow.swagger import schema2jsonschema
7
 from apispec.ext.marshmallow.swagger import schema2jsonschema
9
 
8
 
10
-from hapic.decorator import DecoratedController
9
+from hapic.context import ContextInterface, RouteRepresentation
11
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
10
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
11
+from hapic.decorator import DecoratedController
12
 from hapic.description import ControllerDescription
12
 from hapic.description import ControllerDescription
13
 from hapic.exception import NoRoutesException
13
 from hapic.exception import NoRoutesException
14
 from hapic.exception import RouteNotFound
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
 def find_bottle_route(
17
 def find_bottle_route(
21
     decorated_controller: DecoratedController,
18
     decorated_controller: DecoratedController,
48
 
45
 
49
 def bottle_generate_operations(
46
 def bottle_generate_operations(
50
     spec,
47
     spec,
51
-    bottle_route: bottle.Route,
48
+    route: RouteRepresentation,
52
     description: ControllerDescription,
49
     description: ControllerDescription,
53
 ):
50
 ):
54
     method_operations = dict()
51
     method_operations = dict()
110
             })
107
             })
111
 
108
 
112
     operations = {
109
     operations = {
113
-        bottle_route.method.lower(): method_operations,
110
+        route.method.lower(): method_operations,
114
     }
111
     }
115
 
112
 
116
     return operations
113
     return operations
120
     def get_doc(
117
     def get_doc(
121
         self,
118
         self,
122
         controllers: typing.List[DecoratedController],
119
         controllers: typing.List[DecoratedController],
123
-        app,
120
+        context: ContextInterface,
124
     ) -> dict:
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
         spec = APISpec(
122
         spec = APISpec(
134
             title='Swagger Petstore',
123
             title='Swagger Petstore',
135
             version='1.0.0',
124
             version='1.0.0',
136
             plugins=[
125
             plugins=[
137
-                'apispec.ext.bottle',
126
+                # 'apispec.ext.bottle',
138
                 'apispec.ext.marshmallow',
127
                 'apispec.ext.marshmallow',
139
             ],
128
             ],
140
         )
129
         )
170
         # with app.test_request_context():
159
         # with app.test_request_context():
171
         paths = {}
160
         paths = {}
172
         for controller in controllers:
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
             operations = bottle_generate_operations(
165
             operations = bottle_generate_operations(
177
                 spec,
166
                 spec,
178
-                bottle_route,
167
+                route,
179
                 controller.description,
168
                 controller.description,
180
             )
169
             )
181
 
170
 
189
             spec.add_path(path)
178
             spec.add_path(path)
190
 
179
 
191
         return spec.to_dict()
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
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import json
2
 import json
3
+import re
3
 import typing
4
 import typing
4
 from http import HTTPStatus
5
 from http import HTTPStatus
5
 
6
 
9
 from hapic.exception import OutputValidationException
10
 from hapic.exception import OutputValidationException
10
 from hapic.processor import RequestParameters, ProcessValidationError
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
 class BottleContext(ContextInterface):
17
 class BottleContext(ContextInterface):
14
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
18
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import json
2
 import json
3
+import re
3
 import typing
4
 import typing
4
 from http import HTTPStatus
5
 from http import HTTPStatus
5
 
6
 
6
 from pyramid.request import Request
7
 from pyramid.request import Request
7
 from pyramid.response import Response
8
 from pyramid.response import Response
8
-
9
+from pyramid.config import Configurator
9
 
10
 
10
 from hapic.context import ContextInterface
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
 from hapic.exception import OutputValidationException
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
 class PyramidContext(ContextInterface):
24
 class PyramidContext(ContextInterface):
25
+    def __init__(self, configurator: Configurator):
26
+        self.configurator = configurator
27
+
16
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
28
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
17
         req = args[-1]  # TODO : Check
29
         req = args[-1]  # TODO : Check
18
         assert isinstance(req, Request)
30
         assert isinstance(req, Request)
67
             ],
79
             ],
68
             status=int(http_code),
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
         self._buffer = DecorationBuffer()
50
         self._buffer = DecorationBuffer()
51
         self._controllers = []  # type: typing.List[DecoratedController]
51
         self._controllers = []  # type: typing.List[DecoratedController]
52
         self._context = None  # type: ContextInterface
52
         self._context = None  # type: ContextInterface
53
+        self.doc_generator = DocGenerator()
53
 
54
 
54
         # This local function will be pass to different components
55
         # This local function will be pass to different components
55
         # who will need context but declared (like with decorator)
56
         # who will need context but declared (like with decorator)
289
             return decoration.get_wrapper(func)
290
             return decoration.get_wrapper(func)
290
         return decorator
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)