Skip to content

Commit

Permalink
v0.0.16
Browse files Browse the repository at this point in the history
  • Loading branch information
taojy123 committed Mar 12, 2020
1 parent ea6943b commit 9002bbc
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 31 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,20 @@ yaml_data = doc.to_yaml()
doc3 = Doc(yaml_data)
doc3.build('best3.html')

# 使用 raml 创建文档对象(不完全支持 raml)
from eave.utils import raml2eave
doc = raml2eave('example.raml')
doc.build('example.html', 'zh')

# 在添加 api 时,也通过 from_md 参数,引入单独编写的 markdown 文件作为文档内容
doc.add_api(
title="获取订单列表接口",
uri="/orders/list/",
from_md="orders.md",
)

# 使用 raml 创建文档对象(不完全支持 raml)
from eave.utils import raml2eave
doc = raml2eave('example.raml')
doc.build('example.html', 'zh')

# 根据 django-rest-framework 的代码自动生成 Api 对象
from eave.utils import auto_drf_apis
api_list, api_post, api_detail, actions = auto_drf_apis('用户', '/api/users/', UserViewSet)
doc.add_apis(api_list, api_post, api_detail, *actions)
```
19 changes: 17 additions & 2 deletions eave/eave.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,30 @@ def build(self, target=None, language='en'):
return html

def add_note(self, *args, **kwargs):
note = Note(*args, **kwargs)
if args and isinstance(args[0], Note):
note = args[0]
else:
note = Note(*args, **kwargs)
self.notes.append(note)
return note

def add_api(self, *args, **kwargs):
api = Api(*args, **kwargs)
if args and isinstance(args[0], Api):
api = args[0]
else:
api = Api(*args, **kwargs)
self.apis.append(api)
return api

def add_apis(self, *apis):
if not apis:
return
if isinstance(apis[0], list):
apis = apis[0]
for api in apis:
assert isinstance(api, Api), 'params of add_apis must be Api instance!'
self.add_api(api)

def load_data(self, data):
self.title = data.get('title', self.title)
self.version = data.get('version', self.version)
Expand Down
33 changes: 23 additions & 10 deletions eave/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
.content {
padding: 5px;
}
.power-by {
display: none;
}
}
</style>

Expand Down Expand Up @@ -108,7 +111,9 @@ <h3>{{note.title}}</h3>
<h1 id="{{api.id}}">{{api.title}}</h1>

%if api.uri or not api.from_md:
<div class="endpoint {{api.method.lower()}}">{{api.uri_escape}}</div>
<div class="endpoint {{api.method.lower()}}">
<a href="{{api.uri_escape}}" target="_blank" style="color: inherit">{{api.uri_escape}}</a>
</div>
%endif

%if api.from_md:
Expand Down Expand Up @@ -138,9 +143,9 @@ <h1 id="{{api.id}}">{{api.title}}</h1>
<th>Uri Parameters</th>
<th>Type</th>
<th>Description</th>
<th class="min-width-2">Required</th>
<th class="min-width-3">Default</th>
<th class="min-width-2">Example</th>
<th>Required</th>
<th>Default</th>
<th>Example</th>
%endif
</tr>
</thead>
Expand All @@ -167,9 +172,9 @@ <h1 id="{{api.id}}">{{api.title}}</h1>
<th>Query 参数</th>
<th>类型</th>
<th>描述</th>
<th>必须</th>
<th>默认值</th>
<th>示例</th>
<th class="min-width-2">必须</th>
<th class="min-width-3">默认值</th>
<th class="min-width-2">示例</th>
%else:
<th>Query Parameters</th>
<th>Type</th>
Expand Down Expand Up @@ -203,9 +208,9 @@ <h1 id="{{api.id}}">{{api.title}}</h1>
<th>Body 参数</th>
<th>类型</th>
<th>描述</th>
<th>必须</th>
<th>默认值</th>
<th>示例</th>
<th class="min-width-2">必须</th>
<th class="min-width-3">默认值</th>
<th class="min-width-2">示例</th>
%else:
<th>Body Parameters</th>
<th>Type</th>
Expand Down Expand Up @@ -291,11 +296,19 @@ <h1 id="{{api.id}}">{{api.title}}</h1>
<hr>
%endif


<footer>
<p class="power-by" style="text-align: right; font-weight: bold;">
Power By <a href="https://github.com/taojy123/eave" target="_blank">Eave</a>
</p>
</footer>

</div>


</div>


<script>
{{resource['highlight_js']}}
</script>
Expand Down
183 changes: 170 additions & 13 deletions eave/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import json
import re
import requests

from eave import *
import yaml

__all__ = ['raml2eave', 'auto_drf_apis']

def find_resources(data, base_uri):

def _find_resources(data, base_uri):
for key, value in data.items():
if not key.startswith('/'):
continue
Expand All @@ -19,13 +22,13 @@ def find_resources(data, base_uri):
break


def find_target(data, target, excludes=None):
def _find_target(data, target, excludes=None):
excludes = excludes or []
if isinstance(data, list):
for item in data:
if item in excludes:
continue
r = find_target(item, target, excludes)
r = _find_target(item, target, excludes)
if r:
return r
elif isinstance(data, dict):
Expand All @@ -35,13 +38,13 @@ def find_target(data, target, excludes=None):
for key, value in data.items():
if key in excludes:
continue
r = find_target(value, target, excludes)
r = _find_target(value, target, excludes)
if r:
return r


def append_resource(doc, data, base_uri=''):
for resource in find_resources(data, base_uri):
def _append_resource(doc, data, base_uri=''):
for resource in _find_resources(data, base_uri):
api_uri = resource['uri']
secured_by = resource.get('securedBy')

Expand Down Expand Up @@ -71,22 +74,26 @@ def append_resource(doc, data, base_uri=''):
p_data['name'] = p_name
api.query_params.append(QP(p_data))

properties = find_target(method_data.get('body'), 'properties') or {}
properties = _find_target(method_data.get('body'), 'properties') or {}
for p_name, p_data in properties.items():
p_data = p_data or {}
p_data['name'] = p_name
api.body_params.append(BP(p_data))

api.body_example = find_target(method_data.get('body'), 'example', ['properties']) or ''
api.response_example = find_target(method_data.get('responses'), 'example') or ''
api.body_example = _find_target(method_data.get('body'), 'example', ['properties']) or ''
api.response_example = _find_target(method_data.get('responses'), 'example') or ''
api.content_types = []

doc.apis.append(api)

append_resource(doc, resource, api_uri)
_append_resource(doc, resource, api_uri)


def raml2eave(raml_file, silence=True):
"""
raml2eave('tiadmin.raml').build('tiadmin.html', 'zh')
raml2eave('grs.raml').build('grs.html', 'zh')
"""

raml_content = open(raml_file, encoding='utf8').read()
data = yaml.safe_load(raml_content)
Expand All @@ -108,11 +115,161 @@ def raml2eave(raml_file, silence=True):
content = scheme.get('description', '')
doc.add_note(title=name, content=content)

append_resource(doc, data, '')
_append_resource(doc, data, '')

return doc


# raml2eave('tiadmin.raml').build('tiadmin.html', 'zh')
# raml2eave('grs.raml').build('grs.html', 'zh')
def _action_description_handle(description):
description = description.strip()
lines = description.splitlines()
title = lines[0].strip()
result = {
'title': title,
'description': '',
'query_params': [],
'body_params': [],
'response_description': '',
}
target = 'description'
for line in lines[1:]:
line = line.strip()
if line == 'GET':
target = 'query_params'
continue
elif line == 'POST':
target = 'body_params'
continue
elif line == 'RESPONSE':
target = 'response_description'
continue

if isinstance(result[target], str):
result[target] += line + '\n\n'
elif isinstance(result[target], list):
name, description = line.split(':', 1)
name = name.strip()
description = description.strip()
if target == 'query_params':
item = QP(name=name, description=description)
elif target == 'body_params':
item = BP(name=name, description=description)
else:
assert False
result[target].append(item)
return result


def auto_drf_apis(res_name, uri, view_set, testhost='http://127.0.0.1:8000'):
"""
# todo: 加强这块文档和示例,写进 eave 主页介绍
api_list, api_post, api_detail, actions = auto_drf_apis('用户', '/api/users/', UserViewSet)
doc.add_apis(api_list, api_post, api_detail, *actions)
"""

# ======= GET List =======
api_list = Api()
api_list.title = res_name + '列表'
api_list.uri = uri
api_list.method = 'GET'

api_list.query_params = []
filter_fields = view_set.filter_class.Meta.fields
for field_name, kinds in filter_fields.items():
for kind in kinds:
query_name = f'{field_name}__{kind}'
kind_zh = '筛选'
if kind == 'exact':
kind_zh = '指定'
query_name = field_name
elif kind in ['icontains', 'contains']:
kind_zh = '匹配'
elif kind == 'in':
kind_zh = '命中'
field_zh = view_set.serializer_class.Meta.model._meta.get_field(field_name).verbose_name
description = kind_zh + field_zh
api_list.query_params.append(QP(name=query_name, description=description))

url = testhost + api_list.uri
print(url)
data = requests.get(url).json()
if len(data['results']) > 2:
data['results'] = [data['results'][0]]
api_list.response_example = json.dumps(data, ensure_ascii=False, indent=4)

# ======= POST =======
api_post = Api()
api_post.title = '创建' + res_name
api_post.uri = uri
api_post.method = 'POST'

serializer = view_set.serializer_class()
api_post.body_params = []
for field_name, field in serializer.fields.items():
if field.read_only:
continue
type = 'string'
field_type = str(field.__class__)
if 'IntegerField' in field_type:
type = 'int'
elif 'FloatField' in field_type:
type = 'float'
elif 'DecimalField' in field_type:
type = 'decimal'
elif 'BooleanField' in field_type:
type = 'bool'
description = field.label
if field.help_text:
description += f' [{field.help_text}]'
required = field.required
default = field.default
try:
if 'empty' in str(default.__class__):
default = view_set.serializer_class.Meta.model._meta.get_field(field_name).default
except:
# print(f'Warning: {field_name} field not found in {view_set.serializer_class.Meta.model}')
pass
api_post.body_params.append(BP(name=field_name, type=type, description=description, required=required, default=default))

if data['results']:
api_post.response_example = json.dumps(data['results'][0], ensure_ascii=False, indent=4)

# ======= GET Detail =======
api_detail = Api()
api_detail.title = res_name + '详情'
api_detail.uri = f'{uri.rstrip("/")}/<id>/'
api_detail.method = 'GET'
api_detail.uri_params = [UP(name='id', description=f'{res_name} ID', example=1)]

data = requests.get(url).json()
if data['results']:
url = data['results'][0].get('url')
if not url:
res_id = data['results'][0]['id']
url = f'{testhost}{uri.rstrip("/")}/{res_id}/'
data2 = requests.get(url).json()
api_detail.response_example = json.dumps(data2, ensure_ascii=False, indent=4)

# ======= Actions =======
actions = []
for item in dir(view_set):
func = getattr(view_set, item)
if not all([hasattr(func, 'detail'), hasattr(func, 'url_path'), hasattr(func, 'kwargs')]):
continue
detail = func.detail
url_path = func.url_path
description = func.kwargs['description']
method = list(func.mapping.keys())[0].upper()
result = _action_description_handle(description)
if detail:
action_uri = f'{uri.rstrip("/")}/<id>/{url_path}/'
else:
action_uri = f'{uri.rstrip("/")}/{url_path}/'
api_action = Api(uri=action_uri, method=method, **result)
actions.append(api_action)

# ============================

return api_list, api_post, api_detail, actions


2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

setup(
name='eave',
version='0.0.15',
version='0.0.16',
description='A Restful Api Document Builder For Pythonista',
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down

0 comments on commit 9002bbc

Please sign in to comment.