8000 Merge pull request #2 from encode/add-codec · millerlucas/python-openapi-codec@f68011e · GitHub
[go: up one dir, main page]

Skip to content

Commit f68011e

Browse files
authored
Merge pull request core-api#2 from encode/add-codec
Add codec
2 parents 29e72d9 + 79110a4 commit f68011e

File tree

5 files changed

+216
-2
lines changed

5 files changed

+216
-2
lines changed

openapi_codec/__init__.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,140 @@
1-
__version__ = "0.0.1"
1+
from coreapi.codecs.base import BaseCodec
2+
from coreapi.compat import urlparse
3+
from coreapi.document import Document, Link, Field
4+
from coreapi.exceptions import ParseError
5+
from openapi_codec.utils import _get_string, _get_dict, _get_list, _get_bool, get_strings, get_dicts
6+
import json
7+
8+
9+
__version__ = "0.0.2"
10+
11+
12+
def _expand_schema(schema):
13+
"""
14+
When an OpenAPI parameter uses `in="body"`, and the schema type is "object",
15+
then we expand out the parameters of the object into individual fields.
16+
"""
17+
schema_type = schema.get('type')
18+
schema_properties = _get_dict(schema, 'properties')
19+
schema_required = _get_list(schema, 'required')
20+
if ((schema_type == ['object']) or (schema_type == 'object')) and schema_properties:
21+
return [
22+
(key, key in schema_required)
23+
for key in schema_properties.keys()
24+
]
25+
return None
26+
27+
28+
def _get_document_base_url(data, base_url=None):
29+
"""
30+
Get the base url to use when constructing absolute paths from the
31+
relative ones provided in the schema defination.
32+
"""
33+
prefered_schemes = ['http', 'https']
34+
if base_url:
35+
url_components = urlparse.urlparse(base_url)
36+
default_host = url_components.netloc
37+
default_scheme = url_components.scheme
38+
else:
39+
default_host = ''
40+
default_scheme = None
41+
42+
host = _get_string(data, 'host', default=default_host)
43+
path = _get_string(data, 'basePath', default='/')
44+
45+
if not host:
46+
# No host is provided, and we do not have an initial URL.
47+
return path.strip('/') + '/'
48+
49+
schemes = _get_list(data, 'schemes')
50+
51+
if not schemes:
52+
# No schemes provided, use the initial URL, or a fallback.
53+
scheme = default_scheme or prefered_schemes[0]
54+
elif default_scheme in schemes:
55+
# Schemes provided, the initial URL matches one of them.
56+
scheme = default_scheme
57+
else:
58+
# Schemes provided, the initial URL does not match, pick a fallback.
59+
for scheme in prefered_schemes:
60+
if scheme in schemes:
61+
break
62+
else:
63+
raise ParseError('Unsupported transport schemes "%s"' % schemes)
64+
65+
return '%s://%s/%s/' % (scheme, host, path.strip('/'))
66+
67+
68+
def _parse_document(data, base_url=None):
69+
schema_url = base_url
70+
base_url = _get_document_base_url(data, base_url)
71+
info = _get_dict(data, 'info')
72+
title = _get_string(info, 'title')
73+
paths = _get_dict(data, 'paths')
74+
content = {}
75+
for path in paths.keys():
76+
url = urlparse.urljoin(base_url, path.lstrip('/'))
77+
spec = _get_dict(paths, path)
78+
default_parameters = get_dicts(_get_list(spec, 'parameters'))
79+
for action in spec.keys():
80+
action = action.lower()
81+
if action not in ('get', 'put', 'post', 'delete', 'options', 'head', 'patch'):
82+
continue
83+
operation = _get_dict(spec, action)
84+
85+
# Determine any fields on the link.
86+
fields = []
87+
parameters = get_dicts(_get_list(operation, 'parameters', default_parameters), dereference_using=data)
88+
for parameter in parameters:
89+
name = _get_string(parameter, 'name')
90+
location = _get_string(parameter, 'in')
91+
required = _get_bool(parameter, 'required', default=(location == 'path'))
92+
if location == 'body':
93+
schema = _get_dict(parameter, 'schema', dereference_using=data)
94+
expanded = _expand_schema(schema)
95+
if expanded is not None:
96+
expanded_fields = [
97+
Field(name=field_name, location='form', required=is_required)
98+
for field_name, is_required in expanded
99+
if not any([field.name == name for field in fields])
100+
]
101+
fields += expanded_fields
102+
else:
103+
field = Field(name=name, location='body', required=True)
104+
fields.append(field)
105+
else:
106+
field = Field(name=name, location=location, required=required)
107+
fields.append(field)
108+
link = Link(url=url, action=action, fields=fields)
109+
110+
# Add the link to the document content.
111+
tags = get_strings(_get_list(operation, 'tags'))
112+
operation_id = _get_string(operation, 'operationId')
113+
if tags:
114+
for tag in tags:
115+
if tag not in content:
116+
content[tag] = {}
117+
content[tag][operation_id] = link
118+
else:
119+
content[operation_id] = link
120+
121+
return Document(url=schema_url, title=title, content=content)
122+
123+
124+
class OpenAPICodec(BaseCodec):
125+
media_type = "application/json"
126+
127+
def load(self, bytes, base_url=None):
128+
"""
129+
Takes a bytestring and returns a document.
130+
"""
131+
try:
132+
data = json.loads(bytes.decode('utf-8'))
133+
except ValueError as exc:
134+
raise ParseError('Malformed JSON. %s' % exc)
135+
136+
doc = _parse_document(data, base_url)
137+
if not isinstance(doc, Document):
138+
raise ParseError('Top level node must be a document.')
139+
140+
return doc

