Skip to content

Commit 5f26e5f

Browse files
authored
Merge pull request core-api#10 from core-api/version-1-1
Version 1.1
2 parents ff8edb6 + 35e9c34 commit 5f26e5f

File tree

7 files changed

+516
-57
lines changed

7 files changed

+516
-57
lines changed

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,113 @@
55
[![travis-image]][travis]
66
[![pypi-image]][pypi]
77

8+
## Introduction
9+
10+
This is a Python [Core API][coreapi] codec for the [Open API][openapi] schema format, also known as "Swagger".
11+
812
## Installation
913

1014
Install using pip:
1115

1216
$ pip install openapi-codec
1317

18+
## Creating Swagger schemas
19+
20+
To create a swagger schema from a `coreapi.Document`, use the codec directly.
21+
22+
>>> from openapi_codec import OpenAPICodec
23+
>>> codec = OpenAPICodec()
24+
>>> schema = codec.encode(document)
25+
26+
## Using with the Python Client Library
27+
28+
To use the Python client library to interact with a service that exposes a Swagger schema,
29+
include the codec in [the `decoders` argument][decoders].
30+
31+
>>> from openapi_codec import OpenAPICodec
32+
>>> from coreapi.codecs import JSONCodec
33+
>>> from coreapi import Client
34+
>>> decoders = [OpenAPICodec(), JSONCodec()]
35+
>>> client = Client(decoders=decoders)
36+
37+
If the server exposes the schema without properly using an `application/openapi+json` content type, then you'll need to make sure to include `format='openapi'` on the initial request,
38+
to force the correct codec to be used.
39+
40+
>>> schema = client.get('http://petstore.swagger.io/v2/swagger.json', format='openapi')
41+
42+
At this point you can now start to interact with the API:
43+
44+
>>> client.action(schema, ['pet', 'addPet'], params={'photoUrls': [], 'name': 'fluffy'})
45+
46+
## Using with the Command Line Client
47+
48+
Once the `openapi-codec` package is installed, the codec will automatically become available to the command line client.
49+
50+
$ pip install coreapi-cli openapi-codec
51+
$ coreapi codecs show
52+
Codec name Media type Support Package
53+
corejson | application/coreapi+json | encoding, decoding | coreapi==2.0.7
54+
openapi | application/openapi+json | encoding, decoding | openapi-codec==1.1.0
55+
json | application/json | decoding | coreapi==2.0.7
56+
text | text/* | decoding | coreapi==2.0.7
57+
download | */* | decoding | coreapi==2.0.7
58+
59+
If the server exposes the schema without properly using an `application/openapi+json` content type, then you'll need to make sure to include `format=openapi` on the initial request, to force the correct codec to be used.
60+
61+
$ coreapi get http://petstore.swagger.io/v2/swagger.json --format openapi
62+
<Swagger Petstore "http://petstore.swagger.io/v2/swagger.json">
63+
pet: {
64+
addPet(photoUrls, name, [status], [id], [category], [tags])
65+
deletePet(petId, [api_key])
66+
findPetsByStatus(status)
67+
...
68+
69+
At this point you can start to interact with the API.
70+
71+
$ coreapi action pet addPet --param name=fluffy --param photoUrls=[]
72+
{
73+
"id": 201609262739,
74+
"name": "fluffy",
75+
"photoUrls": [],
76+
"tags": []
77+
}
78+
79+
Use the `--debug` flag to see the full HTTP request and response.
80+
81+
$ coreapi action pet addPet --param name=fluffy --param photoUrls=[] --debug
82+
> POST /v2/pet HTTP/1.1
83+
> Accept-Encoding: gzip, deflate
84+
> Connection: keep-alive
85+
> Content-Length: 35
86+
> Content-Type: application/json
87+
> Accept: application/coreapi+json, */*
88+
> Host: petstore.swagger.io
89+
> User-Agent: coreapi
90+
>
91+
> {"photoUrls": [], "name": "fluffy"}
92+
< 200 OK
93+
< Access-Control-Allow-Headers: Content-Type, api_key, Authorization
94+
< Access-Control-Allow-Methods: GET, POST, DELETE, PUT
95+
< Access-Control-Allow-Origin: *
96+
< Connection: close
97+
< Content-Type: application/json
98+
< Date: Mon, 26 Sep 2016 13:17:33 GMT
99+
< Server: Jetty(9.2.9.v20150224)
100+
<
101+
< {"id":201609262739,"name":"fluffy","photoUrls":[],"tags":[]}
102+
103+
{
104+
"id": 201609262739,
105+
"name": "fluffy",
106+
"photoUrls": [],
107+
"tags": []
108+
}
14109

15110
[travis-image]: https://secure.travis-ci.org/core-api/python-openapi-codec.svg?branch=master
16111
[travis]: http://travis-ci.org/core-api/python-openapi-codec?branch=master
17112
[pypi-image]: https://img.shields.io/pypi/v/openapi-codec.svg
18113
[pypi]: https://pypi.python.org/pypi/openapi-codec
114+
115+
[coreapi]: http://www.coreapi.org/
116+
[openapi]: https://openapis.org/
117+
[decoders]: http://core-api.github.io/python-client/api-guide/client/#instantiating-a-client

openapi_codec/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
from openapi_codec.decode import _parse_document
99

1010

11-
__version__ = "1.0.0"
11+
__version__ = '1.1.0'
1212

1313

1414
class OpenAPICodec(BaseCodec):
15-
media_type = "application/openapi+json"
16-
supports = ['encoding', 'decoding']
15+
media_type = 'application/openapi+json'
16+
format = 'openapi'
1717

18-
def load(self, bytes, base_url=None):
18+
def decode(self, bytes, **options):
1919
"""
2020
Takes a bytestring and returns a document.
2121
"""
@@ -24,12 +24,15 @@ def load(self, bytes, base_url=None):
2424
except ValueError as exc:
2525
raise ParseError('Malformed JSON. %s' % exc)
2626

27+
base_url = options.get('base_url')
2728
doc = _parse_document(data, base_url)
2829
if not isinstance(doc, Document):
2930
raise ParseError('Top level node must be a document.')
3031

3132
return doc
3233

33-
def dump(self, document, **kwargs):
34+
def encode(self, document, **options):
35+
if not isinstance(document, Document):
36+
raise TypeError('Expected a `coreapi.Document` instance')
3437
data = generate_swagger_object(document)
3538
return force_bytes(json.dumps(data))

openapi_codec/decode.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ def _parse_document(data, base_url=None):
88
base_url = _get_document_base_url(data, base_url)
99
info = _get_dict(data, 'info')
1010
title = _get_string(info, 'title')
11+
consumes = get_strings(_get_list(data, 'consumes'))
1112
paths = _get_dict(data, 'paths')
1213
content = {}
1314
for path in paths.keys():
14-
url = urlparse.urljoin(base_url, path.lstrip('/'))
15+
url = base_url + path.lstrip('/')
1516
spec = _get_dict(paths, path)
1617
default_parameters = get_dicts(_get_list(spec, 'parameters'))
1718
for action in spec.keys():
@@ -20,7 +21,13 @@ def _parse_document(data, base_url=None):
2021
continue
2122
operation = _get_dict(spec, action)
2223

24+
link_description = _get_string(operation, 'description')
25+
link_consumes = get_strings(_get_list(operation, 'consumes', consumes))
26+
2327
# Determine any fields on the link.
28+
has_body = False
29+
has_form = False
30+
2431
fields = []
2532
parameters = get_dicts(_get_list(operation, 'parameters', default_parameters), dereference_using=data)
2633
for parameter in parameters:
@@ -29,31 +36,45 @@ def _parse_document(data, base_url=None):
2936
required = _get_bool(parameter, 'required', default=(location == 'path'))
3037
description = _get_string(parameter, 'description')
3138
if location == 'body':
39+
has_body = True
3240
schema = _get_dict(parameter, 'schema', dereference_using=data)
3341
expanded = _expand_schema(schema)
3442
if expanded is not None:
3543
expanded_fields = [
36-
Field(name=field_name, location='form', required=is_required, description=description)
37-
for field_name, is_required in expanded
38-
if not any([field.name == name for field in fields])
44+
Field(name=field_name, location='form', required=is_required, description=field_description)
45+
for field_name, is_required, field_description in expanded
46+
if not any([field.name == field_name for field in fields])
3947
]
4048
fields += expanded_fields
4149
else:
42-
field = Field(name=name, location='body', required=True, description=description)
50+
field = Field(name=name, location='body', required=required, description=description)
4351
fields.append(field)
4452
else:
53+
if location == 'formData':
54+
has_form = True
55+
location = 'form'
4556
field = Field(name=name, location=location, required=required, description=description)
4657
fields.append(field)
47-
link = Link(url=url, action=action, fields=fields)
58+
59+
encoding = ''
60+
if has_body:
61+
encoding = _select_encoding(link_consumes)
62+
elif has_form:
63+
encoding = _select_encoding(link_consumes, form=True)
64+
65+
link = Link(url=url, action=action, encoding=encoding, fields=fields, description=link_description)
4866

4967
# Add the link to the document content.
5068
tags = get_strings(_get_list(operation, 'tags'))
5169
operation_id = _get_string(operation, 'operationId')
5270
if tags:
53-
for tag in tags:
54-
if tag not in content:
55-
content[tag] = {}
56-
content[tag][operation_id] = link
71+
tag = tags[0]
72+
prefix = tag + '_'
73+
if operation_id.startswith(prefix):
74+
operation_id = operation_id[len(prefix):]
75+
if tag not in content:
76+
content[tag] = {}
77+
content[tag][operation_id] = link
5778
else:
5879
content[operation_id] = link
5980

@@ -65,7 +86,7 @@ def _get_document_base_url(data, base_url=None):
6586
Get the base url to use when constructing absolute paths from the
6687
relative ones provided in the schema defination.
6788
"""
68-
prefered_schemes = ['http', 'https']
89+
prefered_schemes = ['https', 'http']
6990
if base_url:
7091
url_components = urlparse.urlparse(base_url)
7192
default_host = url_components.netloc
@@ -76,10 +97,12 @@ def _get_document_base_url(data, base_url=None):
7697

7798
host = _get_string(data, 'host', default=default_host)
7899
path = _get_string(data, 'basePath', default='/')
100+
path = '/' + path.lstrip('/')
101+
path = path.rstrip('/') + '/'
79102

80103
if not host:
81104
# No host is provided, and we do not have an initial URL.
82-
return path.strip('/') + '/'
105+
return path
83106

84107
schemes = _get_list(data, 'schemes')
85108

@@ -97,7 +120,35 @@ def _get_document_base_url(data, base_url=None):
97120
else:
98121
raise ParseError('Unsupported transport schemes "%s"' % schemes)
99122

100-
return '%s://%s/%s/' % (scheme, host, path.strip('/'))
123+
return '%s://%s%s' % (scheme, host, path)
124+
125+
126+
def _select_encoding(consumes, form=False):
127+
"""
128+
Given an OpenAPI 'consumes' list, return a single 'encoding' for CoreAPI.
129+
"""
130+
if form:
131+
preference = [
132+
'multipart/form-data',
133+
'application/x-www-form-urlencoded',
134+
'application/json'
135+
]
136+
else:
137+
preference = [
138+
'application/json',
139+
'multipart/form-data',
140+
'application/x-www-form-urlencoded',
141+
'application/octet-stream'
142+
]
143+
144+
if not consumes:
145+
return preference[0]
146+
147+
for media_type in preference:
148+
if media_type in consumes:
149+
return media_type
150+
151+
return consumes[0]
101152

102153

103154
def _expand_schema(schema):
@@ -110,7 +161,7 @@ def _expand_schema(schema):
110161
schema_required = _get_list(schema, 'required')
111162
if ((schema_type == ['object']) or (schema_type == 'object')) and schema_properties:
112163
return [
113-
(key, key in schema_required)
164+
(key, key in schema_required, schema_properties[key].get('description'))
114165
for key in schema_properties.keys()
115166
]
116167
return None

0 commit comments

Comments
 (0)