swagger
Showing
7 changed files
with
423 additions
and
14 deletions
1 | from django.shortcuts import render | 1 | from django.shortcuts import render |
2 | from webargs import fields | 2 | from webargs import fields, validate |
3 | from webargs.djangoparser import use_args, parser | 3 | from webargs.djangoparser import use_args, parser |
4 | from common.mixins import GenericView | 4 | from common.mixins import GenericView |
5 | from common import response | 5 | from common import response |
... | @@ -14,22 +14,22 @@ def load_data(request, schema): | ... | @@ -14,22 +14,22 @@ def load_data(request, schema): |
14 | return request.data | 14 | return request.data |
15 | 15 | ||
16 | 16 | ||
17 | application_data_args = {'applicationId': fields.Str(required=True)} | 17 | application_data_args = {'applicationId': fields.Str(required=True, validate=validate.Length(max=64))} |
18 | 18 | ||
19 | applicant_data_args = { | 19 | applicant_data_args = { |
20 | 'mainApplicantName': fields.Str(required=True), | 20 | 'mainApplicantName': fields.Str(required=True, validate=validate.Length(max=16)), |
21 | 'coApplicantName': fields.Str(required=True), | 21 | 'coApplicantName': fields.Str(required=True, validate=validate.Length(max=16)), |
22 | 'guarantor1Name': fields.Str(required=True), | 22 | 'guarantor1Name': fields.Str(required=True, validate=validate.Length(max=16)), |
23 | 'guarantor2Name': fields.Str(required=True), | 23 | 'guarantor2Name': fields.Str(required=True, validate=validate.Length(max=16)), |
24 | } | 24 | } |
25 | 25 | ||
26 | document_args = { | 26 | document_args = { |
27 | 'documentName': fields.Str(required=True), | 27 | 'documentName': fields.Str(required=True, validate=validate.Length(max=255)), |
28 | 'documentScheme': fields.Str(required=True), | 28 | 'documentScheme': fields.Str(required=True, validate=validate.Length(max=64)), |
29 | 'businessType': fields.Str(required=True), | 29 | 'businessType': fields.Str(required=True, validate=validate.Length(max=64)), |
30 | 'uploadFinishTime': fields.DateTime(required=True), | 30 | 'uploadFinishTime': fields.DateTime(required=True), |
31 | 'dataSource': fields.Str(required=True), | 31 | 'dataSource': fields.Str(required=True, validate=validate.Length(max=64)), |
32 | 'metadataVersionId': fields.Str(required=True), | 32 | 'metadataVersionId': fields.Str(required=True, validate=validate.Length(max=64)), |
33 | } | 33 | } |
34 | 34 | ||
35 | doc_upload_args = { | 35 | doc_upload_args = { |
... | @@ -63,3 +63,80 @@ class DocView(GenericView): | ... | @@ -63,3 +63,80 @@ class DocView(GenericView): |
63 | ) | 63 | ) |
64 | self.running_log.info('[doc upload success] [args={0}]'.format(args)) | 64 | self.running_log.info('[doc upload success] [args={0}]'.format(args)) |
65 | return response.ok() | 65 | return response.ok() |
66 | |||
67 | post.openapi_doc = ''' | ||
68 | summary: pos上传文件信息 | ||
69 | tags: [doc] | ||
70 | requestBody: | ||
71 | required: true | ||
72 | content: | ||
73 | application/json: | ||
74 | schema: | ||
75 | type: object | ||
76 | properties: | ||
77 | applicationData: | ||
78 | description: 申请信息 | ||
79 | type: object | ||
80 | properties: | ||
81 | applicationId: | ||
82 | description: 申请id | ||
83 | type: string | ||
84 | example: CH-B0011010101 | ||
85 | applicantData: | ||
86 | description: 申请人信息 | ||
87 | type: object | ||
88 | properties: | ||
89 | mainApplicantName: | ||
90 | description: 主申请人 | ||
91 | type: string | ||
92 | example: 王明阳 | ||
93 | coApplicantName: | ||
94 | description: 共同申请人 | ||
95 | type: string | ||
96 | example: 王明月 | ||
97 | guarantor1Name: | ||
98 | description: 担保人1 | ||
99 | type: string | ||
100 | example: 王明日 | ||
101 | guarantor2Name: | ||
102 | description: 担保人2 | ||
103 | type: string | ||
104 | example: 王明雨 | ||
105 | document: | ||
106 | description: 文件信息 | ||
107 | type: object | ||
108 | properties: | ||
109 | documentName: | ||
110 | description: 文件名 | ||
111 | type: string | ||
112 | example: CH-B0011010101王明阳申请表 | ||
113 | documentScheme: | ||
114 | description: 文件格式? | ||
115 | type: string | ||
116 | example: CO00001 | ||
117 | businessType: | ||
118 | description: 业务类型 | ||
119 | type: string | ||
120 | example: HIL | ||
121 | uploadFinishTime: | ||
122 | description: 上传完成时间 | ||
123 | type: string | ||
124 | example: '2020-09-01 12:21:11' | ||
125 | dataSource: | ||
126 | description: 数据源 | ||
127 | type: string | ||
128 | example: POS | ||
129 | metadataVersionId: | ||
130 | description: 元数据版本ID | ||
131 | type: string | ||
132 | example: '8410480' | ||
133 | |||
134 | responses: | ||
135 | 200: | ||
136 | description: ok | ||
137 | content: | ||
138 | application/json: | ||
139 | schema: | ||
140 | type: object | ||
141 | $ref: '#/components/schemas/ApiResponse' | ||
142 | ''' | ... | ... |
src/common/api_doc.py
0 → 100644
1 | |||
2 | base_part = ''' | ||
3 | openapi: 3.0.0 | ||
4 | info: | ||
5 | title: 接口文档 | ||
6 | description: 宝马ocr/biz_logic接口文档 | ||
7 | version: 1.0.0 | ||
8 | servers: | ||
9 | - url: 'http://127.0.0.1:8000' | ||
10 | description: Development server | ||
11 | - url: 'http://127.0.0.1:8000' | ||
12 | description: sit | ||
13 | - url: 'http://127.0.0.1:8000' | ||
14 | description: uat | ||
15 | - url: 'http://127.0.0.1:8000' | ||
16 | description: prd | ||
17 | tags: | ||
18 | - name: user | ||
19 | description: 用户 | ||
20 | - name: doc | ||
21 | description: 文件 | ||
22 | |||
23 | security: | ||
24 | - bearerAuth: [] | ||
25 | ''' | ||
26 | # scheme: bearer | ||
27 | security_schemes = ''' | ||
28 | bearerAuth: # arbitrary name for the security scheme | ||
29 | type: http | ||
30 | scheme: bearer | ||
31 | bearerFormat: JWT | ||
32 | description: > | ||
33 | token过期标志 | ||
34 | |||
35 | 1. Response Headers中的WWW-Authenticate中的status | ||
36 | |||
37 | status: -1 Invalid Authorization header | ||
38 | |||
39 | status: -2 Signature has expired beacause of expire_time | ||
40 | |||
41 | status: -3 Error decoding signature | ||
42 | |||
43 | status: -4 Signature has expired beacause new signature is generated | ||
44 | |||
45 | status: 0 valid Authorization | ||
46 | |||
47 | status: 1 new create, only login would set | ||
48 | |||
49 | 2. Response Body中的meta -> status == 1 | ||
50 | ''' | ||
51 | |||
52 | responses = ''' | ||
53 | ErrorResponse: | ||
54 | description: 调用异常, 具体情况请参考`HTTP`状态码和`meta -> status`字段 | ||
55 | content: | ||
56 | application/json: | ||
57 | schema: | ||
58 | $ref: '#/components/schemas/ApiResponse' | ||
59 | NoContent: | ||
60 | description: 后台接收请求,但是没有响应内容 | ||
61 | content: | ||
62 | application/json: | ||
63 | schema: | ||
64 | $ref: '#/components/schemas/ApiResponse' | ||
65 | ''' | ||
66 | |||
67 | parameters = '' | ||
68 | |||
69 | schemas = ''' | ||
70 | ApiResponse: | ||
71 | description: 响应对象,meta字段用于表示响应的状态; data字段用于存放响应内容 | ||
72 | type: object | ||
73 | properties: | ||
74 | code: | ||
75 | type: integer | ||
76 | description: '0: success | ||
77 | 1: need login | ||
78 | 2: invalid params | ||
79 | 3: internal error | ||
80 | 4: object not exist | ||
81 | 5: async wait | ||
82 | 6: no permission | ||
83 | 7: illegal operation' | ||
84 | msg: | ||
85 | type: string | ||
86 | nullable: true | ||
87 | data: | ||
88 | oneOf: | ||
89 | - type: object | ||
90 | nullable: true | ||
91 | - type: integer | ||
92 | nullable: true | ||
93 | - type: array | ||
94 | nullable: true | ||
95 | |||
96 | Pagination: | ||
97 | description: 分页参数 | ||
98 | type: object | ||
99 | properties: | ||
100 | current: | ||
101 | description: 当前页面 | ||
102 | type: integer | ||
103 | total: | ||
104 | description: 所有元素个数 | ||
105 | type: integer | ||
106 | page_size: | ||
107 | description: 页面大小 | ||
108 | type: integer | ||
109 | required: | ||
110 | - current | ||
111 | - total | ||
112 | - pageSize | ||
113 | ''' | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | import collections | ||
2 | import re | ||
3 | |||
4 | from django.core.management import BaseCommand | ||
5 | from django.urls.resolvers import get_resolver | ||
6 | |||
7 | import yaml | ||
8 | from yaml.scanner import ScannerError | ||
9 | |||
10 | from common.api_doc import (base_part, security_schemes, responses, schemas) | ||
11 | |||
12 | _mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG | ||
13 | |||
14 | |||
15 | def dict_representer(dumper, data): | ||
16 | return dumper.represent_dict(data.items()) | ||
17 | |||
18 | |||
19 | def dict_constructor(loader, node): | ||
20 | return collections.OrderedDict(loader.construct_pairs(node)) | ||
21 | |||
22 | |||
23 | yaml.add_representer(collections.OrderedDict, dict_representer) | ||
24 | yaml.add_constructor(_mapping_tag, dict_constructor) | ||
25 | |||
26 | |||
27 | def unify_url_path_format(string): | ||
28 | return '/%s' % re.sub(r'%\(([^/]+)\)s', lambda m: '{%s}' % m.group(1), | ||
29 | string) | ||
30 | |||
31 | |||
32 | DEFAULT_API_DOC = ''' | ||
33 | summary: 未填写 | ||
34 | responses: | ||
35 | 200: | ||
36 | description: ok | ||
37 | ''' | ||
38 | |||
39 | |||
40 | class Command(BaseCommand): | ||
41 | def add_arguments(self, parser): | ||
42 | parser.add_argument('-o', '--output_file', help='文件名,用于存储文档') | ||
43 | |||
44 | def handle(self, *args, **kwargs): | ||
45 | urls = get_resolver() | ||
46 | api_doc_dct = {} | ||
47 | for view, pattern in urls.reverse_dict.items(): | ||
48 | view_class = view.view_class | ||
49 | url_path, path_parameters = pattern[0][0] | ||
50 | url_path = unify_url_path_format(url_path) | ||
51 | url_path_paramters = getattr(view, 'parameters_doc', None) | ||
52 | if url_path_paramters: | ||
53 | url_path_paramters = yaml.load(url_path_paramters) | ||
54 | else: | ||
55 | url_path_paramters = [{ | ||
56 | 'in': 'path', | ||
57 | 'name': parameter, | ||
58 | 'required': True, | ||
59 | 'schema': { | ||
60 | 'type': 'string' | ||
61 | } | ||
62 | } for parameter in path_parameters] | ||
63 | api_doc_dct[url_path] = {} | ||
64 | if url_path_paramters: | ||
65 | api_doc_dct[url_path]['parameters'] = url_path_paramters | ||
66 | for method in view_class.http_method_names: | ||
67 | method_handler = getattr(view_class, method, None) | ||
68 | doc = getattr(method_handler, 'openapi_doc', None) | ||
69 | if not method_handler or (method == 'options' and not doc): | ||
70 | continue | ||
71 | try: | ||
72 | doc = yaml.load(doc or DEFAULT_API_DOC) | ||
73 | except ScannerError as err: | ||
74 | raise Exception( | ||
75 | 'failed to load doc: """%s"""\nerr: %s' % (doc, err)) | ||
76 | if doc.get('parameters'): | ||
77 | for parameter in doc['parameters']: | ||
78 | if parameter['name'] in path_parameters: | ||
79 | doc['parameters'].pop(parameter) | ||
80 | api_doc_dct[url_path][method] = doc | ||
81 | doc_dct = yaml.load(base_part) | ||
82 | doc_dct['paths'] = api_doc_dct | ||
83 | doc_dct['components'] = { | ||
84 | 'securitySchemes': yaml.load(security_schemes), | ||
85 | 'responses': yaml.load(responses), | ||
86 | 'schemas': yaml.load(schemas), | ||
87 | } | ||
88 | |||
89 | doc_str = yaml.dump( | ||
90 | doc_dct, default_flow_style=False, allow_unicode=True) | ||
91 | if kwargs.get('output_file'): | ||
92 | with open(kwargs['output_file'], 'w') as f: | ||
93 | f.write(doc_str) | ||
94 | self.stdout.write( | ||
95 | self.style.SUCCESS('api doc generated succssfully: %s' % | ||
96 | kwargs['output_file'])) | ||
97 | else: | ||
98 | self.stdout.write(doc_str) |
1 | import os | ||
2 | import sys | ||
3 | |||
4 | from django.core.management import BaseCommand | ||
5 | |||
6 | from openapi_spec_validator import validate_spec_url, validate_v2_spec_url | ||
7 | from openapi_spec_validator.exceptions import ValidationError | ||
8 | |||
9 | |||
10 | # from openapi_spec_validator/__main__.py | ||
11 | class Command(BaseCommand): | ||
12 | def add_arguments(self, parser): | ||
13 | parser.add_argument( | ||
14 | 'filename', help="Absolute or relative path to file") | ||
15 | parser.add_argument( | ||
16 | '--schema', | ||
17 | help="OpenAPI schema (default: 3.0.0)", | ||
18 | type=str, | ||
19 | choices=['2.0', '3.0.0'], | ||
20 | default='3.0.0') | ||
21 | |||
22 | def handle(self, *args, **kwargs): | ||
23 | filename = kwargs['filename'] | ||
24 | filename = os.path.abspath(filename) | ||
25 | # choose the validator | ||
26 | if kwargs['schema'] == '2.0': | ||
27 | validate_url = validate_v2_spec_url | ||
28 | elif kwargs['schema'] == '3.0.0': | ||
29 | validate_url = validate_spec_url | ||
30 | # validate | ||
31 | try: | ||
32 | validate_url('file://' + filename) | ||
33 | except ValidationError as err: | ||
34 | self.stdout.write(self.style.ERROR(err)) | ||
35 | sys.exit(1) | ||
36 | except Exception as err: | ||
37 | self.stdout.write(self.style.ERROR(err)) | ||
38 | sys.exit(2) | ||
39 | else: | ||
40 | self.stdout.write(self.style.SUCCESS('ok')) |
... | @@ -4,9 +4,10 @@ from .named_enum import NamedEnum | ... | @@ -4,9 +4,10 @@ from .named_enum import NamedEnum |
4 | 4 | ||
5 | 5 | ||
6 | def res_content(meta_status, msg, data=None): | 6 | def res_content(meta_status, msg, data=None): |
7 | if data is None: | 7 | res = {'code': meta_status, 'message': msg} |
8 | data = {} | 8 | if data is not None: |
9 | return {'code': meta_status, 'message': msg, 'data': data} | 9 | res['data'] = data |
10 | return res | ||
10 | 11 | ||
11 | 12 | ||
12 | @enum.unique | 13 | @enum.unique | ... | ... |
src/readme.md
0 → 100755
1 | # 录题系统开发规范 | ||
2 | |||
3 | |||
4 | ## 代码规范 | ||
5 | |||
6 | 1. 参考 [python_specification.md](python_specification.md) | ||
7 | 2. 帐号、密码严禁提交到git仓中 | ||
8 | 3. 参与开发前请阅读[十二要素应用宣言](https://12factor.net/zh_cn/) | ||
9 | |||
10 | |||
11 | ## 接口文档 | ||
12 | |||
13 | 接口文档使用`openapi`规范书写, 可以参考[demo](http://editor.swagger.io/)以获取直观感受。 | ||
14 | |||
15 | 目前,接口文档写在代码中, 并通过脚本将代码中的文档提取出来, 示例如下。 | ||
16 | |||
17 | |||
18 | 1. 编写文档 | ||
19 | |||
20 | ```python | ||
21 | class UserInfoView(APIView): | ||
22 | def get(self, request): | ||
23 | user_info = serializers.staff_detail.dump(request.user).data | ||
24 | return APIResponse.ok(user_info) | ||
25 | |||
26 | get.openapi_doc = ''' | ||
27 | summary: 获取个人信息 | ||
28 | tags: | ||
29 | - 帐号 | ||
30 | responses: | ||
31 | 200: | ||
32 | description: ok | ||
33 | content: | ||
34 | application/json: | ||
35 | schema: | ||
36 | type: object | ||
37 | properties: | ||
38 | meta: | ||
39 | $ref: '#/components/schemas/ApiResponseMeta' | ||
40 | data: | ||
41 | $ref: '#/components/schemas/AccountInfoDetail' | ||
42 | ''' | ||
43 | |||
44 | |||
45 | ``` | ||
46 | |||
47 | 2. 提取接口文档 | ||
48 | |||
49 | ``` | ||
50 | $: python manage.py generate_api_doc -o ../docs/main.yaml | ||
51 | api doc generated succssfully: ../docs/main.yaml | ||
52 | ``` | ||
53 | |||
54 | 3. 校对接口文档 | ||
55 | |||
56 | ``` | ||
57 | $: python manage.py validate_api_doc ../docs/main.yaml | ||
58 | ok | ||
59 | ``` | ||
60 | |||
61 | 4. 查看接口文档 | ||
62 | |||
63 | * 方法1: 粘贴到[online editor](http://editor.swagger.io/)中查看 | ||
64 | * 方法2: 安装 https://bitbucket.org/abrahamL/openapi_toolset.git | ||
65 | |||
66 | ``` | ||
67 | $: cd docs | ||
68 | $: ls | ||
69 | main.yaml | ||
70 | $: python -m openapi_toolset | ||
71 | * Serving Flask app "openapi_toolset" (lazy loading) | ||
72 | * Environment: production | ||
73 | WARNING: Do not use the development server in a production environment. | ||
74 | Use a production WSGI server instead. | ||
75 | * Debug mode: off | ||
76 | * Running on http://127.0.0.1:9090/ (Press CTRL+C to quit) | ||
77 | ``` | ||
78 | |||
79 | 浏览器访问http://127.0.0.1:9090即可查看文档 |
-
Please register or sign in to post a comment