schemathesis 3.21.2__py3-none-any.whl → 3.22.1__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/__init__.py +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -14,17 +14,11 @@ from typing import (
|
|
|
14
14
|
Any,
|
|
15
15
|
Callable,
|
|
16
16
|
ClassVar,
|
|
17
|
-
Dict,
|
|
18
17
|
Generator,
|
|
19
18
|
Iterable,
|
|
20
|
-
List,
|
|
21
19
|
NoReturn,
|
|
22
|
-
Optional,
|
|
23
20
|
Sequence,
|
|
24
|
-
Tuple,
|
|
25
|
-
Type,
|
|
26
21
|
TypeVar,
|
|
27
|
-
Union,
|
|
28
22
|
)
|
|
29
23
|
from urllib.parse import urlsplit
|
|
30
24
|
|
|
@@ -37,7 +31,7 @@ from requests.structures import CaseInsensitiveDict
|
|
|
37
31
|
from ... import experimental, failures
|
|
38
32
|
from ..._compat import MultipleFailures
|
|
39
33
|
from ...auths import AuthStorage
|
|
40
|
-
from ...generation import DataGenerationMethod
|
|
34
|
+
from ...generation import DataGenerationMethod, GenerationConfig
|
|
41
35
|
from ...constants import HTTP_METHODS, NOT_SET
|
|
42
36
|
from ...exceptions import (
|
|
43
37
|
OperationSchemaError,
|
|
@@ -93,19 +87,19 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
93
87
|
links_field: ClassVar[str] = ""
|
|
94
88
|
header_required_field: ClassVar[str] = ""
|
|
95
89
|
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
|
96
|
-
_operations_by_id:
|
|
97
|
-
_inline_reference_cache:
|
|
90
|
+
_operations_by_id: dict[str, APIOperation] = field(init=False)
|
|
91
|
+
_inline_reference_cache: dict[str, Any] = field(default_factory=dict)
|
|
98
92
|
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
|
99
93
|
# excessive resolving
|
|
100
94
|
_inline_reference_cache_lock: RLock = field(default_factory=RLock)
|
|
101
|
-
component_locations: ClassVar[
|
|
95
|
+
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = ()
|
|
102
96
|
|
|
103
97
|
@property
|
|
104
98
|
def spec_version(self) -> str:
|
|
105
99
|
raise NotImplementedError
|
|
106
100
|
|
|
107
101
|
def get_stateful_tests(
|
|
108
|
-
self, response: GenericResponse, operation: APIOperation, stateful:
|
|
102
|
+
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
|
109
103
|
) -> Sequence[StatefulTest]:
|
|
110
104
|
if stateful == Stateful.links:
|
|
111
105
|
return links.get_links(response, operation, field=self.links_field)
|
|
@@ -115,7 +109,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
115
109
|
info = self.raw_schema["info"]
|
|
116
110
|
return f"{self.__class__.__name__} for {info['title']} ({info['version']})"
|
|
117
111
|
|
|
118
|
-
def _should_skip(self, method: str, definition:
|
|
112
|
+
def _should_skip(self, method: str, definition: dict[str, Any]) -> bool:
|
|
119
113
|
return (
|
|
120
114
|
method not in HTTP_METHODS
|
|
121
115
|
or should_skip_method(method, self.method)
|
|
@@ -124,13 +118,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
124
118
|
or should_skip_by_operation_id(definition.get("operationId"), self.operation_id)
|
|
125
119
|
)
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
def operations_count(self) -> int:
|
|
121
|
+
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
|
129
122
|
try:
|
|
130
123
|
paths = self.raw_schema["paths"]
|
|
131
124
|
except KeyError:
|
|
132
|
-
return
|
|
133
|
-
total = 0
|
|
125
|
+
return
|
|
134
126
|
resolve = self.resolver.resolve
|
|
135
127
|
for path, methods in paths.items():
|
|
136
128
|
full_path = self.get_full_path(path)
|
|
@@ -145,14 +137,33 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
145
137
|
for method, definition in resolved_methods.items():
|
|
146
138
|
if self._should_skip(method, definition):
|
|
147
139
|
continue
|
|
148
|
-
|
|
140
|
+
yield definition
|
|
149
141
|
except SCHEMA_PARSING_ERRORS:
|
|
150
142
|
# Ignore errors
|
|
151
143
|
continue
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def operations_count(self) -> int:
|
|
147
|
+
total = 0
|
|
148
|
+
# Do not build a list from it
|
|
149
|
+
for _ in self._operation_iter():
|
|
150
|
+
total += 1
|
|
151
|
+
return total
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def links_count(self) -> int:
|
|
155
|
+
total = 0
|
|
156
|
+
for definition in self._operation_iter():
|
|
157
|
+
for response in definition.get("responses", {}).values():
|
|
158
|
+
if "$ref" in response:
|
|
159
|
+
_, response = self.resolver.resolve(response["$ref"])
|
|
160
|
+
defined_links = response.get(self.links_field)
|
|
161
|
+
if defined_links is not None:
|
|
162
|
+
total += len(defined_links)
|
|
152
163
|
return total
|
|
153
164
|
|
|
154
165
|
def get_all_operations(
|
|
155
|
-
self, hooks:
|
|
166
|
+
self, hooks: HookDispatcher | None = None
|
|
156
167
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
157
168
|
"""Iterate over all operations defined in the API.
|
|
158
169
|
|
|
@@ -221,7 +232,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
221
232
|
except SCHEMA_PARSING_ERRORS as exc:
|
|
222
233
|
yield self._into_err(exc, path, method)
|
|
223
234
|
|
|
224
|
-
def _into_err(self, error: Exception, path:
|
|
235
|
+
def _into_err(self, error: Exception, path: str | None, method: str | None) -> Err[OperationSchemaError]:
|
|
225
236
|
__tracebackhide__ = True
|
|
226
237
|
try:
|
|
227
238
|
full_path = self.get_full_path(path) if isinstance(path, str) else None
|
|
@@ -232,9 +243,9 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
232
243
|
def _raise_invalid_schema(
|
|
233
244
|
self,
|
|
234
245
|
error: Exception,
|
|
235
|
-
full_path:
|
|
236
|
-
path:
|
|
237
|
-
method:
|
|
246
|
+
full_path: str | None = None,
|
|
247
|
+
path: str | None = None,
|
|
248
|
+
method: str | None = None,
|
|
238
249
|
) -> NoReturn:
|
|
239
250
|
__tracebackhide__ = True
|
|
240
251
|
if isinstance(error, jsonschema.exceptions.RefResolutionError):
|
|
@@ -257,8 +268,8 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
257
268
|
raise NotImplementedError
|
|
258
269
|
|
|
259
270
|
def collect_parameters(
|
|
260
|
-
self, parameters: Iterable[
|
|
261
|
-
) ->
|
|
271
|
+
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
|
272
|
+
) -> list[OpenAPIParameter]:
|
|
262
273
|
"""Collect Open API parameters.
|
|
263
274
|
|
|
264
275
|
They should be used uniformly during the generation step; therefore, we need to convert them into
|
|
@@ -266,7 +277,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
266
277
|
"""
|
|
267
278
|
raise NotImplementedError
|
|
268
279
|
|
|
269
|
-
def _resolve_methods(self, methods:
|
|
280
|
+
def _resolve_methods(self, methods: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
270
281
|
# We need to know a proper scope in what methods are.
|
|
271
282
|
# It will allow us to provide a proper reference resolving in `response_schema_conformance` and avoid
|
|
272
283
|
# recursion errors
|
|
@@ -278,7 +289,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
278
289
|
self,
|
|
279
290
|
path: str,
|
|
280
291
|
method: str,
|
|
281
|
-
parameters:
|
|
292
|
+
parameters: list[OpenAPIParameter],
|
|
282
293
|
raw_definition: OperationDefinition,
|
|
283
294
|
) -> APIOperation:
|
|
284
295
|
"""Create JSON schemas for the query, body, etc from Swagger parameters definitions."""
|
|
@@ -304,19 +315,19 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
304
315
|
self._resolver = InliningResolver(self.location or "", self.raw_schema)
|
|
305
316
|
return self._resolver
|
|
306
317
|
|
|
307
|
-
def get_content_types(self, operation: APIOperation, response: GenericResponse) ->
|
|
318
|
+
def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
|
|
308
319
|
"""Content types available for this API operation."""
|
|
309
320
|
raise NotImplementedError
|
|
310
321
|
|
|
311
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
322
|
+
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
|
312
323
|
"""Get examples from the API operation."""
|
|
313
324
|
raise NotImplementedError
|
|
314
325
|
|
|
315
|
-
def get_security_requirements(self, operation: APIOperation) ->
|
|
326
|
+
def get_security_requirements(self, operation: APIOperation) -> list[str]:
|
|
316
327
|
"""Get applied security requirements for the given API operation."""
|
|
317
328
|
return self.security.get_security_requirements(self.raw_schema, operation)
|
|
318
329
|
|
|
319
|
-
def get_response_schema(self, definition:
|
|
330
|
+
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
320
331
|
"""Extract response schema from `responses`."""
|
|
321
332
|
raise NotImplementedError
|
|
322
333
|
|
|
@@ -326,7 +337,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
326
337
|
self._operations_by_id = dict(self._group_operations_by_id())
|
|
327
338
|
return self._operations_by_id[operation_id]
|
|
328
339
|
|
|
329
|
-
def _group_operations_by_id(self) -> Generator[
|
|
340
|
+
def _group_operations_by_id(self) -> Generator[tuple[str, APIOperation], None, None]:
|
|
330
341
|
for path, methods in self.raw_schema["paths"].items():
|
|
331
342
|
scope, raw_methods = self._resolve_methods(methods)
|
|
332
343
|
common_parameters = self.resolver.resolve_all(methods.get("parameters", []), RECURSION_DEPTH_LIMIT - 8)
|
|
@@ -365,9 +376,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
365
376
|
def get_case_strategy(
|
|
366
377
|
self,
|
|
367
378
|
operation: APIOperation,
|
|
368
|
-
hooks:
|
|
369
|
-
auth_storage:
|
|
379
|
+
hooks: HookDispatcher | None = None,
|
|
380
|
+
auth_storage: AuthStorage | None = None,
|
|
370
381
|
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
|
382
|
+
generation_config: GenerationConfig | None = None,
|
|
371
383
|
**kwargs: Any,
|
|
372
384
|
) -> SearchStrategy:
|
|
373
385
|
return get_case_strategy(
|
|
@@ -375,10 +387,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
375
387
|
auth_storage=auth_storage,
|
|
376
388
|
hooks=hooks,
|
|
377
389
|
generator=data_generation_method,
|
|
390
|
+
generation_config=generation_config,
|
|
378
391
|
**kwargs,
|
|
379
392
|
)
|
|
380
393
|
|
|
381
|
-
def get_parameter_serializer(self, operation: APIOperation, location: str) ->
|
|
394
|
+
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
|
382
395
|
definitions = [item for item in operation.definition.resolved.get("parameters", []) if item["in"] == location]
|
|
383
396
|
security_parameters = self.security.get_security_definitions_as_parameters(
|
|
384
397
|
self.raw_schema, operation, self.resolver, location
|
|
@@ -390,10 +403,10 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
390
403
|
return self._get_parameter_serializer(definitions)
|
|
391
404
|
return None
|
|
392
405
|
|
|
393
|
-
def _get_parameter_serializer(self, definitions:
|
|
406
|
+
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
|
394
407
|
raise NotImplementedError
|
|
395
408
|
|
|
396
|
-
def _get_response_definitions(self, operation: APIOperation, response: GenericResponse) ->
|
|
409
|
+
def _get_response_definitions(self, operation: APIOperation, response: GenericResponse) -> dict[str, Any] | None:
|
|
397
410
|
try:
|
|
398
411
|
responses = operation.definition.resolved["responses"]
|
|
399
412
|
except KeyError as exc:
|
|
@@ -408,23 +421,23 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
408
421
|
return responses["default"]
|
|
409
422
|
return None
|
|
410
423
|
|
|
411
|
-
def get_headers(self, operation: APIOperation, response: GenericResponse) ->
|
|
424
|
+
def get_headers(self, operation: APIOperation, response: GenericResponse) -> dict[str, dict[str, Any]] | None:
|
|
412
425
|
definitions = self._get_response_definitions(operation, response)
|
|
413
426
|
if not definitions:
|
|
414
427
|
return None
|
|
415
428
|
return definitions.get("headers")
|
|
416
429
|
|
|
417
|
-
def as_state_machine(self) ->
|
|
430
|
+
def as_state_machine(self) -> type[APIStateMachine]:
|
|
418
431
|
return create_state_machine(self)
|
|
419
432
|
|
|
420
433
|
def add_link(
|
|
421
434
|
self,
|
|
422
435
|
source: APIOperation,
|
|
423
|
-
target:
|
|
424
|
-
status_code:
|
|
425
|
-
parameters:
|
|
436
|
+
target: str | APIOperation,
|
|
437
|
+
status_code: str | int,
|
|
438
|
+
parameters: dict[str, str] | None = None,
|
|
426
439
|
request_body: Any = None,
|
|
427
|
-
name:
|
|
440
|
+
name: str | None = None,
|
|
428
441
|
) -> None:
|
|
429
442
|
"""Add a new Open API link to the schema definition.
|
|
430
443
|
|
|
@@ -493,13 +506,13 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
493
506
|
message += " Check if the requested API operation passes the filters in the schema."
|
|
494
507
|
raise ValueError(message)
|
|
495
508
|
|
|
496
|
-
def get_links(self, operation: APIOperation) ->
|
|
497
|
-
result:
|
|
509
|
+
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
|
510
|
+
result: dict[str, dict[str, Any]] = defaultdict(dict)
|
|
498
511
|
for status_code, link in links.get_all_links(operation):
|
|
499
512
|
result[status_code][link.name] = link
|
|
500
513
|
return result
|
|
501
514
|
|
|
502
|
-
def validate_response(self, operation: APIOperation, response: GenericResponse) -> None:
|
|
515
|
+
def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
|
|
503
516
|
responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
|
|
504
517
|
status_code = str(response.status_code)
|
|
505
518
|
if status_code in responses:
|
|
@@ -508,11 +521,11 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
508
521
|
definition = responses["default"]
|
|
509
522
|
else:
|
|
510
523
|
# No response defined for the received response status code
|
|
511
|
-
return
|
|
524
|
+
return None
|
|
512
525
|
scopes, schema = self.get_response_schema(definition, operation.definition.scope)
|
|
513
526
|
if not schema:
|
|
514
527
|
# No schema to check against
|
|
515
|
-
return
|
|
528
|
+
return None
|
|
516
529
|
content_type = response.headers.get("Content-Type")
|
|
517
530
|
errors = []
|
|
518
531
|
if content_type is None:
|
|
@@ -528,7 +541,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
528
541
|
errors.append(exc)
|
|
529
542
|
if content_type and not is_json_media_type(content_type):
|
|
530
543
|
_maybe_raise_one_or_more(errors)
|
|
531
|
-
return
|
|
544
|
+
return None
|
|
532
545
|
try:
|
|
533
546
|
if isinstance(response, (requests.Response, httpx.Response)):
|
|
534
547
|
data = json.loads(response.text)
|
|
@@ -563,15 +576,15 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
563
576
|
return None # explicitly return None for mypy
|
|
564
577
|
|
|
565
578
|
@property
|
|
566
|
-
def rewritten_components(self) ->
|
|
579
|
+
def rewritten_components(self) -> dict[str, Any]:
|
|
567
580
|
if not hasattr(self, "_rewritten_components"):
|
|
568
581
|
|
|
569
|
-
def callback(_schema:
|
|
582
|
+
def callback(_schema: dict[str, Any], nullable_name: str) -> dict[str, Any]:
|
|
570
583
|
_schema = to_json_schema(_schema, nullable_name=nullable_name, copy=False)
|
|
571
584
|
return self._rewrite_references(_schema, self.resolver)
|
|
572
585
|
|
|
573
586
|
# Different spec versions allow different keywords to store possible reference targets
|
|
574
|
-
components:
|
|
587
|
+
components: dict[str, Any] = {}
|
|
575
588
|
for path in self.component_locations:
|
|
576
589
|
schema = self.raw_schema
|
|
577
590
|
target = components
|
|
@@ -624,7 +637,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
624
637
|
stack.append(sub_item)
|
|
625
638
|
return schema
|
|
626
639
|
|
|
627
|
-
def _rewrite_references(self, schema:
|
|
640
|
+
def _rewrite_references(self, schema: dict[str, Any], resolver: InliningResolver) -> dict[str, Any]:
|
|
628
641
|
"""Rewrite references present in the schema.
|
|
629
642
|
|
|
630
643
|
The idea is to resolve references, cache the result and replace these references with new ones
|
|
@@ -646,7 +659,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
646
659
|
return schema
|
|
647
660
|
|
|
648
661
|
|
|
649
|
-
def _maybe_raise_one_or_more(errors:
|
|
662
|
+
def _maybe_raise_one_or_more(errors: list[Exception]) -> None:
|
|
650
663
|
if not errors:
|
|
651
664
|
return
|
|
652
665
|
elif len(errors) == 1:
|
|
@@ -655,7 +668,7 @@ def _maybe_raise_one_or_more(errors: List[Exception]) -> None:
|
|
|
655
668
|
raise MultipleFailures("\n\n".join(str(error) for error in errors), errors)
|
|
656
669
|
|
|
657
670
|
|
|
658
|
-
def _make_reference_key(scopes:
|
|
671
|
+
def _make_reference_key(scopes: list[str], reference: str) -> str:
|
|
659
672
|
"""A name under which the resolved reference data will be stored."""
|
|
660
673
|
# Using a hexdigest is the simplest way to associate practically unique keys with each reference
|
|
661
674
|
digest = sha1()
|
|
@@ -681,7 +694,7 @@ def in_scope(resolver: jsonschema.RefResolver, scope: str) -> Generator[None, No
|
|
|
681
694
|
|
|
682
695
|
|
|
683
696
|
@contextmanager
|
|
684
|
-
def in_scopes(resolver: jsonschema.RefResolver, scopes:
|
|
697
|
+
def in_scopes(resolver: jsonschema.RefResolver, scopes: list[str]) -> Generator[None, None, None]:
|
|
685
698
|
"""Push all available scopes into the resolver.
|
|
686
699
|
|
|
687
700
|
There could be an additional scope change during a schema resolving in `get_response_schema`, so in total there
|
|
@@ -705,7 +718,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
705
718
|
examples_field = "x-examples"
|
|
706
719
|
header_required_field = "x-required"
|
|
707
720
|
security = SwaggerSecurityProcessor()
|
|
708
|
-
component_locations: ClassVar[
|
|
721
|
+
component_locations: ClassVar[tuple[tuple[str, ...], ...]] = (("definitions",),)
|
|
709
722
|
links_field = "x-links"
|
|
710
723
|
|
|
711
724
|
@property
|
|
@@ -723,11 +736,11 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
723
736
|
return self.raw_schema.get("basePath", "/")
|
|
724
737
|
|
|
725
738
|
def collect_parameters(
|
|
726
|
-
self, parameters: Iterable[
|
|
727
|
-
) ->
|
|
739
|
+
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
|
740
|
+
) -> list[OpenAPIParameter]:
|
|
728
741
|
# The main difference with Open API 3.0 is that it has `body` and `form` parameters that we need to handle
|
|
729
742
|
# differently.
|
|
730
|
-
collected:
|
|
743
|
+
collected: list[OpenAPIParameter] = []
|
|
731
744
|
# NOTE. The Open API 2.0 spec doesn't strictly imply having media types in the "consumes" keyword.
|
|
732
745
|
# It is not enforced by the meta schema and has no "MUST" verb in the spec text.
|
|
733
746
|
# Also, not every API has operations with payload (they might have only GET operations without payloads).
|
|
@@ -760,11 +773,11 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
760
773
|
)
|
|
761
774
|
return collected
|
|
762
775
|
|
|
763
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
776
|
+
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
|
764
777
|
"""Get examples from the API operation."""
|
|
765
778
|
return get_strategies_from_examples(operation, self.examples_field)
|
|
766
779
|
|
|
767
|
-
def get_response_schema(self, definition:
|
|
780
|
+
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
768
781
|
scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
|
|
769
782
|
schema = definition.get("schema")
|
|
770
783
|
if not schema:
|
|
@@ -773,18 +786,18 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
773
786
|
# because it is not converted
|
|
774
787
|
return scopes, to_json_schema_recursive(schema, self.nullable_name, is_response_schema=True)
|
|
775
788
|
|
|
776
|
-
def get_content_types(self, operation: APIOperation, response: GenericResponse) ->
|
|
789
|
+
def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
|
|
777
790
|
produces = operation.definition.raw.get("produces", None)
|
|
778
791
|
if produces:
|
|
779
792
|
return produces
|
|
780
793
|
return self.raw_schema.get("produces", [])
|
|
781
794
|
|
|
782
|
-
def _get_parameter_serializer(self, definitions:
|
|
795
|
+
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
|
783
796
|
return serialization.serialize_swagger2_parameters(definitions)
|
|
784
797
|
|
|
785
798
|
def prepare_multipart(
|
|
786
799
|
self, form_data: FormData, operation: APIOperation
|
|
787
|
-
) ->
|
|
800
|
+
) -> tuple[list | None, dict[str, Any] | None]:
|
|
788
801
|
"""Prepare form data for sending with `requests`.
|
|
789
802
|
|
|
790
803
|
:param form_data: Raw generated data as a dictionary.
|
|
@@ -818,20 +831,20 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
818
831
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
|
819
832
|
return files or None, data or None
|
|
820
833
|
|
|
821
|
-
def get_request_payload_content_types(self, operation: APIOperation) ->
|
|
834
|
+
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
|
822
835
|
return self._get_consumes_for_operation(operation.definition.resolved)
|
|
823
836
|
|
|
824
837
|
def make_case(
|
|
825
838
|
self,
|
|
826
839
|
*,
|
|
827
|
-
case_cls:
|
|
840
|
+
case_cls: type[C],
|
|
828
841
|
operation: APIOperation,
|
|
829
|
-
path_parameters:
|
|
830
|
-
headers:
|
|
831
|
-
cookies:
|
|
832
|
-
query:
|
|
833
|
-
body:
|
|
834
|
-
media_type:
|
|
842
|
+
path_parameters: PathParameters | None = None,
|
|
843
|
+
headers: Headers | None = None,
|
|
844
|
+
cookies: Cookies | None = None,
|
|
845
|
+
query: Query | None = None,
|
|
846
|
+
body: Body | NotSet = NOT_SET,
|
|
847
|
+
media_type: str | None = None,
|
|
835
848
|
) -> C:
|
|
836
849
|
if body is not NOT_SET and media_type is None:
|
|
837
850
|
# If the user wants to send payload, then there should be a media type, otherwise the payload is ignored
|
|
@@ -856,7 +869,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
856
869
|
media_type=media_type,
|
|
857
870
|
)
|
|
858
871
|
|
|
859
|
-
def _get_consumes_for_operation(self, definition:
|
|
872
|
+
def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
|
|
860
873
|
"""Get the `consumes` value for the given API operation.
|
|
861
874
|
|
|
862
875
|
:param definition: Raw API operation definition.
|
|
@@ -869,7 +882,7 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
|
869
882
|
consumes = global_consumes
|
|
870
883
|
return consumes
|
|
871
884
|
|
|
872
|
-
def _get_payload_schema(self, definition:
|
|
885
|
+
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
|
873
886
|
for parameter in definition.get("parameters", []):
|
|
874
887
|
if parameter["in"] == "body":
|
|
875
888
|
return parameter["schema"]
|
|
@@ -910,10 +923,10 @@ class OpenApi30(SwaggerV20):
|
|
|
910
923
|
return "/"
|
|
911
924
|
|
|
912
925
|
def collect_parameters(
|
|
913
|
-
self, parameters: Iterable[
|
|
914
|
-
) ->
|
|
926
|
+
self, parameters: Iterable[dict[str, Any]], definition: dict[str, Any]
|
|
927
|
+
) -> list[OpenAPIParameter]:
|
|
915
928
|
# Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
|
|
916
|
-
collected:
|
|
929
|
+
collected: list[OpenAPIParameter] = [OpenAPI30Parameter(definition=parameter) for parameter in parameters]
|
|
917
930
|
if "requestBody" in definition:
|
|
918
931
|
required = definition["requestBody"].get("required", False)
|
|
919
932
|
description = definition["requestBody"].get("description")
|
|
@@ -923,7 +936,7 @@ class OpenApi30(SwaggerV20):
|
|
|
923
936
|
)
|
|
924
937
|
return collected
|
|
925
938
|
|
|
926
|
-
def get_response_schema(self, definition:
|
|
939
|
+
def get_response_schema(self, definition: dict[str, Any], scope: str) -> tuple[list[str], dict[str, Any] | None]:
|
|
927
940
|
scopes, definition = self.resolver.resolve_in_scope(fast_deepcopy(definition), scope)
|
|
928
941
|
options = iter(definition.get("content", {}).values())
|
|
929
942
|
option = next(options, None)
|
|
@@ -934,25 +947,25 @@ class OpenApi30(SwaggerV20):
|
|
|
934
947
|
return scopes, to_json_schema_recursive(option["schema"], self.nullable_name, is_response_schema=True)
|
|
935
948
|
return scopes, None
|
|
936
949
|
|
|
937
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
950
|
+
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
|
938
951
|
"""Get examples from the API operation."""
|
|
939
952
|
return get_strategies_from_examples(operation, self.examples_field)
|
|
940
953
|
|
|
941
|
-
def get_content_types(self, operation: APIOperation, response: GenericResponse) ->
|
|
954
|
+
def get_content_types(self, operation: APIOperation, response: GenericResponse) -> list[str]:
|
|
942
955
|
definitions = self._get_response_definitions(operation, response)
|
|
943
956
|
if not definitions:
|
|
944
957
|
return []
|
|
945
958
|
return list(definitions.get("content", {}).keys())
|
|
946
959
|
|
|
947
|
-
def _get_parameter_serializer(self, definitions:
|
|
960
|
+
def _get_parameter_serializer(self, definitions: list[dict[str, Any]]) -> Callable | None:
|
|
948
961
|
return serialization.serialize_openapi3_parameters(definitions)
|
|
949
962
|
|
|
950
|
-
def get_request_payload_content_types(self, operation: APIOperation) ->
|
|
963
|
+
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
|
951
964
|
return list(operation.definition.resolved["requestBody"]["content"].keys())
|
|
952
965
|
|
|
953
966
|
def prepare_multipart(
|
|
954
967
|
self, form_data: FormData, operation: APIOperation
|
|
955
|
-
) ->
|
|
968
|
+
) -> tuple[list | None, dict[str, Any] | None]:
|
|
956
969
|
"""Prepare form data for sending with `requests`.
|
|
957
970
|
|
|
958
971
|
:param form_data: Raw generated data as a dictionary.
|
|
@@ -975,7 +988,7 @@ class OpenApi30(SwaggerV20):
|
|
|
975
988
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
|
976
989
|
return files or None, None
|
|
977
990
|
|
|
978
|
-
def _get_payload_schema(self, definition:
|
|
991
|
+
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
|
979
992
|
if "requestBody" in definition:
|
|
980
993
|
if "$ref" in definition["requestBody"]:
|
|
981
994
|
body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Processing of ``securityDefinitions`` or ``securitySchemes`` keywords."""
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, ClassVar,
|
|
4
|
+
from typing import Any, ClassVar, Generator
|
|
4
5
|
|
|
5
6
|
from jsonschema import RefResolver
|
|
6
7
|
|
|
@@ -10,11 +11,11 @@ from .parameters import OpenAPI20Parameter, OpenAPI30Parameter, OpenAPIParameter
|
|
|
10
11
|
|
|
11
12
|
@dataclass
|
|
12
13
|
class BaseSecurityProcessor:
|
|
13
|
-
api_key_locations: ClassVar[
|
|
14
|
+
api_key_locations: ClassVar[tuple[str, ...]] = ("header", "query")
|
|
14
15
|
http_security_name: ClassVar[str] = "basic"
|
|
15
|
-
parameter_cls: ClassVar[
|
|
16
|
+
parameter_cls: ClassVar[type[OpenAPIParameter]] = OpenAPI20Parameter
|
|
16
17
|
|
|
17
|
-
def process_definitions(self, schema:
|
|
18
|
+
def process_definitions(self, schema: dict[str, Any], operation: APIOperation, resolver: RefResolver) -> None:
|
|
18
19
|
"""Add relevant security parameters to data generation."""
|
|
19
20
|
__tracebackhide__ = True
|
|
20
21
|
for definition in self._get_active_definitions(schema, operation, resolver):
|
|
@@ -28,7 +29,7 @@ class BaseSecurityProcessor:
|
|
|
28
29
|
self.process_http_security_definition(definition, operation)
|
|
29
30
|
|
|
30
31
|
@staticmethod
|
|
31
|
-
def get_security_requirements(schema:
|
|
32
|
+
def get_security_requirements(schema: dict[str, Any], operation: APIOperation) -> list[str]:
|
|
32
33
|
"""Get applied security requirements for the given API operation."""
|
|
33
34
|
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
|
|
34
35
|
# > This definition overrides any declared top-level security.
|
|
@@ -42,8 +43,8 @@ class BaseSecurityProcessor:
|
|
|
42
43
|
return [key for requirement in requirements for key in requirement]
|
|
43
44
|
|
|
44
45
|
def _get_active_definitions(
|
|
45
|
-
self, schema:
|
|
46
|
-
) -> Generator[
|
|
46
|
+
self, schema: dict[str, Any], operation: APIOperation, resolver: RefResolver
|
|
47
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
47
48
|
"""Get only security definitions active for the given API operation."""
|
|
48
49
|
definitions = self.get_security_definitions(schema, resolver)
|
|
49
50
|
requirements = self.get_security_requirements(schema, operation)
|
|
@@ -51,12 +52,12 @@ class BaseSecurityProcessor:
|
|
|
51
52
|
if name in requirements:
|
|
52
53
|
yield definition
|
|
53
54
|
|
|
54
|
-
def get_security_definitions(self, schema:
|
|
55
|
+
def get_security_definitions(self, schema: dict[str, Any], resolver: RefResolver) -> dict[str, Any]:
|
|
55
56
|
return schema.get("securityDefinitions", {})
|
|
56
57
|
|
|
57
58
|
def get_security_definitions_as_parameters(
|
|
58
|
-
self, schema:
|
|
59
|
-
) ->
|
|
59
|
+
self, schema: dict[str, Any], operation: APIOperation, resolver: RefResolver, location: str
|
|
60
|
+
) -> list[dict[str, Any]]:
|
|
60
61
|
"""Security definitions converted to OAS parameters.
|
|
61
62
|
|
|
62
63
|
We need it to get proper serialization that will be applied on generated values. For this case it is only
|
|
@@ -68,45 +69,45 @@ class BaseSecurityProcessor:
|
|
|
68
69
|
if self._is_match(definition, location)
|
|
69
70
|
]
|
|
70
71
|
|
|
71
|
-
def process_api_key_security_definition(self, definition:
|
|
72
|
+
def process_api_key_security_definition(self, definition: dict[str, Any], operation: APIOperation) -> None:
|
|
72
73
|
parameter = self.parameter_cls(self._make_api_key_parameter(definition))
|
|
73
74
|
operation.add_parameter(parameter)
|
|
74
75
|
|
|
75
|
-
def process_http_security_definition(self, definition:
|
|
76
|
+
def process_http_security_definition(self, definition: dict[str, Any], operation: APIOperation) -> None:
|
|
76
77
|
if definition["type"] == self.http_security_name:
|
|
77
78
|
parameter = self.parameter_cls(self._make_http_auth_parameter(definition))
|
|
78
79
|
operation.add_parameter(parameter)
|
|
79
80
|
|
|
80
|
-
def _is_match(self, definition:
|
|
81
|
+
def _is_match(self, definition: dict[str, Any], location: str) -> bool:
|
|
81
82
|
return (definition["type"] == "apiKey" and location in self.api_key_locations) or (
|
|
82
83
|
definition["type"] == self.http_security_name and location == "header"
|
|
83
84
|
)
|
|
84
85
|
|
|
85
|
-
def _to_parameter(self, definition:
|
|
86
|
+
def _to_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
86
87
|
func = {
|
|
87
88
|
"apiKey": self._make_api_key_parameter,
|
|
88
89
|
self.http_security_name: self._make_http_auth_parameter,
|
|
89
90
|
}[definition["type"]]
|
|
90
91
|
return func(definition)
|
|
91
92
|
|
|
92
|
-
def _make_http_auth_parameter(self, definition:
|
|
93
|
+
def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
93
94
|
schema = make_auth_header_schema(definition)
|
|
94
95
|
return make_auth_header(**schema)
|
|
95
96
|
|
|
96
|
-
def _make_api_key_parameter(self, definition:
|
|
97
|
+
def _make_api_key_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
97
98
|
return make_api_key_schema(definition, type="string")
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
def make_auth_header_schema(definition:
|
|
101
|
+
def make_auth_header_schema(definition: dict[str, Any]) -> dict[str, str]:
|
|
101
102
|
schema = definition.get("scheme", "basic").lower()
|
|
102
103
|
return {"type": "string", "format": f"_{schema}_auth"}
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
def make_auth_header(**kwargs: Any) ->
|
|
106
|
+
def make_auth_header(**kwargs: Any) -> dict[str, Any]:
|
|
106
107
|
return {"name": "Authorization", "in": "header", "required": True, **kwargs}
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
def make_api_key_schema(definition:
|
|
110
|
+
def make_api_key_schema(definition: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
|
|
110
111
|
return {"name": definition["name"], "required": True, "in": definition["in"], **kwargs}
|
|
111
112
|
|
|
112
113
|
|
|
@@ -115,11 +116,11 @@ SwaggerSecurityProcessor = BaseSecurityProcessor
|
|
|
115
116
|
|
|
116
117
|
@dataclass
|
|
117
118
|
class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
118
|
-
api_key_locations: ClassVar[
|
|
119
|
+
api_key_locations: ClassVar[tuple[str, ...]] = ("header", "cookie", "query")
|
|
119
120
|
http_security_name: ClassVar[str] = "http"
|
|
120
|
-
parameter_cls: ClassVar[
|
|
121
|
+
parameter_cls: ClassVar[type[OpenAPIParameter]] = OpenAPI30Parameter
|
|
121
122
|
|
|
122
|
-
def get_security_definitions(self, schema:
|
|
123
|
+
def get_security_definitions(self, schema: dict[str, Any], resolver: RefResolver) -> dict[str, Any]:
|
|
123
124
|
"""In Open API 3 security definitions are located in ``components`` and may have references inside."""
|
|
124
125
|
components = schema.get("components", {})
|
|
125
126
|
security_schemes = components.get("securitySchemes", {})
|
|
@@ -127,9 +128,9 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
|
|
|
127
128
|
return resolver.resolve(security_schemes["$ref"])[1]
|
|
128
129
|
return security_schemes
|
|
129
130
|
|
|
130
|
-
def _make_http_auth_parameter(self, definition:
|
|
131
|
+
def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
131
132
|
schema = make_auth_header_schema(definition)
|
|
132
133
|
return make_auth_header(schema=schema)
|
|
133
134
|
|
|
134
|
-
def _make_api_key_parameter(self, definition:
|
|
135
|
+
def _make_api_key_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
|
|
135
136
|
return make_api_key_schema(definition, schema={"type": "string"})
|