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