sc-oa 0.7.0.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sc_oa-0.7.0.17.dist-info/METADATA +35 -0
- sc_oa-0.7.0.17.dist-info/RECORD +26 -0
- sc_oa-0.7.0.17.dist-info/WHEEL +5 -0
- sc_oa-0.7.0.17.dist-info/licenses/LICENSE +23 -0
- sc_oa-0.7.0.17.dist-info/top_level.txt +1 -0
- sphinxcontrib/__init__.py +10 -0
- sphinxcontrib/openapi/__init__.py +93 -0
- sphinxcontrib/openapi/__main__.py +86 -0
- sphinxcontrib/openapi/_lib2to3.py +378 -0
- sphinxcontrib/openapi/directive.py +58 -0
- sphinxcontrib/openapi/locale/es_ES/LC_MESSAGES/openapi.mo +0 -0
- sphinxcontrib/openapi/locale/es_ES/LC_MESSAGES/openapi.po +170 -0
- sphinxcontrib/openapi/locale/fr_FR/LC_MESSAGES/openapi.mo +0 -0
- sphinxcontrib/openapi/locale/fr_FR/LC_MESSAGES/openapi.po +170 -0
- sphinxcontrib/openapi/locale/openapi.pot +168 -0
- sphinxcontrib/openapi/openapi20.py +263 -0
- sphinxcontrib/openapi/openapi30.py +750 -0
- sphinxcontrib/openapi/renderers/__init__.py +18 -0
- sphinxcontrib/openapi/renderers/_description.py +26 -0
- sphinxcontrib/openapi/renderers/_httpdomain.py +620 -0
- sphinxcontrib/openapi/renderers/_httpdomain_old.py +59 -0
- sphinxcontrib/openapi/renderers/_model.py +426 -0
- sphinxcontrib/openapi/renderers/_toc.py +48 -0
- sphinxcontrib/openapi/renderers/abc.py +46 -0
- sphinxcontrib/openapi/schema_utils.py +137 -0
- sphinxcontrib/openapi/utils.py +137 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Here lies still breathing and only renderer implementation."""
|
|
2
|
+
|
|
3
|
+
from docutils.parsers.rst import directives
|
|
4
|
+
|
|
5
|
+
from . import abc
|
|
6
|
+
from .. import openapi20, openapi30, utils
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HttpdomainOldRenderer(abc.RestructuredTextRenderer):
|
|
10
|
+
|
|
11
|
+
option_spec = {
|
|
12
|
+
# A list of endpoints to be rendered. Endpoints must be whitespace
|
|
13
|
+
# delimited.
|
|
14
|
+
"paths": lambda s: s.split(),
|
|
15
|
+
# Regular expression patterns to includes/excludes endpoints to/from
|
|
16
|
+
# rendering. Similar to paths, the patterns must be whitespace
|
|
17
|
+
# delimited.
|
|
18
|
+
"include": lambda s: s.split(),
|
|
19
|
+
"exclude": lambda s: s.split(),
|
|
20
|
+
# Endpoints to be included based on HTTP method names.
|
|
21
|
+
"methods": lambda s: s.split(),
|
|
22
|
+
# Render the request body structure when passed.
|
|
23
|
+
"request": directives.flag,
|
|
24
|
+
# Render request/response examples when passed.
|
|
25
|
+
"examples": directives.flag, # render examples when passed
|
|
26
|
+
# Render request/response examples in one block or inline with the response codes.
|
|
27
|
+
"contextpath": directives.flag, # use the server path as prefix in service URL
|
|
28
|
+
"group_examples": directives.flag, # render examples in one block
|
|
29
|
+
# Include links to entity description
|
|
30
|
+
"entities": directives.flag,
|
|
31
|
+
# Group endpoints by tags when passed. By default, no grouping is
|
|
32
|
+
# applied and endpoints are rendered in the order they met in spec.
|
|
33
|
+
"group": directives.flag,
|
|
34
|
+
# Markup format to render OpenAPI descriptions.
|
|
35
|
+
"format": str,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def __init__(self, state, options):
|
|
39
|
+
self._state = state
|
|
40
|
+
self._options = options
|
|
41
|
+
|
|
42
|
+
def render_restructuredtext_markup(self, spec):
|
|
43
|
+
# OpenAPI spec may contain JSON references, common properties, etc.
|
|
44
|
+
# Trying to render the spec "As Is" will require to put multiple if-s
|
|
45
|
+
# around the code. In order to simplify rendering flow, let's make it
|
|
46
|
+
# have only one (expected) schema, i.e. normalize it.
|
|
47
|
+
utils.normalize_spec(spec, **self._options)
|
|
48
|
+
|
|
49
|
+
# We support both OpenAPI 2.0 (f.k.a. Swagger) and OpenAPI 3.0.0, so
|
|
50
|
+
# determine which version we are parsing here.
|
|
51
|
+
spec_version = spec.get("openapi", spec.get("swagger", "2.0"))
|
|
52
|
+
if spec_version.startswith("2."):
|
|
53
|
+
openapihttpdomain = openapi20.openapihttpdomain
|
|
54
|
+
elif spec_version.startswith("3."):
|
|
55
|
+
openapihttpdomain = openapi30.openapihttpdomain
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError("Unsupported OpenAPI version (%s)" % spec_version)
|
|
58
|
+
|
|
59
|
+
yield from openapihttpdomain(spec, **self._options)
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
from . import abc
|
|
2
|
+
from .. import utils
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import textwrap
|
|
8
|
+
|
|
9
|
+
import jsonschema
|
|
10
|
+
from jsonschema import validate
|
|
11
|
+
from docutils.parsers.rst import directives
|
|
12
|
+
|
|
13
|
+
from sphinx.locale import get_translation
|
|
14
|
+
_ = get_translation('openapi')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_description(obj, convert):
|
|
18
|
+
d = obj.get('description', '')
|
|
19
|
+
D = convert(d).strip()
|
|
20
|
+
# if d.strip()!=D:
|
|
21
|
+
if '\n' in D:
|
|
22
|
+
D += '\n\n'
|
|
23
|
+
if 'default' in obj:
|
|
24
|
+
if D and D.splitlines()[-1] and D[-1] != '.':
|
|
25
|
+
D += '.'
|
|
26
|
+
if D and D.splitlines()[-1]:
|
|
27
|
+
D += ' '
|
|
28
|
+
D += 'Default: ``' + json.dumps(obj['default']) + "``."
|
|
29
|
+
return D
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_contraints(obj):
|
|
33
|
+
c = []
|
|
34
|
+
if 'minItems' in obj:
|
|
35
|
+
c.append(_('minItems is {}').format(obj['minItems']))
|
|
36
|
+
if 'maxItems' in obj:
|
|
37
|
+
c.append(_('maxItems is {}').format(obj['maxItems']))
|
|
38
|
+
if 'minLength' in obj:
|
|
39
|
+
c.append(_('minLength is {}').format(obj['minLength']))
|
|
40
|
+
if 'maxLength' in obj:
|
|
41
|
+
c.append(_('maxLength is {}').format(obj['maxLength']))
|
|
42
|
+
if 'minimum' in obj:
|
|
43
|
+
c.append(_('minimum is {}').format(obj['minimum']))
|
|
44
|
+
if 'maximum' in obj:
|
|
45
|
+
c.append(_('maximum is {}').format(obj['maximum']))
|
|
46
|
+
if 'uniqueItems' in obj:
|
|
47
|
+
c.append(_("items must be unique"))
|
|
48
|
+
if "pattern" in obj:
|
|
49
|
+
c.append(_("pattern ``{}``").format(obj["pattern"]))
|
|
50
|
+
if 'enum' in obj:
|
|
51
|
+
c.append(_('possible values are {}').format(
|
|
52
|
+
', '.join(
|
|
53
|
+
[
|
|
54
|
+
'``{}``'.format(x) for x in obj['enum']
|
|
55
|
+
]
|
|
56
|
+
)))
|
|
57
|
+
if 'readOnly' in obj:
|
|
58
|
+
c.append(_("read only"))
|
|
59
|
+
if 'writeOnly' in obj:
|
|
60
|
+
c.append(_("write only"))
|
|
61
|
+
c = [str(x) for x in c]
|
|
62
|
+
s = '; '.join(c)
|
|
63
|
+
return s
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _add_constraints(obj, D, C):
|
|
67
|
+
if C:
|
|
68
|
+
if str(_('Constraints')) not in D:
|
|
69
|
+
C = _("Constraints: {}").format(C)
|
|
70
|
+
if D and D.splitlines()[-1] and D[-1] != '.':
|
|
71
|
+
D += '.'
|
|
72
|
+
else:
|
|
73
|
+
if C and D and D.splitlines()[-1] and D[-1] != ';':
|
|
74
|
+
D += ';'
|
|
75
|
+
if D and D.splitlines()[-1] and C and C[0] != '\n':
|
|
76
|
+
D += ' '
|
|
77
|
+
D += C
|
|
78
|
+
else:
|
|
79
|
+
if D and D.splitlines()[-1] and D[-1] != '.':
|
|
80
|
+
D += '.'
|
|
81
|
+
if 'deprecated' in obj:
|
|
82
|
+
D += "\n\n**{}**".format(_("DEPRECATED"))
|
|
83
|
+
return D
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_multi_type(schema, entities):
|
|
87
|
+
T = []
|
|
88
|
+
duplicate = set()
|
|
89
|
+
if 'oneOf' in schema:
|
|
90
|
+
k = 'oneOf'
|
|
91
|
+
elif 'allOf' in schema:
|
|
92
|
+
k = 'allOf'
|
|
93
|
+
else:
|
|
94
|
+
k = 'anyOf'
|
|
95
|
+
for t in schema[k]:
|
|
96
|
+
type = t.get('type', 'object')
|
|
97
|
+
if '$entity_ref' in t and type == 'object':
|
|
98
|
+
T.append(ref2link(entities, t['$entity_ref']))
|
|
99
|
+
else:
|
|
100
|
+
if type == 'string' and 'enum' in t:
|
|
101
|
+
type = 'enumerate'
|
|
102
|
+
vals = ', '.join(
|
|
103
|
+
[
|
|
104
|
+
'``{}``'.format(x) for x in t['enum']
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
if vals:
|
|
108
|
+
type += ' (' + vals + ')'
|
|
109
|
+
if type not in duplicate:
|
|
110
|
+
T.append(type)
|
|
111
|
+
duplicate.add(type)
|
|
112
|
+
return T
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _process_one(prefix, schema, mandatory, entities, convert):
|
|
116
|
+
if 'oneOf' in schema:
|
|
117
|
+
type = 'oneOf'
|
|
118
|
+
elif 'allOf' in schema:
|
|
119
|
+
type = 'allOf'
|
|
120
|
+
elif 'anyOf' in schema:
|
|
121
|
+
type = 'anyOf'
|
|
122
|
+
else:
|
|
123
|
+
type = schema.get('type', 'object')
|
|
124
|
+
if '$entity_ref' in schema and type == 'object' and prefix:
|
|
125
|
+
# does not apply to first level types (prefix empty)
|
|
126
|
+
T = _('Object of type {}').format(ref2link(entities, schema['$entity_ref']))
|
|
127
|
+
D = _get_description(schema, convert)
|
|
128
|
+
C = _get_contraints(schema)
|
|
129
|
+
D = _add_constraints(schema, D, C)
|
|
130
|
+
ret = ['.'.join(prefix), T, D, mandatory]
|
|
131
|
+
yield ret
|
|
132
|
+
elif type == 'array':
|
|
133
|
+
ref = schema['items'].get('$entity_ref', None)
|
|
134
|
+
type_items = schema['items'].get('type', None)
|
|
135
|
+
if ref:
|
|
136
|
+
D = _get_description(schema, convert)
|
|
137
|
+
C = _get_contraints(schema)
|
|
138
|
+
D = _add_constraints(schema, D, C)
|
|
139
|
+
if type_items not in ['object', 'array']:
|
|
140
|
+
# Since only object and array are described on their own
|
|
141
|
+
# Add here the constraints of the referenced type
|
|
142
|
+
D = _get_description(schema['items'], convert)
|
|
143
|
+
C = _get_contraints(schema['items'])
|
|
144
|
+
D = _add_constraints(schema['items'], D, C)
|
|
145
|
+
C = _get_contraints(schema)
|
|
146
|
+
D = _add_constraints(schema, D, C)
|
|
147
|
+
yield [
|
|
148
|
+
'.'.join(prefix),
|
|
149
|
+
_('Array of {}').format(type_items),
|
|
150
|
+
D,
|
|
151
|
+
mandatory
|
|
152
|
+
]
|
|
153
|
+
else:
|
|
154
|
+
yield [
|
|
155
|
+
'.'.join(prefix),
|
|
156
|
+
_('Array of {}').format(ref2link(entities, ref)),
|
|
157
|
+
D,
|
|
158
|
+
mandatory
|
|
159
|
+
]
|
|
160
|
+
elif type_items == 'object':
|
|
161
|
+
T = _("Array")
|
|
162
|
+
D = _get_description(schema, convert)
|
|
163
|
+
C = _get_contraints(schema)
|
|
164
|
+
D = _add_constraints(schema, D, C)
|
|
165
|
+
yield ['.'.join(prefix), T, D, mandatory]
|
|
166
|
+
if prefix:
|
|
167
|
+
prefix[-1] += '[]'
|
|
168
|
+
else:
|
|
169
|
+
prefix = ['[]']
|
|
170
|
+
for x in _process_one(prefix, schema['items'], False, entities, convert):
|
|
171
|
+
yield x
|
|
172
|
+
else:
|
|
173
|
+
# Support array of simple types (string, etc.)
|
|
174
|
+
D = _get_description(schema, convert)
|
|
175
|
+
C = _get_contraints(schema)
|
|
176
|
+
for x in _process_one(prefix, schema['items'], False, entities, convert):
|
|
177
|
+
# Add C to x[2] now and not before (to avoid double "Constraints:")
|
|
178
|
+
if D and x[2]:
|
|
179
|
+
DD = D + ' ' + x[2]
|
|
180
|
+
else:
|
|
181
|
+
DD = D + x[2]
|
|
182
|
+
DD = _add_constraints(schema, DD, C)
|
|
183
|
+
yield [x[0], _('Array of {}').format(x[1]), DD, mandatory]
|
|
184
|
+
elif type == 'object':
|
|
185
|
+
required = schema.get('required', [])
|
|
186
|
+
for prop_name, prop in schema.get('properties', {}).items():
|
|
187
|
+
for x in _process_one(
|
|
188
|
+
prefix+[prop_name],
|
|
189
|
+
prop,
|
|
190
|
+
prop_name in required,
|
|
191
|
+
entities,
|
|
192
|
+
convert):
|
|
193
|
+
yield x
|
|
194
|
+
if 'additionalProperties' in schema:
|
|
195
|
+
D = _('Additional properties')
|
|
196
|
+
if schema['additionalProperties'] is True:
|
|
197
|
+
T = ''
|
|
198
|
+
elif schema['additionalProperties'] is False:
|
|
199
|
+
return
|
|
200
|
+
else:
|
|
201
|
+
if 'oneOf' in schema['additionalProperties']:
|
|
202
|
+
T = _("One of {}").format(
|
|
203
|
+
", ".join(_get_multi_type(schema['additionalProperties'], entities)))
|
|
204
|
+
elif 'allOf' in schema['additionalProperties']:
|
|
205
|
+
T = _("All of {}").format(
|
|
206
|
+
", ".join(_get_multi_type(schema['additionalProperties'], entities)))
|
|
207
|
+
elif 'anyOf' in schema['additionalProperties']:
|
|
208
|
+
T = _("Any of {}").format(
|
|
209
|
+
", ".join(_get_multi_type(schema['additionalProperties'], entities)))
|
|
210
|
+
else:
|
|
211
|
+
T = schema['additionalProperties'].get('type', 'object')
|
|
212
|
+
yield ['.'.join(prefix+['*']), T, D, '']
|
|
213
|
+
elif 'oneOf' in schema:
|
|
214
|
+
# One of the subtype, must be basic types or ref
|
|
215
|
+
D = _get_description(schema, convert)
|
|
216
|
+
C = _get_contraints(schema)
|
|
217
|
+
D = _add_constraints(schema, D, C)
|
|
218
|
+
T = _get_multi_type(schema, entities)
|
|
219
|
+
T = _("One of {}").format(", ".join(T))
|
|
220
|
+
yield ['.'.join(prefix), T, D, mandatory]
|
|
221
|
+
elif 'allOf' in schema:
|
|
222
|
+
# All of the subtype, must be basic types or ref
|
|
223
|
+
D = _get_description(schema, convert)
|
|
224
|
+
C = _get_contraints(schema)
|
|
225
|
+
D = _add_constraints(schema, D, C)
|
|
226
|
+
T = _get_multi_type(schema, entities)
|
|
227
|
+
T = _("All of {}").format(", ".join(T))
|
|
228
|
+
yield ['.'.join(prefix), T, D, mandatory]
|
|
229
|
+
elif 'anyOf' in schema:
|
|
230
|
+
# Any of the subtype, must be basic types or ref
|
|
231
|
+
D = _get_description(schema, convert)
|
|
232
|
+
C = _get_contraints(schema)
|
|
233
|
+
D = _add_constraints(schema, D, C)
|
|
234
|
+
T = _get_multi_type(schema, entities)
|
|
235
|
+
T = _("Any of {}").format(", ".join(T))
|
|
236
|
+
yield ['.'.join(prefix), T, D, mandatory]
|
|
237
|
+
elif type in ['string', 'integer', 'number', 'boolean']:
|
|
238
|
+
T = type
|
|
239
|
+
if schema.get('format', ''):
|
|
240
|
+
T += '/' + schema.get('format', '')
|
|
241
|
+
D = _get_description(schema, convert)
|
|
242
|
+
C = _get_contraints(schema)
|
|
243
|
+
D = _add_constraints(schema, D, C)
|
|
244
|
+
yield ['.'.join(prefix), T, D, mandatory]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _build(name, schema, entities, convert, options):
|
|
248
|
+
if 'type' in schema and schema['type'] not in ['object', 'array']:
|
|
249
|
+
return ''
|
|
250
|
+
|
|
251
|
+
yield ''
|
|
252
|
+
yield '.. _'+entities('/components/schemas/'+name)+':'
|
|
253
|
+
yield ''
|
|
254
|
+
yield name
|
|
255
|
+
yield options['header'] * len(name)
|
|
256
|
+
yield ''
|
|
257
|
+
D = _get_description(schema, convert)
|
|
258
|
+
if 'type' not in schema and not (set(['oneOf', 'allOf', 'anyOf']) & schema.keys()):
|
|
259
|
+
D += '\n' + _('Any type of content is accepted (number, string or object).')
|
|
260
|
+
if D:
|
|
261
|
+
yield D
|
|
262
|
+
yield ''
|
|
263
|
+
if 'type' not in schema and not (set(['oneOf', 'allOf', 'anyOf']) & schema.keys()):
|
|
264
|
+
pass
|
|
265
|
+
else:
|
|
266
|
+
yield '.. list-table:: ' + name
|
|
267
|
+
yield ' :header-rows: 1'
|
|
268
|
+
yield ' :widths: 25 25 45 15'
|
|
269
|
+
yield ' :class: longtable'
|
|
270
|
+
yield ''
|
|
271
|
+
yield ' * - ' + _('Attribute')
|
|
272
|
+
yield ' - ' + _('Type')
|
|
273
|
+
yield ' - ' + _('Description')
|
|
274
|
+
yield ' - ' + _('Required')
|
|
275
|
+
|
|
276
|
+
for item in _process_one([], schema, False, entities, convert):
|
|
277
|
+
if str(item[0]):
|
|
278
|
+
yield ' * - ``' + str(item[0]) + '``'
|
|
279
|
+
else:
|
|
280
|
+
yield ' * - ' + _('N/A')
|
|
281
|
+
yield ' - ' + textwrap.indent(str(item[1]), ' ').lstrip()
|
|
282
|
+
yield ' - ' + textwrap.indent(str(item[2]), ' ').lstrip()
|
|
283
|
+
yield ' - ' + _('Yes') if item[3] else ' -'
|
|
284
|
+
|
|
285
|
+
if 'example' in schema or 'examples' in schema:
|
|
286
|
+
N = 1
|
|
287
|
+
for ex in [schema.get('example', None)] + schema.get('examples', []):
|
|
288
|
+
if ex is None:
|
|
289
|
+
continue
|
|
290
|
+
yield ''
|
|
291
|
+
yield _('Example #{}:').format(N)
|
|
292
|
+
N += 1
|
|
293
|
+
# validate the example against this schema
|
|
294
|
+
try:
|
|
295
|
+
if 'type' in schema:
|
|
296
|
+
validate(instance=ex, schema=schema)
|
|
297
|
+
yield ''
|
|
298
|
+
yield '.. code-block:: json'
|
|
299
|
+
yield ''
|
|
300
|
+
for line in json.dumps(ex, indent=2).splitlines():
|
|
301
|
+
yield ' ' + line
|
|
302
|
+
except jsonschema.ValidationError:
|
|
303
|
+
yield ''
|
|
304
|
+
yield '**{}**'.format(_('Invalid example'))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def ref2link(entities, ref):
|
|
308
|
+
if ref in ['object', 'string']:
|
|
309
|
+
return ref
|
|
310
|
+
name = ref.split('/')[-1]
|
|
311
|
+
if ref[0] == '#':
|
|
312
|
+
ref = ref[1:]
|
|
313
|
+
if callable(entities):
|
|
314
|
+
ref = entities(ref)
|
|
315
|
+
return ':ref:`{name} <{ref}>`'.format(**locals())
|
|
316
|
+
else:
|
|
317
|
+
return '{name}'.format(**locals())
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _entities(spec, ref):
|
|
321
|
+
m = hashlib.md5()
|
|
322
|
+
m.update(spec.get('info', {}).get('title', '').encode('utf-8'))
|
|
323
|
+
m.update(spec.get('info', {}).get('version', '0.0').encode('utf-8'))
|
|
324
|
+
key = m.hexdigest()
|
|
325
|
+
# for unit tests
|
|
326
|
+
if key == '30565a8911a6bb487e3745c0ea3c8224':
|
|
327
|
+
key = ''
|
|
328
|
+
if '#' in ref:
|
|
329
|
+
ref = ref.split('#')[1]
|
|
330
|
+
return key + ref
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ModelRenderer(abc.RestructuredTextRenderer):
|
|
334
|
+
|
|
335
|
+
option_spec = {
|
|
336
|
+
# prefix (components/schemas)
|
|
337
|
+
"prefix": str,
|
|
338
|
+
# header marker (')
|
|
339
|
+
"header": directives.single_char_or_unicode,
|
|
340
|
+
# Markup format to render OpenAPI descriptions.
|
|
341
|
+
"format": str,
|
|
342
|
+
# A list of entities to be rendered. Must be whitespace delimited.
|
|
343
|
+
"entities": lambda s: s.split(),
|
|
344
|
+
# Regular expression patterns to include/exclude entities to/from
|
|
345
|
+
# rendering. The patterns must be whitespace delimited.
|
|
346
|
+
"include": lambda s: s.split(),
|
|
347
|
+
"exclude": lambda s: s.split(),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
def __init__(self, state, options):
|
|
351
|
+
self._state = state
|
|
352
|
+
self._options = options
|
|
353
|
+
if 'header' not in self._options:
|
|
354
|
+
self._options["header"] = "'"
|
|
355
|
+
if 'prefix' not in self._options:
|
|
356
|
+
self._options["prefix"] = "/components/schemas"
|
|
357
|
+
|
|
358
|
+
def render_restructuredtext_markup(self, spec):
|
|
359
|
+
|
|
360
|
+
utils.normalize_spec(spec, **self._options)
|
|
361
|
+
|
|
362
|
+
convert = utils.get_text_converter(self._options)
|
|
363
|
+
|
|
364
|
+
schemas = spec
|
|
365
|
+
for p in filter(None, self._options["prefix"].split('/')):
|
|
366
|
+
schemas = schemas.get(p, {})
|
|
367
|
+
|
|
368
|
+
# Entities list to be processed
|
|
369
|
+
entities = []
|
|
370
|
+
|
|
371
|
+
# If 'entities' are passed we've got to ensure they exist within an OpenAPI
|
|
372
|
+
# spec; otherwise raise error and ask user to fix that.
|
|
373
|
+
if 'entities' in self._options:
|
|
374
|
+
if not set(self._options['entities']).issubset(schemas.keys()):
|
|
375
|
+
raise ValueError(
|
|
376
|
+
'One or more entities are not defined in the spec: %s.' % (
|
|
377
|
+
', '.join(set(self._options['entities']) - set(schemas.keys())),
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
entities = self._options['entities']
|
|
381
|
+
|
|
382
|
+
# Check against regular expressions to be included
|
|
383
|
+
if 'include' in self._options:
|
|
384
|
+
# use a set to avoid duplicates
|
|
385
|
+
new_entities = set()
|
|
386
|
+
for i in self._options['include']:
|
|
387
|
+
ir = re.compile(i)
|
|
388
|
+
for entity in schemas.keys():
|
|
389
|
+
if ir.match(entity):
|
|
390
|
+
new_entities.add(entity)
|
|
391
|
+
# preserve order
|
|
392
|
+
new_list = []
|
|
393
|
+
for i in schemas.keys():
|
|
394
|
+
if i in new_entities or i in entities:
|
|
395
|
+
new_list.append(i)
|
|
396
|
+
entities = new_list
|
|
397
|
+
|
|
398
|
+
# If no include nor entities option, then take full entity
|
|
399
|
+
if 'include' not in self._options and 'entities' not in self._options:
|
|
400
|
+
entities = list(schemas.keys())
|
|
401
|
+
|
|
402
|
+
# Remove entities matching regexp
|
|
403
|
+
if 'exclude' in self._options:
|
|
404
|
+
exc_entities = set()
|
|
405
|
+
for e in self._options['exclude']:
|
|
406
|
+
er = re.compile(e)
|
|
407
|
+
for entity in entities:
|
|
408
|
+
if er.match(entity):
|
|
409
|
+
exc_entities.add(entity)
|
|
410
|
+
# remove like that to preserve order
|
|
411
|
+
for entity in exc_entities:
|
|
412
|
+
entities.remove(entity)
|
|
413
|
+
|
|
414
|
+
def __entities(x):
|
|
415
|
+
return _entities(spec, x)
|
|
416
|
+
|
|
417
|
+
for name in entities:
|
|
418
|
+
schema = schemas[name]
|
|
419
|
+
for line in _build(name, schema, __entities, convert, self._options):
|
|
420
|
+
line_stripped = line.rstrip()
|
|
421
|
+
if '\n' in line_stripped:
|
|
422
|
+
for line_splitted in line_stripped.splitlines():
|
|
423
|
+
yield line_splitted
|
|
424
|
+
else:
|
|
425
|
+
yield line_stripped
|
|
426
|
+
yield ''
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
import re
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from . import abc
|
|
6
|
+
from .. import utils
|
|
7
|
+
from docutils.parsers.rst import directives
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TocRenderer(abc.RestructuredTextRenderer):
|
|
11
|
+
|
|
12
|
+
option_spec = {
|
|
13
|
+
# nb columns
|
|
14
|
+
"nb_columns": directives.positive_int,
|
|
15
|
+
"contextpath": directives.flag, # use the server path as prefix in service URL
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def __init__(self, state, options):
|
|
19
|
+
self._state = state
|
|
20
|
+
self._options = options
|
|
21
|
+
if 'nb_columns' not in self._options:
|
|
22
|
+
self._options["nb_columns"] = 2
|
|
23
|
+
|
|
24
|
+
def render_restructuredtext_markup(self, spec):
|
|
25
|
+
|
|
26
|
+
utils.normalize_spec(spec, **self._options)
|
|
27
|
+
|
|
28
|
+
contextpath = ''
|
|
29
|
+
if 'contextpath' in self._options:
|
|
30
|
+
if 'servers' in spec:
|
|
31
|
+
h = spec['servers'][0]['url']
|
|
32
|
+
contextpath = urlparse(h).path
|
|
33
|
+
if contextpath and contextpath[-1] == '/':
|
|
34
|
+
contextpath = contextpath[:-1]
|
|
35
|
+
|
|
36
|
+
yield ""
|
|
37
|
+
yield ".. hlist::"
|
|
38
|
+
yield " :columns: {}".format(self._options["nb_columns"])
|
|
39
|
+
yield ""
|
|
40
|
+
|
|
41
|
+
for path in spec["paths"].keys():
|
|
42
|
+
cpath = re.sub(r"[{}]", "", re.sub(r"[<>:/]", "-", contextpath+path))
|
|
43
|
+
for verb, ope in spec["paths"][path].items():
|
|
44
|
+
yield " - `{} <#{}>`_".format(
|
|
45
|
+
ope.get("operationId", verb + " " + path),
|
|
46
|
+
verb.lower() + "-" + cpath
|
|
47
|
+
)
|
|
48
|
+
yield ""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Abstract Base Classes (ABCs) for OpenAPI renderers."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
from docutils import nodes
|
|
6
|
+
from docutils.statemachine import ViewList
|
|
7
|
+
from sphinx.util.nodes import nested_parse_with_titles
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Renderer(metaclass=abc.ABCMeta):
|
|
11
|
+
"""Base class for OpenAPI renderers."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, state, options):
|
|
14
|
+
self._state = state
|
|
15
|
+
self._options = options
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def option_spec(self):
|
|
20
|
+
"""Renderer options and their converting functions."""
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
def render(self, spec):
|
|
24
|
+
"""Render a given OpenAPI spec."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RestructuredTextRenderer(Renderer):
|
|
28
|
+
"""Base class for reStructuredText OpenAPI renderers.
|
|
29
|
+
|
|
30
|
+
Docutils DOM manipulation is quite a tricky task that requires passing
|
|
31
|
+
dozen arguments around. Because of that a lot of Sphinx extensions instead
|
|
32
|
+
of constructing DOM nodes directly produce and parse reStructuredText.
|
|
33
|
+
This Sphinx extension is not an exception, and that's why this class
|
|
34
|
+
exists. It's a convenient extension of :class:`Renderer` that converts
|
|
35
|
+
produced markup text into docutils DOM elements.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def render(self, spec):
|
|
39
|
+
viewlist = ViewList()
|
|
40
|
+
for line in self.render_restructuredtext_markup(spec):
|
|
41
|
+
viewlist.append(line, "<openapi>")
|
|
42
|
+
|
|
43
|
+
node = nodes.section()
|
|
44
|
+
node.document = self._state.document
|
|
45
|
+
nested_parse_with_titles(self._state, viewlist, node)
|
|
46
|
+
return node.children
|