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