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
schemathesis/schemas.py
CHANGED
|
@@ -16,18 +16,12 @@ from typing import (
|
|
|
16
16
|
Any,
|
|
17
17
|
Callable,
|
|
18
18
|
ContextManager,
|
|
19
|
-
Dict,
|
|
20
19
|
Generator,
|
|
21
20
|
Iterable,
|
|
22
21
|
Iterator,
|
|
23
|
-
List,
|
|
24
22
|
NoReturn,
|
|
25
|
-
Optional,
|
|
26
23
|
Sequence,
|
|
27
|
-
Tuple,
|
|
28
|
-
Type,
|
|
29
24
|
TypeVar,
|
|
30
|
-
Union,
|
|
31
25
|
TYPE_CHECKING,
|
|
32
26
|
)
|
|
33
27
|
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
|
@@ -41,7 +35,12 @@ from .constants import NOT_SET
|
|
|
41
35
|
from ._hypothesis import create_test
|
|
42
36
|
from .auths import AuthStorage
|
|
43
37
|
from .code_samples import CodeSampleStyle
|
|
44
|
-
from .generation import
|
|
38
|
+
from .generation import (
|
|
39
|
+
DEFAULT_DATA_GENERATION_METHODS,
|
|
40
|
+
DataGenerationMethod,
|
|
41
|
+
DataGenerationMethodInput,
|
|
42
|
+
GenerationConfig,
|
|
43
|
+
)
|
|
45
44
|
from .exceptions import OperationSchemaError, UsageError
|
|
46
45
|
from .hooks import HookContext, HookDispatcher, HookScope, dispatch
|
|
47
46
|
from .internal.result import Result, Ok
|
|
@@ -84,31 +83,32 @@ class MethodsDict(CaseInsensitiveDict):
|
|
|
84
83
|
C = TypeVar("C", bound=Case)
|
|
85
84
|
|
|
86
85
|
|
|
87
|
-
@lru_cache
|
|
86
|
+
@lru_cache
|
|
88
87
|
def get_full_path(base_path: str, path: str) -> str:
|
|
89
88
|
return unquote(urljoin(base_path, quote(path.lstrip("/"))))
|
|
90
89
|
|
|
91
90
|
|
|
92
91
|
@dataclass(eq=False)
|
|
93
92
|
class BaseSchema(Mapping):
|
|
94
|
-
raw_schema:
|
|
95
|
-
location:
|
|
96
|
-
base_url:
|
|
97
|
-
method:
|
|
98
|
-
endpoint:
|
|
99
|
-
tag:
|
|
100
|
-
operation_id:
|
|
93
|
+
raw_schema: dict[str, Any]
|
|
94
|
+
location: str | None = None
|
|
95
|
+
base_url: str | None = None
|
|
96
|
+
method: Filter | None = None
|
|
97
|
+
endpoint: Filter | None = None
|
|
98
|
+
tag: Filter | None = None
|
|
99
|
+
operation_id: Filter | None = None
|
|
101
100
|
app: Any = None
|
|
102
101
|
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
|
103
102
|
auth: AuthStorage = field(default_factory=AuthStorage)
|
|
104
|
-
test_function:
|
|
103
|
+
test_function: GenericTest | None = None
|
|
105
104
|
validate_schema: bool = True
|
|
106
105
|
skip_deprecated_operations: bool = False
|
|
107
|
-
data_generation_methods:
|
|
106
|
+
data_generation_methods: list[DataGenerationMethod] = field(
|
|
108
107
|
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
|
109
108
|
)
|
|
109
|
+
generation_config: GenerationConfig = field(default_factory=GenerationConfig)
|
|
110
110
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
|
111
|
-
rate_limiter:
|
|
111
|
+
rate_limiter: Limiter | None = None
|
|
112
112
|
sanitize_output: bool = True
|
|
113
113
|
|
|
114
114
|
def __iter__(self) -> Iterator[str]:
|
|
@@ -127,7 +127,7 @@ class BaseSchema(Mapping):
|
|
|
127
127
|
def __len__(self) -> int:
|
|
128
128
|
return len(self.operations)
|
|
129
129
|
|
|
130
|
-
def hook(self, hook:
|
|
130
|
+
def hook(self, hook: str | Callable) -> Callable:
|
|
131
131
|
return self.hooks.register(hook)
|
|
132
132
|
|
|
133
133
|
@property
|
|
@@ -169,7 +169,7 @@ class BaseSchema(Mapping):
|
|
|
169
169
|
raise NotImplementedError
|
|
170
170
|
|
|
171
171
|
@property
|
|
172
|
-
def operations(self) ->
|
|
172
|
+
def operations(self) -> dict[str, MethodsDict]:
|
|
173
173
|
if not hasattr(self, "_operations"):
|
|
174
174
|
operations = self.get_all_operations()
|
|
175
175
|
self._operations = operations_to_dict(operations)
|
|
@@ -179,38 +179,43 @@ class BaseSchema(Mapping):
|
|
|
179
179
|
def operations_count(self) -> int:
|
|
180
180
|
raise NotImplementedError
|
|
181
181
|
|
|
182
|
+
@property
|
|
183
|
+
def links_count(self) -> int:
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
182
186
|
def get_all_operations(
|
|
183
|
-
self, hooks:
|
|
187
|
+
self, hooks: HookDispatcher | None = None
|
|
184
188
|
) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
|
|
185
189
|
raise NotImplementedError
|
|
186
190
|
|
|
187
|
-
def get_strategies_from_examples(self, operation: APIOperation) ->
|
|
191
|
+
def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
|
|
188
192
|
"""Get examples from the API operation."""
|
|
189
193
|
raise NotImplementedError
|
|
190
194
|
|
|
191
|
-
def get_security_requirements(self, operation: APIOperation) ->
|
|
195
|
+
def get_security_requirements(self, operation: APIOperation) -> list[str]:
|
|
192
196
|
"""Get applied security requirements for the given API operation."""
|
|
193
197
|
raise NotImplementedError
|
|
194
198
|
|
|
195
199
|
def get_stateful_tests(
|
|
196
|
-
self, response: GenericResponse, operation: APIOperation, stateful:
|
|
200
|
+
self, response: GenericResponse, operation: APIOperation, stateful: Stateful | None
|
|
197
201
|
) -> Sequence[StatefulTest]:
|
|
198
202
|
"""Get a list of additional tests, that should be executed after this response from the API operation."""
|
|
199
203
|
raise NotImplementedError
|
|
200
204
|
|
|
201
|
-
def get_parameter_serializer(self, operation: APIOperation, location: str) ->
|
|
205
|
+
def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
|
|
202
206
|
"""Get a function that serializes parameters for the given location."""
|
|
203
207
|
raise NotImplementedError
|
|
204
208
|
|
|
205
209
|
def get_all_tests(
|
|
206
210
|
self,
|
|
207
211
|
func: Callable,
|
|
208
|
-
settings:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
settings: hypothesis.settings | None = None,
|
|
213
|
+
generation_config: GenerationConfig | None = None,
|
|
214
|
+
seed: int | None = None,
|
|
215
|
+
as_strategy_kwargs: dict[str, Any] | None = None,
|
|
216
|
+
hooks: HookDispatcher | None = None,
|
|
217
|
+
_given_kwargs: dict[str, GivenInput] | None = None,
|
|
218
|
+
) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
|
|
214
219
|
"""Generate all operations and Hypothesis tests for them."""
|
|
215
220
|
for result in self.get_all_operations(hooks=hooks):
|
|
216
221
|
if isinstance(result, Ok):
|
|
@@ -220,6 +225,7 @@ class BaseSchema(Mapping):
|
|
|
220
225
|
settings=settings,
|
|
221
226
|
seed=seed,
|
|
222
227
|
data_generation_methods=self.data_generation_methods,
|
|
228
|
+
generation_config=generation_config,
|
|
223
229
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
224
230
|
_given_kwargs=_given_kwargs,
|
|
225
231
|
)
|
|
@@ -229,14 +235,14 @@ class BaseSchema(Mapping):
|
|
|
229
235
|
|
|
230
236
|
def parametrize(
|
|
231
237
|
self,
|
|
232
|
-
method:
|
|
233
|
-
endpoint:
|
|
234
|
-
tag:
|
|
235
|
-
operation_id:
|
|
236
|
-
validate_schema:
|
|
237
|
-
skip_deprecated_operations:
|
|
238
|
-
data_generation_methods:
|
|
239
|
-
code_sample_style:
|
|
238
|
+
method: Filter | None = NOT_SET,
|
|
239
|
+
endpoint: Filter | None = NOT_SET,
|
|
240
|
+
tag: Filter | None = NOT_SET,
|
|
241
|
+
operation_id: Filter | None = NOT_SET,
|
|
242
|
+
validate_schema: bool | NotSet = NOT_SET,
|
|
243
|
+
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
244
|
+
data_generation_methods: Iterable[DataGenerationMethod] | NotSet = NOT_SET,
|
|
245
|
+
code_sample_style: str | NotSet = NOT_SET,
|
|
240
246
|
) -> Callable:
|
|
241
247
|
"""Mark a test function as a parametrized one."""
|
|
242
248
|
_code_sample_style = (
|
|
@@ -278,22 +284,23 @@ class BaseSchema(Mapping):
|
|
|
278
284
|
def clone(
|
|
279
285
|
self,
|
|
280
286
|
*,
|
|
281
|
-
base_url:
|
|
282
|
-
test_function:
|
|
283
|
-
method:
|
|
284
|
-
endpoint:
|
|
285
|
-
tag:
|
|
286
|
-
operation_id:
|
|
287
|
+
base_url: str | None | NotSet = NOT_SET,
|
|
288
|
+
test_function: GenericTest | None = None,
|
|
289
|
+
method: Filter | None = NOT_SET,
|
|
290
|
+
endpoint: Filter | None = NOT_SET,
|
|
291
|
+
tag: Filter | None = NOT_SET,
|
|
292
|
+
operation_id: Filter | None = NOT_SET,
|
|
287
293
|
app: Any = NOT_SET,
|
|
288
|
-
hooks:
|
|
289
|
-
auth:
|
|
290
|
-
validate_schema:
|
|
291
|
-
skip_deprecated_operations:
|
|
292
|
-
data_generation_methods:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
hooks: HookDispatcher | NotSet = NOT_SET,
|
|
295
|
+
auth: AuthStorage | NotSet = NOT_SET,
|
|
296
|
+
validate_schema: bool | NotSet = NOT_SET,
|
|
297
|
+
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
298
|
+
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
299
|
+
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
300
|
+
code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
|
|
301
|
+
rate_limiter: Limiter | None = NOT_SET,
|
|
302
|
+
sanitize_output: bool | NotSet | None = NOT_SET,
|
|
303
|
+
) -> BaseSchema:
|
|
297
304
|
if base_url is NOT_SET:
|
|
298
305
|
base_url = self.base_url
|
|
299
306
|
if method is NOT_SET:
|
|
@@ -316,6 +323,8 @@ class BaseSchema(Mapping):
|
|
|
316
323
|
auth = self.auth
|
|
317
324
|
if data_generation_methods is NOT_SET:
|
|
318
325
|
data_generation_methods = self.data_generation_methods
|
|
326
|
+
if generation_config is NOT_SET:
|
|
327
|
+
generation_config = self.generation_config
|
|
319
328
|
if code_sample_style is NOT_SET:
|
|
320
329
|
code_sample_style = self.code_sample_style
|
|
321
330
|
if rate_limiter is NOT_SET:
|
|
@@ -338,12 +347,13 @@ class BaseSchema(Mapping):
|
|
|
338
347
|
validate_schema=validate_schema, # type: ignore
|
|
339
348
|
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
|
340
349
|
data_generation_methods=data_generation_methods, # type: ignore
|
|
350
|
+
generation_config=generation_config, # type: ignore
|
|
341
351
|
code_sample_style=code_sample_style, # type: ignore
|
|
342
352
|
rate_limiter=rate_limiter, # type: ignore
|
|
343
353
|
sanitize_output=sanitize_output, # type: ignore
|
|
344
354
|
)
|
|
345
355
|
|
|
346
|
-
def get_local_hook_dispatcher(self) ->
|
|
356
|
+
def get_local_hook_dispatcher(self) -> HookDispatcher | None:
|
|
347
357
|
"""Get a HookDispatcher instance bound to the test if present."""
|
|
348
358
|
# It might be not present when it is used without pytest via `APIOperation.as_strategy()`
|
|
349
359
|
if self.test_function is not None:
|
|
@@ -361,51 +371,52 @@ class BaseSchema(Mapping):
|
|
|
361
371
|
|
|
362
372
|
def prepare_multipart(
|
|
363
373
|
self, form_data: FormData, operation: APIOperation
|
|
364
|
-
) ->
|
|
374
|
+
) -> tuple[list | None, dict[str, Any] | None]:
|
|
365
375
|
"""Split content of `form_data` into files & data.
|
|
366
376
|
|
|
367
377
|
Forms may contain file fields, that we should send via `files` argument in `requests`.
|
|
368
378
|
"""
|
|
369
379
|
raise NotImplementedError
|
|
370
380
|
|
|
371
|
-
def get_request_payload_content_types(self, operation: APIOperation) ->
|
|
381
|
+
def get_request_payload_content_types(self, operation: APIOperation) -> list[str]:
|
|
372
382
|
raise NotImplementedError
|
|
373
383
|
|
|
374
384
|
def make_case(
|
|
375
385
|
self,
|
|
376
386
|
*,
|
|
377
|
-
case_cls:
|
|
387
|
+
case_cls: type[C],
|
|
378
388
|
operation: APIOperation,
|
|
379
|
-
path_parameters:
|
|
380
|
-
headers:
|
|
381
|
-
cookies:
|
|
382
|
-
query:
|
|
383
|
-
body:
|
|
384
|
-
media_type:
|
|
389
|
+
path_parameters: PathParameters | None = None,
|
|
390
|
+
headers: Headers | None = None,
|
|
391
|
+
cookies: Cookies | None = None,
|
|
392
|
+
query: Query | None = None,
|
|
393
|
+
body: Body | NotSet = NOT_SET,
|
|
394
|
+
media_type: str | None = None,
|
|
385
395
|
) -> C:
|
|
386
396
|
raise NotImplementedError
|
|
387
397
|
|
|
388
398
|
def get_case_strategy(
|
|
389
399
|
self,
|
|
390
400
|
operation: APIOperation,
|
|
391
|
-
hooks:
|
|
392
|
-
auth_storage:
|
|
401
|
+
hooks: HookDispatcher | None = None,
|
|
402
|
+
auth_storage: AuthStorage | None = None,
|
|
393
403
|
data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
|
|
404
|
+
generation_config: GenerationConfig | None = None,
|
|
394
405
|
**kwargs: Any,
|
|
395
406
|
) -> SearchStrategy:
|
|
396
407
|
raise NotImplementedError
|
|
397
408
|
|
|
398
|
-
def as_state_machine(self) ->
|
|
409
|
+
def as_state_machine(self) -> type[APIStateMachine]:
|
|
399
410
|
"""Create a state machine class.
|
|
400
411
|
|
|
401
412
|
Use it for stateful testing.
|
|
402
413
|
"""
|
|
403
414
|
raise NotImplementedError
|
|
404
415
|
|
|
405
|
-
def get_links(self, operation: APIOperation) ->
|
|
416
|
+
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
|
406
417
|
raise NotImplementedError
|
|
407
418
|
|
|
408
|
-
def validate_response(self, operation: APIOperation, response: GenericResponse) -> None:
|
|
419
|
+
def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
|
|
409
420
|
raise NotImplementedError
|
|
410
421
|
|
|
411
422
|
def prepare_schema(self, schema: Any) -> Any:
|
|
@@ -418,14 +429,14 @@ class BaseSchema(Mapping):
|
|
|
418
429
|
return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
|
|
419
430
|
return nullcontext()
|
|
420
431
|
|
|
421
|
-
def _get_payload_schema(self, definition:
|
|
432
|
+
def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
|
|
422
433
|
raise NotImplementedError
|
|
423
434
|
|
|
424
435
|
|
|
425
436
|
def operations_to_dict(
|
|
426
437
|
operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
|
|
427
|
-
) ->
|
|
428
|
-
output:
|
|
438
|
+
) -> dict[str, MethodsDict]:
|
|
439
|
+
output: dict[str, MethodsDict] = {}
|
|
429
440
|
for result in operations:
|
|
430
441
|
if isinstance(result, Ok):
|
|
431
442
|
operation = result.ok()
|
schemathesis/serializers.py
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import binascii
|
|
2
3
|
import os
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from io import BytesIO
|
|
5
|
-
from typing import
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
9
|
+
Callable,
|
|
10
|
+
Collection,
|
|
11
|
+
Dict,
|
|
12
|
+
Generator,
|
|
13
|
+
cast,
|
|
14
|
+
Protocol,
|
|
15
|
+
runtime_checkable,
|
|
16
|
+
)
|
|
8
17
|
|
|
9
18
|
from .internal.copy import fast_deepcopy
|
|
10
19
|
from ._xml import _to_xml
|
|
@@ -19,7 +28,7 @@ if TYPE_CHECKING:
|
|
|
19
28
|
from .models import Case
|
|
20
29
|
|
|
21
30
|
|
|
22
|
-
SERIALIZERS:
|
|
31
|
+
SERIALIZERS: dict[str, type[Serializer]] = {}
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
@dataclass
|
|
@@ -40,7 +49,7 @@ class SerializerContext:
|
|
|
40
49
|
:ivar Case case: Generated example that is being processed.
|
|
41
50
|
"""
|
|
42
51
|
|
|
43
|
-
case:
|
|
52
|
+
case: Case
|
|
44
53
|
|
|
45
54
|
@property
|
|
46
55
|
def media_type(self) -> str:
|
|
@@ -52,11 +61,11 @@ class SerializerContext:
|
|
|
52
61
|
# Therefore `schema` is never `None` if called from here. However, `APIOperation.get_raw_payload_schema` is
|
|
53
62
|
# generic and can be called from other places where it may return `None`
|
|
54
63
|
|
|
55
|
-
def get_raw_payload_schema(self) ->
|
|
64
|
+
def get_raw_payload_schema(self) -> dict[str, Any]:
|
|
56
65
|
schema = self.case.operation.get_raw_payload_schema(self.media_type)
|
|
57
66
|
return cast(Dict[str, Any], schema)
|
|
58
67
|
|
|
59
|
-
def get_resolved_payload_schema(self) ->
|
|
68
|
+
def get_resolved_payload_schema(self) -> dict[str, Any]:
|
|
60
69
|
schema = self.case.operation.get_resolved_payload_schema(self.media_type)
|
|
61
70
|
return cast(Dict[str, Any], schema)
|
|
62
71
|
|
|
@@ -69,14 +78,14 @@ class Serializer(Protocol):
|
|
|
69
78
|
`requests` and `werkzeug` transports.
|
|
70
79
|
"""
|
|
71
80
|
|
|
72
|
-
def as_requests(self, context: SerializerContext, payload: Any) ->
|
|
81
|
+
def as_requests(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
|
|
73
82
|
raise NotImplementedError
|
|
74
83
|
|
|
75
|
-
def as_werkzeug(self, context: SerializerContext, payload: Any) ->
|
|
84
|
+
def as_werkzeug(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
|
|
76
85
|
raise NotImplementedError
|
|
77
86
|
|
|
78
87
|
|
|
79
|
-
def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[
|
|
88
|
+
def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[type[Serializer]], type[Serializer]]:
|
|
80
89
|
"""Register a serializer for the given media type.
|
|
81
90
|
|
|
82
91
|
Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
|
|
@@ -99,7 +108,7 @@ def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Typ
|
|
|
99
108
|
|
|
100
109
|
"""
|
|
101
110
|
|
|
102
|
-
def wrapper(serializer:
|
|
111
|
+
def wrapper(serializer: type[Serializer]) -> type[Serializer]:
|
|
103
112
|
if not issubclass(serializer, Serializer):
|
|
104
113
|
raise TypeError(
|
|
105
114
|
f"`{serializer.__name__}` is not a valid serializer. "
|
|
@@ -118,7 +127,7 @@ def unregister(media_type: str) -> None:
|
|
|
118
127
|
del SERIALIZERS[media_type]
|
|
119
128
|
|
|
120
129
|
|
|
121
|
-
def _to_json(value: Any) ->
|
|
130
|
+
def _to_json(value: Any) -> dict[str, Any]:
|
|
122
131
|
if isinstance(value, bytes):
|
|
123
132
|
# Possible to get via explicit examples, e.g. `externalValue`
|
|
124
133
|
return {"data": value}
|
|
@@ -132,14 +141,14 @@ def _to_json(value: Any) -> Dict[str, Any]:
|
|
|
132
141
|
|
|
133
142
|
@register("application/json")
|
|
134
143
|
class JSONSerializer:
|
|
135
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
144
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
136
145
|
return _to_json(value)
|
|
137
146
|
|
|
138
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
147
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
139
148
|
return _to_json(value)
|
|
140
149
|
|
|
141
150
|
|
|
142
|
-
def _to_yaml(value: Any) ->
|
|
151
|
+
def _to_yaml(value: Any) -> dict[str, Any]:
|
|
143
152
|
import yaml
|
|
144
153
|
|
|
145
154
|
try:
|
|
@@ -154,19 +163,19 @@ def _to_yaml(value: Any) -> Dict[str, Any]:
|
|
|
154
163
|
|
|
155
164
|
@register("text/yaml", aliases=("text/x-yaml", "application/x-yaml", "text/vnd.yaml"))
|
|
156
165
|
class YAMLSerializer:
|
|
157
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
166
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
158
167
|
return _to_yaml(value)
|
|
159
168
|
|
|
160
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
169
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
161
170
|
return _to_yaml(value)
|
|
162
171
|
|
|
163
172
|
|
|
164
173
|
@register("application/xml")
|
|
165
174
|
class XMLSerializer:
|
|
166
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
175
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
167
176
|
return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
|
|
168
177
|
|
|
169
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
178
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
170
179
|
return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
|
|
171
180
|
|
|
172
181
|
|
|
@@ -176,7 +185,7 @@ def _should_coerce_to_bytes(item: Any) -> bool:
|
|
|
176
185
|
return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
|
|
177
186
|
|
|
178
187
|
|
|
179
|
-
def _prepare_form_data(data:
|
|
188
|
+
def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
|
|
180
189
|
"""Make the generated data suitable for sending as multipart.
|
|
181
190
|
|
|
182
191
|
If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
|
|
@@ -222,7 +231,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
|
|
|
222
231
|
|
|
223
232
|
@register("multipart/form-data")
|
|
224
233
|
class MultipartSerializer:
|
|
225
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
234
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
226
235
|
if isinstance(value, bytes):
|
|
227
236
|
return {"data": value}
|
|
228
237
|
if isinstance(value, dict):
|
|
@@ -236,27 +245,27 @@ class MultipartSerializer:
|
|
|
236
245
|
content_type = f"multipart/form-data; boundary={boundary}"
|
|
237
246
|
return {"data": raw_data, "headers": {"Content-Type": content_type}}
|
|
238
247
|
|
|
239
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
248
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
240
249
|
return {"data": value}
|
|
241
250
|
|
|
242
251
|
|
|
243
252
|
@register("application/x-www-form-urlencoded")
|
|
244
253
|
class URLEncodedFormSerializer:
|
|
245
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
254
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
246
255
|
return {"data": value}
|
|
247
256
|
|
|
248
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
257
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
249
258
|
return {"data": value}
|
|
250
259
|
|
|
251
260
|
|
|
252
261
|
@register("text/plain")
|
|
253
262
|
class TextSerializer:
|
|
254
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
263
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
255
264
|
if isinstance(value, bytes):
|
|
256
265
|
return {"data": value}
|
|
257
266
|
return {"data": str(value).encode("utf8")}
|
|
258
267
|
|
|
259
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
268
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
260
269
|
if isinstance(value, bytes):
|
|
261
270
|
return {"data": value}
|
|
262
271
|
return {"data": str(value)}
|
|
@@ -264,10 +273,10 @@ class TextSerializer:
|
|
|
264
273
|
|
|
265
274
|
@register("application/octet-stream")
|
|
266
275
|
class OctetStreamSerializer:
|
|
267
|
-
def as_requests(self, context: SerializerContext, value: Any) ->
|
|
276
|
+
def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
268
277
|
return {"data": _to_bytes(value)}
|
|
269
278
|
|
|
270
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) ->
|
|
279
|
+
def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
|
|
271
280
|
return {"data": _to_bytes(value)}
|
|
272
281
|
|
|
273
282
|
|
|
@@ -287,11 +296,11 @@ def get_matching_media_types(media_type: str) -> Generator[str, None, None]:
|
|
|
287
296
|
yield registered_media_type
|
|
288
297
|
|
|
289
298
|
|
|
290
|
-
def get_first_matching_media_type(media_type: str) ->
|
|
299
|
+
def get_first_matching_media_type(media_type: str) -> str | None:
|
|
291
300
|
return next(get_matching_media_types(media_type), None)
|
|
292
301
|
|
|
293
302
|
|
|
294
|
-
def get(media_type: str) ->
|
|
303
|
+
def get(media_type: str) -> type[Serializer] | None:
|
|
295
304
|
"""Get an appropriate serializer for the given media type."""
|
|
296
305
|
if is_json_media_type(media_type):
|
|
297
306
|
media_type = "application/json"
|
schemathesis/service/ci.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import enum
|
|
2
3
|
import os
|
|
3
4
|
from dataclasses import asdict, dataclass
|
|
4
|
-
from typing import
|
|
5
|
-
|
|
6
|
-
from typing_extensions import Protocol, runtime_checkable
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
@enum.unique
|
|
@@ -25,17 +24,17 @@ class Environment(Protocol):
|
|
|
25
24
|
pass
|
|
26
25
|
|
|
27
26
|
@classmethod
|
|
28
|
-
def from_env(cls) ->
|
|
27
|
+
def from_env(cls) -> Environment:
|
|
29
28
|
pass
|
|
30
29
|
|
|
31
|
-
def asdict(self) ->
|
|
30
|
+
def asdict(self) -> dict[str, str | None]:
|
|
32
31
|
pass
|
|
33
32
|
|
|
34
|
-
def as_env(self) ->
|
|
33
|
+
def as_env(self) -> dict[str, str | None]:
|
|
35
34
|
pass
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
def environment() ->
|
|
37
|
+
def environment() -> Environment | None:
|
|
39
38
|
"""Collect environment data for a supported CI provider."""
|
|
40
39
|
provider = detect()
|
|
41
40
|
if provider == CIProvider.GITHUB:
|
|
@@ -45,7 +44,7 @@ def environment() -> Optional[Environment]:
|
|
|
45
44
|
return None
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
def detect() ->
|
|
47
|
+
def detect() -> CIProvider | None:
|
|
49
48
|
"""Detect the current CI provider."""
|
|
50
49
|
if GitHubActionsEnvironment.is_set():
|
|
51
50
|
return GitHubActionsEnvironment.provider
|
|
@@ -54,7 +53,7 @@ def detect() -> Optional[CIProvider]:
|
|
|
54
53
|
return None
|
|
55
54
|
|
|
56
55
|
|
|
57
|
-
def _asdict(env: Environment) ->
|
|
56
|
+
def _asdict(env: Environment) -> dict[str, str | None]:
|
|
58
57
|
data = asdict(env) # type: ignore
|
|
59
58
|
data["provider"] = env.provider.value
|
|
60
59
|
return data
|
|
@@ -89,24 +88,24 @@ class GitHubActionsEnvironment:
|
|
|
89
88
|
workflow: str
|
|
90
89
|
# The head ref or source branch of the pull request in a workflow run.
|
|
91
90
|
# For example, `dd/report-ci`.
|
|
92
|
-
head_ref:
|
|
91
|
+
head_ref: str | None
|
|
93
92
|
# The name of the base ref or target branch of the pull request in a workflow run.
|
|
94
93
|
# For example, `main`.
|
|
95
|
-
base_ref:
|
|
94
|
+
base_ref: str | None
|
|
96
95
|
# The branch or tag ref that triggered the workflow run.
|
|
97
96
|
# This is only set if a branch or tag is available for the event type.
|
|
98
97
|
# For example, `refs/pull/1533/merge`
|
|
99
|
-
ref:
|
|
98
|
+
ref: str | None
|
|
100
99
|
# The Schemathesis GitHub Action version.
|
|
101
100
|
# For example `v1.0.1`
|
|
102
|
-
action_ref:
|
|
101
|
+
action_ref: str | None
|
|
103
102
|
|
|
104
103
|
@classmethod
|
|
105
104
|
def is_set(cls) -> bool:
|
|
106
105
|
return os.getenv(cls.variable_name) == "true"
|
|
107
106
|
|
|
108
107
|
@classmethod
|
|
109
|
-
def from_env(cls) ->
|
|
108
|
+
def from_env(cls) -> GitHubActionsEnvironment:
|
|
110
109
|
return cls(
|
|
111
110
|
api_url=os.environ["GITHUB_API_URL"],
|
|
112
111
|
repository=os.environ["GITHUB_REPOSITORY"],
|
|
@@ -120,7 +119,7 @@ class GitHubActionsEnvironment:
|
|
|
120
119
|
action_ref=os.getenv("SCHEMATHESIS_ACTION_REF"),
|
|
121
120
|
)
|
|
122
121
|
|
|
123
|
-
def as_env(self) ->
|
|
122
|
+
def as_env(self) -> dict[str, str | None]:
|
|
124
123
|
return {
|
|
125
124
|
"GITHUB_API_URL": self.api_url,
|
|
126
125
|
"GITHUB_REPOSITORY": self.repository,
|
|
@@ -161,23 +160,23 @@ class GitLabCIEnvironment:
|
|
|
161
160
|
# not documented.
|
|
162
161
|
# The commit branch name. Not available in merge request pipelines or tag pipelines.
|
|
163
162
|
# For example, `dd/report-ci`.
|
|
164
|
-
commit_branch:
|
|
163
|
+
commit_branch: str | None
|
|
165
164
|
# The source branch name of the merge request. Only available in merge request pipelines.
|
|
166
165
|
# For example, `dd/report-ci`.
|
|
167
|
-
merge_request_source_branch_name:
|
|
166
|
+
merge_request_source_branch_name: str | None
|
|
168
167
|
# The target branch name of the merge request.
|
|
169
168
|
# For example, `main`.
|
|
170
|
-
merge_request_target_branch_name:
|
|
169
|
+
merge_request_target_branch_name: str | None
|
|
171
170
|
# The project-level internal ID of the merge request.
|
|
172
171
|
# For example, `42`.
|
|
173
|
-
merge_request_iid:
|
|
172
|
+
merge_request_iid: str | None
|
|
174
173
|
|
|
175
174
|
@classmethod
|
|
176
175
|
def is_set(cls) -> bool:
|
|
177
176
|
return os.getenv(cls.variable_name) == "true"
|
|
178
177
|
|
|
179
178
|
@classmethod
|
|
180
|
-
def from_env(cls) ->
|
|
179
|
+
def from_env(cls) -> GitLabCIEnvironment:
|
|
181
180
|
return cls(
|
|
182
181
|
api_v4_url=os.environ["CI_API_V4_URL"],
|
|
183
182
|
project_id=os.environ["CI_PROJECT_ID"],
|
|
@@ -189,7 +188,7 @@ class GitLabCIEnvironment:
|
|
|
189
188
|
merge_request_iid=os.getenv("CI_MERGE_REQUEST_IID"),
|
|
190
189
|
)
|
|
191
190
|
|
|
192
|
-
def as_env(self) ->
|
|
191
|
+
def as_env(self) -> dict[str, str | None]:
|
|
193
192
|
return {
|
|
194
193
|
"CI_API_V4_URL": self.api_v4_url,
|
|
195
194
|
"CI_PROJECT_ID": self.project_id,
|