sc-oa 0.7.0.13__py3-none-any.whl → 0.7.0.14__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.
Potentially problematic release.
This version of sc-oa might be problematic. Click here for more details.
- {sc_oa-0.7.0.13.dist-info → sc_oa-0.7.0.14.dist-info}/LICENSE +23 -23
- {sc_oa-0.7.0.13.dist-info → sc_oa-0.7.0.14.dist-info}/METADATA +45 -42
- sc_oa-0.7.0.14.dist-info/RECORD +26 -0
- {sc_oa-0.7.0.13.dist-info → sc_oa-0.7.0.14.dist-info}/WHEEL +1 -1
- sphinxcontrib/__init__.py +10 -0
- sphinxcontrib/openapi/__init__.py +93 -93
- sphinxcontrib/openapi/__main__.py +86 -86
- sphinxcontrib/openapi/_lib2to3.py +378 -378
- sphinxcontrib/openapi/directive.py +58 -58
- sphinxcontrib/openapi/locale/es_ES/LC_MESSAGES/openapi.po +170 -170
- sphinxcontrib/openapi/locale/fr_FR/LC_MESSAGES/openapi.po +170 -170
- sphinxcontrib/openapi/locale/openapi.pot +168 -168
- sphinxcontrib/openapi/openapi20.py +263 -263
- sphinxcontrib/openapi/openapi30.py +748 -748
- sphinxcontrib/openapi/renderers/__init__.py +18 -18
- sphinxcontrib/openapi/renderers/_description.py +26 -26
- sphinxcontrib/openapi/renderers/_httpdomain.py +620 -620
- sphinxcontrib/openapi/renderers/_httpdomain_old.py +59 -59
- sphinxcontrib/openapi/renderers/_model.py +422 -422
- sphinxcontrib/openapi/renderers/_toc.py +48 -48
- sphinxcontrib/openapi/renderers/abc.py +46 -46
- sphinxcontrib/openapi/schema_utils.py +137 -137
- sphinxcontrib/openapi/utils.py +137 -137
- sc_oa-0.7.0.13-py3.9-nspkg.pth +0 -1
- sc_oa-0.7.0.13.dist-info/RECORD +0 -27
- sc_oa-0.7.0.13.dist-info/namespace_packages.txt +0 -1
- {sc_oa-0.7.0.13.dist-info → sc_oa-0.7.0.14.dist-info}/top_level.txt +0 -0
|
@@ -1,422 +1,422 @@
|
|
|
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' not in schema:
|
|
249
|
-
schema['type'] = 'object'
|
|
250
|
-
if schema.get('type', '') not in ['object', 'array']:
|
|
251
|
-
return ''
|
|
252
|
-
|
|
253
|
-
yield ''
|
|
254
|
-
yield '.. _'+entities('/components/schemas/'+name)+':'
|
|
255
|
-
yield ''
|
|
256
|
-
yield name
|
|
257
|
-
yield options['header'] * len(name)
|
|
258
|
-
yield ''
|
|
259
|
-
D = _get_description(schema, convert)
|
|
260
|
-
if D:
|
|
261
|
-
yield D
|
|
262
|
-
yield ''
|
|
263
|
-
yield '.. list-table:: ' + name
|
|
264
|
-
yield ' :header-rows: 1'
|
|
265
|
-
yield ' :widths: 25 25 45 15'
|
|
266
|
-
yield ' :class: longtable'
|
|
267
|
-
yield ''
|
|
268
|
-
yield ' * - ' + _('Attribute')
|
|
269
|
-
yield ' - ' + _('Type')
|
|
270
|
-
yield ' - ' + _('Description')
|
|
271
|
-
yield ' - ' + _('Required')
|
|
272
|
-
|
|
273
|
-
for item in _process_one([], schema, False, entities, convert):
|
|
274
|
-
if str(item[0]):
|
|
275
|
-
yield ' * - ``' + str(item[0]) + '``'
|
|
276
|
-
else:
|
|
277
|
-
yield ' * - ' + _('N/A')
|
|
278
|
-
yield ' - ' + textwrap.indent(str(item[1]), ' ').lstrip()
|
|
279
|
-
yield ' - ' + textwrap.indent(str(item[2]), ' ').lstrip()
|
|
280
|
-
yield ' - ' + _('Yes') if item[3] else ' -'
|
|
281
|
-
|
|
282
|
-
if 'example' in schema or 'examples' in schema:
|
|
283
|
-
N = 1
|
|
284
|
-
for ex in [schema.get('example', None)] + schema.get('examples', []):
|
|
285
|
-
if ex is None:
|
|
286
|
-
continue
|
|
287
|
-
yield ''
|
|
288
|
-
yield _('Example #{}:').format(N)
|
|
289
|
-
N += 1
|
|
290
|
-
# validate the example against this schema
|
|
291
|
-
try:
|
|
292
|
-
validate(instance=ex, schema=schema)
|
|
293
|
-
yield ''
|
|
294
|
-
yield '.. code-block:: json'
|
|
295
|
-
yield ''
|
|
296
|
-
for line in json.dumps(ex, indent=2).splitlines():
|
|
297
|
-
yield ' ' + line
|
|
298
|
-
except jsonschema.ValidationError:
|
|
299
|
-
yield ''
|
|
300
|
-
yield '**{}**'.format(_('Invalid example'))
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def ref2link(entities, ref):
|
|
304
|
-
if ref in ['object', 'string']:
|
|
305
|
-
return ref
|
|
306
|
-
name = ref.split('/')[-1]
|
|
307
|
-
if ref[0] == '#':
|
|
308
|
-
ref = ref[1:]
|
|
309
|
-
if callable(entities):
|
|
310
|
-
ref = entities(ref)
|
|
311
|
-
return ':ref:`{name} <{ref}>`'.format(**locals())
|
|
312
|
-
else:
|
|
313
|
-
return '{name}'.format(**locals())
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def _entities(spec, ref):
|
|
317
|
-
m = hashlib.md5()
|
|
318
|
-
m.update(spec.get('info', {}).get('title', '').encode('utf-8'))
|
|
319
|
-
m.update(spec.get('info', {}).get('version', '0.0').encode('utf-8'))
|
|
320
|
-
key = m.hexdigest()
|
|
321
|
-
# for unit tests
|
|
322
|
-
if key == '30565a8911a6bb487e3745c0ea3c8224':
|
|
323
|
-
key = ''
|
|
324
|
-
if '#' in ref:
|
|
325
|
-
ref = ref.split('#')[1]
|
|
326
|
-
return key + ref
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
class ModelRenderer(abc.RestructuredTextRenderer):
|
|
330
|
-
|
|
331
|
-
option_spec = {
|
|
332
|
-
# prefix (components/schemas)
|
|
333
|
-
"prefix": str,
|
|
334
|
-
# header marker (')
|
|
335
|
-
"header": directives.single_char_or_unicode,
|
|
336
|
-
# Markup format to render OpenAPI descriptions.
|
|
337
|
-
"format": str,
|
|
338
|
-
# A list of entities to be rendered. Must be whitespace delimited.
|
|
339
|
-
"entities": lambda s: s.split(),
|
|
340
|
-
# Regular expression patterns to include/exclude entities to/from
|
|
341
|
-
# rendering. The patterns must be whitespace delimited.
|
|
342
|
-
"include": lambda s: s.split(),
|
|
343
|
-
"exclude": lambda s: s.split(),
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
def __init__(self, state, options):
|
|
347
|
-
self._state = state
|
|
348
|
-
self._options = options
|
|
349
|
-
if 'header' not in self._options:
|
|
350
|
-
self._options["header"] = "'"
|
|
351
|
-
if 'prefix' not in self._options:
|
|
352
|
-
self._options["prefix"] = "/components/schemas"
|
|
353
|
-
|
|
354
|
-
def render_restructuredtext_markup(self, spec):
|
|
355
|
-
|
|
356
|
-
utils.normalize_spec(spec, **self._options)
|
|
357
|
-
|
|
358
|
-
convert = utils.get_text_converter(self._options)
|
|
359
|
-
|
|
360
|
-
schemas = spec
|
|
361
|
-
for p in filter(None, self._options["prefix"].split('/')):
|
|
362
|
-
schemas = schemas.get(p, {})
|
|
363
|
-
|
|
364
|
-
# Entities list to be processed
|
|
365
|
-
entities = []
|
|
366
|
-
|
|
367
|
-
# If 'entities' are passed we've got to ensure they exist within an OpenAPI
|
|
368
|
-
# spec; otherwise raise error and ask user to fix that.
|
|
369
|
-
if 'entities' in self._options:
|
|
370
|
-
if not set(self._options['entities']).issubset(schemas.keys()):
|
|
371
|
-
raise ValueError(
|
|
372
|
-
'One or more entities are not defined in the spec: %s.' % (
|
|
373
|
-
', '.join(set(self._options['entities']) - set(schemas.keys())),
|
|
374
|
-
)
|
|
375
|
-
)
|
|
376
|
-
entities = self._options['entities']
|
|
377
|
-
|
|
378
|
-
# Check against regular expressions to be included
|
|
379
|
-
if 'include' in self._options:
|
|
380
|
-
# use a set to avoid duplicates
|
|
381
|
-
new_entities = set()
|
|
382
|
-
for i in self._options['include']:
|
|
383
|
-
ir = re.compile(i)
|
|
384
|
-
for entity in schemas.keys():
|
|
385
|
-
if ir.match(entity):
|
|
386
|
-
new_entities.add(entity)
|
|
387
|
-
# preserve order
|
|
388
|
-
new_list = []
|
|
389
|
-
for i in schemas.keys():
|
|
390
|
-
if i in new_entities or i in entities:
|
|
391
|
-
new_list.append(i)
|
|
392
|
-
entities = new_list
|
|
393
|
-
|
|
394
|
-
# If no include nor entities option, then take full entity
|
|
395
|
-
if 'include' not in self._options and 'entities' not in self._options:
|
|
396
|
-
entities = list(schemas.keys())
|
|
397
|
-
|
|
398
|
-
# Remove entities matching regexp
|
|
399
|
-
if 'exclude' in self._options:
|
|
400
|
-
exc_entities = set()
|
|
401
|
-
for e in self._options['exclude']:
|
|
402
|
-
er = re.compile(e)
|
|
403
|
-
for entity in entities:
|
|
404
|
-
if er.match(entity):
|
|
405
|
-
exc_entities.add(entity)
|
|
406
|
-
# remove like that to preserve order
|
|
407
|
-
for entity in exc_entities:
|
|
408
|
-
entities.remove(entity)
|
|
409
|
-
|
|
410
|
-
def __entities(x):
|
|
411
|
-
return _entities(spec, x)
|
|
412
|
-
|
|
413
|
-
for name in entities:
|
|
414
|
-
schema = schemas[name]
|
|
415
|
-
for line in _build(name, schema, __entities, convert, self._options):
|
|
416
|
-
line_stripped = line.rstrip()
|
|
417
|
-
if '\n' in line_stripped:
|
|
418
|
-
for line_splitted in line_stripped.splitlines():
|
|
419
|
-
yield line_splitted
|
|
420
|
-
else:
|
|
421
|
-
yield line_stripped
|
|
422
|
-
yield ''
|
|
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' not in schema:
|
|
249
|
+
schema['type'] = 'object'
|
|
250
|
+
if schema.get('type', '') not in ['object', 'array']:
|
|
251
|
+
return ''
|
|
252
|
+
|
|
253
|
+
yield ''
|
|
254
|
+
yield '.. _'+entities('/components/schemas/'+name)+':'
|
|
255
|
+
yield ''
|
|
256
|
+
yield name
|
|
257
|
+
yield options['header'] * len(name)
|
|
258
|
+
yield ''
|
|
259
|
+
D = _get_description(schema, convert)
|
|
260
|
+
if D:
|
|
261
|
+
yield D
|
|
262
|
+
yield ''
|
|
263
|
+
yield '.. list-table:: ' + name
|
|
264
|
+
yield ' :header-rows: 1'
|
|
265
|
+
yield ' :widths: 25 25 45 15'
|
|
266
|
+
yield ' :class: longtable'
|
|
267
|
+
yield ''
|
|
268
|
+
yield ' * - ' + _('Attribute')
|
|
269
|
+
yield ' - ' + _('Type')
|
|
270
|
+
yield ' - ' + _('Description')
|
|
271
|
+
yield ' - ' + _('Required')
|
|
272
|
+
|
|
273
|
+
for item in _process_one([], schema, False, entities, convert):
|
|
274
|
+
if str(item[0]):
|
|
275
|
+
yield ' * - ``' + str(item[0]) + '``'
|
|
276
|
+
else:
|
|
277
|
+
yield ' * - ' + _('N/A')
|
|
278
|
+
yield ' - ' + textwrap.indent(str(item[1]), ' ').lstrip()
|
|
279
|
+
yield ' - ' + textwrap.indent(str(item[2]), ' ').lstrip()
|
|
280
|
+
yield ' - ' + _('Yes') if item[3] else ' -'
|
|
281
|
+
|
|
282
|
+
if 'example' in schema or 'examples' in schema:
|
|
283
|
+
N = 1
|
|
284
|
+
for ex in [schema.get('example', None)] + schema.get('examples', []):
|
|
285
|
+
if ex is None:
|
|
286
|
+
continue
|
|
287
|
+
yield ''
|
|
288
|
+
yield _('Example #{}:').format(N)
|
|
289
|
+
N += 1
|
|
290
|
+
# validate the example against this schema
|
|
291
|
+
try:
|
|
292
|
+
validate(instance=ex, schema=schema)
|
|
293
|
+
yield ''
|
|
294
|
+
yield '.. code-block:: json'
|
|
295
|
+
yield ''
|
|
296
|
+
for line in json.dumps(ex, indent=2).splitlines():
|
|
297
|
+
yield ' ' + line
|
|
298
|
+
except jsonschema.ValidationError:
|
|
299
|
+
yield ''
|
|
300
|
+
yield '**{}**'.format(_('Invalid example'))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def ref2link(entities, ref):
|
|
304
|
+
if ref in ['object', 'string']:
|
|
305
|
+
return ref
|
|
306
|
+
name = ref.split('/')[-1]
|
|
307
|
+
if ref[0] == '#':
|
|
308
|
+
ref = ref[1:]
|
|
309
|
+
if callable(entities):
|
|
310
|
+
ref = entities(ref)
|
|
311
|
+
return ':ref:`{name} <{ref}>`'.format(**locals())
|
|
312
|
+
else:
|
|
313
|
+
return '{name}'.format(**locals())
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _entities(spec, ref):
|
|
317
|
+
m = hashlib.md5()
|
|
318
|
+
m.update(spec.get('info', {}).get('title', '').encode('utf-8'))
|
|
319
|
+
m.update(spec.get('info', {}).get('version', '0.0').encode('utf-8'))
|
|
320
|
+
key = m.hexdigest()
|
|
321
|
+
# for unit tests
|
|
322
|
+
if key == '30565a8911a6bb487e3745c0ea3c8224':
|
|
323
|
+
key = ''
|
|
324
|
+
if '#' in ref:
|
|
325
|
+
ref = ref.split('#')[1]
|
|
326
|
+
return key + ref
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class ModelRenderer(abc.RestructuredTextRenderer):
|
|
330
|
+
|
|
331
|
+
option_spec = {
|
|
332
|
+
# prefix (components/schemas)
|
|
333
|
+
"prefix": str,
|
|
334
|
+
# header marker (')
|
|
335
|
+
"header": directives.single_char_or_unicode,
|
|
336
|
+
# Markup format to render OpenAPI descriptions.
|
|
337
|
+
"format": str,
|
|
338
|
+
# A list of entities to be rendered. Must be whitespace delimited.
|
|
339
|
+
"entities": lambda s: s.split(),
|
|
340
|
+
# Regular expression patterns to include/exclude entities to/from
|
|
341
|
+
# rendering. The patterns must be whitespace delimited.
|
|
342
|
+
"include": lambda s: s.split(),
|
|
343
|
+
"exclude": lambda s: s.split(),
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
def __init__(self, state, options):
|
|
347
|
+
self._state = state
|
|
348
|
+
self._options = options
|
|
349
|
+
if 'header' not in self._options:
|
|
350
|
+
self._options["header"] = "'"
|
|
351
|
+
if 'prefix' not in self._options:
|
|
352
|
+
self._options["prefix"] = "/components/schemas"
|
|
353
|
+
|
|
354
|
+
def render_restructuredtext_markup(self, spec):
|
|
355
|
+
|
|
356
|
+
utils.normalize_spec(spec, **self._options)
|
|
357
|
+
|
|
358
|
+
convert = utils.get_text_converter(self._options)
|
|
359
|
+
|
|
360
|
+
schemas = spec
|
|
361
|
+
for p in filter(None, self._options["prefix"].split('/')):
|
|
362
|
+
schemas = schemas.get(p, {})
|
|
363
|
+
|
|
364
|
+
# Entities list to be processed
|
|
365
|
+
entities = []
|
|
366
|
+
|
|
367
|
+
# If 'entities' are passed we've got to ensure they exist within an OpenAPI
|
|
368
|
+
# spec; otherwise raise error and ask user to fix that.
|
|
369
|
+
if 'entities' in self._options:
|
|
370
|
+
if not set(self._options['entities']).issubset(schemas.keys()):
|
|
371
|
+
raise ValueError(
|
|
372
|
+
'One or more entities are not defined in the spec: %s.' % (
|
|
373
|
+
', '.join(set(self._options['entities']) - set(schemas.keys())),
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
entities = self._options['entities']
|
|
377
|
+
|
|
378
|
+
# Check against regular expressions to be included
|
|
379
|
+
if 'include' in self._options:
|
|
380
|
+
# use a set to avoid duplicates
|
|
381
|
+
new_entities = set()
|
|
382
|
+
for i in self._options['include']:
|
|
383
|
+
ir = re.compile(i)
|
|
384
|
+
for entity in schemas.keys():
|
|
385
|
+
if ir.match(entity):
|
|
386
|
+
new_entities.add(entity)
|
|
387
|
+
# preserve order
|
|
388
|
+
new_list = []
|
|
389
|
+
for i in schemas.keys():
|
|
390
|
+
if i in new_entities or i in entities:
|
|
391
|
+
new_list.append(i)
|
|
392
|
+
entities = new_list
|
|
393
|
+
|
|
394
|
+
# If no include nor entities option, then take full entity
|
|
395
|
+
if 'include' not in self._options and 'entities' not in self._options:
|
|
396
|
+
entities = list(schemas.keys())
|
|
397
|
+
|
|
398
|
+
# Remove entities matching regexp
|
|
399
|
+
if 'exclude' in self._options:
|
|
400
|
+
exc_entities = set()
|
|
401
|
+
for e in self._options['exclude']:
|
|
402
|
+
er = re.compile(e)
|
|
403
|
+
for entity in entities:
|
|
404
|
+
if er.match(entity):
|
|
405
|
+
exc_entities.add(entity)
|
|
406
|
+
# remove like that to preserve order
|
|
407
|
+
for entity in exc_entities:
|
|
408
|
+
entities.remove(entity)
|
|
409
|
+
|
|
410
|
+
def __entities(x):
|
|
411
|
+
return _entities(spec, x)
|
|
412
|
+
|
|
413
|
+
for name in entities:
|
|
414
|
+
schema = schemas[name]
|
|
415
|
+
for line in _build(name, schema, __entities, convert, self._options):
|
|
416
|
+
line_stripped = line.rstrip()
|
|
417
|
+
if '\n' in line_stripped:
|
|
418
|
+
for line_splitted in line_stripped.splitlines():
|
|
419
|
+
yield line_splitted
|
|
420
|
+
else:
|
|
421
|
+
yield line_stripped
|
|
422
|
+
yield ''
|