schemathesis 3.28.0__py3-none-any.whl → 3.29.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.
- schemathesis/exceptions.py +4 -3
- schemathesis/models.py +2 -10
- schemathesis/runner/impl/core.py +1 -1
- schemathesis/schemas.py +19 -33
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/schemas.py +79 -46
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +2 -0
- schemathesis/specs/openapi/links.py +36 -26
- schemathesis/specs/openapi/references.py +17 -6
- schemathesis/specs/openapi/schemas.py +229 -158
- schemathesis/specs/openapi/security.py +3 -5
- schemathesis/specs/openapi/stateful/__init__.py +1 -2
- schemathesis/specs/openapi/stateful/links.py +5 -5
- schemathesis/stateful/__init__.py +10 -2
- schemathesis/transports/content_types.py +2 -0
- {schemathesis-3.28.0.dist-info → schemathesis-3.29.0.dist-info}/METADATA +6 -2
- {schemathesis-3.28.0.dist-info → schemathesis-3.29.0.dist-info}/RECORD +21 -19
- {schemathesis-3.28.0.dist-info → schemathesis-3.29.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.28.0.dist-info → schemathesis-3.29.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.28.0.dist-info → schemathesis-3.29.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,9 +15,12 @@ from typing import (
|
|
|
15
15
|
ClassVar,
|
|
16
16
|
Generator,
|
|
17
17
|
Iterable,
|
|
18
|
+
Iterator,
|
|
19
|
+
Mapping,
|
|
18
20
|
NoReturn,
|
|
19
21
|
Sequence,
|
|
20
22
|
TypeVar,
|
|
23
|
+
cast,
|
|
21
24
|
)
|
|
22
25
|
from urllib.parse import urlsplit
|
|
23
26
|
|
|
@@ -28,33 +31,34 @@ from requests.structures import CaseInsensitiveDict
|
|
|
28
31
|
|
|
29
32
|
from ... import experimental, failures
|
|
30
33
|
from ..._compat import MultipleFailures
|
|
31
|
-
from ..._override import CaseOverride,
|
|
34
|
+
from ..._override import CaseOverride, check_no_override_mark, set_override_mark
|
|
32
35
|
from ...auths import AuthStorage
|
|
33
|
-
from ...generation import DataGenerationMethod, GenerationConfig
|
|
34
36
|
from ...constants import HTTP_METHODS, NOT_SET
|
|
35
37
|
from ...exceptions import (
|
|
36
38
|
InternalError,
|
|
39
|
+
OperationNotFound,
|
|
37
40
|
OperationSchemaError,
|
|
41
|
+
SchemaError,
|
|
42
|
+
SchemaErrorType,
|
|
38
43
|
UsageError,
|
|
39
44
|
get_missing_content_type_error,
|
|
40
45
|
get_response_parsing_error,
|
|
41
46
|
get_schema_validation_error,
|
|
42
|
-
SchemaError,
|
|
43
|
-
SchemaErrorType,
|
|
44
|
-
OperationNotFound,
|
|
45
47
|
)
|
|
48
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
46
49
|
from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, should_skip_operation
|
|
47
50
|
from ...internal.copy import fast_deepcopy
|
|
48
51
|
from ...internal.jsonschema import traverse_schema
|
|
49
52
|
from ...internal.result import Err, Ok, Result
|
|
50
53
|
from ...models import APIOperation, Case, OperationDefinition
|
|
51
|
-
from ...schemas import
|
|
54
|
+
from ...schemas import APIOperationMap, BaseSchema
|
|
52
55
|
from ...stateful import Stateful, StatefulTest
|
|
53
56
|
from ...stateful.state_machine import APIStateMachine
|
|
54
57
|
from ...transports.content_types import is_json_media_type, parse_content_type
|
|
55
58
|
from ...transports.responses import get_json
|
|
56
|
-
from ...types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
|
|
59
|
+
from ...types import Body, Cookies, FormData, GenericTest, Headers, NotSet, PathParameters, Query
|
|
57
60
|
from . import links, serialization
|
|
61
|
+
from ._cache import OperationCache
|
|
58
62
|
from ._hypothesis import get_case_strategy
|
|
59
63
|
from .converter import to_json_schema, to_json_schema_recursive
|
|
60
64
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
|
@@ -74,7 +78,7 @@ from .parameters import (
|
|
|
74
78
|
OpenAPI30Parameter,
|
|
75
79
|
OpenAPIParameter,
|
|
76
80
|
)
|
|
77
|
-
from .references import RECURSION_DEPTH_LIMIT, ConvertingResolver, InliningResolver, resolve_pointer
|
|
81
|
+
from .references import RECURSION_DEPTH_LIMIT, UNRESOLVABLE, ConvertingResolver, InliningResolver, resolve_pointer
|
|
78
82
|
from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
|
|
79
83
|
from .stateful import create_state_machine
|
|
80
84
|
|
|
@@ -91,7 +95,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
91
95
|
links_field: ClassVar[str] = ""
|
|
92
96
|
header_required_field: ClassVar[str] = ""
|
|
93
97
|
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
|
94
|
-
|
|
98
|
+
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
|
95
99
|
_inline_reference_cache: dict[str, Any] = field(default_factory=dict)
|
|
96
100
|
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
|
97
101
|
# excessive resolving
|
|
@@ -114,13 +118,24 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
114
118
|
info = self.raw_schema["info"]
|
|
115
119
|
return f"<{self.__class__.__name__} for {info['title']} {info['version']}>"
|
|
116
120
|
|
|
117
|
-
def
|
|
118
|
-
self,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
def __iter__(self) -> Iterator[str]:
|
|
122
|
+
return iter(self.raw_schema.get("paths", {}))
|
|
123
|
+
|
|
124
|
+
def _get_operation_map(self, path: str) -> APIOperationMap:
|
|
125
|
+
cache = self._operation_cache
|
|
126
|
+
map = cache.get_map(path)
|
|
127
|
+
if map is not None:
|
|
128
|
+
return map
|
|
129
|
+
path_item = self.raw_schema.get("paths", {})[path]
|
|
130
|
+
scope, path_item = self._resolve_path_item(path_item)
|
|
131
|
+
self.dispatch_hook("before_process_path", HookContext(), path, path_item)
|
|
132
|
+
map = APIOperationMap(self, {})
|
|
133
|
+
map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
|
|
134
|
+
cache.insert_map(path, map)
|
|
135
|
+
return map
|
|
121
136
|
|
|
122
137
|
def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
|
|
123
|
-
matches = get_close_matches(item, list(self
|
|
138
|
+
matches = get_close_matches(item, list(self))
|
|
124
139
|
self._on_missing_operation(item, exc, matches)
|
|
125
140
|
|
|
126
141
|
def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
|
|
@@ -143,19 +158,20 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
143
158
|
paths = self.raw_schema["paths"]
|
|
144
159
|
except KeyError:
|
|
145
160
|
return
|
|
161
|
+
get_full_path = self.get_full_path
|
|
162
|
+
endpoint = self.endpoint
|
|
146
163
|
resolve = self.resolver.resolve
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
should_skip = self._should_skip
|
|
165
|
+
for path, path_item in paths.items():
|
|
166
|
+
full_path = get_full_path(path)
|
|
167
|
+
if should_skip_endpoint(full_path, endpoint):
|
|
150
168
|
continue
|
|
151
169
|
try:
|
|
152
|
-
if "$ref" in
|
|
153
|
-
_,
|
|
154
|
-
else:
|
|
155
|
-
resolved_methods = methods
|
|
170
|
+
if "$ref" in path_item:
|
|
171
|
+
_, path_item = resolve(path_item["$ref"])
|
|
156
172
|
# Straightforward iteration is faster than converting to a set & calculating length.
|
|
157
|
-
for method, definition in
|
|
158
|
-
if
|
|
173
|
+
for method, definition in path_item.items():
|
|
174
|
+
if should_skip(method, definition):
|
|
159
175
|
continue
|
|
160
176
|
yield definition
|
|
161
177
|
except SCHEMA_PARSING_ERRORS:
|
|
@@ -173,11 +189,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
173
189
|
@property
|
|
174
190
|
def links_count(self) -> int:
|
|
175
191
|
total = 0
|
|
192
|
+
resolve = self.resolver.resolve
|
|
193
|
+
links_field = self.links_field
|
|
176
194
|
for definition in self._operation_iter():
|
|
177
195
|
for response in definition.get("responses", {}).values():
|
|
178
196
|
if "$ref" in response:
|
|
179
|
-
_, response =
|
|
180
|
-
defined_links = response.get(
|
|
197
|
+
_, response = resolve(response["$ref"])
|
|
198
|
+
defined_links = response.get(links_field)
|
|
181
199
|
if defined_links is not None:
|
|
182
200
|
total += len(defined_links)
|
|
183
201
|
return total
|
|
@@ -202,6 +220,24 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
202
220
|
|
|
203
221
|
return _add_override
|
|
204
222
|
|
|
223
|
+
def _resolve_until_no_references(self, value: dict[str, Any]) -> dict[str, Any]:
|
|
224
|
+
while "$ref" in value:
|
|
225
|
+
_, value = self.resolver.resolve(value["$ref"])
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
def _resolve_shared_parameters(self, path_item: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
229
|
+
return self.resolver.resolve_all(path_item.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
|
|
230
|
+
|
|
231
|
+
def _resolve_operation(self, operation: dict[str, Any]) -> dict[str, Any]:
|
|
232
|
+
return self.resolver.resolve_all(operation, RECURSION_DEPTH_LIMIT - 8)
|
|
233
|
+
|
|
234
|
+
def _collect_operation_parameters(
|
|
235
|
+
self, path_item: Mapping[str, Any], operation: dict[str, Any]
|
|
236
|
+
) -> list[OpenAPIParameter]:
|
|
237
|
+
shared_parameters = self._resolve_shared_parameters(path_item)
|
|
238
|
+
parameters = operation.get("parameters", ())
|
|
239
|
+
return self.collect_parameters(itertools.chain(parameters, shared_parameters), operation)
|
|
240
|
+
|
|
205
241
|
def get_all_operations(
|
|
206
242
|
self, hooks: HookDispatcher | None = None
|
|
207
243
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
@@ -231,41 +267,40 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
231
267
|
self._raise_invalid_schema(exc)
|
|
232
268
|
|
|
233
269
|
context = HookContext()
|
|
234
|
-
|
|
270
|
+
# Optimization: local variables are faster than attribute access
|
|
271
|
+
get_full_path = self.get_full_path
|
|
272
|
+
endpoint = self.endpoint
|
|
273
|
+
dispatch_hook = self.dispatch_hook
|
|
274
|
+
resolve_path_item = self._resolve_path_item
|
|
275
|
+
resolve_shared_parameters = self._resolve_shared_parameters
|
|
276
|
+
resolve_operation = self._resolve_operation
|
|
277
|
+
should_skip = self._should_skip
|
|
278
|
+
collect_parameters = self.collect_parameters
|
|
279
|
+
make_operation = self.make_operation
|
|
280
|
+
hooks = self.hooks
|
|
281
|
+
for path, path_item in paths.items():
|
|
235
282
|
method = None
|
|
236
283
|
try:
|
|
237
|
-
full_path =
|
|
238
|
-
if should_skip_endpoint(full_path,
|
|
284
|
+
full_path = get_full_path(path) # Should be available for later use
|
|
285
|
+
if should_skip_endpoint(full_path, endpoint):
|
|
239
286
|
continue
|
|
240
|
-
|
|
241
|
-
scope,
|
|
242
|
-
|
|
243
|
-
for method,
|
|
287
|
+
dispatch_hook("before_process_path", context, path, path_item)
|
|
288
|
+
scope, path_item = resolve_path_item(path_item)
|
|
289
|
+
shared_parameters = resolve_shared_parameters(path_item)
|
|
290
|
+
for method, entry in path_item.items():
|
|
291
|
+
if method not in HTTP_METHODS:
|
|
292
|
+
continue
|
|
244
293
|
try:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
self.resolver.push_scope(scope)
|
|
248
|
-
try:
|
|
249
|
-
resolved_definition = self.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8)
|
|
250
|
-
finally:
|
|
251
|
-
self.resolver.pop_scope()
|
|
252
|
-
# Only method definitions are parsed
|
|
253
|
-
if self._should_skip(method, resolved_definition):
|
|
294
|
+
resolved = resolve_operation(entry)
|
|
295
|
+
if should_skip(method, resolved):
|
|
254
296
|
continue
|
|
255
|
-
parameters =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
259
|
-
# To prevent recursion errors we need to pass not resolved schema as well
|
|
260
|
-
# It could be used for response validation
|
|
261
|
-
raw_definition = OperationDefinition(
|
|
262
|
-
raw_methods[method], resolved_definition, scope, parameters
|
|
263
|
-
)
|
|
264
|
-
operation = self.make_operation(path, method, parameters, raw_definition)
|
|
297
|
+
parameters = resolved.get("parameters", ())
|
|
298
|
+
parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
|
|
299
|
+
operation = make_operation(path, method, parameters, entry, resolved, scope)
|
|
265
300
|
context = HookContext(operation=operation)
|
|
266
301
|
if (
|
|
267
302
|
should_skip_operation(GLOBAL_HOOK_DISPATCHER, context)
|
|
268
|
-
or should_skip_operation(
|
|
303
|
+
or should_skip_operation(hooks, context)
|
|
269
304
|
or (hooks and should_skip_operation(hooks, context))
|
|
270
305
|
):
|
|
271
306
|
continue
|
|
@@ -320,20 +355,22 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
320
355
|
"""
|
|
321
356
|
raise NotImplementedError
|
|
322
357
|
|
|
323
|
-
def
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
358
|
+
def _resolve_path_item(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
359
|
+
# The path item could be behind a reference
|
|
360
|
+
# In this case, we need to resolve it to get the proper scope for reference inside the item.
|
|
361
|
+
# It is mostly for validating responses.
|
|
327
362
|
if "$ref" in methods:
|
|
328
|
-
return
|
|
329
|
-
return self.resolver.resolution_scope,
|
|
363
|
+
return self.resolver.resolve(methods["$ref"])
|
|
364
|
+
return self.resolver.resolution_scope, methods
|
|
330
365
|
|
|
331
366
|
def make_operation(
|
|
332
367
|
self,
|
|
333
368
|
path: str,
|
|
334
369
|
method: str,
|
|
335
370
|
parameters: list[OpenAPIParameter],
|
|
336
|
-
|
|
371
|
+
raw: dict[str, Any],
|
|
372
|
+
resolved: dict[str, Any],
|
|
373
|
+
scope: str,
|
|
337
374
|
) -> APIOperation:
|
|
338
375
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
|
339
376
|
__tracebackhide__ = True
|
|
@@ -341,7 +378,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
341
378
|
operation: APIOperation[OpenAPIParameter, Case] = APIOperation(
|
|
342
379
|
path=path,
|
|
343
380
|
method=method,
|
|
344
|
-
definition=
|
|
381
|
+
definition=OperationDefinition(raw, resolved, scope),
|
|
345
382
|
base_url=base_url,
|
|
346
383
|
app=self.app,
|
|
347
384
|
schema=self,
|
|
@@ -376,49 +413,77 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
376
413
|
|
|
377
414
|
def get_operation_by_id(self, operation_id: str) -> APIOperation:
|
|
378
415
|
"""Get an `APIOperation` instance by its `operationId`."""
|
|
379
|
-
|
|
380
|
-
|
|
416
|
+
cache = self._operation_cache
|
|
417
|
+
cached = cache.get_operation_by_id(operation_id)
|
|
418
|
+
if cached is not None:
|
|
419
|
+
return cached
|
|
420
|
+
# Operation has not been accessed yet, need to populate the cache
|
|
421
|
+
if not cache.has_ids_to_definitions:
|
|
422
|
+
self._populate_operation_id_cache(cache)
|
|
381
423
|
try:
|
|
382
|
-
|
|
424
|
+
entry = cache.get_definition_by_id(operation_id)
|
|
383
425
|
except KeyError as exc:
|
|
384
|
-
matches = get_close_matches(operation_id,
|
|
426
|
+
matches = get_close_matches(operation_id, cache.known_operation_ids)
|
|
385
427
|
self._on_missing_operation(operation_id, exc, matches)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
# It could've been already accessed in a different place
|
|
429
|
+
traversal_key = (entry.scope, entry.path, entry.method)
|
|
430
|
+
instance = cache.get_operation_by_traversal_key(traversal_key)
|
|
431
|
+
if instance is not None:
|
|
432
|
+
return instance
|
|
433
|
+
resolved = self._resolve_operation(entry.operation)
|
|
434
|
+
parameters = self._collect_operation_parameters(entry.path_item, resolved)
|
|
435
|
+
initialized = self.make_operation(entry.path, entry.method, parameters, entry.operation, resolved, entry.scope)
|
|
436
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=operation_id)
|
|
437
|
+
return initialized
|
|
438
|
+
|
|
439
|
+
def _populate_operation_id_cache(self, cache: OperationCache) -> None:
|
|
440
|
+
"""Collect all operation IDs from the schema."""
|
|
441
|
+
resolve = self.resolver.resolve
|
|
442
|
+
default_scope = self.resolver.resolution_scope
|
|
443
|
+
for path, path_item in self.raw_schema.get("paths", {}).items():
|
|
444
|
+
# If the path is behind a reference we have to keep the scope
|
|
445
|
+
# The scope is used to resolve nested components later on
|
|
446
|
+
if "$ref" in path_item:
|
|
447
|
+
scope, path_item = resolve(path_item["$ref"])
|
|
448
|
+
else:
|
|
449
|
+
scope = default_scope
|
|
450
|
+
for key, entry in path_item.items():
|
|
451
|
+
if key not in HTTP_METHODS:
|
|
393
452
|
continue
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
yield resolved_definition["operationId"], self.make_operation(path, method, parameters, raw_definition)
|
|
453
|
+
if "operationId" in entry:
|
|
454
|
+
cache.insert_definition_by_id(
|
|
455
|
+
entry["operationId"],
|
|
456
|
+
path=path,
|
|
457
|
+
method=key,
|
|
458
|
+
scope=scope,
|
|
459
|
+
path_item=path_item,
|
|
460
|
+
operation=entry,
|
|
461
|
+
)
|
|
404
462
|
|
|
405
463
|
def get_operation_by_reference(self, reference: str) -> APIOperation:
|
|
406
464
|
"""Get local or external `APIOperation` instance by reference.
|
|
407
465
|
|
|
408
466
|
Reference example: #/paths/~1users~1{user_id}/patch
|
|
409
467
|
"""
|
|
410
|
-
|
|
468
|
+
cache = self._operation_cache
|
|
469
|
+
cached = cache.get_operation_by_reference(reference)
|
|
470
|
+
if cached is not None:
|
|
471
|
+
return cached
|
|
472
|
+
scope, operation = self.resolver.resolve(reference)
|
|
411
473
|
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
|
412
474
|
path = path.replace("~1", "/").replace("~0", "~")
|
|
413
|
-
|
|
475
|
+
# Check the traversal cache as it could've been populated in other places
|
|
476
|
+
traversal_key = (self.resolver.resolution_scope, path, method)
|
|
477
|
+
cached = cache.get_operation_by_traversal_key(traversal_key)
|
|
478
|
+
if cached is not None:
|
|
479
|
+
return cached
|
|
480
|
+
resolved = self._resolve_operation(operation)
|
|
414
481
|
parent_ref, _ = reference.rsplit("/", maxsplit=1)
|
|
415
|
-
_,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
raw_definition = OperationDefinition(data, resolved_definition, scope, parameters)
|
|
421
|
-
return self.make_operation(path, method, parameters, raw_definition)
|
|
482
|
+
_, path_item = self.resolver.resolve(parent_ref)
|
|
483
|
+
parameters = self._collect_operation_parameters(path_item, resolved)
|
|
484
|
+
initialized = self.make_operation(path, method, parameters, operation, resolved, scope)
|
|
485
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, reference=reference)
|
|
486
|
+
return initialized
|
|
422
487
|
|
|
423
488
|
def get_case_strategy(
|
|
424
489
|
self,
|
|
@@ -439,7 +504,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
439
504
|
)
|
|
440
505
|
|
|
441
506
|
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
|
442
|
-
definitions = [item for item in operation.
|
|
507
|
+
definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
|
|
443
508
|
security_parameters = self.security.get_security_definitions_as_parameters(
|
|
444
509
|
self.raw_schema, operation, self.resolver, location
|
|
445
510
|
)
|
|
@@ -455,7 +520,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
455
520
|
|
|
456
521
|
def _get_response_definitions(self, operation: APIOperation, response: GenericResponse) -> dict[str, Any] | None:
|
|
457
522
|
try:
|
|
458
|
-
responses = operation.definition.
|
|
523
|
+
responses = operation.definition.raw["responses"]
|
|
459
524
|
except KeyError as exc:
|
|
460
525
|
# Possible to get if `validate_schema=False` is passed during schema creation
|
|
461
526
|
path = operation.path
|
|
@@ -463,9 +528,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
463
528
|
self._raise_invalid_schema(exc, full_path, path, operation.method)
|
|
464
529
|
status_code = str(response.status_code)
|
|
465
530
|
if status_code in responses:
|
|
466
|
-
|
|
531
|
+
_, response = self.resolver.resolve_in_scope(responses[status_code], operation.definition.scope)
|
|
532
|
+
return response
|
|
467
533
|
if "default" in responses:
|
|
468
|
-
|
|
534
|
+
_, response = self.resolver.resolve_in_scope(responses["default"], operation.definition.scope)
|
|
535
|
+
return response
|
|
469
536
|
return None
|
|
470
537
|
|
|
471
538
|
def get_headers(self, operation: APIOperation, response: GenericResponse) -> dict[str, dict[str, Any]] | None:
|
|
@@ -518,46 +585,16 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
518
585
|
"""
|
|
519
586
|
if parameters is None and request_body is None:
|
|
520
587
|
raise ValueError("You need to provide `parameters` or `request_body`.")
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
found = True
|
|
532
|
-
links.add_link(
|
|
533
|
-
responses=definition["responses"],
|
|
534
|
-
links_field=self.links_field,
|
|
535
|
-
parameters=parameters,
|
|
536
|
-
request_body=request_body,
|
|
537
|
-
status_code=status_code,
|
|
538
|
-
target=target,
|
|
539
|
-
name=name,
|
|
540
|
-
)
|
|
541
|
-
# If methods are behind a reference, then on the next resolving they will miss the new link
|
|
542
|
-
# Therefore we need to modify it this way
|
|
543
|
-
self.raw_schema["paths"][operation][method] = definition
|
|
544
|
-
# The reference should be removed completely, otherwise new keys in this dictionary will be ignored
|
|
545
|
-
# due to the `$ref` keyword behavior
|
|
546
|
-
self.raw_schema["paths"][operation].pop("$ref", None)
|
|
547
|
-
if found:
|
|
548
|
-
return
|
|
549
|
-
name = f"{source.method.upper()} {source.path}"
|
|
550
|
-
# Use a name without basePath, as the user doesn't use it.
|
|
551
|
-
# E.g. `source=schema["/users/"]["POST"]` without a prefix
|
|
552
|
-
message = f"No such API operation: `{name}`."
|
|
553
|
-
possibilities = [
|
|
554
|
-
f"{op.ok().method.upper()} {op.ok().path}" for op in self.get_all_operations() if isinstance(op, Ok)
|
|
555
|
-
]
|
|
556
|
-
matches = get_close_matches(name, possibilities)
|
|
557
|
-
if matches:
|
|
558
|
-
message += f" Did you mean `{matches[0]}`?"
|
|
559
|
-
message += " Check if the requested API operation passes the filters in the schema."
|
|
560
|
-
raise ValueError(message)
|
|
588
|
+
links.add_link(
|
|
589
|
+
resolver=self.resolver,
|
|
590
|
+
responses=self[source.path][source.method].definition.raw["responses"],
|
|
591
|
+
links_field=self.links_field,
|
|
592
|
+
parameters=parameters,
|
|
593
|
+
request_body=request_body,
|
|
594
|
+
status_code=status_code,
|
|
595
|
+
target=target,
|
|
596
|
+
name=name,
|
|
597
|
+
)
|
|
561
598
|
|
|
562
599
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
|
563
600
|
result: dict[str, dict[str, Any]] = defaultdict(dict)
|
|
@@ -566,7 +603,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
566
603
|
return result
|
|
567
604
|
|
|
568
605
|
def get_tags(self, operation: APIOperation) -> list[str] | None:
|
|
569
|
-
return operation.definition.
|
|
606
|
+
return operation.definition.raw.get("tags")
|
|
570
607
|
|
|
571
608
|
def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
|
|
572
609
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
|
@@ -766,30 +803,58 @@ def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[
|
|
|
766
803
|
yield
|
|
767
804
|
|
|
768
805
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
) -> dict[str, APIOperationMap]:
|
|
772
|
-
output: dict[str, APIOperationMap] = {}
|
|
773
|
-
for result in operations:
|
|
774
|
-
if isinstance(result, Ok):
|
|
775
|
-
operation = result.ok()
|
|
776
|
-
output.setdefault(operation.path, APIOperationMap(MethodMap()))
|
|
777
|
-
output[operation.path][operation.method] = operation
|
|
778
|
-
return output
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
class MethodMap(CaseInsensitiveDict):
|
|
806
|
+
@dataclass
|
|
807
|
+
class MethodMap(Mapping):
|
|
782
808
|
"""Container for accessing API operations.
|
|
783
809
|
|
|
784
810
|
Provides a more specific error message if API operation is not found.
|
|
785
811
|
"""
|
|
786
812
|
|
|
813
|
+
_parent: APIOperationMap
|
|
814
|
+
# Reference resolution scope
|
|
815
|
+
_scope: str
|
|
816
|
+
# Methods are stored for this path
|
|
817
|
+
_path: str
|
|
818
|
+
# Storage for definitions
|
|
819
|
+
_path_item: CaseInsensitiveDict
|
|
820
|
+
|
|
821
|
+
__slots__ = ("_parent", "_scope", "_path", "_path_item")
|
|
822
|
+
|
|
823
|
+
def __len__(self) -> int:
|
|
824
|
+
return len(self._path_item)
|
|
825
|
+
|
|
826
|
+
def __iter__(self) -> Iterator[str]:
|
|
827
|
+
return iter(self._path_item)
|
|
828
|
+
|
|
829
|
+
def _init_operation(self, method: str) -> APIOperation:
|
|
830
|
+
method = method.lower()
|
|
831
|
+
operation = self._path_item[method]
|
|
832
|
+
schema = cast(BaseOpenAPISchema, self._parent._schema)
|
|
833
|
+
cache = schema._operation_cache
|
|
834
|
+
path = self._path
|
|
835
|
+
scope = self._scope
|
|
836
|
+
traversal_key = (scope, path, method)
|
|
837
|
+
cached = cache.get_operation_by_traversal_key(traversal_key)
|
|
838
|
+
if cached is not None:
|
|
839
|
+
return cached
|
|
840
|
+
schema.resolver.push_scope(scope)
|
|
841
|
+
try:
|
|
842
|
+
resolved = schema._resolve_operation(operation)
|
|
843
|
+
finally:
|
|
844
|
+
schema.resolver.pop_scope()
|
|
845
|
+
parameters = schema._collect_operation_parameters(self._path_item, resolved)
|
|
846
|
+
initialized = schema.make_operation(path, method, parameters, operation, resolved, scope)
|
|
847
|
+
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=resolved.get("operationId"))
|
|
848
|
+
return initialized
|
|
849
|
+
|
|
787
850
|
def __getitem__(self, item: str) -> APIOperation:
|
|
788
851
|
try:
|
|
789
|
-
return
|
|
852
|
+
return self._init_operation(item)
|
|
790
853
|
except KeyError as exc:
|
|
791
854
|
available_methods = ", ".join(map(str.upper, self))
|
|
792
|
-
message = f"Method `{item}` not found.
|
|
855
|
+
message = f"Method `{item.upper()}` not found."
|
|
856
|
+
if available_methods:
|
|
857
|
+
message += f" Available methods: {available_methods}"
|
|
793
858
|
raise KeyError(message) from exc
|
|
794
859
|
|
|
795
860
|
|
|
@@ -864,7 +929,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
864
929
|
return get_strategies_from_examples(operation, self.examples_field)
|
|
865
930
|
|
|
866
931
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
867
|
-
scopes, definition = self.resolver.resolve_in_scope(
|
|
932
|
+
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
|
868
933
|
schema = definition.get("schema")
|
|
869
934
|
if not schema:
|
|
870
935
|
return scopes, None
|
|
@@ -903,7 +968,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
903
968
|
else:
|
|
904
969
|
files.append((name, file_value))
|
|
905
970
|
|
|
906
|
-
for parameter in operation.
|
|
971
|
+
for parameter in operation.body:
|
|
907
972
|
if isinstance(parameter, OpenAPI20CompositeBody):
|
|
908
973
|
for form_parameter in parameter.definition:
|
|
909
974
|
name = form_parameter.name
|
|
@@ -918,7 +983,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
918
983
|
return files or None, data or None
|
|
919
984
|
|
|
920
985
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
|
921
|
-
return self._get_consumes_for_operation(operation.definition.
|
|
986
|
+
return self._get_consumes_for_operation(operation.definition.raw)
|
|
922
987
|
|
|
923
988
|
def make_case(
|
|
924
989
|
self,
|
|
@@ -1024,7 +1089,7 @@ class OpenApi30(SwaggerV20):
|
|
|
1024
1089
|
return collected
|
|
1025
1090
|
|
|
1026
1091
|
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
1027
|
-
scopes, definition = self.resolver.resolve_in_scope(
|
|
1092
|
+
scopes, definition = self.resolver.resolve_in_scope(definition, scope)
|
|
1028
1093
|
options = iter(definition.get("content", {}).values())
|
|
1029
1094
|
option = next(options, None)
|
|
1030
1095
|
# "schema" is an optional key in the `MediaType` object
|
|
@@ -1048,7 +1113,8 @@ class OpenApi30(SwaggerV20):
|
|
|
1048
1113
|
return serialization.serialize_openapi3_parameters(definitions)
|
|
1049
1114
|
|
|
1050
1115
|
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
|
1051
|
-
|
|
1116
|
+
request_body = self._resolve_until_no_references(operation.definition.raw["requestBody"])
|
|
1117
|
+
return list(request_body["content"])
|
|
1052
1118
|
|
|
1053
1119
|
def prepare_multipart(
|
|
1054
1120
|
self, form_data: FormData, operation: APIOperation
|
|
@@ -1060,17 +1126,22 @@ class OpenApi30(SwaggerV20):
|
|
|
1060
1126
|
:return: `files` and `data` values for `requests.request`.
|
|
1061
1127
|
"""
|
|
1062
1128
|
files = []
|
|
1063
|
-
|
|
1129
|
+
definition = operation.definition.raw
|
|
1130
|
+
if "$ref" in definition["requestBody"]:
|
|
1131
|
+
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
|
1132
|
+
else:
|
|
1133
|
+
body = definition["requestBody"]
|
|
1134
|
+
content = body["content"]
|
|
1064
1135
|
# Open API 3.0 requires media types to be present. We can get here only if the schema defines
|
|
1065
1136
|
# the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
|
|
1066
1137
|
for media_type, entry in content.items():
|
|
1067
1138
|
main, sub = parse_content_type(media_type)
|
|
1068
1139
|
if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
|
|
1069
|
-
schema = entry
|
|
1140
|
+
schema = entry.get("schema")
|
|
1070
1141
|
break
|
|
1071
1142
|
else:
|
|
1072
1143
|
raise InternalError("No 'multipart/form-data' media type found in the schema")
|
|
1073
|
-
for name, property_schema in schema.get("properties", {}).items():
|
|
1144
|
+
for name, property_schema in (schema or {}).get("properties", {}).items():
|
|
1074
1145
|
if name in form_data:
|
|
1075
1146
|
if isinstance(form_data[name], list):
|
|
1076
1147
|
files.extend([(name, item) for item in form_data[name]])
|
|
@@ -125,12 +125,10 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
|
125
125
|
"""In Open API 3 security definitions are located in ``components`` and may have references inside."""
|
|
126
126
|
components = schema.get("components", {})
|
|
127
127
|
security_schemes = components.get("securitySchemes", {})
|
|
128
|
+
resolve = resolver.resolve
|
|
128
129
|
if "$ref" in security_schemes:
|
|
129
|
-
return
|
|
130
|
-
return {
|
|
131
|
-
key: resolver.resolve(value["$ref"])[1] if "$ref" in value else value
|
|
132
|
-
for key, value in security_schemes.items()
|
|
133
|
-
}
|
|
130
|
+
return resolve(security_schemes["$ref"])[1]
|
|
131
|
+
return {key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()}
|
|
134
132
|
|
|
135
133
|
def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
136
134
|
schema = make_auth_header_schema(definition)
|
|
@@ -10,8 +10,7 @@ from ....internal.result import Ok
|
|
|
10
10
|
from ....stateful.state_machine import APIStateMachine, Direction, StepResult
|
|
11
11
|
from ....utils import combine_strategies
|
|
12
12
|
from .. import expressions
|
|
13
|
-
from . import
|
|
14
|
-
from .links import APIOperationConnections, Connection, _convert_strategy, apply, make_response_filter
|
|
13
|
+
from .links import APIOperationConnections, Connection, apply
|
|
15
14
|
|
|
16
15
|
if TYPE_CHECKING:
|
|
17
16
|
from ....models import APIOperation, Case
|