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,750 @@
1
+ """
2
+ sphinxcontrib.openapi.openapi30
3
+ -------------------------------
4
+
5
+ The OpenAPI 3.0.0 spec renderer. Based on ``sphinxcontrib-httpdomain``.
6
+
7
+ :copyright: (c) 2016, Ihor Kalnytskyi.
8
+ :license: BSD, see LICENSE for details.
9
+ """
10
+
11
+ import copy
12
+
13
+ import collections
14
+ import collections.abc
15
+ import textwrap
16
+ import urllib.parse
17
+
18
+ from datetime import datetime
19
+ import itertools
20
+ import json
21
+ import re
22
+ from urllib import parse
23
+ from http.client import responses as http_status_codes
24
+ from sphinxcontrib.openapi.renderers._model import _process_one, _entities
25
+
26
+ from sphinx.util import logging
27
+ from sphinx.locale import get_translation
28
+
29
+ from sphinxcontrib.openapi import utils
30
+
31
+ _ = get_translation('openapi')
32
+
33
+
34
+ LOG = logging.getLogger(__name__)
35
+
36
+ # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#data-types
37
+ _TYPE_MAPPING = {
38
+ ('integer', 'int32'): 1, # integer
39
+ ('integer', 'int64'): 1, # long
40
+ ('number', 'float'): 1.0, # float
41
+ ('number', 'double'): 1.0, # double
42
+ ('boolean', None): True, # boolean
43
+ ('string', None): 'string', # string
44
+ ('string', 'byte'): 'c3RyaW5n', # b'string' encoded in base64, # byte
45
+ ('string', 'binary'): '01010101', # binary
46
+ ('string', 'date'): datetime.now().date().isoformat(), # date
47
+ ('string', 'date-time'): datetime.now().isoformat(), # dateTime
48
+ ('string', 'password'): '********', # password
49
+
50
+ # custom extensions to handle common formats
51
+ ('string', 'email'): 'name@example.com',
52
+ ('string', 'zip-code'): '90210',
53
+ ('string', 'uri'): 'https://example.com',
54
+
55
+ # additional fallthrough cases
56
+ ('integer', None): 1, # integer
57
+ ('number', None): 1.0, # <fallthrough>
58
+ }
59
+
60
+ _READONLY_PROPERTY = object() # sentinel for values not included in requests
61
+
62
+
63
+ def _dict_merge(dct, merge_dct):
64
+ """Recursive dict merge.
65
+
66
+ Inspired by :meth:``dict.update()``, instead of updating only top-level
67
+ keys, dict_merge recurses down into dicts nested to an arbitrary depth,
68
+ updating keys. The ``merge_dct`` is merged into ``dct``.
69
+
70
+ From https://gist.github.com/angstwad/bf22d1822c38a92ec0a9
71
+
72
+ Arguments:
73
+ dct: dict onto which the merge is executed
74
+ merge_dct: dct merged into dct
75
+ """
76
+ for k in merge_dct.keys():
77
+ if (k in dct and isinstance(dct[k], dict)
78
+ and isinstance(merge_dct[k], collections.abc.Mapping)):
79
+ _dict_merge(dct[k], merge_dct[k])
80
+ else:
81
+ dct[k] = merge_dct[k]
82
+
83
+
84
+ def _parse_schema(schema, method):
85
+ """
86
+ Convert a Schema Object to a Python object.
87
+
88
+ Args:
89
+ schema: An ``OrderedDict`` representing the schema object.
90
+ """
91
+ if method and schema.get('readOnly', False):
92
+ return _READONLY_PROPERTY
93
+
94
+ # allOf: Must be valid against all of the subschemas
95
+ if 'allOf' in schema:
96
+ schema_ = copy.deepcopy(schema['allOf'][0])
97
+ for x in schema['allOf'][1:]:
98
+ _dict_merge(schema_, x)
99
+
100
+ return _parse_schema(schema_, method)
101
+
102
+ # anyOf: Must be valid against any of the subschemas
103
+ # TODO(stephenfin): Handle anyOf
104
+
105
+ # oneOf: Must be valid against exactly one of the subschemas
106
+ if 'oneOf' in schema:
107
+ # we only show the first one since we can't show everything
108
+ return _parse_schema(schema['oneOf'][0], method)
109
+
110
+ if 'enum' in schema:
111
+ if 'example' in schema:
112
+ return schema['example']
113
+ if 'default' in schema:
114
+ return schema['default']
115
+ # we show the first one
116
+ return schema['enum'][0]
117
+
118
+ schema_type = schema.get('type', 'object')
119
+
120
+ if schema_type == 'array':
121
+ # special case oneOf and anyOf so that we can show examples for all
122
+ # possible combinations
123
+ if 'oneOf' in schema['items']:
124
+ return [
125
+ _parse_schema(x, method) for x in schema['items']['oneOf']
126
+ ]
127
+
128
+ if 'anyOf' in schema['items']:
129
+ return [
130
+ _parse_schema(x, method) for x in schema['items']['anyOf']
131
+ ]
132
+
133
+ return [_parse_schema(schema['items'], method)]
134
+
135
+ if schema_type == 'object':
136
+ if 'example' in schema:
137
+ example = schema.get('example')
138
+ if example:
139
+ example = copy.deepcopy(example)
140
+ # filters out readonly properties
141
+ if method and 'properties' in schema:
142
+ for k, v in schema.get('properties', {}).items():
143
+ if v.get('readOnly', False) and k in example:
144
+ del example[k]
145
+ ret = collections.OrderedDict(example)
146
+ # XXX should be True to be compliant with OpenAPI
147
+ if (schema.get('additionalProperties', False) or 'type' not in schema) and \
148
+ '...' not in example:
149
+ # materialize in the example the fact that additional properties can be added
150
+ ret['...'] = '...'
151
+ return ret
152
+ if method and 'properties' in schema and \
153
+ all(v.get('readOnly', False)
154
+ for v in schema['properties'].values()):
155
+ return _READONLY_PROPERTY
156
+
157
+ results = []
158
+ for name, prop in schema.get('properties', {}).items():
159
+ result = _parse_schema(prop, method)
160
+ if result != _READONLY_PROPERTY:
161
+ results.append((name, result))
162
+
163
+ # XXX should be True to be compliant with OpenAPI
164
+ if schema.get('additionalProperties', False) or 'type' not in schema:
165
+ # materialize in the example the fact that additional properties can be added
166
+ results.append(("...", "..."))
167
+
168
+ return collections.OrderedDict(results)
169
+
170
+ if 'example' in schema:
171
+ return schema['example']
172
+ if 'default' in schema:
173
+ return schema['default']
174
+ if (schema_type, schema.get('format')) in _TYPE_MAPPING:
175
+ return _TYPE_MAPPING[(schema_type, schema.get('format'))]
176
+
177
+ return _TYPE_MAPPING[(schema_type, None)] # unrecognized format
178
+
179
+
180
+ def _example(media_type_objects, method=None, endpoint=None, status=None,
181
+ reqheader_examples={},
182
+ resheader_examples={},
183
+ nb_indent=0):
184
+ """
185
+ Format examples in `Media Type Object` openapi v3 to HTTP request or
186
+ HTTP response example.
187
+ If method and endpoint is provided, this function prints a request example
188
+ else status should be provided to print a response example.
189
+
190
+ Arguments:
191
+ media_type_objects (Dict[str, Dict]): Dict containing
192
+ Media Type Objects.
193
+ method: The HTTP method to use in example.
194
+ endpoint: The HTTP route to use in example.
195
+ status: The HTTP status to use in example.
196
+ """
197
+ indent = ' '
198
+ extra_indent = indent * nb_indent
199
+
200
+ if method is not None:
201
+ method = method.upper()
202
+ else:
203
+ try:
204
+ # one of possible values for status might be 'default'.
205
+ # in the case, just fallback to '-'
206
+ status_text = http_status_codes[int(status)]
207
+ except (ValueError, KeyError):
208
+ status_text = '-'
209
+
210
+ # Provide request samples for GET requests
211
+ if method == 'GET':
212
+ media_type_objects[''] = {
213
+ 'examples': {_('Example request'): {'value': '\n'}}}
214
+
215
+ for content_type, content in media_type_objects.items():
216
+ examples = content.get('examples')
217
+ example = content.get('example')
218
+
219
+ # Try to get the example from the schema
220
+ if example is None and 'schema' in content:
221
+ example = content['schema'].get('example')
222
+ if example:
223
+ example = copy.deepcopy(example)
224
+ # filters out readonly properties
225
+ if method and 'properties' in content['schema']:
226
+ for k, v in content['schema'].get('properties', {}).items():
227
+ if v.get('readOnly', False) and k in example:
228
+ del example[k]
229
+ # XXX should be True to be compliant with OpenAPI
230
+ if content['schema'].get('additionalProperties', False) and '...' not in example:
231
+ # materialize in the example the fact that additional properties can be added
232
+ example['...'] = '...'
233
+
234
+ if examples is None:
235
+ examples = {}
236
+ if not example:
237
+ if re.match(r"application/[a-zA-Z\+]*json", content_type) is \
238
+ None:
239
+ LOG.info('skipping non-JSON example generation.')
240
+ continue
241
+ example = _parse_schema(content['schema'], method=method)
242
+
243
+ if method is None:
244
+ examples[_('Example response')] = {
245
+ 'value': example,
246
+ }
247
+ else:
248
+ examples[_('Example request')] = {
249
+ 'value': example,
250
+ }
251
+
252
+ for example in examples.values():
253
+ # According to OpenAPI v3 specs, string examples should be left unchanged
254
+ if not isinstance(example['value'], str):
255
+ example['value'] = json.dumps(
256
+ example['value'], indent=4, separators=(',', ': '), ensure_ascii=False)
257
+
258
+ for example_name, example in examples.items():
259
+ if 'summary' in example:
260
+ example_title = '{example_name} - {example[summary]}'.format(
261
+ **locals())
262
+ else:
263
+ example_title = example_name
264
+
265
+ yield ''
266
+ yield '{extra_indent}**{example_title}:**'.format(**locals())
267
+ yield ''
268
+ yield '{extra_indent}.. sourcecode:: http'.format(**locals())
269
+ yield ''
270
+
271
+ # Print http request example
272
+ if method:
273
+ yield '{extra_indent}{indent}{method} {endpoint} HTTP/1.1' \
274
+ .format(**locals())
275
+ yield '{extra_indent}{indent}Host: example.com' \
276
+ .format(**locals())
277
+ if content_type:
278
+ yield '{extra_indent}{indent}Content-Type: {content_type}'\
279
+ .format(**locals())
280
+ for k, v in reqheader_examples.items():
281
+ yield '{extra_indent}{indent}{k}: {v}'\
282
+ .format(**locals())
283
+ # Print http response example
284
+ else:
285
+ yield '{extra_indent}{indent}HTTP/1.1 {status} {status_text}' \
286
+ .format(**locals())
287
+ yield '{extra_indent}{indent}Content-Type: {content_type}' \
288
+ .format(**locals())
289
+ for k, v in resheader_examples.items():
290
+ yield '{extra_indent}{indent}{k}: {v}'\
291
+ .format(**locals())
292
+
293
+ if content_type:
294
+ yield ''
295
+ for example_line in example['value'].splitlines():
296
+ yield '{extra_indent}{indent}{example_line}'.format(
297
+ **locals()
298
+ )
299
+ if example['value'].splitlines():
300
+ yield ''
301
+
302
+
303
+ def ref2link(entities, ref):
304
+ name = ref.split('/')[-1]
305
+ ref = entities(ref)
306
+ return ':ref:`{name} <{ref}>`'.format(**locals())
307
+
308
+
309
+ def _httpresource(endpoint, method, properties, convert, render_examples,
310
+ render_request, group_examples=False, entities=False):
311
+ # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#operation-object
312
+ endpoint_novar = endpoint
313
+ parameters = properties.get('parameters', [])
314
+ responses = properties['responses']
315
+ query_param_examples = []
316
+ indent = ' '
317
+
318
+ yield '.. http:{0}:: {1}'.format(method, endpoint)
319
+ yield ' :synopsis: {0}'.format(properties.get('summary', 'null'))
320
+ yield ''
321
+
322
+ if 'summary' in properties:
323
+ for line in properties['summary'].splitlines():
324
+ yield '{indent}**{line}**'.format(**locals())
325
+ yield ''
326
+
327
+ if 'description' in properties:
328
+ for line in convert(properties['description']).strip().splitlines():
329
+ yield '{indent}{line}'.format(**locals())
330
+ yield ''
331
+
332
+ if properties.get('deprecated', False):
333
+ yield '{indent}**DEPRECATED**'.format(**locals())
334
+ yield ''
335
+
336
+ if 'security' in properties:
337
+ for sec_schema in properties['security']:
338
+ sec_scope = ' or '.join([
339
+ '``{}``'.format(s) for sch in sec_schema.values() for s in sch
340
+ ])
341
+ s = '{indent}**' + _('Scope required') + '**: {sec_scope}'
342
+ yield s.format(**locals())
343
+ yield ''
344
+
345
+ def get_desc(desc, schema, indent, deep=True):
346
+ if entities:
347
+ doc = next(_process_one(['R'], schema, False, entities, convert))
348
+ if desc:
349
+ if not desc[-1] == '.':
350
+ desc = desc + '.'
351
+ if doc[1]:
352
+ if not doc[1].startswith(str(_("Object of"))) and \
353
+ not doc[1].startswith(str(_("Array of"))):
354
+ doc[1] = _("Object of type {}").format(doc[1])
355
+ if not doc[1][-1] == '.':
356
+ doc[1] = doc[1] + '.'
357
+ desc += '\n' + doc[1]
358
+ if deep and doc[2] and doc[2] != _('Additional properties'):
359
+ if not doc[2][-1] == '.':
360
+ doc[2] = doc[2] + '.'
361
+ desc += '\n' + doc[2]
362
+ desc = desc.rstrip()
363
+ else:
364
+ doc = next(_process_one(['R'], schema, False, entities, convert))
365
+ if desc:
366
+ if not desc[-1] == '.':
367
+ desc = desc + '.'
368
+ if doc[2] and doc[2] != _('Additional properties'):
369
+ if not doc[2][-1] == '.':
370
+ doc[2] = doc[2] + '.'
371
+ desc += '\n' + doc[2]
372
+ desc = desc.rstrip()
373
+ desc = textwrap.indent(desc, '{indent}{indent}'.format(**locals())).lstrip()
374
+ return desc
375
+
376
+ # print request's path params
377
+ for param in filter(lambda p: p['in'] == 'path', parameters):
378
+ yield indent + ':param {type} {name}:'.format(
379
+ type=param['schema']['type'],
380
+ name=param['name'])
381
+
382
+ desc = param.get('description', '')
383
+ if desc:
384
+ # in case the description uses markdown format
385
+ desc = convert(desc).strip()
386
+ desc = get_desc(desc, param['schema'], indent)
387
+ if desc:
388
+ yield '{indent}{indent}{desc}'.format(**locals())
389
+
390
+ example = _parse_schema(param['schema'], method)
391
+ example = param.get('example', example)
392
+ if example and type(example) == str:
393
+ endpoint_novar = \
394
+ endpoint_novar.replace('{'+param['name']+'}', urllib.parse.quote(example))
395
+ # print request's query params
396
+ for param in filter(lambda p: p['in'] == 'query', parameters):
397
+ yield indent + ':query {type} {name}:'.format(
398
+ type=param['schema']['type'],
399
+ name=param['name'])
400
+ desc = param.get('description', '')
401
+ if desc:
402
+ # in case the description uses markdown format
403
+ desc = convert(desc).strip()
404
+ desc = get_desc(desc, param['schema'], indent)
405
+ if desc:
406
+ yield '{indent}{indent}{desc}'.format(**locals())
407
+ if param.get('required', False):
408
+ yield '{indent}{indent}'.format(**locals()) + \
409
+ '({})'.format(_('Required'))
410
+ example = _parse_schema(param['schema'], method)
411
+ example = param.get('example', example)
412
+ if param.get('explode', False) and isinstance(example, list):
413
+ for v in example:
414
+ if isinstance(v, bool):
415
+ v = {True: 'true', False: 'false'}[v]
416
+ query_param_examples.append((param['name'], v))
417
+ elif param.get('explode', False) and isinstance(example, dict):
418
+ for k, v in example.items():
419
+ if isinstance(v, bool):
420
+ v = {True: 'true', False: 'false'}[v]
421
+ query_param_examples.append((k, v))
422
+ else:
423
+ v = example
424
+ if isinstance(v, bool):
425
+ v = {True: 'true', False: 'false'}[v]
426
+ query_param_examples.append((param['name'], v))
427
+
428
+ # print request content
429
+ if render_request:
430
+ request_content = properties.get('requestBody', {}).get('content', {})
431
+ if request_content and 'application/json' in request_content:
432
+ schema = request_content['application/json']['schema']
433
+ req_properties = json.dumps(schema['properties'], indent=2,
434
+ separators=(',', ':'),
435
+ ensure_ascii=False)
436
+ yield '{indent}'.format(**locals()) + '**{}**'.format(_('Request body:'))
437
+ yield ''
438
+ yield '{indent}.. sourcecode:: json'.format(**locals())
439
+ yield ''
440
+ for line in req_properties.splitlines():
441
+ # yield indent + line
442
+ yield '{indent}{indent}{line}'.format(**locals())
443
+ # yield ''
444
+ else:
445
+ desc = properties.get('requestBody', {}).get('description', '')
446
+ if desc:
447
+ # in case the description uses markdown format
448
+ desc = convert(desc).strip()
449
+ request_content = properties.get('requestBody', {}).get('content', {})
450
+ if request_content and 'application/json' in request_content:
451
+ schema = request_content['application/json'].get('schema', {})
452
+ if '$entity_ref' in schema or schema.get('type', 'object') == 'array':
453
+ desc = get_desc(desc, schema, indent, deep=False)
454
+ if desc:
455
+ yield '{indent}:form body: {desc}'.format(**locals())
456
+ else:
457
+ for prop, v in schema.get('properties', {}).items():
458
+ ptype = v.get('type', '')
459
+ desc = v.get('description', '')
460
+ if desc:
461
+ # in case the description uses markdown format
462
+ desc = convert(desc).strip()
463
+ yield '{indent}:jsonparam {ptype} {prop}: {desc}'.format(**locals()).rstrip()
464
+ else:
465
+ if desc:
466
+ yield '{indent}:form body: {desc}.'.format(**locals())
467
+
468
+ # print request header params
469
+ reqheader_examples = {}
470
+ for param in filter(lambda p: p['in'] == 'header', parameters):
471
+ yield indent + ':reqheader {name}:'.format(**param)
472
+ desc = param.get('description', '')
473
+ if desc:
474
+ # in case the description uses markdown format
475
+ desc = convert(desc).strip()
476
+ desc = get_desc(desc, param['schema'], indent)
477
+ if desc:
478
+ yield '{indent}{indent}{desc}'.format(**locals())
479
+ if param.get('required', False):
480
+ yield '{indent}{indent}(Required)'.format(**locals())
481
+ ex = param.get('example', param.get('schema', {}).get('example', None))
482
+ if ex is None:
483
+ # try examples
484
+ ex = param.get('examples', param.get('schema', {}).get('examples', [None]))[0]
485
+ if ex:
486
+ reqheader_examples[param['name']] = ex
487
+
488
+ # print request example
489
+ if render_examples and not group_examples:
490
+ endpoint_examples = endpoint_novar
491
+ if query_param_examples:
492
+ endpoint_examples = endpoint_novar + "?" + \
493
+ parse.urlencode(query_param_examples)
494
+
495
+ # print request example
496
+ request_content = properties.get('requestBody', {}).get('content', {})
497
+ for line in _example(
498
+ request_content,
499
+ method,
500
+ endpoint=endpoint_examples,
501
+ reqheader_examples=reqheader_examples,
502
+ nb_indent=1):
503
+ yield line
504
+
505
+ # print response headers
506
+ resheader_examples = {}
507
+ for status, response in responses.items():
508
+ for headername, header in response.get('headers', {}).items():
509
+ yield indent + ':resheader {name}:'.format(name=headername)
510
+ desc = header.get('description', '')
511
+ if desc:
512
+ # in case the description uses markdown format
513
+ desc = convert(desc).strip()
514
+ desc = get_desc(desc, header.get('schema', {}), indent)
515
+ if desc:
516
+ yield '{indent}{indent}{desc}'.format(**locals())
517
+ ex = header.get('example', header.get('schema', {}).get('example', None))
518
+ if ex is None:
519
+ # try examples
520
+ ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
521
+ if ex:
522
+ resheader_examples[headername] = ex
523
+
524
+ # print response status codes
525
+ for status, response in responses.items():
526
+ resheader_examples = {}
527
+ for headername, header in response.get('headers', {}).items():
528
+ ex = header.get('example', header.get('schema', {}).get('example', None))
529
+ if ex is None:
530
+ # try examples
531
+ ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
532
+ if ex:
533
+ resheader_examples[headername] = ex
534
+ yield '{indent}:status {status}:'.format(**locals())
535
+ content = response.get('content', {})
536
+ if entities and content and 'application/json' in content:
537
+ schema = content['application/json']['schema']
538
+ desc = response.get('description', '')
539
+ if desc:
540
+ # in case the description uses markdown format
541
+ desc = convert(desc).strip()
542
+ desc = get_desc(desc, schema, indent, deep=False)
543
+ if desc:
544
+ yield '{indent}{indent}{desc}'.format(**locals())
545
+ else:
546
+ desc = response.get('description', '')
547
+ if desc:
548
+ # in case the description uses markdown format
549
+ desc = convert(desc).strip()
550
+ if desc and desc[-1] != '.':
551
+ desc += '.'
552
+ for line in convert(desc.splitlines()):
553
+ yield '{indent}{indent}{line}'.format(**locals())
554
+
555
+ # print response example
556
+ if render_examples and not group_examples:
557
+ for line in _example(
558
+ response.get('content', {}),
559
+ status=status,
560
+ resheader_examples=resheader_examples,
561
+ nb_indent=2):
562
+ yield line
563
+
564
+ if render_examples and group_examples:
565
+ endpoint_examples = endpoint_novar
566
+ if query_param_examples:
567
+ endpoint_examples = endpoint_novar + "?" + \
568
+ parse.urlencode(query_param_examples)
569
+
570
+ # print request example
571
+ request_content = properties.get('requestBody', {}).get('content', {})
572
+ for line in _example(
573
+ request_content,
574
+ method,
575
+ endpoint=endpoint_examples,
576
+ reqheader_examples=reqheader_examples,
577
+ resheader_examples=resheader_examples,
578
+ nb_indent=1):
579
+ yield line
580
+
581
+ # print response example
582
+ for status, response in responses.items():
583
+ resheader_examples = {}
584
+ for headername, header in response.get('headers', {}).items():
585
+ ex = header.get('example', header.get('schema', {}).get('example', None))
586
+ if ex is None:
587
+ # try examples
588
+ ex = header.get('examples', header.get('schema', {}).get(
589
+ 'examples',
590
+ [None]))[0]
591
+ if ex:
592
+ resheader_examples[headername] = ex
593
+ for line in _example(
594
+ response.get('content', {}),
595
+ status=status,
596
+ reqheader_examples=reqheader_examples,
597
+ resheader_examples=resheader_examples,
598
+ nb_indent=1):
599
+ yield line
600
+
601
+ for cb_name, cb_specs in properties.get('callbacks', {}).items():
602
+ yield ''
603
+ yield indent + '.. admonition:: Callback: ' + cb_name
604
+ yield ''
605
+
606
+ for cb_endpoint in cb_specs.keys():
607
+ for cb_method, cb_properties in cb_specs[cb_endpoint].items():
608
+ for line in _httpresource(
609
+ cb_endpoint,
610
+ cb_method,
611
+ cb_properties,
612
+ convert=convert,
613
+ render_examples=render_examples,
614
+ render_request=render_request,
615
+ group_examples=group_examples,
616
+ entities=entities):
617
+ if line:
618
+ yield indent+indent+line
619
+ else:
620
+ yield ''
621
+
622
+ yield ''
623
+
624
+
625
+ def _header(title):
626
+ yield title
627
+ yield '=' * len(title)
628
+ yield ''
629
+
630
+
631
+ def openapihttpdomain(spec, **options):
632
+ generators = []
633
+
634
+ # OpenAPI spec may contain JSON references, common properties, etc.
635
+ # Trying to render the spec "As Is" will require to put multiple
636
+ # if-s around the code. In order to simplify flow, let's make the
637
+ # spec to have only one (expected) schema, i.e. normalize it.
638
+ utils.normalize_spec(spec, **options)
639
+
640
+ # Paths list to be processed
641
+ paths = []
642
+
643
+ # If 'paths' are passed we've got to ensure they exist within an OpenAPI
644
+ # spec; otherwise raise error and ask user to fix that.
645
+ if 'paths' in options:
646
+ if not set(options['paths']).issubset(spec['paths']):
647
+ raise ValueError(
648
+ 'One or more paths are not defined in the spec: %s.' % (
649
+ ', '.join(set(options['paths']) - set(spec['paths'])),
650
+ )
651
+ )
652
+ paths = options['paths']
653
+
654
+ contextpath = ''
655
+ if 'contextpath' in options:
656
+ if 'servers' in spec:
657
+ h = spec['servers'][0]['url']
658
+ contextpath = urllib.parse.urlparse(h).path
659
+ if contextpath and contextpath[-1] == '/':
660
+ contextpath = contextpath[:-1]
661
+
662
+ # Check against regular expressions to be included
663
+ if 'include' in options:
664
+ # use a set to avoid duplicates
665
+ new_paths = set()
666
+ for i in options['include']:
667
+ ir = re.compile(i)
668
+ for path in spec['paths']:
669
+ if ir.match(path):
670
+ new_paths.add(path)
671
+ # preserve order
672
+ new_list = []
673
+ for p in spec['paths']:
674
+ if p in new_paths or p in paths:
675
+ new_list.append(p)
676
+ paths = new_list
677
+
678
+ # If no include nor paths option, then take full path
679
+ if 'include' not in options and 'paths' not in options:
680
+ paths = list(spec['paths'].keys())
681
+
682
+ # Remove paths matching regexp
683
+ if 'exclude' in options:
684
+ exc_paths = set()
685
+ for e in options['exclude']:
686
+ er = re.compile(e)
687
+ for path in paths:
688
+ if er.match(path):
689
+ exc_paths.add(path)
690
+ # remove like that to preserve order
691
+ for path in exc_paths:
692
+ paths.remove(path)
693
+
694
+ render_request = False
695
+ if 'request' in options:
696
+ render_request = True
697
+
698
+ convert = utils.get_text_converter(options)
699
+
700
+ if 'entities' in options:
701
+ def f_entities(x):
702
+ return _entities(spec, x)
703
+ entities = f_entities
704
+ else:
705
+ entities = False
706
+
707
+ # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#paths-object
708
+ if 'group' in options:
709
+ groups = collections.OrderedDict(
710
+ [(x['name'], []) for x in spec.get('tags', {})]
711
+ )
712
+
713
+ for endpoint in paths:
714
+ for method, properties in spec['paths'][endpoint].items():
715
+ if options.get('methods') and method not in options.get('methods'):
716
+ continue
717
+ key = properties.get('tags', [''])[0]
718
+ groups.setdefault(key, []).append(_httpresource(
719
+ contextpath+endpoint,
720
+ method,
721
+ properties,
722
+ convert,
723
+ render_examples='examples' in options,
724
+ render_request=render_request,
725
+ group_examples='group_examples' in options,
726
+ entities=entities))
727
+
728
+ for key in groups.keys():
729
+ if key:
730
+ generators.append(_header(key))
731
+ else:
732
+ generators.append(_header('default'))
733
+
734
+ generators.extend(groups[key])
735
+ else:
736
+ for endpoint in paths:
737
+ for method, properties in spec['paths'][endpoint].items():
738
+ if options.get('methods') and method not in options.get('methods'):
739
+ continue
740
+ generators.append(_httpresource(
741
+ contextpath+endpoint,
742
+ method,
743
+ properties,
744
+ convert,
745
+ render_examples='examples' in options,
746
+ render_request=render_request,
747
+ group_examples='group_examples' in options,
748
+ entities=entities))
749
+
750
+ return iter(itertools.chain(*generators))