소스 검색

Add swagger ui doc visualisation

Bastien Sevajol 7 년 전
부모
커밋
64819802ee

+ 1 - 0
example/example_a_flask.py 파일 보기

90
 controllers.bind(app)
90
 controllers.bind(app)
91
 
91
 
92
 hapic.set_context(FlaskContext(app))
92
 hapic.set_context(FlaskContext(app))
93
+hapic.add_documentation_view('/api-doc', 'DOC', 'Generated doc')
93
 print(json.dumps(hapic.generate_doc()))
94
 print(json.dumps(hapic.generate_doc()))
94
 app.run(host='localhost', port=8080, debug=True)
95
 app.run(host='localhost', port=8080, debug=True)

+ 1 - 0
hapic/__init__.py 파일 보기

16
 output_file = _hapic_default.output_file
16
 output_file = _hapic_default.output_file
17
 generate_doc = _hapic_default.generate_doc
17
 generate_doc = _hapic_default.generate_doc
18
 set_context = _hapic_default.set_context
18
 set_context = _hapic_default.set_context
19
+add_documentation_view = _hapic_default.add_documentation_view
19
 handle_exception = _hapic_default.handle_exception
20
 handle_exception = _hapic_default.handle_exception

+ 28 - 1
hapic/context.py 파일 보기

34
     def get_response(
34
     def get_response(
35
         self,
35
         self,
36
         # TODO BS 20171228: rename into response_content
36
         # TODO BS 20171228: rename into response_content
37
-        response: dict,
37
+        response: str,
38
         http_code: int,
38
         http_code: int,
39
+        mimetype: str='application/json',
39
     ) -> typing.Any:
40
     ) -> typing.Any:
40
         raise NotImplementedError()
41
         raise NotImplementedError()
41
 
42
 
79
         """
80
         """
80
         raise NotImplementedError()
81
         raise NotImplementedError()
81
 
82
 
83
+    def add_view(
84
+        self,
85
+        route: str,
86
+        http_method: str,
87
+        view_func: typing.Callable[..., typing.Any],
88
+    ) -> None:
89
+        """
90
+        This method must permit to add a view in current context
91
+        :param route: The route depending of framework format, ex "/foo"
92
+        :param http_method: HTTP method like GET, POST, etc ...
93
+        :param view_func: The view callable
94
+        """
95
+        raise NotImplementedError()
96
+
97
+    def serve_directory(
98
+        self,
99
+        route_prefix: str,
100
+        directory_path: str,
101
+    ) -> None:
102
+        """
103
+        Configure a path to serve a directory content
104
+        :param route_prefix: The base url for serve the directory, eg /static
105
+        :param directory_path: The file system path
106
+        """
107
+        raise NotImplementedError()
108
+
82
 
109
 
83
 class BaseContext(ContextInterface):
110
 class BaseContext(ContextInterface):
84
     def get_default_error_builder(self) -> ErrorBuilderInterface:
111
     def get_default_error_builder(self) -> ErrorBuilderInterface:

+ 4 - 2
hapic/decorator.py 파일 보기

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import json
3
+
2
 import functools
4
 import functools
3
 import typing
5
 import typing
4
 try:  # Python 3.5+
6
 try:  # Python 3.5+
221
 
223
 
222
             processed_response = self.processor.process(response)
224
             processed_response = self.processor.process(response)
223
             prepared_response = self.context.get_response(
225
             prepared_response = self.context.get_response(
224
-                processed_response,
226
+                json.dumps(processed_response),
225
                 self.default_http_code,
227
                 self.default_http_code,
226
             )
228
             )
227
             return prepared_response
229
             return prepared_response
432
                 )
434
                 )
433
 
435
 
434
             error_response = self.context.get_response(
436
             error_response = self.context.get_response(
435
-                response_content,
437
+                json.dumps(response_content),
436
                 self.http_code,
438
                 self.http_code,
437
             )
439
             )
438
             return error_response
440
             return error_response

+ 27 - 0
hapic/doc.py 파일 보기

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import json
3
+
2
 import typing
4
 import typing
5
+import yaml
3
 
6
 
4
 from apispec import APISpec
7
 from apispec import APISpec
5
 from apispec import Path
8
 from apispec import Path
188
 
191
 
189
         return spec.to_dict()
192
         return spec.to_dict()
190
 
193
 
