arkindex-client 1.0.15__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+ from urllib.parse import urljoin
4
+
5
+ import typesystem
6
+
7
+ from arkindex.document import Document, Field, Link, Section
8
+ from arkindex.schema.jsonschema import JSON_SCHEMA
9
+
10
+ SCHEMA_REF = typesystem.Object(
11
+ properties={"$ref": typesystem.String(pattern="^#/components/schemas/")}
12
+ )
13
+ REQUESTBODY_REF = typesystem.Object(
14
+ properties={"$ref": typesystem.String(pattern="^#/components/requestBodies/")}
15
+ )
16
+ RESPONSE_REF = typesystem.Object(
17
+ properties={"$ref": typesystem.String(pattern="^#/components/responses/")}
18
+ )
19
+
20
+ definitions = typesystem.SchemaDefinitions()
21
+
22
+ OPEN_API = typesystem.Object(
23
+ title="OpenAPI",
24
+ properties={
25
+ "openapi": typesystem.String(),
26
+ "info": typesystem.Reference("Info", definitions=definitions),
27
+ "servers": typesystem.Array(
28
+ items=typesystem.Reference("Server", definitions=definitions)
29
+ ),
30
+ "paths": typesystem.Reference("Paths", definitions=definitions),
31
+ "components": typesystem.Reference("Components", definitions=definitions),
32
+ "security": typesystem.Array(
33
+ items=typesystem.Reference("SecurityRequirement", definitions=definitions)
34
+ ),
35
+ "tags": typesystem.Array(
36
+ items=typesystem.Reference("Tag", definitions=definitions)
37
+ ),
38
+ "externalDocs": typesystem.Reference(
39
+ "ExternalDocumentation", definitions=definitions
40
+ ),
41
+ },
42
+ pattern_properties={"^x-": typesystem.Any()},
43
+ additional_properties=False,
44
+ required=["openapi", "info", "paths"],
45
+ )
46
+
47
+ definitions["Info"] = typesystem.Object(
48
+ properties={
49
+ "title": typesystem.String(allow_blank=True),
50
+ "description": typesystem.Text(allow_blank=True),
51
+ "termsOfService": typesystem.String(format="url"),
52
+ "contact": typesystem.Reference("Contact", definitions=definitions),
53
+ "license": typesystem.Reference("License", definitions=definitions),
54
+ "version": typesystem.String(allow_blank=True),
55
+ },
56
+ pattern_properties={"^x-": typesystem.Any()},
57
+ additional_properties=False,
58
+ required=["title", "version"],
59
+ )
60
+
61
+ definitions["Contact"] = typesystem.Object(
62
+ properties={
63
+ "name": typesystem.String(allow_blank=True),
64
+ "url": typesystem.String(format="url"),
65
+ "email": typesystem.String(format="email"),
66
+ },
67
+ pattern_properties={"^x-": typesystem.Any()},
68
+ additional_properties=False,
69
+ )
70
+
71
+ definitions["License"] = typesystem.Object(
72
+ properties={"name": typesystem.String(), "url": typesystem.String(format="url")},
73
+ required=["name"],
74
+ pattern_properties={"^x-": typesystem.Any()},
75
+ additional_properties=False,
76
+ )
77
+
78
+ definitions["Server"] = typesystem.Object(
79
+ properties={
80
+ "url": typesystem.String(),
81
+ "description": typesystem.Text(allow_blank=True),
82
+ "variables": typesystem.Object(
83
+ additional_properties=typesystem.Reference(
84
+ "ServerVariable", definitions=definitions
85
+ )
86
+ ),
87
+ },
88
+ pattern_properties={"^x-": typesystem.Any()},
89
+ additional_properties=False,
90
+ required=["url"],
91
+ )
92
+
93
+ definitions["ServerVariable"] = typesystem.Object(
94
+ properties={
95
+ "enum": typesystem.Array(items=typesystem.String()),
96
+ "default": typesystem.String(),
97
+ "description": typesystem.Text(allow_blank=True),
98
+ },
99
+ pattern_properties={"^x-": typesystem.Any()},
100
+ additional_properties=False,
101
+ required=["default"],
102
+ )
103
+
104
+ definitions["Paths"] = typesystem.Object(
105
+ pattern_properties={
106
+ "^/": typesystem.Reference("Path", definitions=definitions),
107
+ "^x-": typesystem.Any(),
108
+ },
109
+ additional_properties=False,
110
+ )
111
+
112
+ definitions["Path"] = typesystem.Object(
113
+ properties={
114
+ "summary": typesystem.String(allow_blank=True),
115
+ "description": typesystem.Text(allow_blank=True),
116
+ "get": typesystem.Reference("Operation", definitions=definitions),
117
+ "put": typesystem.Reference("Operation", definitions=definitions),
118
+ "post": typesystem.Reference("Operation", definitions=definitions),
119
+ "delete": typesystem.Reference("Operation", definitions=definitions),
120
+ "options": typesystem.Reference("Operation", definitions=definitions),
121
+ "head": typesystem.Reference("Operation", definitions=definitions),
122
+ "patch": typesystem.Reference("Operation", definitions=definitions),
123
+ "trace": typesystem.Reference("Operation", definitions=definitions),
124
+ "servers": typesystem.Array(
125
+ items=typesystem.Reference("Server", definitions=definitions)
126
+ ),
127
+ "parameters": typesystem.Array(
128
+ items=typesystem.Reference("Parameter", definitions=definitions)
129
+ ),
130
+ # TODO: | ReferenceObject
131
+ },
132
+ pattern_properties={"^x-": typesystem.Any()},
133
+ additional_properties=False,
134
+ )
135
+
136
+ definitions["Operation"] = typesystem.Object(
137
+ properties={
138
+ "tags": typesystem.Array(items=typesystem.String()),
139
+ "summary": typesystem.String(allow_blank=True),
140
+ "description": typesystem.Text(allow_blank=True),
141
+ "externalDocs": typesystem.Reference(
142
+ "ExternalDocumentation", definitions=definitions
143
+ ),
144
+ "operationId": typesystem.String(),
145
+ "parameters": typesystem.Array(
146
+ items=typesystem.Reference("Parameter", definitions=definitions)
147
+ ), # TODO: | ReferenceObject
148
+ "requestBody": REQUESTBODY_REF
149
+ | typesystem.Reference(
150
+ "RequestBody", definitions=definitions
151
+ ), # TODO: RequestBody | ReferenceObject
152
+ "responses": typesystem.Reference("Responses", definitions=definitions),
153
+ # TODO: 'callbacks'
154
+ "deprecated": typesystem.Boolean(),
155
+ "security": typesystem.Array(
156
+ typesystem.Reference("SecurityRequirement", definitions=definitions)
157
+ ),
158
+ "servers": typesystem.Array(
159
+ items=typesystem.Reference("Server", definitions=definitions)
160
+ ),
161
+ },
162
+ pattern_properties={"^x-": typesystem.Any()},
163
+ additional_properties=False,
164
+ )
165
+
166
+ definitions["ExternalDocumentation"] = typesystem.Object(
167
+ properties={
168
+ "description": typesystem.Text(allow_blank=True),
169
+ "url": typesystem.String(format="url"),
170
+ },
171
+ pattern_properties={"^x-": typesystem.Any()},
172
+ additional_properties=False,
173
+ required=["url"],
174
+ )
175
+
176
+ definitions["Parameter"] = typesystem.Object(
177
+ properties={
178
+ "name": typesystem.String(),
179
+ "in": typesystem.Choice(choices=["query", "header", "path", "cookie"]),
180
+ "description": typesystem.Text(allow_blank=True),
181
+ "required": typesystem.Boolean(),
182
+ "deprecated": typesystem.Boolean(),
183
+ "allowEmptyValue": typesystem.Boolean(),
184
+ "style": typesystem.Choice(
185
+ choices=[
186
+ "matrix",
187
+ "label",
188
+ "form",
189
+ "simple",
190
+ "spaceDelimited",
191
+ "pipeDelimited",
192
+ "deepObject",
193
+ ]
194
+ ),
195
+ "schema": JSON_SCHEMA | SCHEMA_REF,
196
+ "example": typesystem.Any(),
197
+ # TODO: Other fields
198
+ },
199
+ pattern_properties={"^x-": typesystem.Any()},
200
+ additional_properties=False,
201
+ required=["name", "in"],
202
+ )
203
+
204
+ definitions["RequestBody"] = typesystem.Object(
205
+ properties={
206
+ "description": typesystem.String(allow_blank=True),
207
+ "content": typesystem.Object(
208
+ additional_properties=typesystem.Reference(
209
+ "MediaType", definitions=definitions
210
+ )
211
+ ),
212
+ "required": typesystem.Boolean(),
213
+ },
214
+ pattern_properties={"^x-": typesystem.Any()},
215
+ additional_properties=False,
216
+ )
217
+
218
+ definitions["Responses"] = typesystem.Object(
219
+ properties={
220
+ "default": typesystem.Reference("Response", definitions=definitions)
221
+ | RESPONSE_REF
222
+ },
223
+ pattern_properties={
224
+ "^([1-5][0-9][0-9]|[1-5]XX)$": typesystem.Reference(
225
+ "Response", definitions=definitions
226
+ )
227
+ | RESPONSE_REF,
228
+ "^x-": typesystem.Any(),
229
+ },
230
+ additional_properties=False,
231
+ )
232
+
233
+ definitions["Response"] = typesystem.Object(
234
+ properties={
235
+ "description": typesystem.String(allow_blank=True),
236
+ "content": typesystem.Object(
237
+ additional_properties=typesystem.Reference(
238
+ "MediaType", definitions=definitions
239
+ )
240
+ ),
241
+ "headers": typesystem.Object(
242
+ additional_properties=typesystem.Reference(
243
+ "Header", definitions=definitions
244
+ )
245
+ ),
246
+ # TODO: Header | ReferenceObject
247
+ # TODO: links
248
+ },
249
+ pattern_properties={"^x-": typesystem.Any()},
250
+ additional_properties=False,
251
+ )
252
+
253
+ definitions["MediaType"] = typesystem.Object(
254
+ properties={
255
+ "schema": JSON_SCHEMA | SCHEMA_REF,
256
+ "example": typesystem.Any(),
257
+ # TODO 'examples', 'encoding'
258
+ },
259
+ pattern_properties={"^x-": typesystem.Any()},
260
+ additional_properties=False,
261
+ )
262
+
263
+ definitions["Header"] = typesystem.Object(
264
+ properties={
265
+ "description": typesystem.Text(),
266
+ "required": typesystem.Boolean(),
267
+ "deprecated": typesystem.Boolean(),
268
+ "allowEmptyValue": typesystem.Boolean(),
269
+ "style": typesystem.Choice(
270
+ choices=[
271
+ "matrix",
272
+ "label",
273
+ "form",
274
+ "simple",
275
+ "spaceDelimited",
276
+ "pipeDelimited",
277
+ "deepObject",
278
+ ]
279
+ ),
280
+ "schema": JSON_SCHEMA | SCHEMA_REF,
281
+ "example": typesystem.Any(),
282
+ # TODO: Other fields
283
+ },
284
+ pattern_properties={"^x-": typesystem.Any()},
285
+ additional_properties=False,
286
+ )
287
+
288
+ definitions["Components"] = typesystem.Object(
289
+ properties={
290
+ "schemas": typesystem.Object(additional_properties=JSON_SCHEMA),
291
+ "responses": typesystem.Object(
292
+ additional_properties=typesystem.Reference(
293
+ "Response", definitions=definitions
294
+ )
295
+ ),
296
+ "parameters": typesystem.Object(
297
+ additional_properties=typesystem.Reference(
298
+ "Parameter", definitions=definitions
299
+ )
300
+ ),
301
+ "requestBodies": typesystem.Object(
302
+ additional_properties=typesystem.Reference(
303
+ "RequestBody", definitions=definitions
304
+ )
305
+ ),
306
+ "securitySchemes": typesystem.Object(
307
+ additional_properties=typesystem.Reference(
308
+ "SecurityScheme", definitions=definitions
309
+ )
310
+ ),
311
+ # TODO: Other fields
312
+ },
313
+ pattern_properties={"^x-": typesystem.Any()},
314
+ additional_properties=False,
315
+ )
316
+
317
+ definitions["Tag"] = typesystem.Object(
318
+ properties={
319
+ "name": typesystem.String(),
320
+ "description": typesystem.Text(allow_blank=True),
321
+ "externalDocs": typesystem.Reference(
322
+ "ExternalDocumentation", definitions=definitions
323
+ ),
324
+ },
325
+ pattern_properties={"^x-": typesystem.Any()},
326
+ additional_properties=False,
327
+ required=["name"],
328
+ )
329
+
330
+ definitions["SecurityRequirement"] = typesystem.Object(
331
+ additional_properties=typesystem.Array(items=typesystem.String())
332
+ )
333
+
334
+ definitions["SecurityScheme"] = typesystem.Object(
335
+ properties={
336
+ "type": typesystem.Choice(
337
+ choices=["apiKey", "http", "oauth2", "openIdConnect"]
338
+ ),
339
+ "description": typesystem.Text(allow_blank=True),
340
+ "name": typesystem.String(),
341
+ "in": typesystem.Choice(choices=["query", "header", "cookie"]),
342
+ "scheme": typesystem.String(),
343
+ "bearerFormat": typesystem.String(),
344
+ "flows": typesystem.Any(), # TODO: OAuthFlows
345
+ "openIdConnectUrl": typesystem.String(format="url"),
346
+ },
347
+ pattern_properties={"^x-": typesystem.Any()},
348
+ additional_properties=False,
349
+ required=["type"],
350
+ )
351
+
352
+
353
+ METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]
354
+
355
+
356
+ def lookup(value, keys, default=None):
357
+ for key in keys:
358
+ try:
359
+ value = value[key]
360
+ except (KeyError, IndexError, TypeError):
361
+ return default
362
+ return value
363
+
364
+
365
+ def _simple_slugify(text):
366
+ if text is None:
367
+ return None
368
+ text = text.lower()
369
+ text = re.sub(r"[^a-z0-9]+", "_", text)
370
+ text = re.sub(r"[_]+", "_", text)
371
+ return text.strip("_")
372
+
373
+
374
+ class OpenAPI:
375
+ def load(self, data):
376
+ title = lookup(data, ["info", "title"])
377
+ description = lookup(data, ["info", "description"])
378
+ version = lookup(data, ["info", "version"])
379
+ base_url = lookup(data, ["servers", 0, "url"])
380
+ schema_definitions = self.get_schema_definitions(data)
381
+ content = self.get_content(data, base_url, schema_definitions)
382
+
383
+ return Document(
384
+ title=title,
385
+ description=description,
386
+ version=version,
387
+ url=base_url,
388
+ content=content,
389
+ )
390
+
391
+ def get_schema_definitions(self, data):
392
+ definitions = typesystem.SchemaDefinitions()
393
+ schemas = lookup(data, ["components", "schemas"], {})
394
+ for key, value in schemas.items():
395
+ ref = f"#/components/schemas/{key}"
396
+ definitions[ref] = typesystem.from_json_schema(
397
+ value, definitions=definitions
398
+ )
399
+ return definitions
400
+
401
+ def get_content(self, data, base_url, schema_definitions):
402
+ """
403
+ Return all the links in the document, laid out by tag and operationId.
404
+ """
405
+ links_by_tag = {}
406
+ links = []
407
+
408
+ for path, path_info in data.get("paths", {}).items():
409
+ operations = {key: path_info[key] for key in path_info if key in METHODS}
410
+ for operation, operation_info in operations.items():
411
+ tag = lookup(operation_info, ["tags", 0])
412
+ link = self.get_link(
413
+ base_url,
414
+ path,
415
+ path_info,
416
+ operation,
417
+ operation_info,
418
+ schema_definitions,
419
+ )
420
+ if link is None:
421
+ continue
422
+
423
+ if tag is None:
424
+ links.append(link)
425
+ elif tag not in links_by_tag:
426
+ links_by_tag[tag] = [link]
427
+ else:
428
+ links_by_tag[tag].append(link)
429
+
430
+ sections = [
431
+ Section(name=_simple_slugify(tag), title=tag.title(), content=links)
432
+ for tag, links in links_by_tag.items()
433
+ ]
434
+ return links + sections
435
+
436
+ def get_link(
437
+ self, base_url, path, path_info, operation, operation_info, schema_definitions
438
+ ):
439
+ """
440
+ Return a single link in the document.
441
+ """
442
+ name = operation_info.get("operationId")
443
+ title = operation_info.get("summary")
444
+ description = operation_info.get("description")
445
+
446
+ if name is None:
447
+ name = _simple_slugify(title)
448
+ if not name:
449
+ return None
450
+
451
+ # Allow path info and operation info to override the base url.
452
+ base_url = lookup(path_info, ["servers", 0, "url"], default=base_url)
453
+ base_url = lookup(operation_info, ["servers", 0, "url"], default=base_url)
454
+
455
+ # Parameters are taken both from the path info, and from the operation.
456
+ parameters = path_info.get("parameters", [])
457
+ parameters += operation_info.get("parameters", [])
458
+
459
+ fields = [
460
+ self.get_field(parameter, schema_definitions) for parameter in parameters
461
+ ]
462
+
463
+ # TODO: Handle media type generically here...
464
+ body_schema = lookup(
465
+ operation_info, ["requestBody", "content", "application/json", "schema"]
466
+ )
467
+
468
+ encoding = None
469
+ if body_schema:
470
+ encoding = "application/json"
471
+ if "$ref" in body_schema:
472
+ ref = body_schema["$ref"]
473
+ schema = schema_definitions.get(ref)
474
+ prefix_length = len("#/components/schemas/")
475
+ field_name = ref[prefix_length:].lower()
476
+ else:
477
+ schema = typesystem.from_json_schema(
478
+ body_schema, definitions=schema_definitions
479
+ )
480
+ field_name = "body"
481
+ field_name = lookup(
482
+ operation_info, ["requestBody", "x-name"], default=field_name
483
+ )
484
+ fields += [Field(name=field_name, location="body", schema=schema)]
485
+
486
+ return Link(
487
+ name=name,
488
+ url=urljoin(base_url, path),
489
+ method=operation,
490
+ title=title,
491
+ description=description,
492
+ fields=fields,
493
+ encoding=encoding,
494
+ )
495
+
496
+ def get_field(self, parameter, schema_definitions):
497
+ """
498
+ Return a single field in a link.
499
+ """
500
+ name = parameter.get("name")
501
+ location = parameter.get("in")
502
+ description = parameter.get("description")
503
+ required = parameter.get("required", False)
504
+ schema = parameter.get("schema")
505
+ example = parameter.get("example")
506
+
507
+ if schema is not None:
508
+ if "$ref" in schema:
509
+ ref = schema["$ref"]
510
+ schema = schema_definitions.get(ref)
511
+ else:
512
+ schema = typesystem.from_json_schema(
513
+ schema, definitions=schema_definitions
514
+ )
515
+
516
+ return Field(
517
+ name=name,
518
+ location=location,
519
+ description=description,
520
+ required=required,
521
+ schema=schema,
522
+ example=example,
523
+ )
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+ import typing
4
+
5
+ import typesystem
6
+
7
+ from arkindex.schema.openapi import OPEN_API, OpenAPI
8
+
9
+ ENCODING_CHOICES = ["json", "yaml", None]
10
+
11
+ # The regexs give us a best-guess for the encoding if none is specified.
12
+ # They check to see if the document looks like it is probably a YAML object or
13
+ # probably a JSON object. It'll typically be best to specify the encoding
14
+ # explicitly, but this should do for convenience.
15
+ INFER_YAML = re.compile(r"^([ \t]*#.*\n|---[ \t]*\n)*\s*[A-Za-z0-9_-]+[ \t]*:")
16
+ INFER_JSON = re.compile(r'^\s*{\s*"[A-Za-z0-9_-]+"\s*:')
17
+
18
+
19
+ def validate(schema: typing.Union[dict, str, bytes], encoding: str = None):
20
+ if not isinstance(schema, (dict, str, bytes)):
21
+ raise ValueError("schema must be either str, bytes, or dict.")
22
+ if encoding not in ENCODING_CHOICES:
23
+ raise ValueError(f"encoding must be one of {ENCODING_CHOICES!r}")
24
+
25
+ if isinstance(schema, bytes):
26
+ schema = schema.decode("utf8", "ignore")
27
+
28
+ if isinstance(schema, str):
29
+ if encoding is None:
30
+ if INFER_YAML.match(schema):
31
+ encoding = "yaml"
32
+ elif INFER_JSON.match(schema):
33
+ encoding = "json"
34
+ else:
35
+ text = "Could not determine if content is JSON or YAML."
36
+ code = "unknown_encoding"
37
+ position = typesystem.Position(line_no=1, column_no=1, char_index=0)
38
+ raise typesystem.ParseError(text=text, code=code, position=position)
39
+
40
+ tokenize = {"yaml": typesystem.tokenize_yaml, "json": typesystem.tokenize_json}[
41
+ encoding
42
+ ]
43
+ token = tokenize(schema)
44
+ value = token.value
45
+ else:
46
+ token = None
47
+ value = schema
48
+
49
+ if token is not None:
50
+ value = typesystem.validate_with_positions(token=token, validator=OpenAPI)
51
+ else:
52
+ value = OPEN_API.validate(value)
53
+
54
+ return OpenAPI().load(value)