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.

@@ -1,748 +1,748 @@
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) and '...' not in example:
148
- # materialize in the example the fact that additional properties can be added
149
- ret['...'] = '...'
150
- return ret
151
- if method and 'properties' in schema and \
152
- all(v.get('readOnly', False)
153
- for v in schema['properties'].values()):
154
- return _READONLY_PROPERTY
155
-
156
- results = []
157
- for name, prop in schema.get('properties', {}).items():
158
- result = _parse_schema(prop, method)
159
- if result != _READONLY_PROPERTY:
160
- results.append((name, result))
161
-
162
- # XXX should be True to be compliant with OpenAPI
163
- if schema.get('additionalProperties', False):
164
- # materialize in the example the fact that additional properties can be added
165
- results.append(("...", "..."))
166
-
167
- return collections.OrderedDict(results)
168
-
169
- if 'example' in schema:
170
- return schema['example']
171
- if 'default' in schema:
172
- return schema['default']
173
- if (schema_type, schema.get('format')) in _TYPE_MAPPING:
174
- return _TYPE_MAPPING[(schema_type, schema.get('format'))]
175
-
176
- return _TYPE_MAPPING[(schema_type, None)] # unrecognized format
177
-
178
-
179
- def _example(media_type_objects, method=None, endpoint=None, status=None,
180
- reqheader_examples={},
181
- resheader_examples={},
182
- nb_indent=0):
183
- """
184
- Format examples in `Media Type Object` openapi v3 to HTTP request or
185
- HTTP response example.
186
- If method and endpoint is provided, this function prints a request example
187
- else status should be provided to print a response example.
188
-
189
- Arguments:
190
- media_type_objects (Dict[str, Dict]): Dict containing
191
- Media Type Objects.
192
- method: The HTTP method to use in example.
193
- endpoint: The HTTP route to use in example.
194
- status: The HTTP status to use in example.
195
- """
196
- indent = ' '
197
- extra_indent = indent * nb_indent
198
-
199
- if method is not None:
200
- method = method.upper()
201
- else:
202
- try:
203
- # one of possible values for status might be 'default'.
204
- # in the case, just fallback to '-'
205
- status_text = http_status_codes[int(status)]
206
- except (ValueError, KeyError):
207
- status_text = '-'
208
-
209
- # Provide request samples for GET requests
210
- if method == 'GET':
211
- media_type_objects[''] = {
212
- 'examples': {_('Example request'): {'value': '\n'}}}
213
-
214
- for content_type, content in media_type_objects.items():
215
- examples = content.get('examples')
216
- example = content.get('example')
217
-
218
- # Try to get the example from the schema
219
- if example is None and 'schema' in content:
220
- example = content['schema'].get('example')
221
- if example:
222
- example = copy.deepcopy(example)
223
- # filters out readonly properties
224
- if method and 'properties' in content['schema']:
225
- for k, v in content['schema'].get('properties', {}).items():
226
- if v.get('readOnly', False) and k in example:
227
- del example[k]
228
- # XXX should be True to be compliant with OpenAPI
229
- if content['schema'].get('additionalProperties', False) and '...' not in example:
230
- # materialize in the example the fact that additional properties can be added
231
- example['...'] = '...'
232
-
233
- if examples is None:
234
- examples = {}
235
- if not example:
236
- if re.match(r"application/[a-zA-Z\+]*json", content_type) is \
237
- None:
238
- LOG.info('skipping non-JSON example generation.')
239
- continue
240
- example = _parse_schema(content['schema'], method=method)
241
-
242
- if method is None:
243
- examples[_('Example response')] = {
244
- 'value': example,
245
- }
246
- else:
247
- examples[_('Example request')] = {
248
- 'value': example,
249
- }
250
-
251
- for example in examples.values():
252
- # According to OpenAPI v3 specs, string examples should be left unchanged
253
- if not isinstance(example['value'], str):
254
- example['value'] = json.dumps(
255
- example['value'], indent=4, separators=(',', ': '))
256
-
257
- for example_name, example in examples.items():
258
- if 'summary' in example:
259
- example_title = '{example_name} - {example[summary]}'.format(
260
- **locals())
261
- else:
262
- example_title = example_name
263
-
264
- yield ''
265
- yield '{extra_indent}**{example_title}:**'.format(**locals())
266
- yield ''
267
- yield '{extra_indent}.. sourcecode:: http'.format(**locals())
268
- yield ''
269
-
270
- # Print http request example
271
- if method:
272
- yield '{extra_indent}{indent}{method} {endpoint} HTTP/1.1' \
273
- .format(**locals())
274
- yield '{extra_indent}{indent}Host: example.com' \
275
- .format(**locals())
276
- if content_type:
277
- yield '{extra_indent}{indent}Content-Type: {content_type}'\
278
- .format(**locals())
279
- for k, v in reqheader_examples.items():
280
- yield '{extra_indent}{indent}{k}: {v}'\
281
- .format(**locals())
282
- # Print http response example
283
- else:
284
- yield '{extra_indent}{indent}HTTP/1.1 {status} {status_text}' \
285
- .format(**locals())
286
- yield '{extra_indent}{indent}Content-Type: {content_type}' \
287
- .format(**locals())
288
- for k, v in resheader_examples.items():
289
- yield '{extra_indent}{indent}{k}: {v}'\
290
- .format(**locals())
291
-
292
- if content_type:
293
- yield ''
294
- for example_line in example['value'].splitlines():
295
- yield '{extra_indent}{indent}{example_line}'.format(
296
- **locals()
297
- )
298
- if example['value'].splitlines():
299
- yield ''
300
-
301
-
302
- def ref2link(entities, ref):
303
- name = ref.split('/')[-1]
304
- ref = entities(ref)
305
- return ':ref:`{name} <{ref}>`'.format(**locals())
306
-
307
-
308
- def _httpresource(endpoint, method, properties, convert, render_examples,
309
- render_request, group_examples=False, entities=False):
310
- # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#operation-object
311
- endpoint_novar = endpoint
312
- parameters = properties.get('parameters', [])
313
- responses = properties['responses']
314
- query_param_examples = []
315
- indent = ' '
316
-
317
- yield '.. http:{0}:: {1}'.format(method, endpoint)
318
- yield ' :synopsis: {0}'.format(properties.get('summary', 'null'))
319
- yield ''
320
-
321
- if 'summary' in properties:
322
- for line in properties['summary'].splitlines():
323
- yield '{indent}**{line}**'.format(**locals())
324
- yield ''
325
-
326
- if 'description' in properties:
327
- for line in convert(properties['description']).strip().splitlines():
328
- yield '{indent}{line}'.format(**locals())
329
- yield ''
330
-
331
- if properties.get('deprecated', False):
332
- yield '{indent}**DEPRECATED**'.format(**locals())
333
- yield ''
334
-
335
- if 'security' in properties:
336
- for sec_schema in properties['security']:
337
- sec_scope = ' or '.join([
338
- '``{}``'.format(s) for sch in sec_schema.values() for s in sch
339
- ])
340
- s = '{indent}**' + _('Scope required') + '**: {sec_scope}'
341
- yield s.format(**locals())
342
- yield ''
343
-
344
- def get_desc(desc, schema, indent, deep=True):
345
- if entities:
346
- doc = next(_process_one(['R'], schema, False, entities, convert))
347
- if desc:
348
- if not desc[-1] == '.':
349
- desc = desc + '.'
350
- if doc[1]:
351
- if not doc[1].startswith(str(_("Object of"))) and \
352
- not doc[1].startswith(str(_("Array of"))):
353
- doc[1] = _("Object of type {}").format(doc[1])
354
- if not doc[1][-1] == '.':
355
- doc[1] = doc[1] + '.'
356
- desc += '\n' + doc[1]
357
- if deep and doc[2] and doc[2] != _('Additional properties'):
358
- if not doc[2][-1] == '.':
359
- doc[2] = doc[2] + '.'
360
- desc += '\n' + doc[2]
361
- desc = desc.rstrip()
362
- else:
363
- doc = next(_process_one(['R'], schema, False, entities, convert))
364
- if desc:
365
- if not desc[-1] == '.':
366
- desc = desc + '.'
367
- if doc[2] and doc[2] != _('Additional properties'):
368
- if not doc[2][-1] == '.':
369
- doc[2] = doc[2] + '.'
370
- desc += '\n' + doc[2]
371
- desc = desc.rstrip()
372
- desc = textwrap.indent(desc, '{indent}{indent}'.format(**locals())).lstrip()
373
- return desc
374
-
375
- # print request's path params
376
- for param in filter(lambda p: p['in'] == 'path', parameters):
377
- yield indent + ':param {type} {name}:'.format(
378
- type=param['schema']['type'],
379
- name=param['name'])
380
-
381
- desc = param.get('description', '')
382
- if desc:
383
- # in case the description uses markdown format
384
- desc = convert(desc).strip()
385
- desc = get_desc(desc, param['schema'], indent)
386
- if desc:
387
- yield '{indent}{indent}{desc}'.format(**locals())
388
-
389
- example = _parse_schema(param['schema'], method)
390
- example = param.get('example', example)
391
- if example and type(example) == str:
392
- endpoint_novar = \
393
- endpoint_novar.replace('{'+param['name']+'}', urllib.parse.quote(example))
394
- # print request's query params
395
- for param in filter(lambda p: p['in'] == 'query', parameters):
396
- yield indent + ':query {type} {name}:'.format(
397
- type=param['schema']['type'],
398
- name=param['name'])
399
- desc = param.get('description', '')
400
- if desc:
401
- # in case the description uses markdown format
402
- desc = convert(desc).strip()
403
- desc = get_desc(desc, param['schema'], indent)
404
- if desc:
405
- yield '{indent}{indent}{desc}'.format(**locals())
406
- if param.get('required', False):
407
- yield '{indent}{indent}'.format(**locals()) + \
408
- '({})'.format(_('Required'))
409
- example = _parse_schema(param['schema'], method)
410
- example = param.get('example', example)
411
- if param.get('explode', False) and isinstance(example, list):
412
- for v in example:
413
- if isinstance(v, bool):
414
- v = {True: 'true', False: 'false'}[v]
415
- query_param_examples.append((param['name'], v))
416
- elif param.get('explode', False) and isinstance(example, dict):
417
- for k, v in example.items():
418
- if isinstance(v, bool):
419
- v = {True: 'true', False: 'false'}[v]
420
- query_param_examples.append((k, v))
421
- else:
422
- v = example
423
- if isinstance(v, bool):
424
- v = {True: 'true', False: 'false'}[v]
425
- query_param_examples.append((param['name'], v))
426
-
427
- # print request content
428
- if render_request:
429
- request_content = properties.get('requestBody', {}).get('content', {})
430
- if request_content and 'application/json' in request_content:
431
- schema = request_content['application/json']['schema']
432
- req_properties = json.dumps(schema['properties'], indent=2,
433
- separators=(',', ':'))
434
- yield '{indent}'.format(**locals()) + '**{}**'.format(_('Request body:'))
435
- yield ''
436
- yield '{indent}.. sourcecode:: json'.format(**locals())
437
- yield ''
438
- for line in req_properties.splitlines():
439
- # yield indent + line
440
- yield '{indent}{indent}{line}'.format(**locals())
441
- # yield ''
442
- else:
443
- desc = properties.get('requestBody', {}).get('description', '')
444
- if desc:
445
- # in case the description uses markdown format
446
- desc = convert(desc).strip()
447
- request_content = properties.get('requestBody', {}).get('content', {})
448
- if request_content and 'application/json' in request_content:
449
- schema = request_content['application/json'].get('schema', {})
450
- if '$entity_ref' in schema or schema.get('type', 'object') == 'array':
451
- desc = get_desc(desc, schema, indent, deep=False)
452
- if desc:
453
- yield '{indent}:form body: {desc}'.format(**locals())
454
- else:
455
- for prop, v in schema.get('properties', {}).items():
456
- ptype = v.get('type', '')
457
- desc = v.get('description', '')
458
- if desc:
459
- # in case the description uses markdown format
460
- desc = convert(desc).strip()
461
- yield '{indent}:jsonparam {ptype} {prop}: {desc}'.format(**locals()).rstrip()
462
- else:
463
- if desc:
464
- yield '{indent}:form body: {desc}.'.format(**locals())
465
-
466
- # print request header params
467
- reqheader_examples = {}
468
- for param in filter(lambda p: p['in'] == 'header', parameters):
469
- yield indent + ':reqheader {name}:'.format(**param)
470
- desc = param.get('description', '')
471
- if desc:
472
- # in case the description uses markdown format
473
- desc = convert(desc).strip()
474
- desc = get_desc(desc, param['schema'], indent)
475
- if desc:
476
- yield '{indent}{indent}{desc}'.format(**locals())
477
- if param.get('required', False):
478
- yield '{indent}{indent}(Required)'.format(**locals())
479
- ex = param.get('example', param.get('schema', {}).get('example', None))
480
- if ex is None:
481
- # try examples
482
- ex = param.get('examples', param.get('schema', {}).get('examples', [None]))[0]
483
- if ex:
484
- reqheader_examples[param['name']] = ex
485
-
486
- # print request example
487
- if render_examples and not group_examples:
488
- endpoint_examples = endpoint_novar
489
- if query_param_examples:
490
- endpoint_examples = endpoint_novar + "?" + \
491
- parse.urlencode(query_param_examples)
492
-
493
- # print request example
494
- request_content = properties.get('requestBody', {}).get('content', {})
495
- for line in _example(
496
- request_content,
497
- method,
498
- endpoint=endpoint_examples,
499
- reqheader_examples=reqheader_examples,
500
- nb_indent=1):
501
- yield line
502
-
503
- # print response headers
504
- resheader_examples = {}
505
- for status, response in responses.items():
506
- for headername, header in response.get('headers', {}).items():
507
- yield indent + ':resheader {name}:'.format(name=headername)
508
- desc = header.get('description', '')
509
- if desc:
510
- # in case the description uses markdown format
511
- desc = convert(desc).strip()
512
- desc = get_desc(desc, header.get('schema', {}), indent)
513
- if desc:
514
- yield '{indent}{indent}{desc}'.format(**locals())
515
- ex = header.get('example', header.get('schema', {}).get('example', None))
516
- if ex is None:
517
- # try examples
518
- ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
519
- if ex:
520
- resheader_examples[headername] = ex
521
-
522
- # print response status codes
523
- for status, response in responses.items():
524
- resheader_examples = {}
525
- for headername, header in response.get('headers', {}).items():
526
- ex = header.get('example', header.get('schema', {}).get('example', None))
527
- if ex is None:
528
- # try examples
529
- ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
530
- if ex:
531
- resheader_examples[headername] = ex
532
- yield '{indent}:status {status}:'.format(**locals())
533
- content = response.get('content', {})
534
- if entities and content and 'application/json' in content:
535
- schema = content['application/json']['schema']
536
- desc = response.get('description', '')
537
- if desc:
538
- # in case the description uses markdown format
539
- desc = convert(desc).strip()
540
- desc = get_desc(desc, schema, indent, deep=False)
541
- if desc:
542
- yield '{indent}{indent}{desc}'.format(**locals())
543
- else:
544
- desc = response.get('description', '')
545
- if desc:
546
- # in case the description uses markdown format
547
- desc = convert(desc).strip()
548
- if desc and desc[-1] != '.':
549
- desc += '.'
550
- for line in convert(desc.splitlines()):
551
- yield '{indent}{indent}{line}'.format(**locals())
552
-
553
- # print response example
554
- if render_examples and not group_examples:
555
- for line in _example(
556
- response.get('content', {}),
557
- status=status,
558
- resheader_examples=resheader_examples,
559
- nb_indent=2):
560
- yield line
561
-
562
- if render_examples and group_examples:
563
- endpoint_examples = endpoint_novar
564
- if query_param_examples:
565
- endpoint_examples = endpoint_novar + "?" + \
566
- parse.urlencode(query_param_examples)
567
-
568
- # print request example
569
- request_content = properties.get('requestBody', {}).get('content', {})
570
- for line in _example(
571
- request_content,
572
- method,
573
- endpoint=endpoint_examples,
574
- reqheader_examples=reqheader_examples,
575
- resheader_examples=resheader_examples,
576
- nb_indent=1):
577
- yield line
578
-
579
- # print response example
580
- for status, response in responses.items():
581
- resheader_examples = {}
582
- for headername, header in response.get('headers', {}).items():
583
- ex = header.get('example', header.get('schema', {}).get('example', None))
584
- if ex is None:
585
- # try examples
586
- ex = header.get('examples', header.get('schema', {}).get(
587
- 'examples',
588
- [None]))[0]
589
- if ex:
590
- resheader_examples[headername] = ex
591
- for line in _example(
592
- response.get('content', {}),
593
- status=status,
594
- reqheader_examples=reqheader_examples,
595
- resheader_examples=resheader_examples,
596
- nb_indent=1):
597
- yield line
598
-
599
- for cb_name, cb_specs in properties.get('callbacks', {}).items():
600
- yield ''
601
- yield indent + '.. admonition:: Callback: ' + cb_name
602
- yield ''
603
-
604
- for cb_endpoint in cb_specs.keys():
605
- for cb_method, cb_properties in cb_specs[cb_endpoint].items():
606
- for line in _httpresource(
607
- cb_endpoint,
608
- cb_method,
609
- cb_properties,
610
- convert=convert,
611
- render_examples=render_examples,
612
- render_request=render_request,
613
- group_examples=group_examples,
614
- entities=entities):
615
- if line:
616
- yield indent+indent+line
617
- else:
618
- yield ''
619
-
620
- yield ''
621
-
622
-
623
- def _header(title):
624
- yield title
625
- yield '=' * len(title)
626
- yield ''
627
-
628
-
629
- def openapihttpdomain(spec, **options):
630
- generators = []
631
-
632
- # OpenAPI spec may contain JSON references, common properties, etc.
633
- # Trying to render the spec "As Is" will require to put multiple
634
- # if-s around the code. In order to simplify flow, let's make the
635
- # spec to have only one (expected) schema, i.e. normalize it.
636
- utils.normalize_spec(spec, **options)
637
-
638
- # Paths list to be processed
639
- paths = []
640
-
641
- # If 'paths' are passed we've got to ensure they exist within an OpenAPI
642
- # spec; otherwise raise error and ask user to fix that.
643
- if 'paths' in options:
644
- if not set(options['paths']).issubset(spec['paths']):
645
- raise ValueError(
646
- 'One or more paths are not defined in the spec: %s.' % (
647
- ', '.join(set(options['paths']) - set(spec['paths'])),
648
- )
649
- )
650
- paths = options['paths']
651
-
652
- contextpath = ''
653
- if 'contextpath' in options:
654
- if 'servers' in spec:
655
- h = spec['servers'][0]['url']
656
- contextpath = urllib.parse.urlparse(h).path
657
- if contextpath and contextpath[-1] == '/':
658
- contextpath = contextpath[:-1]
659
-
660
- # Check against regular expressions to be included
661
- if 'include' in options:
662
- # use a set to avoid duplicates
663
- new_paths = set()
664
- for i in options['include']:
665
- ir = re.compile(i)
666
- for path in spec['paths']:
667
- if ir.match(path):
668
- new_paths.add(path)
669
- # preserve order
670
- new_list = []
671
- for p in spec['paths']:
672
- if p in new_paths or p in paths:
673
- new_list.append(p)
674
- paths = new_list
675
-
676
- # If no include nor paths option, then take full path
677
- if 'include' not in options and 'paths' not in options:
678
- paths = list(spec['paths'].keys())
679
-
680
- # Remove paths matching regexp
681
- if 'exclude' in options:
682
- exc_paths = set()
683
- for e in options['exclude']:
684
- er = re.compile(e)
685
- for path in paths:
686
- if er.match(path):
687
- exc_paths.add(path)
688
- # remove like that to preserve order
689
- for path in exc_paths:
690
- paths.remove(path)
691
-
692
- render_request = False
693
- if 'request' in options:
694
- render_request = True
695
-
696
- convert = utils.get_text_converter(options)
697
-
698
- if 'entities' in options:
699
- def f_entities(x):
700
- return _entities(spec, x)
701
- entities = f_entities
702
- else:
703
- entities = False
704
-
705
- # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#paths-object
706
- if 'group' in options:
707
- groups = collections.OrderedDict(
708
- [(x['name'], []) for x in spec.get('tags', {})]
709
- )
710
-
711
- for endpoint in paths:
712
- for method, properties in spec['paths'][endpoint].items():
713
- if options.get('methods') and method not in options.get('methods'):
714
- continue
715
- key = properties.get('tags', [''])[0]
716
- groups.setdefault(key, []).append(_httpresource(
717
- contextpath+endpoint,
718
- method,
719
- properties,
720
- convert,
721
- render_examples='examples' in options,
722
- render_request=render_request,
723
- group_examples='group_examples' in options,
724
- entities=entities))
725
-
726
- for key in groups.keys():
727
- if key:
728
- generators.append(_header(key))
729
- else:
730
- generators.append(_header('default'))
731
-
732
- generators.extend(groups[key])
733
- else:
734
- for endpoint in paths:
735
- for method, properties in spec['paths'][endpoint].items():
736
- if options.get('methods') and method not in options.get('methods'):
737
- continue
738
- generators.append(_httpresource(
739
- contextpath+endpoint,
740
- method,
741
- properties,
742
- convert,
743
- render_examples='examples' in options,
744
- render_request=render_request,
745
- group_examples='group_examples' in options,
746
- entities=entities))
747
-
748
- return iter(itertools.chain(*generators))
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) and '...' not in example:
148
+ # materialize in the example the fact that additional properties can be added
149
+ ret['...'] = '...'
150
+ return ret
151
+ if method and 'properties' in schema and \
152
+ all(v.get('readOnly', False)
153
+ for v in schema['properties'].values()):
154
+ return _READONLY_PROPERTY
155
+
156
+ results = []
157
+ for name, prop in schema.get('properties', {}).items():
158
+ result = _parse_schema(prop, method)
159
+ if result != _READONLY_PROPERTY:
160
+ results.append((name, result))
161
+
162
+ # XXX should be True to be compliant with OpenAPI
163
+ if schema.get('additionalProperties', False):
164
+ # materialize in the example the fact that additional properties can be added
165
+ results.append(("...", "..."))
166
+
167
+ return collections.OrderedDict(results)
168
+
169
+ if 'example' in schema:
170
+ return schema['example']
171
+ if 'default' in schema:
172
+ return schema['default']
173
+ if (schema_type, schema.get('format')) in _TYPE_MAPPING:
174
+ return _TYPE_MAPPING[(schema_type, schema.get('format'))]
175
+
176
+ return _TYPE_MAPPING[(schema_type, None)] # unrecognized format
177
+
178
+
179
+ def _example(media_type_objects, method=None, endpoint=None, status=None,
180
+ reqheader_examples={},
181
+ resheader_examples={},
182
+ nb_indent=0):
183
+ """
184
+ Format examples in `Media Type Object` openapi v3 to HTTP request or
185
+ HTTP response example.
186
+ If method and endpoint is provided, this function prints a request example
187
+ else status should be provided to print a response example.
188
+
189
+ Arguments:
190
+ media_type_objects (Dict[str, Dict]): Dict containing
191
+ Media Type Objects.
192
+ method: The HTTP method to use in example.
193
+ endpoint: The HTTP route to use in example.
194
+ status: The HTTP status to use in example.
195
+ """
196
+ indent = ' '
197
+ extra_indent = indent * nb_indent
198
+
199
+ if method is not None:
200
+ method = method.upper()
201
+ else:
202
+ try:
203
+ # one of possible values for status might be 'default'.
204
+ # in the case, just fallback to '-'
205
+ status_text = http_status_codes[int(status)]
206
+ except (ValueError, KeyError):
207
+ status_text = '-'
208
+
209
+ # Provide request samples for GET requests
210
+ if method == 'GET':
211
+ media_type_objects[''] = {
212
+ 'examples': {_('Example request'): {'value': '\n'}}}
213
+
214
+ for content_type, content in media_type_objects.items():
215
+ examples = content.get('examples')
216
+ example = content.get('example')
217
+
218
+ # Try to get the example from the schema
219
+ if example is None and 'schema' in content:
220
+ example = content['schema'].get('example')
221
+ if example:
222
+ example = copy.deepcopy(example)
223
+ # filters out readonly properties
224
+ if method and 'properties' in content['schema']:
225
+ for k, v in content['schema'].get('properties', {}).items():
226
+ if v.get('readOnly', False) and k in example:
227
+ del example[k]
228
+ # XXX should be True to be compliant with OpenAPI
229
+ if content['schema'].get('additionalProperties', False) and '...' not in example:
230
+ # materialize in the example the fact that additional properties can be added
231
+ example['...'] = '...'
232
+
233
+ if examples is None:
234
+ examples = {}
235
+ if not example:
236
+ if re.match(r"application/[a-zA-Z\+]*json", content_type) is \
237
+ None:
238
+ LOG.info('skipping non-JSON example generation.')
239
+ continue
240
+ example = _parse_schema(content['schema'], method=method)
241
+
242
+ if method is None:
243
+ examples[_('Example response')] = {
244
+ 'value': example,
245
+ }
246
+ else:
247
+ examples[_('Example request')] = {
248
+ 'value': example,
249
+ }
250
+
251
+ for example in examples.values():
252
+ # According to OpenAPI v3 specs, string examples should be left unchanged
253
+ if not isinstance(example['value'], str):
254
+ example['value'] = json.dumps(
255
+ example['value'], indent=4, separators=(',', ': '))
256
+
257
+ for example_name, example in examples.items():
258
+ if 'summary' in example:
259
+ example_title = '{example_name} - {example[summary]}'.format(
260
+ **locals())
261
+ else:
262
+ example_title = example_name
263
+
264
+ yield ''
265
+ yield '{extra_indent}**{example_title}:**'.format(**locals())
266
+ yield ''
267
+ yield '{extra_indent}.. sourcecode:: http'.format(**locals())
268
+ yield ''
269
+
270
+ # Print http request example
271
+ if method:
272
+ yield '{extra_indent}{indent}{method} {endpoint} HTTP/1.1' \
273
+ .format(**locals())
274
+ yield '{extra_indent}{indent}Host: example.com' \
275
+ .format(**locals())
276
+ if content_type:
277
+ yield '{extra_indent}{indent}Content-Type: {content_type}'\
278
+ .format(**locals())
279
+ for k, v in reqheader_examples.items():
280
+ yield '{extra_indent}{indent}{k}: {v}'\
281
+ .format(**locals())
282
+ # Print http response example
283
+ else:
284
+ yield '{extra_indent}{indent}HTTP/1.1 {status} {status_text}' \
285
+ .format(**locals())
286
+ yield '{extra_indent}{indent}Content-Type: {content_type}' \
287
+ .format(**locals())
288
+ for k, v in resheader_examples.items():
289
+ yield '{extra_indent}{indent}{k}: {v}'\
290
+ .format(**locals())
291
+
292
+ if content_type:
293
+ yield ''
294
+ for example_line in example['value'].splitlines():
295
+ yield '{extra_indent}{indent}{example_line}'.format(
296
+ **locals()
297
+ )
298
+ if example['value'].splitlines():
299
+ yield ''
300
+
301
+
302
+ def ref2link(entities, ref):
303
+ name = ref.split('/')[-1]
304
+ ref = entities(ref)
305
+ return ':ref:`{name} <{ref}>`'.format(**locals())
306
+
307
+
308
+ def _httpresource(endpoint, method, properties, convert, render_examples,
309
+ render_request, group_examples=False, entities=False):
310
+ # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#operation-object
311
+ endpoint_novar = endpoint
312
+ parameters = properties.get('parameters', [])
313
+ responses = properties['responses']
314
+ query_param_examples = []
315
+ indent = ' '
316
+
317
+ yield '.. http:{0}:: {1}'.format(method, endpoint)
318
+ yield ' :synopsis: {0}'.format(properties.get('summary', 'null'))
319
+ yield ''
320
+
321
+ if 'summary' in properties:
322
+ for line in properties['summary'].splitlines():
323
+ yield '{indent}**{line}**'.format(**locals())
324
+ yield ''
325
+
326
+ if 'description' in properties:
327
+ for line in convert(properties['description']).strip().splitlines():
328
+ yield '{indent}{line}'.format(**locals())
329
+ yield ''
330
+
331
+ if properties.get('deprecated', False):
332
+ yield '{indent}**DEPRECATED**'.format(**locals())
333
+ yield ''
334
+
335
+ if 'security' in properties:
336
+ for sec_schema in properties['security']:
337
+ sec_scope = ' or '.join([
338
+ '``{}``'.format(s) for sch in sec_schema.values() for s in sch
339
+ ])
340
+ s = '{indent}**' + _('Scope required') + '**: {sec_scope}'
341
+ yield s.format(**locals())
342
+ yield ''
343
+
344
+ def get_desc(desc, schema, indent, deep=True):
345
+ if entities:
346
+ doc = next(_process_one(['R'], schema, False, entities, convert))
347
+ if desc:
348
+ if not desc[-1] == '.':
349
+ desc = desc + '.'
350
+ if doc[1]:
351
+ if not doc[1].startswith(str(_("Object of"))) and \
352
+ not doc[1].startswith(str(_("Array of"))):
353
+ doc[1] = _("Object of type {}").format(doc[1])
354
+ if not doc[1][-1] == '.':
355
+ doc[1] = doc[1] + '.'
356
+ desc += '\n' + doc[1]
357
+ if deep and doc[2] and doc[2] != _('Additional properties'):
358
+ if not doc[2][-1] == '.':
359
+ doc[2] = doc[2] + '.'
360
+ desc += '\n' + doc[2]
361
+ desc = desc.rstrip()
362
+ else:
363
+ doc = next(_process_one(['R'], schema, False, entities, convert))
364
+ if desc:
365
+ if not desc[-1] == '.':
366
+ desc = desc + '.'
367
+ if doc[2] and doc[2] != _('Additional properties'):
368
+ if not doc[2][-1] == '.':
369
+ doc[2] = doc[2] + '.'
370
+ desc += '\n' + doc[2]
371
+ desc = desc.rstrip()
372
+ desc = textwrap.indent(desc, '{indent}{indent}'.format(**locals())).lstrip()
373
+ return desc
374
+
375
+ # print request's path params
376
+ for param in filter(lambda p: p['in'] == 'path', parameters):
377
+ yield indent + ':param {type} {name}:'.format(
378
+ type=param['schema']['type'],
379
+ name=param['name'])
380
+
381
+ desc = param.get('description', '')
382
+ if desc:
383
+ # in case the description uses markdown format
384
+ desc = convert(desc).strip()
385
+ desc = get_desc(desc, param['schema'], indent)
386
+ if desc:
387
+ yield '{indent}{indent}{desc}'.format(**locals())
388
+
389
+ example = _parse_schema(param['schema'], method)
390
+ example = param.get('example', example)
391
+ if example and type(example) == str:
392
+ endpoint_novar = \
393
+ endpoint_novar.replace('{'+param['name']+'}', urllib.parse.quote(example))
394
+ # print request's query params
395
+ for param in filter(lambda p: p['in'] == 'query', parameters):
396
+ yield indent + ':query {type} {name}:'.format(
397
+ type=param['schema']['type'],
398
+ name=param['name'])
399
+ desc = param.get('description', '')
400
+ if desc:
401
+ # in case the description uses markdown format
402
+ desc = convert(desc).strip()
403
+ desc = get_desc(desc, param['schema'], indent)
404
+ if desc:
405
+ yield '{indent}{indent}{desc}'.format(**locals())
406
+ if param.get('required', False):
407
+ yield '{indent}{indent}'.format(**locals()) + \
408
+ '({})'.format(_('Required'))
409
+ example = _parse_schema(param['schema'], method)
410
+ example = param.get('example', example)
411
+ if param.get('explode', False) and isinstance(example, list):
412
+ for v in example:
413
+ if isinstance(v, bool):
414
+ v = {True: 'true', False: 'false'}[v]
415
+ query_param_examples.append((param['name'], v))
416
+ elif param.get('explode', False) and isinstance(example, dict):
417
+ for k, v in example.items():
418
+ if isinstance(v, bool):
419
+ v = {True: 'true', False: 'false'}[v]
420
+ query_param_examples.append((k, v))
421
+ else:
422
+ v = example
423
+ if isinstance(v, bool):
424
+ v = {True: 'true', False: 'false'}[v]
425
+ query_param_examples.append((param['name'], v))
426
+
427
+ # print request content
428
+ if render_request:
429
+ request_content = properties.get('requestBody', {}).get('content', {})
430
+ if request_content and 'application/json' in request_content:
431
+ schema = request_content['application/json']['schema']
432
+ req_properties = json.dumps(schema['properties'], indent=2,
433
+ separators=(',', ':'))
434
+ yield '{indent}'.format(**locals()) + '**{}**'.format(_('Request body:'))
435
+ yield ''
436
+ yield '{indent}.. sourcecode:: json'.format(**locals())
437
+ yield ''
438
+ for line in req_properties.splitlines():
439
+ # yield indent + line
440
+ yield '{indent}{indent}{line}'.format(**locals())
441
+ # yield ''
442
+ else:
443
+ desc = properties.get('requestBody', {}).get('description', '')
444
+ if desc:
445
+ # in case the description uses markdown format
446
+ desc = convert(desc).strip()
447
+ request_content = properties.get('requestBody', {}).get('content', {})
448
+ if request_content and 'application/json' in request_content:
449
+ schema = request_content['application/json'].get('schema', {})
450
+ if '$entity_ref' in schema or schema.get('type', 'object') == 'array':
451
+ desc = get_desc(desc, schema, indent, deep=False)
452
+ if desc:
453
+ yield '{indent}:form body: {desc}'.format(**locals())
454
+ else:
455
+ for prop, v in schema.get('properties', {}).items():
456
+ ptype = v.get('type', '')
457
+ desc = v.get('description', '')
458
+ if desc:
459
+ # in case the description uses markdown format
460
+ desc = convert(desc).strip()
461
+ yield '{indent}:jsonparam {ptype} {prop}: {desc}'.format(**locals()).rstrip()
462
+ else:
463
+ if desc:
464
+ yield '{indent}:form body: {desc}.'.format(**locals())
465
+
466
+ # print request header params
467
+ reqheader_examples = {}
468
+ for param in filter(lambda p: p['in'] == 'header', parameters):
469
+ yield indent + ':reqheader {name}:'.format(**param)
470
+ desc = param.get('description', '')
471
+ if desc:
472
+ # in case the description uses markdown format
473
+ desc = convert(desc).strip()
474
+ desc = get_desc(desc, param['schema'], indent)
475
+ if desc:
476
+ yield '{indent}{indent}{desc}'.format(**locals())
477
+ if param.get('required', False):
478
+ yield '{indent}{indent}(Required)'.format(**locals())
479
+ ex = param.get('example', param.get('schema', {}).get('example', None))
480
+ if ex is None:
481
+ # try examples
482
+ ex = param.get('examples', param.get('schema', {}).get('examples', [None]))[0]
483
+ if ex:
484
+ reqheader_examples[param['name']] = ex
485
+
486
+ # print request example
487
+ if render_examples and not group_examples:
488
+ endpoint_examples = endpoint_novar
489
+ if query_param_examples:
490
+ endpoint_examples = endpoint_novar + "?" + \
491
+ parse.urlencode(query_param_examples)
492
+
493
+ # print request example
494
+ request_content = properties.get('requestBody', {}).get('content', {})
495
+ for line in _example(
496
+ request_content,
497
+ method,
498
+ endpoint=endpoint_examples,
499
+ reqheader_examples=reqheader_examples,
500
+ nb_indent=1):
501
+ yield line
502
+
503
+ # print response headers
504
+ resheader_examples = {}
505
+ for status, response in responses.items():
506
+ for headername, header in response.get('headers', {}).items():
507
+ yield indent + ':resheader {name}:'.format(name=headername)
508
+ desc = header.get('description', '')
509
+ if desc:
510
+ # in case the description uses markdown format
511
+ desc = convert(desc).strip()
512
+ desc = get_desc(desc, header.get('schema', {}), indent)
513
+ if desc:
514
+ yield '{indent}{indent}{desc}'.format(**locals())
515
+ ex = header.get('example', header.get('schema', {}).get('example', None))
516
+ if ex is None:
517
+ # try examples
518
+ ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
519
+ if ex:
520
+ resheader_examples[headername] = ex
521
+
522
+ # print response status codes
523
+ for status, response in responses.items():
524
+ resheader_examples = {}
525
+ for headername, header in response.get('headers', {}).items():
526
+ ex = header.get('example', header.get('schema', {}).get('example', None))
527
+ if ex is None:
528
+ # try examples
529
+ ex = header.get('examples', header.get('schema', {}).get('examples', [None]))[0]
530
+ if ex:
531
+ resheader_examples[headername] = ex
532
+ yield '{indent}:status {status}:'.format(**locals())
533
+ content = response.get('content', {})
534
+ if entities and content and 'application/json' in content:
535
+ schema = content['application/json']['schema']
536
+ desc = response.get('description', '')
537
+ if desc:
538
+ # in case the description uses markdown format
539
+ desc = convert(desc).strip()
540
+ desc = get_desc(desc, schema, indent, deep=False)
541
+ if desc:
542
+ yield '{indent}{indent}{desc}'.format(**locals())
543
+ else:
544
+ desc = response.get('description', '')
545
+ if desc:
546
+ # in case the description uses markdown format
547
+ desc = convert(desc).strip()
548
+ if desc and desc[-1] != '.':
549
+ desc += '.'
550
+ for line in convert(desc.splitlines()):
551
+ yield '{indent}{indent}{line}'.format(**locals())
552
+
553
+ # print response example
554
+ if render_examples and not group_examples:
555
+ for line in _example(
556
+ response.get('content', {}),
557
+ status=status,
558
+ resheader_examples=resheader_examples,
559
+ nb_indent=2):
560
+ yield line
561
+
562
+ if render_examples and group_examples:
563
+ endpoint_examples = endpoint_novar
564
+ if query_param_examples:
565
+ endpoint_examples = endpoint_novar + "?" + \
566
+ parse.urlencode(query_param_examples)
567
+
568
+ # print request example
569
+ request_content = properties.get('requestBody', {}).get('content', {})
570
+ for line in _example(
571
+ request_content,
572
+ method,
573
+ endpoint=endpoint_examples,
574
+ reqheader_examples=reqheader_examples,
575
+ resheader_examples=resheader_examples,
576
+ nb_indent=1):
577
+ yield line
578
+
579
+ # print response example
580
+ for status, response in responses.items():
581
+ resheader_examples = {}
582
+ for headername, header in response.get('headers', {}).items():
583
+ ex = header.get('example', header.get('schema', {}).get('example', None))
584
+ if ex is None:
585
+ # try examples
586
+ ex = header.get('examples', header.get('schema', {}).get(
587
+ 'examples',
588
+ [None]))[0]
589
+ if ex:
590
+ resheader_examples[headername] = ex
591
+ for line in _example(
592
+ response.get('content', {}),
593
+ status=status,
594
+ reqheader_examples=reqheader_examples,
595
+ resheader_examples=resheader_examples,
596
+ nb_indent=1):
597
+ yield line
598
+
599
+ for cb_name, cb_specs in properties.get('callbacks', {}).items():
600
+ yield ''
601
+ yield indent + '.. admonition:: Callback: ' + cb_name
602
+ yield ''
603
+
604
+ for cb_endpoint in cb_specs.keys():
605
+ for cb_method, cb_properties in cb_specs[cb_endpoint].items():
606
+ for line in _httpresource(
607
+ cb_endpoint,
608
+ cb_method,
609
+ cb_properties,
610
+ convert=convert,
611
+ render_examples=render_examples,
612
+ render_request=render_request,
613
+ group_examples=group_examples,
614
+ entities=entities):
615
+ if line:
616
+ yield indent+indent+line
617
+ else:
618
+ yield ''
619
+
620
+ yield ''
621
+
622
+
623
+ def _header(title):
624
+ yield title
625
+ yield '=' * len(title)
626
+ yield ''
627
+
628
+
629
+ def openapihttpdomain(spec, **options):
630
+ generators = []
631
+
632
+ # OpenAPI spec may contain JSON references, common properties, etc.
633
+ # Trying to render the spec "As Is" will require to put multiple
634
+ # if-s around the code. In order to simplify flow, let's make the
635
+ # spec to have only one (expected) schema, i.e. normalize it.
636
+ utils.normalize_spec(spec, **options)
637
+
638
+ # Paths list to be processed
639
+ paths = []
640
+
641
+ # If 'paths' are passed we've got to ensure they exist within an OpenAPI
642
+ # spec; otherwise raise error and ask user to fix that.
643
+ if 'paths' in options:
644
+ if not set(options['paths']).issubset(spec['paths']):
645
+ raise ValueError(
646
+ 'One or more paths are not defined in the spec: %s.' % (
647
+ ', '.join(set(options['paths']) - set(spec['paths'])),
648
+ )
649
+ )
650
+ paths = options['paths']
651
+
652
+ contextpath = ''
653
+ if 'contextpath' in options:
654
+ if 'servers' in spec:
655
+ h = spec['servers'][0]['url']
656
+ contextpath = urllib.parse.urlparse(h).path
657
+ if contextpath and contextpath[-1] == '/':
658
+ contextpath = contextpath[:-1]
659
+
660
+ # Check against regular expressions to be included
661
+ if 'include' in options:
662
+ # use a set to avoid duplicates
663
+ new_paths = set()
664
+ for i in options['include']:
665
+ ir = re.compile(i)
666
+ for path in spec['paths']:
667
+ if ir.match(path):
668
+ new_paths.add(path)
669
+ # preserve order
670
+ new_list = []
671
+ for p in spec['paths']:
672
+ if p in new_paths or p in paths:
673
+ new_list.append(p)
674
+ paths = new_list
675
+
676
+ # If no include nor paths option, then take full path
677
+ if 'include' not in options and 'paths' not in options:
678
+ paths = list(spec['paths'].keys())
679
+
680
+ # Remove paths matching regexp
681
+ if 'exclude' in options:
682
+ exc_paths = set()
683
+ for e in options['exclude']:
684
+ er = re.compile(e)
685
+ for path in paths:
686
+ if er.match(path):
687
+ exc_paths.add(path)
688
+ # remove like that to preserve order
689
+ for path in exc_paths:
690
+ paths.remove(path)
691
+
692
+ render_request = False
693
+ if 'request' in options:
694
+ render_request = True
695
+
696
+ convert = utils.get_text_converter(options)
697
+
698
+ if 'entities' in options:
699
+ def f_entities(x):
700
+ return _entities(spec, x)
701
+ entities = f_entities
702
+ else:
703
+ entities = False
704
+
705
+ # https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.0.md#paths-object
706
+ if 'group' in options:
707
+ groups = collections.OrderedDict(
708
+ [(x['name'], []) for x in spec.get('tags', {})]
709
+ )
710
+
711
+ for endpoint in paths:
712
+ for method, properties in spec['paths'][endpoint].items():
713
+ if options.get('methods') and method not in options.get('methods'):
714
+ continue
715
+ key = properties.get('tags', [''])[0]
716
+ groups.setdefault(key, []).append(_httpresource(
717
+ contextpath+endpoint,
718
+ method,
719
+ properties,
720
+ convert,
721
+ render_examples='examples' in options,
722
+ render_request=render_request,
723
+ group_examples='group_examples' in options,
724
+ entities=entities))
725
+
726
+ for key in groups.keys():
727
+ if key:
728
+ generators.append(_header(key))
729
+ else:
730
+ generators.append(_header('default'))
731
+
732
+ generators.extend(groups[key])
733
+ else:
734
+ for endpoint in paths:
735
+ for method, properties in spec['paths'][endpoint].items():
736
+ if options.get('methods') and method not in options.get('methods'):
737
+ continue
738
+ generators.append(_httpresource(
739
+ contextpath+endpoint,
740
+ method,
741
+ properties,
742
+ convert,
743
+ render_examples='examples' in options,
744
+ render_request=render_request,
745
+ group_examples='group_examples' in options,
746
+ entities=entities))
747
+
748
+ return iter(itertools.chain(*generators))