194
+    def save_in_file(
195
+        self,
196
+        doc_file_path: str,
197
+        controllers: typing.List[DecoratedController],
198
+        context: ContextInterface,
199
+        title: str='',
200
+        description: str='',
201
+    ) -> None:
202
+        # generate this file
203
+        dict_doc = self.get_doc(
204
+            controllers=controllers,
205
+            context=context,
206
+            title=title,
207
+            description=description,
208
+        )
209
+        json_doc = json.dumps(dict_doc)
210
+
211
+        # We dump then load with json to use real scalar dict.
212
+        # If not, yaml dump dict-like objects
213
+        clean_dict_doc = json.loads(json_doc)
214
+        yaml_doc = yaml.dump(clean_dict_doc, default_flow_style=False)
215
+        with open(doc_file_path, 'w+') as doc_file:
216
+            doc_file.write(yaml_doc)
217
+
191
 
218
 
192
 # TODO BS 20171109: Must take care of already existing definition names
219
 # TODO BS 20171109: Must take care of already existing definition names
193
 def generate_schema_name(schema):
220
 def generate_schema_name(schema):

+ 4 - 3
hapic/ext/bottle/context.py 파일 보기

56
 
56
 
57
     def get_response(
57
     def get_response(
58
         self,
58
         self,
59
-        response: dict,
59
+        response: str,
60
         http_code: int,
60
         http_code: int,
61
+        mimetype: str='application/json',
61
     ) -> bottle.HTTPResponse:
62
     ) -> bottle.HTTPResponse:
62
         return bottle.HTTPResponse(
63
         return bottle.HTTPResponse(
63
-            body=json.dumps(response),
64
+            body=response,
64
             headers=[
65
             headers=[
65
-                ('Content-Type', 'application/json'),
66
+                ('Content-Type', mimetype),
66
             ],
67
             ],
67
             status=http_code,
68
             status=http_code,
68
         )
69
         )

+ 36 - 3
hapic/ext/flask/context.py 파일 보기

18
 from hapic.error import DefaultErrorBuilder
18
 from hapic.error import DefaultErrorBuilder
19
 from hapic.error import ErrorBuilderInterface
19
 from hapic.error import ErrorBuilderInterface
20
 from flask import Flask
20
 from flask import Flask
21
+from flask import send_from_directory
21
 
22
 
22
 if typing.TYPE_CHECKING:
23
 if typing.TYPE_CHECKING:
23
     from flask import Response
24
     from flask import Response
49
 
50
 
50
     def get_response(
51
     def get_response(
51
         self,
52
         self,
52
-        response: dict,
53
+        response: str,
53
         http_code: int,
54
         http_code: int,
55
+        mimetype: str='application/json',
54
     ) -> 'Response':
56
     ) -> 'Response':
55
         from flask import Response
57
         from flask import Response
56
         return Response(
58
         return Response(
57
-            response=json.dumps(response),
58
-            mimetype='application/json',
59
+            response=response,
60
+            mimetype=mimetype,
59
             status=http_code,
61
             status=http_code,
60
         )
62
         )
61
 
63
 
123
     def by_pass_output_wrapping(self, response: typing.Any) -> bool:
125
     def by_pass_output_wrapping(self, response: typing.Any) -> bool:
124
         from flask import Response
126
         from flask import Response
125
         return isinstance(response, Response)
127
         return isinstance(response, Response)
128
+
129
+    def add_view(
130
+        self,
131
+        route: str,
132
+        http_method: str,
133
+        view_func: typing.Callable[..., typing.Any],
134
+    ) -> None:
135
+        self.app.add_url_rule(
136
+            rule=route,
137
+            view_func=view_func,
138
+        )
139
+
140
+    def serve_directory(
141
+        self,
142
+        route_prefix: str,
143
+        directory_path: str,
144
+    ) -> None:
145
+        if not route_prefix.endswith('/'):
146
+            route_prefix = '{}/'.format(route_prefix)
147
+
148
+        @self.app.route(
149
+            route_prefix,
150
+            defaults={
151
+                'path': 'index.html',
152
+            }
153
+        )
154
+        @self.app.route(
155
+            '{}<path:path>'.format(route_prefix),
156
+        )
157
+        def api_doc(path):
158
+            return send_from_directory(directory_path, path)

+ 4 - 3
hapic/ext/pyramid/context.py 파일 보기

57
 
57
 
58
     def get_response(
58
     def get_response(
59
         self,
59
         self,
60
-        response: dict,
60
+        response: str,
61
         http_code: int,
61
         http_code: int,
62
+        mimetype: str='application/json',
62
     ) -> 'Response':
63
     ) -> 'Response':
63
         from pyramid.response import Response
64
         from pyramid.response import Response
