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.
@@ -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