schemathesis 3.39.16__py3-none-any.whl → 4.0.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/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
3
3
|
from itertools import chain
|
4
4
|
from typing import Any, Callable
|
5
5
|
|
6
|
-
from
|
7
|
-
|
6
|
+
from schemathesis.core.transforms import deepclone, transform
|
7
|
+
|
8
8
|
from .patterns import update_quantifier
|
9
9
|
|
10
10
|
|
@@ -22,7 +22,7 @@ def to_json_schema(
|
|
22
22
|
See a recursive version below.
|
23
23
|
"""
|
24
24
|
if copy:
|
25
|
-
schema =
|
25
|
+
schema = deepclone(schema)
|
26
26
|
if schema.get(nullable_name) is True:
|
27
27
|
del schema[nullable_name]
|
28
28
|
schema = {"anyOf": [schema, {"type": "null"}]}
|
@@ -94,7 +94,7 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
|
|
94
94
|
def to_json_schema_recursive(
|
95
95
|
schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
|
96
96
|
) -> dict[str, Any]:
|
97
|
-
return
|
97
|
+
return transform(
|
98
98
|
schema,
|
99
99
|
to_json_schema,
|
100
100
|
nullable_name=nullable_name,
|
@@ -9,13 +9,16 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
|
9
9
|
import requests
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
|
+
from schemathesis.config import GenerationConfig
|
13
|
+
from schemathesis.core.transforms import deepclone
|
14
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
15
|
+
from schemathesis.generation.case import Case
|
16
|
+
from schemathesis.generation.hypothesis import examples
|
17
|
+
from schemathesis.generation.meta import TestPhase
|
18
|
+
from schemathesis.schemas import APIOperation
|
12
19
|
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
13
20
|
|
14
|
-
from
|
15
|
-
from ...generation import get_single_example
|
16
|
-
from ...internal.copy import fast_deepcopy
|
17
|
-
from ...models import APIOperation, Case, TestPhase
|
18
|
-
from ._hypothesis import get_case_strategy, get_default_format_strategies
|
21
|
+
from ._hypothesis import get_default_format_strategies, openapi_cases
|
19
22
|
from .constants import LOCATION_TO_CONTAINER
|
20
23
|
from .formats import STRING_FORMATS
|
21
24
|
from .parameters import OpenAPIBody, OpenAPIParameter
|
@@ -23,8 +26,6 @@ from .parameters import OpenAPIBody, OpenAPIParameter
|
|
23
26
|
if TYPE_CHECKING:
|
24
27
|
from hypothesis.strategies import SearchStrategy
|
25
28
|
|
26
|
-
from ...generation import GenerationConfig
|
27
|
-
|
28
29
|
|
29
30
|
@dataclass
|
30
31
|
class ParameterExample:
|
@@ -47,7 +48,7 @@ Example = Union[ParameterExample, BodyExample]
|
|
47
48
|
|
48
49
|
|
49
50
|
def get_strategies_from_examples(
|
50
|
-
operation: APIOperation[OpenAPIParameter
|
51
|
+
operation: APIOperation[OpenAPIParameter], **kwargs: Any
|
51
52
|
) -> list[SearchStrategy[Case]]:
|
52
53
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
53
54
|
maps = get_serializers_for_operation(operation)
|
@@ -66,15 +67,15 @@ def get_strategies_from_examples(
|
|
66
67
|
examples = list(extract_top_level(operation))
|
67
68
|
# Add examples from parameter's schemas
|
68
69
|
examples.extend(extract_from_schemas(operation))
|
69
|
-
as_strategy_kwargs = as_strategy_kwargs or {}
|
70
|
-
as_strategy_kwargs["phase"] = TestPhase.EXPLICIT
|
71
70
|
return [
|
72
|
-
|
71
|
+
openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXAMPLES}).map(
|
72
|
+
serialize_components
|
73
|
+
)
|
73
74
|
for parameters in produce_combinations(examples)
|
74
75
|
]
|
75
76
|
|
76
77
|
|
77
|
-
def extract_top_level(operation: APIOperation[OpenAPIParameter
|
78
|
+
def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
78
79
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
79
80
|
responses = find_in_responses(operation)
|
80
81
|
for parameter in operation.iter_parameters():
|
@@ -142,7 +143,7 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
|
|
142
143
|
for subschema in schema[key]:
|
143
144
|
yield subschema
|
144
145
|
if "allOf" in schema:
|
145
|
-
subschema =
|
146
|
+
subschema = deepclone(schema["allOf"][0])
|
146
147
|
for sub in schema["allOf"][1:]:
|
147
148
|
if isinstance(sub, dict):
|
148
149
|
for key, value in sub.items():
|
@@ -160,7 +161,7 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
|
|
160
161
|
|
161
162
|
|
162
163
|
def _find_parameter_examples_definition(
|
163
|
-
operation: APIOperation[OpenAPIParameter
|
164
|
+
operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
|
164
165
|
) -> dict[str, Any]:
|
165
166
|
"""Find the original, unresolved `examples` definition of a parameter."""
|
166
167
|
from .schemas import BaseOpenAPISchema
|
@@ -178,13 +179,13 @@ def _find_parameter_examples_definition(
|
|
178
179
|
|
179
180
|
|
180
181
|
def _find_request_body_examples_definition(
|
181
|
-
operation: APIOperation[OpenAPIParameter
|
182
|
+
operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
|
182
183
|
) -> dict[str, Any]:
|
183
184
|
"""Find the original, unresolved `examples` definition of a request body variant."""
|
184
185
|
from .schemas import BaseOpenAPISchema
|
185
186
|
|
186
187
|
schema = cast(BaseOpenAPISchema, operation.schema)
|
187
|
-
if schema.
|
188
|
+
if schema.specification.version == "2.0":
|
188
189
|
raw_schema = schema.raw_schema
|
189
190
|
path_data = raw_schema["paths"][operation.path]
|
190
191
|
parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
|
@@ -220,12 +221,12 @@ def extract_inner_examples(
|
|
220
221
|
@lru_cache
|
221
222
|
def load_external_example(url: str) -> bytes:
|
222
223
|
"""Load examples the `externalValue` keyword."""
|
223
|
-
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT
|
224
|
+
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
224
225
|
response.raise_for_status()
|
225
226
|
return response.content
|
226
227
|
|
227
228
|
|
228
|
-
def extract_from_schemas(operation: APIOperation[OpenAPIParameter
|
229
|
+
def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
229
230
|
"""Extract examples from parameters' schema definitions."""
|
230
231
|
for parameter in operation.iter_parameters():
|
231
232
|
schema = parameter.as_json_schema(operation)
|
@@ -242,7 +243,7 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
|
|
242
243
|
|
243
244
|
|
244
245
|
def extract_from_schema(
|
245
|
-
operation: APIOperation[OpenAPIParameter
|
246
|
+
operation: APIOperation[OpenAPIParameter],
|
246
247
|
schema: dict[str, Any],
|
247
248
|
example_field_name: str,
|
248
249
|
examples_field_name: str,
|
@@ -273,12 +274,13 @@ def extract_from_schema(
|
|
273
274
|
continue
|
274
275
|
variants[name] = values
|
275
276
|
if variants:
|
277
|
+
config = operation.schema.config.generation_for(operation=operation, phase="examples")
|
276
278
|
for name, subschema in to_generate.items():
|
277
279
|
if name in variants:
|
278
280
|
# Generated by one of `anyOf` or similar sub-schemas
|
279
281
|
continue
|
280
282
|
subschema = operation.schema.prepare_schema(subschema)
|
281
|
-
generated = _generate_single_example(subschema,
|
283
|
+
generated = _generate_single_example(subschema, config)
|
282
284
|
variants[name] = [generated]
|
283
285
|
# Calculate the maximum number of examples any property has
|
284
286
|
total_combos = max(len(examples) for examples in variants.values())
|
@@ -304,7 +306,7 @@ def _generate_single_example(
|
|
304
306
|
allow_x00=generation_config.allow_x00,
|
305
307
|
codec=generation_config.codec,
|
306
308
|
)
|
307
|
-
return
|
309
|
+
return examples.generate_one(strategy)
|
308
310
|
|
309
311
|
|
310
312
|
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
@@ -8,42 +8,54 @@ from __future__ import annotations
|
|
8
8
|
import json
|
9
9
|
from typing import Any
|
10
10
|
|
11
|
+
from schemathesis.core.transforms import UNRESOLVABLE, Unresolvable
|
12
|
+
from schemathesis.generation.stateful.state_machine import StepOutput
|
13
|
+
|
11
14
|
from . import lexer, nodes, parser
|
12
|
-
from .context import ExpressionContext
|
13
15
|
|
14
|
-
__all__ = [
|
15
|
-
"lexer",
|
16
|
-
"nodes",
|
17
|
-
"parser",
|
18
|
-
"ExpressionContext",
|
19
|
-
]
|
16
|
+
__all__ = ["lexer", "nodes", "parser"]
|
20
17
|
|
21
18
|
|
22
|
-
def evaluate(expr: Any,
|
19
|
+
def evaluate(expr: Any, output: StepOutput, evaluate_nested: bool = False) -> Any:
|
23
20
|
"""Evaluate runtime expression in context."""
|
24
21
|
if isinstance(expr, (dict, list)) and evaluate_nested:
|
25
|
-
return _evaluate_nested(expr,
|
22
|
+
return _evaluate_nested(expr, output)
|
26
23
|
if not isinstance(expr, str):
|
27
24
|
# Can be a non-string constant
|
28
25
|
return expr
|
29
|
-
parts = [node.evaluate(
|
26
|
+
parts = [node.evaluate(output) for node in parser.parse(expr)]
|
30
27
|
if len(parts) == 1:
|
31
28
|
return parts[0] # keep the return type the same as the internal value type
|
32
|
-
|
29
|
+
if any(isinstance(part, Unresolvable) for part in parts):
|
30
|
+
return UNRESOLVABLE
|
33
31
|
return "".join(str(part) for part in parts if part is not None)
|
34
32
|
|
35
33
|
|
36
|
-
def _evaluate_nested(expr: dict[str, Any] | list,
|
34
|
+
def _evaluate_nested(expr: dict[str, Any] | list, output: StepOutput) -> Any:
|
37
35
|
if isinstance(expr, dict):
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
result_dict = {}
|
37
|
+
for key, value in expr.items():
|
38
|
+
new_key = _evaluate_object_key(key, output)
|
39
|
+
if new_key is UNRESOLVABLE:
|
40
|
+
return new_key
|
41
|
+
new_value = evaluate(value, output, evaluate_nested=True)
|
42
|
+
if new_value is UNRESOLVABLE:
|
43
|
+
return new_value
|
44
|
+
result_dict[new_key] = new_value
|
45
|
+
return result_dict
|
46
|
+
result_list = []
|
47
|
+
for item in expr:
|
48
|
+
new_value = evaluate(item, output, evaluate_nested=True)
|
49
|
+
if new_value is UNRESOLVABLE:
|
50
|
+
return new_value
|
51
|
+
result_list.append(new_value)
|
52
|
+
return result_list
|
43
53
|
|
44
54
|
|
45
|
-
def _evaluate_object_key(key: str,
|
46
|
-
evaluated = evaluate(key,
|
55
|
+
def _evaluate_object_key(key: str, output: StepOutput) -> Any:
|
56
|
+
evaluated = evaluate(key, output)
|
57
|
+
if evaluated is UNRESOLVABLE:
|
58
|
+
return evaluated
|
47
59
|
if isinstance(evaluated, str):
|
48
60
|
return evaluated
|
49
61
|
if isinstance(evaluated, bool):
|
@@ -4,14 +4,15 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from enum import Enum, unique
|
7
|
-
from typing import TYPE_CHECKING, Any
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
8
8
|
|
9
9
|
from requests.structures import CaseInsensitiveDict
|
10
10
|
|
11
|
-
from
|
11
|
+
from schemathesis.core.transforms import UNRESOLVABLE, Unresolvable, resolve_pointer
|
12
|
+
from schemathesis.generation.stateful.state_machine import StepOutput
|
13
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
12
14
|
|
13
15
|
if TYPE_CHECKING:
|
14
|
-
from .context import ExpressionContext
|
15
16
|
from .extractors import Extractor
|
16
17
|
|
17
18
|
|
@@ -19,12 +20,12 @@ if TYPE_CHECKING:
|
|
19
20
|
class Node:
|
20
21
|
"""Generic expression node."""
|
21
22
|
|
22
|
-
def evaluate(self,
|
23
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
23
24
|
raise NotImplementedError
|
24
25
|
|
25
26
|
|
26
27
|
@unique
|
27
|
-
class NodeType(Enum):
|
28
|
+
class NodeType(str, Enum):
|
28
29
|
URL = "$url"
|
29
30
|
METHOD = "$method"
|
30
31
|
STATUS_CODE = "$statusCode"
|
@@ -38,7 +39,7 @@ class String(Node):
|
|
38
39
|
|
39
40
|
value: str
|
40
41
|
|
41
|
-
def evaluate(self,
|
42
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
42
43
|
"""String tokens are passed as they are.
|
43
44
|
|
44
45
|
``foo{$request.path.id}``
|
@@ -52,24 +53,29 @@ class String(Node):
|
|
52
53
|
class URL(Node):
|
53
54
|
"""A node for `$url` expression."""
|
54
55
|
|
55
|
-
def evaluate(self,
|
56
|
-
|
56
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
57
|
+
import requests
|
58
|
+
|
59
|
+
base_url = output.case.operation.base_url or "http://127.0.0.1"
|
60
|
+
kwargs = REQUESTS_TRANSPORT.serialize_case(output.case, base_url=base_url)
|
61
|
+
prepared = requests.Request(**kwargs).prepare()
|
62
|
+
return cast(str, prepared.url)
|
57
63
|
|
58
64
|
|
59
65
|
@dataclass
|
60
66
|
class Method(Node):
|
61
67
|
"""A node for `$method` expression."""
|
62
68
|
|
63
|
-
def evaluate(self,
|
64
|
-
return
|
69
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
70
|
+
return output.case.operation.method.upper()
|
65
71
|
|
66
72
|
|
67
73
|
@dataclass
|
68
74
|
class StatusCode(Node):
|
69
75
|
"""A node for `$statusCode` expression."""
|
70
76
|
|
71
|
-
def evaluate(self,
|
72
|
-
return str(
|
77
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
78
|
+
return str(output.response.status_code)
|
73
79
|
|
74
80
|
|
75
81
|
@dataclass
|
@@ -80,19 +86,19 @@ class NonBodyRequest(Node):
|
|
80
86
|
parameter: str
|
81
87
|
extractor: Extractor | None = None
|
82
88
|
|
83
|
-
def evaluate(self,
|
84
|
-
container
|
85
|
-
"query":
|
86
|
-
"path":
|
87
|
-
"header":
|
89
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
90
|
+
container = {
|
91
|
+
"query": output.case.query,
|
92
|
+
"path": output.case.path_parameters,
|
93
|
+
"header": output.case.headers,
|
88
94
|
}[self.location] or {}
|
89
95
|
if self.location == "header":
|
90
96
|
container = CaseInsensitiveDict(container)
|
91
97
|
value = container.get(self.parameter)
|
92
98
|
if value is None:
|
93
|
-
return
|
99
|
+
return UNRESOLVABLE
|
94
100
|
if self.extractor is not None:
|
95
|
-
return self.extractor.extract(value) or
|
101
|
+
return self.extractor.extract(value) or UNRESOLVABLE
|
96
102
|
return value
|
97
103
|
|
98
104
|
|
@@ -102,14 +108,11 @@ class BodyRequest(Node):
|
|
102
108
|
|
103
109
|
pointer: str | None = None
|
104
110
|
|
105
|
-
def evaluate(self,
|
106
|
-
document =
|
111
|
+
def evaluate(self, output: StepOutput) -> Any | Unresolvable:
|
112
|
+
document = output.case.body
|
107
113
|
if self.pointer is None:
|
108
114
|
return document
|
109
|
-
|
110
|
-
if resolved is references.UNRESOLVABLE:
|
111
|
-
return None
|
112
|
-
return resolved
|
115
|
+
return resolve_pointer(document, self.pointer[1:])
|
113
116
|
|
114
117
|
|
115
118
|
@dataclass
|
@@ -119,13 +122,13 @@ class HeaderResponse(Node):
|
|
119
122
|
parameter: str
|
120
123
|
extractor: Extractor | None = None
|
121
124
|
|
122
|
-
def evaluate(self,
|
123
|
-
value =
|
125
|
+
def evaluate(self, output: StepOutput) -> str | Unresolvable:
|
126
|
+
value = output.response.headers.get(self.parameter.lower())
|
124
127
|
if value is None:
|
125
|
-
return
|
128
|
+
return UNRESOLVABLE
|
126
129
|
if self.extractor is not None:
|
127
|
-
return self.extractor.extract(value) or
|
128
|
-
return value
|
130
|
+
return self.extractor.extract(value[0]) or UNRESOLVABLE
|
131
|
+
return value[0]
|
129
132
|
|
130
133
|
|
131
134
|
@dataclass
|
@@ -134,17 +137,9 @@ class BodyResponse(Node):
|
|
134
137
|
|
135
138
|
pointer: str | None = None
|
136
139
|
|
137
|
-
def evaluate(self,
|
138
|
-
|
139
|
-
|
140
|
-
if isinstance(context.response, WSGIResponse):
|
141
|
-
document = context.response.json
|
142
|
-
else:
|
143
|
-
document = context.response.json()
|
140
|
+
def evaluate(self, output: StepOutput) -> Any:
|
141
|
+
document = output.response.json()
|
144
142
|
if self.pointer is None:
|
145
143
|
# We need the parsed document - data will be serialized before sending to the application
|
146
144
|
return document
|
147
|
-
|
148
|
-
if resolved is references.UNRESOLVABLE:
|
149
|
-
return None
|
150
|
-
return resolved
|
145
|
+
return resolve_pointer(document, self.pointer[1:])
|
@@ -46,7 +46,7 @@ def _parse_variable(tokens: lexer.TokenGenerator, token: lexer.Token, expr: str)
|
|
46
46
|
elif token.value == nodes.NodeType.RESPONSE.value:
|
47
47
|
yield _parse_response(tokens, expr)
|
48
48
|
else:
|
49
|
-
raise UnknownToken(token.value)
|
49
|
+
raise UnknownToken(f"Invalid expression `{expr}`. Unknown token: `{token.value}`")
|
50
50
|
|
51
51
|
|
52
52
|
def _parse_request(tokens: lexer.TokenGenerator, expr: str) -> nodes.BodyRequest | nodes.NonBodyRequest:
|
@@ -5,6 +5,8 @@ from base64 import b64encode
|
|
5
5
|
from functools import lru_cache
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
+
from schemathesis.transport.serialization import Binary
|
9
|
+
|
8
10
|
if TYPE_CHECKING:
|
9
11
|
from hypothesis import strategies as st
|
10
12
|
|
@@ -13,10 +15,37 @@ STRING_FORMATS: dict[str, st.SearchStrategy] = {}
|
|
13
15
|
|
14
16
|
|
15
17
|
def register_string_format(name: str, strategy: st.SearchStrategy) -> None:
|
16
|
-
"""Register a
|
18
|
+
r"""Register a custom Hypothesis strategy for generating string format data.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
name: String format name that matches the "format" keyword in your API schema
|
22
|
+
strategy: Hypothesis strategy to generate values for this format
|
23
|
+
|
24
|
+
Example:
|
25
|
+
```python
|
26
|
+
import schemathesis
|
27
|
+
from hypothesis import strategies as st
|
28
|
+
|
29
|
+
# Register phone number format
|
30
|
+
phone_strategy = st.from_regex(r"\+1-\d{3}-\d{3}-\d{4}")
|
31
|
+
schemathesis.openapi.format("phone", phone_strategy)
|
32
|
+
|
33
|
+
# Register email with specific domain
|
34
|
+
email_strategy = st.from_regex(r"[a-z]+@company\.com")
|
35
|
+
schemathesis.openapi.format("company-email", email_strategy)
|
36
|
+
```
|
37
|
+
|
38
|
+
Schema usage:
|
39
|
+
```yaml
|
40
|
+
properties:
|
41
|
+
phone:
|
42
|
+
type: string
|
43
|
+
format: phone # Uses your phone_strategy
|
44
|
+
contact_email:
|
45
|
+
type: string
|
46
|
+
format: company-email # Uses your email_strategy
|
47
|
+
```
|
17
48
|
|
18
|
-
:param str name: Format name. It should correspond the one used in the API schema as the "format" keyword value.
|
19
|
-
:param strategy: Hypothesis strategy you'd like to use to generate values for this format.
|
20
49
|
"""
|
21
50
|
from hypothesis.strategies import SearchStrategy
|
22
51
|
|
@@ -36,11 +65,11 @@ def unregister_string_format(name: str) -> None:
|
|
36
65
|
raise ValueError(f"Unknown Open API format: {name}") from exc
|
37
66
|
|
38
67
|
|
39
|
-
def header_values(
|
68
|
+
def header_values(exclude_characters: str = "\n\r") -> st.SearchStrategy[str]:
|
40
69
|
from hypothesis import strategies as st
|
41
70
|
|
42
71
|
return st.text(
|
43
|
-
alphabet=st.characters(min_codepoint=0, max_codepoint=255,
|
72
|
+
alphabet=st.characters(min_codepoint=0, max_codepoint=255, exclude_characters=exclude_characters)
|
44
73
|
# Header values with leading non-visible chars can't be sent with `requests`
|
45
74
|
).map(str.lstrip)
|
46
75
|
|
@@ -54,8 +83,6 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
|
|
54
83
|
from hypothesis import strategies as st
|
55
84
|
from requests.auth import _basic_auth_str
|
56
85
|
|
57
|
-
from ...serializers import Binary
|
58
|
-
|
59
86
|
def make_basic_auth_str(item: tuple[str, str]) -> str:
|
60
87
|
return _basic_auth_str(*item)
|
61
88
|
|
@@ -67,6 +94,7 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
|
|
67
94
|
return {
|
68
95
|
"binary": st.binary().map(Binary),
|
69
96
|
"byte": st.binary().map(lambda x: b64encode(x).decode()),
|
97
|
+
"uuid": st.uuids().map(str),
|
70
98
|
# RFC 7230, Section 3.2.6
|
71
99
|
"_header_name": st.text(
|
72
100
|
min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
|
@@ -2,6 +2,11 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import TYPE_CHECKING, Any, Collection
|
4
4
|
|
5
|
+
from schemathesis.transport import SerializationContext
|
6
|
+
from schemathesis.transport.asgi import ASGI_TRANSPORT
|
7
|
+
from schemathesis.transport.requests import REQUESTS_TRANSPORT
|
8
|
+
from schemathesis.transport.wsgi import WSGI_TRANSPORT
|
9
|
+
|
5
10
|
if TYPE_CHECKING:
|
6
11
|
from hypothesis import strategies as st
|
7
12
|
|
@@ -10,16 +15,56 @@ MEDIA_TYPES: dict[str, st.SearchStrategy[bytes]] = {}
|
|
10
15
|
|
11
16
|
|
12
17
|
def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliases: Collection[str] = ()) -> None:
|
13
|
-
"""Register a strategy for
|
14
|
-
|
18
|
+
r"""Register a custom Hypothesis strategy for generating media type content.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
name: Media type name that matches your OpenAPI requestBody content type
|
22
|
+
strategy: Hypothesis strategy that generates bytes for this media type
|
23
|
+
aliases: Additional media type names that use the same strategy
|
24
|
+
|
25
|
+
Example:
|
26
|
+
```python
|
27
|
+
import schemathesis
|
28
|
+
from hypothesis import strategies as st
|
29
|
+
|
30
|
+
# Register PDF file strategy
|
31
|
+
pdf_strategy = st.sampled_from([
|
32
|
+
b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n%%EOF",
|
33
|
+
b"%PDF-1.5\n%\xe2\xe3\xcf\xd3\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n%%EOF"
|
34
|
+
])
|
35
|
+
schemathesis.openapi.media_type("application/pdf", pdf_strategy)
|
15
36
|
|
16
|
-
|
17
|
-
|
18
|
-
def
|
19
|
-
|
37
|
+
# Dynamic content generation
|
38
|
+
@st.composite
|
39
|
+
def xml_content(draw):
|
40
|
+
tag = draw(st.text(min_size=3, max_size=10))
|
41
|
+
content = draw(st.text(min_size=1, max_size=50))
|
42
|
+
return f"<?xml version='1.0'?><{tag}>{content}</{tag}>".encode()
|
20
43
|
|
21
|
-
|
22
|
-
|
44
|
+
schemathesis.openapi.media_type("application/xml", xml_content())
|
45
|
+
```
|
46
|
+
|
47
|
+
Schema usage:
|
48
|
+
```yaml
|
49
|
+
requestBody:
|
50
|
+
content:
|
51
|
+
application/pdf: # Uses your PDF strategy
|
52
|
+
schema:
|
53
|
+
type: string
|
54
|
+
format: binary
|
55
|
+
application/xml: # Uses your XML strategy
|
56
|
+
schema:
|
57
|
+
type: string
|
58
|
+
format: binary
|
59
|
+
```
|
60
|
+
|
61
|
+
"""
|
62
|
+
|
63
|
+
@REQUESTS_TRANSPORT.serializer(name, *aliases)
|
64
|
+
@ASGI_TRANSPORT.serializer(name, *aliases)
|
65
|
+
@WSGI_TRANSPORT.serializer(name, *aliases)
|
66
|
+
def serialize(ctx: SerializationContext, value: Any) -> dict[str, Any]:
|
67
|
+
return {"data": value}
|
23
68
|
|
24
69
|
MEDIA_TYPES[name] = strategy
|
25
70
|
for alias in aliases:
|
@@ -27,8 +72,4 @@ def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliase
|
|
27
72
|
|
28
73
|
|
29
74
|
def unregister_all() -> None:
|
30
|
-
from ...serializers import unregister
|
31
|
-
|
32
|
-
for media_type in MEDIA_TYPES:
|
33
|
-
unregister(media_type)
|
34
75
|
MEDIA_TYPES.clear()
|
@@ -9,11 +9,12 @@ import jsonschema
|
|
9
9
|
from hypothesis import strategies as st
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
|
+
from schemathesis.config import GenerationConfig
|
13
|
+
|
12
14
|
from ..constants import ALL_KEYWORDS
|
13
15
|
from .mutations import MutationContext
|
14
16
|
|
15
17
|
if TYPE_CHECKING:
|
16
|
-
from ....generation import GenerationConfig
|
17
18
|
from .types import Draw, Schema
|
18
19
|
|
19
20
|
|
@@ -27,16 +28,17 @@ class CacheKey:
|
|
27
28
|
operation_name: str
|
28
29
|
location: str
|
29
30
|
schema: Schema
|
31
|
+
validator_cls: type[jsonschema.Validator]
|
30
32
|
|
31
33
|
def __hash__(self) -> int:
|
32
34
|
return hash((self.operation_name, self.location))
|
33
35
|
|
34
36
|
|
35
37
|
@lru_cache
|
36
|
-
def get_validator(cache_key: CacheKey) -> jsonschema.
|
38
|
+
def get_validator(cache_key: CacheKey) -> jsonschema.Validator:
|
37
39
|
"""Get JSON Schema validator for the given schema."""
|
38
40
|
# Each operation / location combo has only a single schema, therefore could be cached
|
39
|
-
return
|
41
|
+
return cache_key.validator_cls(cache_key.schema)
|
40
42
|
|
41
43
|
|
42
44
|
@lru_cache
|
@@ -62,6 +64,7 @@ def negative_schema(
|
|
62
64
|
generation_config: GenerationConfig,
|
63
65
|
*,
|
64
66
|
custom_formats: dict[str, st.SearchStrategy[str]],
|
67
|
+
validator_cls: type[jsonschema.Validator],
|
65
68
|
) -> st.SearchStrategy:
|
66
69
|
"""A strategy for instances that DO NOT match the input schema.
|
67
70
|
|
@@ -69,7 +72,7 @@ def negative_schema(
|
|
69
72
|
"""
|
70
73
|
# The mutated schema is passed to `from_schema` and guarded against producing instances valid against
|
71
74
|
# the original schema.
|
72
|
-
cache_key = CacheKey(operation_name, location, schema)
|
75
|
+
cache_key = CacheKey(operation_name, location, schema, validator_cls)
|
73
76
|
validator = get_validator(cache_key)
|
74
77
|
keywords, non_keywords = split_schema(cache_key)
|
75
78
|
|