64
         return Response(
65
         return Response(
65
-            body=json.dumps(response),
66
+            body=response,
66
             headers=[
67
             headers=[
67
-                ('Content-Type', 'application/json'),
68
+                ('Content-Type', mimetype),
68
             ],
69
             ],
69
             status=http_code,
70
             status=http_code,
70
         )
71
         )

+ 71 - 0
hapic/hapic.py 파일 보기

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import os
2
 import typing
3
 import typing
3
 import uuid
4
 import uuid
4
 import functools
5
 import functools
376
             title=title,
377
             title=title,
377
             description=description,
378
             description=description,
378
         )
379
         )
380
+
381
+    def save_doc_in_file(
382
+        self,
383
+        file_path: str,
384
+        title: str='',
385
+        description: str='',
386
+    ) -> None:
387
+        """
388
+        See hapic.doc.DocGenerator#get_doc docstring
389
+        :param file_path: The file path to write doc in YAML format
390
+        :param title: Title of generated doc
391
+        :param description: Description of generated doc
392
+        """
393
+        self.doc_generator.save_in_file(
394
+            file_path,
395
+            controllers=self._controllers,
396
+            context=self.context,
397
+            title=title,
398
+            description=description,
399
+        )
400
+
401
+    def add_documentation_view(
402
+        self,
403
+        route: str,
404
+        title: str='',
405
+        description: str='',
406
+    ) -> None:
407
+        # Ensure "/" at end of route, else web browser will not consider it as
408
+        # a path
409
+        if not route.endswith('/'):
410
+            route = '{}/'.format(route)
411
+
412
+        # Add swagger directory as served static dir
413
+        swaggerui_path = os.path.join(
414
+            os.path.dirname(os.path.abspath(__file__)),
415
+            'static',
416
+            'swaggerui',
417
+        )
418
+        self.context.serve_directory(
419
+            route,
420
+            swaggerui_path,
421
+        )
422
+
423
+        # Generate documentation file
424
+        doc_page_path = os.path.join(swaggerui_path, 'spec.yml')
425
+        self.save_doc_in_file(doc_page_path)
426
+
427
+        # Prepare views html content
428
+        doc_index_path = os.path.join(swaggerui_path, 'index.html')
429
+        with open(doc_index_path, 'r') as doc_page:
430
+            doc_page_content = doc_page.read()
431
+        doc_page_content = doc_page_content.replace(
432
+            '{{ spec_uri }}',
433
+            'spec.yml',
434
+        )
435
+
436
+        # Declare the swaggerui view
437
+        def api_doc_view():
438
+            return self.context.get_response(
439
+                doc_page_content,
440
+                http_code=HTTPStatus.OK,
441
+                mimetype='text/html',
442
+            )
443
+
444
+        # Add a view to generate the html index page of swaggerui
445
+        self.context.add_view(
446
+            route=route,
447
+            http_method='GET',
448
+            view_func=api_doc_view,
449
+        )

BIN
hapic/static/swaggerui/favicon-16x16.png 파일 보기


BIN
hapic/static/swaggerui/favicon-32x32.png 파일 보기


+ 95 - 0
hapic/static/swaggerui/index.html 파일 보기

1
+<!-- HTML for static distribution bundle build -->
2
+<!DOCTYPE html>
3
+<html lang="en">
4
+<head>
5
+  <meta charset="UTF-8">
6
+  <title>Swagger UI</title>
7
+  <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
8
+  <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
9
+  <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
10
+  <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
11
+  <style>
12
+    html
13
+    {
14
+      box-sizing: border-box;
15
+      overflow: -moz-scrollbars-vertical;
16
+      overflow-y: scroll;
17
+    }
18
+    *,
19
+    *:before,
20
+    *:after
21
+    {
22
+      box-sizing: inherit;
23
+    }
24
+
25
+    body {
26
+      margin:0;
27
+      background: #fafafa;
28
+    }
29
+  </style>
30
+</head>
31
+
32
+<body>
33
+
34
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
35
+  <defs>
36
+    <symbol viewBox="0 0 20 20" id="unlocked">
37
+          <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
38
+    </symbol>
39
+
40
+    <symbol viewBox="0 0 20 20" id="locked">
41
+      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
42
+    </symbol>
43
+
44
+    <symbol viewBox="0 0 20 20" id="close">
45
+      <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
46
+    </symbol>
47
+
48
+    <symbol viewBox="0 0 20 20" id="large-arrow">
49
+      <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
50
+    </symbol>
51
+
52
+    <symbol viewBox="0 0 20 20" id="large-arrow-down">
53
+      <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
54
+    </symbol>
55
+
56
+
57
+    <symbol viewBox="0 0 24 24" id="jump-to">
58
+      <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
59
+    </symbol>
60
+
61
+    <symbol viewBox="0 0 24 24" id="expand">
62
+      <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
63
+    </symbol>
64
+
65
+  </defs>
66
+</svg>
67
+
68
+<div id="swagger-ui"></div>
69
+
70
+<script src="./swagger-ui-bundle.js"> </script>
71
+<script src="./swagger-ui-standalone-preset.js"> </script>
72
+<script>
73
+window.onload = function() {
74
+  
75
+  // Build a system
76
+  const ui = SwaggerUIBundle({
77
+    url: "{{ spec_uri }}",
78
+    dom_id: '#swagger-ui',
79
+    deepLinking: true,
80
+    presets: [
81
+      SwaggerUIBundle.presets.apis,
82
+      SwaggerUIStandalonePreset
83
+    ],
84
+    plugins: [
85
+      SwaggerUIBundle.plugins.DownloadUrl
86
+    ],
87
+    layout: "StandaloneLayout"
88
+  })
89
+
90
+  window.ui = ui
91
+}
92
+</script>
93
+</body>
94
+
95
+</html>