openapi_codec/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from coreapi.compat import string_types
2+
3+
4+
# Helper functions to get an expected type from a dictionary.
5+
6+
def dereference(lookup_string, struct):
7+
"""
8+
Dereference a JSON pointer.
9+
http://tools.ietf.org/html/rfc6901
10+
"""
11+
keys = lookup_string.strip('#/').split('/')
12+
node = struct
13+
for key in keys:
14+
node = _get_dict(node, key)
15+
return node
16+
17+
18+
def is_json_pointer(value):
19+
return isinstance(value, dict) and ('$ref' in value) and (len(value) == 1)
20+
21+
22+
def _get_string(item, key, default=''):
23+
value = item.get(key)
24+
return value if isinstance(value, string_types) else default
25+
26+
27+
def _get_dict(item, key, default={}, dereference_using=None):
28+
value = item.get(key)
29+
if isinstance(value, dict):
30+
if dereference_using and is_json_pointer(value):
31+
return dereference(value['$ref'], dereference_using)
32+
return value
33+
return default.copy()
34+
35+
36+
def _get_list(item, key, default=[]):
37+
value = item.get(key)
38+
return value if isinstance(value, list) else list(default)
39+
40+
41+
def _get_bool(item, key, default=False):
42+
value = item.get(key)
43+
return value if isinstance(value, bool) else default
44+
45+
46+
# Helper functions to get an expected type from a list.
47+
48+
def get_dicts(item, dereference_using=None):
49+
ret = [value for value in item if isinstance(value, dict)]
50+
if dereference_using:
51+
return [
52+
dereference(value['$ref'], dereference_using) if is_json_pointer(value) else value
53+
for value in ret
54+
]
55+
return ret
56+
57+
58+
def get_strings(item):
59+
return [value for value in item if isinstance(value, string_types)]

requirements-unfrozen.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Package requirements
2+
coreapi
3+
14
# Testing requirements
25
coverage
36
flake8

requirements.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
click==6.6
2+
coreapi==1.24.0
13
coverage==4.1
24
flake8==2.6.2
5+
itypes==1.1.0
6+
Jinja2==2.8
7+
MarkupSafe==0.23
38
mccabe==0.5.0
49
py==1.4.31
510
pycodestyle==2.0.0
611
pyflakes==1.2.3
712
pytest==2.9.2
13+
requests==2.10.0
14+
simplejson==3.8.2
15+
uritemplate==0.6

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,10 @@ def get_package_data(package):
6868
'Operating System :: OS Independent',
6969
'Programming Language :: Python',
7070
'Programming Language :: Python :: 3',
71-
]
71+
],
72+
entry_points={
73+
'coreapi.codecs': [
74+
'openapi=openapi_codec:OpenAPICodec'
75+
]
76+
}
7277
)

0 commit comments

Comments
 (0)
0