+ 94 - 0
hapic/static/swaggerui/spec.yml 파일 보기

1
+definitions:
2
+  DefaultErrorBuilder:
3
+    properties:
4
+      code:
5
+        type: string
6
+        x-nullable: true
7
+      details:
8
+        type: object
9
+      message:
10
+        type: string
11
+    required:
12
+    - message
13
+    type: object
14
+  HelloJsonSchema:
15
+    properties:
16
+      color:
17
+        minLength: 3
18
+        type: string
19
+    required:
20
+    - color
21
+    type: object
22
+  HelloResponseSchema:
23
+    properties:
24
+      color:
25
+        type: string
26
+      name:
27
+        type: string
28
+      sentence:
29
+        type: string
30
+    required:
31
+    - name
32
+    - sentence
33
+    type: object
34
+info:
35
+  description: ''
36
+  title: ''
37
+  version: 1.0.0
38
+parameters: {}
39
+paths:
40
+  /hello/{name}:
41
+    get:
42
+      description: "my endpoint hello\n        ---\n        get:\n            description:\
43
+        \ my description\n            parameters:\n                - in: \"path\"\n\
44
+        \                  description: \"hello\"\n                  name: \"name\"\
45
+        \n                  type: \"string\"\n            responses:\n           \
46
+        \     200:\n                    description: A pet to be returned\n      \
47
+        \              schema: HelloResponseSchema"
48
+      parameters:
49
+      - in: path
50
+        name: name
51
+        required: true
52
+        type: string
53
+      - in: query
54
+        name: alive
55
+        required: false
56
+        type: boolean
57
+      responses:
58
+        '200':
59
+          description: '200'
60
+          schema:
61
+            $ref: '#/definitions/HelloResponseSchema'
62
+        '400':
63
+          description: '400'
64
+          schema:
65
+            $ref: '#/definitions/DefaultErrorBuilder'
66
+    post:
67
+      parameters:
68
+      - in: body
69
+        name: body
70
+        schema:
71
+          $ref: '#/definitions/HelloJsonSchema'
72
+      - in: path
73
+        name: name
74
+        required: true
75
+        type: string
76
+      responses:
77
+        '200':
78
+          description: '200'
79
+          schema:
80
+            $ref: '#/definitions/HelloResponseSchema'
81
+  /hello3/{name}:
82
+    get:
83
+      parameters:
84
+      - in: path
85
+        name: name
86
+        required: true
87
+        type: string
88
+      responses:
89
+        '200':
90
+          description: '200'
91
+          schema:
92
+            $ref: '#/definitions/HelloResponseSchema'
93
+swagger: '2.0'
94
+tags: []

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 87 - 0
hapic/static/swaggerui/swagger-ui-bundle.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 13 - 0
hapic/static/swaggerui/swagger-ui-standalone-preset.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2 - 0
hapic/static/swaggerui/swagger-ui.css


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 8 - 0
hapic/static/swaggerui/swagger-ui.js


+ 2 - 1
tests/base.py 파일 보기

48
 
48
 
49
     def get_response(
49
     def get_response(
50
         self,
50
         self,
51
-        response: dict,
51
+        response: str,
52
         http_code: int,
52
         http_code: int,
53
+        mimetype: str='application/json',
53
     ) -> typing.Any:
54
     ) -> typing.Any:
54
         return {
55
         return {
55
             'original_response': response,
56
             'original_response': response,