schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# These schemas are copied from https://github.com/OAI/OpenAPI-Specification/tree/master/schemas
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
from ..._lazy_import import lazy_import
|
|
6
7
|
|
|
@@ -1329,6 +1330,8 @@ OPENAPI_30 = {
|
|
|
1329
1330
|
},
|
|
1330
1331
|
},
|
|
1331
1332
|
}
|
|
1333
|
+
# Generated from the updated schema.yaml from 0035208, which includes unpublished bugfixes
|
|
1334
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/0035208611701b4f7f2c959eb99a8725cca41e6e/schemas/v3.1/schema.yaml
|
|
1332
1335
|
OPENAPI_31 = {
|
|
1333
1336
|
"$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
|
|
1334
1337
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -1344,7 +1347,7 @@ OPENAPI_31 = {
|
|
|
1344
1347
|
},
|
|
1345
1348
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}, "default": [{"url": "/"}]},
|
|
1346
1349
|
"paths": {"$ref": "#/$defs/paths"},
|
|
1347
|
-
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1350
|
+
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1348
1351
|
"components": {"$ref": "#/$defs/components"},
|
|
1349
1352
|
"security": {"type": "array", "items": {"$ref": "#/$defs/security-requirement"}},
|
|
1350
1353
|
"tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}},
|
|
@@ -1399,7 +1402,7 @@ OPENAPI_31 = {
|
|
|
1399
1402
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#server-object",
|
|
1400
1403
|
"type": "object",
|
|
1401
1404
|
"properties": {
|
|
1402
|
-
"url": {"type": "string"
|
|
1405
|
+
"url": {"type": "string"},
|
|
1403
1406
|
"description": {"type": "string"},
|
|
1404
1407
|
"variables": {"type": "object", "additionalProperties": {"$ref": "#/$defs/server-variable"}},
|
|
1405
1408
|
},
|
|
@@ -1438,7 +1441,7 @@ OPENAPI_31 = {
|
|
|
1438
1441
|
},
|
|
1439
1442
|
"links": {"type": "object", "additionalProperties": {"$ref": "#/$defs/link-or-reference"}},
|
|
1440
1443
|
"callbacks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/callbacks-or-reference"}},
|
|
1441
|
-
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
|
1444
|
+
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
|
1442
1445
|
},
|
|
1443
1446
|
"patternProperties": {
|
|
1444
1447
|
"^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": {
|
|
@@ -1460,6 +1463,7 @@ OPENAPI_31 = {
|
|
|
1460
1463
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object",
|
|
1461
1464
|
"type": "object",
|
|
1462
1465
|
"properties": {
|
|
1466
|
+
"$ref": {"type": "string", "format": "uri-reference"},
|
|
1463
1467
|
"summary": {"type": "string"},
|
|
1464
1468
|
"description": {"type": "string"},
|
|
1465
1469
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}},
|
|
@@ -1476,11 +1480,6 @@ OPENAPI_31 = {
|
|
|
1476
1480
|
"$ref": "#/$defs/specification-extensions",
|
|
1477
1481
|
"unevaluatedProperties": False,
|
|
1478
1482
|
},
|
|
1479
|
-
"path-item-or-reference": {
|
|
1480
|
-
"if": {"type": "object", "required": ["$ref"]},
|
|
1481
|
-
"then": {"$ref": "#/$defs/reference"},
|
|
1482
|
-
"else": {"$ref": "#/$defs/path-item"},
|
|
1483
|
-
},
|
|
1484
1483
|
"operation": {
|
|
1485
1484
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object",
|
|
1486
1485
|
"type": "object",
|
|
@@ -1541,7 +1540,6 @@ OPENAPI_31 = {
|
|
|
1541
1540
|
"if": {"properties": {"in": {"const": "path"}}, "required": ["in"]},
|
|
1542
1541
|
"then": {
|
|
1543
1542
|
"properties": {
|
|
1544
|
-
"name": {"pattern": "[^/#?]+$"},
|
|
1545
1543
|
"style": {"default": "simple", "enum": ["matrix", "label", "simple"]},
|
|
1546
1544
|
"required": {"const": True},
|
|
1547
1545
|
},
|
|
@@ -1661,7 +1659,7 @@ OPENAPI_31 = {
|
|
|
1661
1659
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object",
|
|
1662
1660
|
"type": "object",
|
|
1663
1661
|
"$ref": "#/$defs/specification-extensions",
|
|
1664
|
-
"additionalProperties": {"$ref": "#/$defs/path-item
|
|
1662
|
+
"additionalProperties": {"$ref": "#/$defs/path-item"},
|
|
1665
1663
|
},
|
|
1666
1664
|
"callbacks-or-reference": {
|
|
1667
1665
|
"if": {"type": "object", "required": ["$ref"]},
|
|
@@ -1754,7 +1752,6 @@ OPENAPI_31 = {
|
|
|
1754
1752
|
"summary": {"type": "string"},
|
|
1755
1753
|
"description": {"type": "string"},
|
|
1756
1754
|
},
|
|
1757
|
-
"unevaluatedProperties": False,
|
|
1758
1755
|
},
|
|
1759
1756
|
"schema": {
|
|
1760
1757
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
|
|
@@ -1906,11 +1903,7 @@ _VALIDATORS = [
|
|
|
1906
1903
|
"OPENAPI_31_VALIDATOR",
|
|
1907
1904
|
]
|
|
1908
1905
|
|
|
1909
|
-
__all__ = [
|
|
1910
|
-
"SWAGGER_20",
|
|
1911
|
-
"OPENAPI_30",
|
|
1912
|
-
"OPENAPI_31",
|
|
1913
|
-
] + _VALIDATORS
|
|
1906
|
+
__all__ = ["SWAGGER_20", "OPENAPI_30", "OPENAPI_31", *_VALIDATORS]
|
|
1914
1907
|
|
|
1915
1908
|
_imports = {
|
|
1916
1909
|
"SWAGGER_20_VALIDATOR": lambda: make_validator(SWAGGER_20),
|
|
@@ -3,19 +3,25 @@ from __future__ import annotations
|
|
|
3
3
|
from contextlib import suppress
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from functools import lru_cache
|
|
6
|
-
from itertools import
|
|
7
|
-
from typing import Any, Generator, Union, cast
|
|
6
|
+
from itertools import chain, cycle, islice
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
|
-
import hypothesis
|
|
11
10
|
from hypothesis_jsonschema import from_schema
|
|
12
|
-
from hypothesis.strategies import SearchStrategy
|
|
13
11
|
|
|
14
|
-
from .parameters import OpenAPIParameter, OpenAPIBody
|
|
15
12
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
|
16
|
-
from ...
|
|
17
|
-
from .
|
|
13
|
+
from ...generation import get_single_example
|
|
14
|
+
from ...internal.copy import fast_deepcopy
|
|
15
|
+
from ...models import APIOperation, Case, TestPhase
|
|
16
|
+
from ._hypothesis import get_case_strategy, get_default_format_strategies
|
|
18
17
|
from .constants import LOCATION_TO_CONTAINER
|
|
18
|
+
from .formats import STRING_FORMATS
|
|
19
|
+
from .parameters import OpenAPIBody, OpenAPIParameter
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from hypothesis.strategies import SearchStrategy
|
|
23
|
+
|
|
24
|
+
from ...generation import GenerationConfig
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
@dataclass
|
|
@@ -39,7 +45,7 @@ Example = Union[ParameterExample, BodyExample]
|
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
def get_strategies_from_examples(
|
|
42
|
-
operation: APIOperation[OpenAPIParameter, Case],
|
|
48
|
+
operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
|
|
43
49
|
) -> list[SearchStrategy[Case]]:
|
|
44
50
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
|
45
51
|
maps = {}
|
|
@@ -62,14 +68,17 @@ def get_strategies_from_examples(
|
|
|
62
68
|
examples = list(extract_top_level(operation))
|
|
63
69
|
# Add examples from parameter's schemas
|
|
64
70
|
examples.extend(extract_from_schemas(operation))
|
|
71
|
+
as_strategy_kwargs = as_strategy_kwargs or {}
|
|
72
|
+
as_strategy_kwargs["phase"] = TestPhase.EXPLICIT
|
|
65
73
|
return [
|
|
66
|
-
get_case_strategy(operation=operation, **parameters).map(serialize_components)
|
|
74
|
+
get_case_strategy(operation=operation, **{**parameters, **(as_strategy_kwargs or {})}).map(serialize_components)
|
|
67
75
|
for parameters in produce_combinations(examples)
|
|
68
76
|
]
|
|
69
77
|
|
|
70
78
|
|
|
71
79
|
def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
|
|
72
80
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
|
81
|
+
responses = find_in_responses(operation)
|
|
73
82
|
for parameter in operation.iter_parameters():
|
|
74
83
|
if "schema" in parameter.definition:
|
|
75
84
|
definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
|
|
@@ -99,6 +108,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
|
99
108
|
yield ParameterExample(
|
|
100
109
|
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
|
101
110
|
)
|
|
111
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
|
112
|
+
yield ParameterExample(
|
|
113
|
+
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
|
114
|
+
)
|
|
102
115
|
for alternative in operation.body:
|
|
103
116
|
alternative = cast(OpenAPIBody, alternative)
|
|
104
117
|
if "schema" in alternative.definition:
|
|
@@ -126,10 +139,26 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
|
126
139
|
def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
|
|
127
140
|
yield schema
|
|
128
141
|
if isinstance(schema, dict):
|
|
129
|
-
for key in ("anyOf", "oneOf"
|
|
142
|
+
for key in ("anyOf", "oneOf"):
|
|
130
143
|
if key in schema:
|
|
131
144
|
for subschema in schema[key]:
|
|
132
145
|
yield subschema
|
|
146
|
+
if "allOf" in schema:
|
|
147
|
+
subschema = fast_deepcopy(schema["allOf"][0])
|
|
148
|
+
for sub in schema["allOf"][1:]:
|
|
149
|
+
if isinstance(sub, dict):
|
|
150
|
+
for key, value in sub.items():
|
|
151
|
+
if key == "properties":
|
|
152
|
+
subschema.setdefault("properties", {}).update(value)
|
|
153
|
+
elif key == "required":
|
|
154
|
+
subschema.setdefault("required", []).extend(value)
|
|
155
|
+
elif key == "examples":
|
|
156
|
+
subschema.setdefault("examples", []).extend(value)
|
|
157
|
+
elif key == "example":
|
|
158
|
+
subschema.setdefault("examples", []).append(value)
|
|
159
|
+
else:
|
|
160
|
+
subschema[key] = value
|
|
161
|
+
yield subschema
|
|
133
162
|
|
|
134
163
|
|
|
135
164
|
def _find_parameter_examples_definition(
|
|
@@ -178,7 +207,7 @@ def extract_inner_examples(
|
|
|
178
207
|
) -> Generator[Any, None, None]:
|
|
179
208
|
"""Extract exact examples values from the `examples` dictionary."""
|
|
180
209
|
for name, example in examples.items():
|
|
181
|
-
if "$ref" in unresolved_definition[name]:
|
|
210
|
+
if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
|
|
182
211
|
# The example here is a resolved example and should be yielded as is
|
|
183
212
|
yield example
|
|
184
213
|
if isinstance(example, dict):
|
|
@@ -209,8 +238,9 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
|
|
|
209
238
|
for alternative in operation.body:
|
|
210
239
|
alternative = cast(OpenAPIBody, alternative)
|
|
211
240
|
schema = alternative.as_json_schema(operation)
|
|
212
|
-
for
|
|
213
|
-
|
|
241
|
+
for example_field, examples_field in (("example", "examples"), ("x-example", "x-examples")):
|
|
242
|
+
for value in extract_from_schema(operation, schema, example_field, examples_field):
|
|
243
|
+
yield BodyExample(value=value, media_type=alternative.media_type)
|
|
214
244
|
|
|
215
245
|
|
|
216
246
|
def extract_from_schema(
|
|
@@ -236,6 +266,8 @@ def extract_from_schema(
|
|
|
236
266
|
if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
|
|
237
267
|
# These are JSON Schema examples, which is an array of values
|
|
238
268
|
values.extend(subsubschema[examples_field_name])
|
|
269
|
+
# Check nested examples as well
|
|
270
|
+
values.extend(extract_from_schema(operation, subsubschema, example_field_name, examples_field_name))
|
|
239
271
|
if not values:
|
|
240
272
|
if name in required:
|
|
241
273
|
# Defer generation to only generate these variants if at least one property has examples
|
|
@@ -248,7 +280,7 @@ def extract_from_schema(
|
|
|
248
280
|
# Generated by one of `anyOf` or similar sub-schemas
|
|
249
281
|
continue
|
|
250
282
|
subschema = operation.schema.prepare_schema(subschema)
|
|
251
|
-
generated = _generate_single_example(subschema)
|
|
283
|
+
generated = _generate_single_example(subschema, operation.schema.generation_config)
|
|
252
284
|
variants[name] = [generated]
|
|
253
285
|
# Calculate the maximum number of examples any property has
|
|
254
286
|
total_combos = max(len(examples) for examples in variants.values())
|
|
@@ -264,24 +296,17 @@ def extract_from_schema(
|
|
|
264
296
|
yield [value]
|
|
265
297
|
|
|
266
298
|
|
|
267
|
-
def _generate_single_example(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
phases=(hypothesis.Phase.generate,),
|
|
277
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
|
299
|
+
def _generate_single_example(
|
|
300
|
+
schema: dict[str, Any],
|
|
301
|
+
generation_config: GenerationConfig,
|
|
302
|
+
) -> Any:
|
|
303
|
+
strategy = from_schema(
|
|
304
|
+
schema,
|
|
305
|
+
custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
|
|
306
|
+
allow_x00=generation_config.allow_x00,
|
|
307
|
+
codec=generation_config.codec,
|
|
278
308
|
)
|
|
279
|
-
|
|
280
|
-
examples.append(ex)
|
|
281
|
-
|
|
282
|
-
example_generating_inner_function()
|
|
283
|
-
|
|
284
|
-
return examples[0]
|
|
309
|
+
return get_single_example(strategy)
|
|
285
310
|
|
|
286
311
|
|
|
287
312
|
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
|
@@ -330,3 +355,90 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
|
|
|
330
355
|
}
|
|
331
356
|
for container, variants in parameters.items()
|
|
332
357
|
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
|
|
361
|
+
"""Find schema examples in responses."""
|
|
362
|
+
output: dict[str, list[dict[str, Any]]] = {}
|
|
363
|
+
for status_code, response in operation.definition.raw.get("responses", {}).items():
|
|
364
|
+
if not str(status_code).startswith("2"):
|
|
365
|
+
# Check only 2xx responses
|
|
366
|
+
continue
|
|
367
|
+
if isinstance(response, dict) and "$ref" in response:
|
|
368
|
+
_, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
|
|
369
|
+
for media_type, definition in response.get("content", {}).items():
|
|
370
|
+
schema_ref = definition.get("schema", {}).get("$ref")
|
|
371
|
+
if schema_ref:
|
|
372
|
+
name = schema_ref.split("/")[-1]
|
|
373
|
+
else:
|
|
374
|
+
name = f"{status_code}/{media_type}"
|
|
375
|
+
for examples_field, example_field in (
|
|
376
|
+
("examples", "example"),
|
|
377
|
+
("x-examples", "x-example"),
|
|
378
|
+
):
|
|
379
|
+
examples = definition.get(examples_field, {})
|
|
380
|
+
for example in examples.values():
|
|
381
|
+
if "value" in example:
|
|
382
|
+
output.setdefault(name, []).append(example["value"])
|
|
383
|
+
if example_field in definition:
|
|
384
|
+
output.setdefault(name, []).append(definition[example_field])
|
|
385
|
+
return output
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
NOT_FOUND = object()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
|
|
392
|
+
"""Find matching parameter examples."""
|
|
393
|
+
normalized = param.lower()
|
|
394
|
+
is_id_param = normalized.endswith("id")
|
|
395
|
+
# Extract values from response examples that match input parameters.
|
|
396
|
+
# E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
|
|
397
|
+
# as examples for the "id" path parameter.
|
|
398
|
+
for schema_name, schema_examples in examples.items():
|
|
399
|
+
for example in schema_examples:
|
|
400
|
+
if not isinstance(example, dict):
|
|
401
|
+
continue
|
|
402
|
+
# Unwrapping example from `{"item": [{...}]}`
|
|
403
|
+
if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
|
|
404
|
+
inner = list(example.values())[0]
|
|
405
|
+
if isinstance(inner, list):
|
|
406
|
+
for sub_example in inner:
|
|
407
|
+
found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
|
|
408
|
+
if found is not NOT_FOUND:
|
|
409
|
+
yield found
|
|
410
|
+
continue
|
|
411
|
+
if isinstance(inner, dict):
|
|
412
|
+
example = inner
|
|
413
|
+
found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
|
|
414
|
+
if found is not NOT_FOUND:
|
|
415
|
+
yield found
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _find_matching_in_responses(
|
|
419
|
+
example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
|
|
420
|
+
) -> Any:
|
|
421
|
+
# Check for exact match
|
|
422
|
+
if param in example:
|
|
423
|
+
return example[param]
|
|
424
|
+
if is_id_param and param[:-2] in example:
|
|
425
|
+
return example[param[:-2]]
|
|
426
|
+
|
|
427
|
+
# Check for case-insensitive match
|
|
428
|
+
for key in example:
|
|
429
|
+
if key.lower() == normalized:
|
|
430
|
+
return example[key]
|
|
431
|
+
else:
|
|
432
|
+
# If no match found and it's an ID parameter, try additional checks
|
|
433
|
+
if is_id_param:
|
|
434
|
+
# Check for 'id' if parameter is '{something}Id'
|
|
435
|
+
if "id" in example:
|
|
436
|
+
return example["id"]
|
|
437
|
+
# Check for '{schemaName}Id' or '{schemaName}_id'
|
|
438
|
+
if normalized == "id" or normalized.startswith(schema_name.lower()):
|
|
439
|
+
for key in (schema_name, schema_name.lower()):
|
|
440
|
+
for suffix in ("_id", "Id"):
|
|
441
|
+
with_suffix = f"{key}{suffix}"
|
|
442
|
+
if with_suffix in example:
|
|
443
|
+
return example[with_suffix]
|
|
444
|
+
return NOT_FOUND
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
https://swagger.io/docs/specification/links/#runtime-expressions
|
|
4
4
|
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
5
9
|
from typing import Any
|
|
6
10
|
|
|
7
11
|
from . import lexer, nodes, parser
|
|
8
12
|
from .context import ExpressionContext
|
|
9
13
|
|
|
14
|
+
__all__ = [
|
|
15
|
+
"lexer",
|
|
16
|
+
"nodes",
|
|
17
|
+
"parser",
|
|
18
|
+
"ExpressionContext",
|
|
19
|
+
]
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
|
|
22
|
+
def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
|
|
12
23
|
"""Evaluate runtime expression in context."""
|
|
24
|
+
if isinstance(expr, (dict, list)) and evaluate_nested:
|
|
25
|
+
return _evaluate_nested(expr, context)
|
|
13
26
|
if not isinstance(expr, str):
|
|
14
27
|
# Can be a non-string constant
|
|
15
28
|
return expr
|
|
@@ -17,4 +30,26 @@ def evaluate(expr: Any, context: ExpressionContext) -> str:
|
|
|
17
30
|
if len(parts) == 1:
|
|
18
31
|
return parts[0] # keep the return type the same as the internal value type
|
|
19
32
|
# otherwise, concatenate into a string
|
|
20
|
-
return "".join(
|
|
33
|
+
return "".join(str(part) for part in parts if part is not None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _evaluate_nested(expr: dict[str, Any] | list, context: ExpressionContext) -> Any:
|
|
37
|
+
if isinstance(expr, dict):
|
|
38
|
+
return {
|
|
39
|
+
_evaluate_object_key(key, context): evaluate(value, context, evaluate_nested=True)
|
|
40
|
+
for key, value in expr.items()
|
|
41
|
+
}
|
|
42
|
+
return [evaluate(item, context, evaluate_nested=True) for item in expr]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _evaluate_object_key(key: str, context: ExpressionContext) -> Any:
|
|
46
|
+
evaluated = evaluate(key, context)
|
|
47
|
+
if isinstance(evaluated, str):
|
|
48
|
+
return evaluated
|
|
49
|
+
if isinstance(evaluated, bool):
|
|
50
|
+
return "true" if evaluated else "false"
|
|
51
|
+
if isinstance(evaluated, (int, float)):
|
|
52
|
+
return str(evaluated)
|
|
53
|
+
if evaluated is None:
|
|
54
|
+
return "null"
|
|
55
|
+
return json.dumps(evaluated)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Extractor:
|
|
12
|
+
def extract(self, value: str) -> str | None:
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RegexExtractor(Extractor):
|
|
18
|
+
"""Extract value via a regex."""
|
|
19
|
+
|
|
20
|
+
value: re.Pattern
|
|
21
|
+
|
|
22
|
+
def extract(self, value: str) -> str | None:
|
|
23
|
+
match = self.value.search(value)
|
|
24
|
+
if match is None:
|
|
25
|
+
return None
|
|
26
|
+
return match.group(1)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Lexical analysis of runtime expressions."""
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from enum import Enum, unique
|
|
4
5
|
from typing import Callable, Generator
|
|
@@ -19,33 +20,34 @@ class Token:
|
|
|
19
20
|
"""Lexical token that may occur in a runtime expression."""
|
|
20
21
|
|
|
21
22
|
value: str
|
|
23
|
+
end: int
|
|
22
24
|
type_: TokenType
|
|
23
25
|
|
|
24
26
|
# Helpers for cleaner instantiation
|
|
25
27
|
|
|
26
28
|
@classmethod
|
|
27
|
-
def variable(cls, value: str) -> "Token":
|
|
28
|
-
return cls(value, TokenType.VARIABLE)
|
|
29
|
+
def variable(cls, value: str, end: int) -> "Token":
|
|
30
|
+
return cls(value, end, TokenType.VARIABLE)
|
|
29
31
|
|
|
30
32
|
@classmethod
|
|
31
|
-
def string(cls, value: str) -> "Token":
|
|
32
|
-
return cls(value, TokenType.STRING)
|
|
33
|
+
def string(cls, value: str, end: int) -> "Token":
|
|
34
|
+
return cls(value, end, TokenType.STRING)
|
|
33
35
|
|
|
34
36
|
@classmethod
|
|
35
|
-
def pointer(cls, value: str) -> "Token":
|
|
36
|
-
return cls(value, TokenType.POINTER)
|
|
37
|
+
def pointer(cls, value: str, end: int) -> "Token":
|
|
38
|
+
return cls(value, end, TokenType.POINTER)
|
|
37
39
|
|
|
38
40
|
@classmethod
|
|
39
|
-
def lbracket(cls) -> "Token":
|
|
40
|
-
return cls("{", TokenType.LBRACKET)
|
|
41
|
+
def lbracket(cls, end: int) -> "Token":
|
|
42
|
+
return cls("{", end, TokenType.LBRACKET)
|
|
41
43
|
|
|
42
44
|
@classmethod
|
|
43
|
-
def rbracket(cls) -> "Token":
|
|
44
|
-
return cls("}", TokenType.RBRACKET)
|
|
45
|
+
def rbracket(cls, end: int) -> "Token":
|
|
46
|
+
return cls("}", end, TokenType.RBRACKET)
|
|
45
47
|
|
|
46
48
|
@classmethod
|
|
47
|
-
def dot(cls) -> "Token":
|
|
48
|
-
return cls(".", TokenType.DOT)
|
|
49
|
+
def dot(cls, end: int) -> "Token":
|
|
50
|
+
return cls(".", end, TokenType.DOT)
|
|
49
51
|
|
|
50
52
|
# Helpers for simpler type comparison
|
|
51
53
|
|
|
@@ -102,15 +104,15 @@ def tokenize(expression: str) -> TokenGenerator:
|
|
|
102
104
|
if current_symbol() == "$":
|
|
103
105
|
start = cursor
|
|
104
106
|
move_until(lambda: is_eol() or current_symbol() in stop_symbols)
|
|
105
|
-
yield Token.variable(expression[start:cursor])
|
|
107
|
+
yield Token.variable(expression[start:cursor], cursor - 1)
|
|
106
108
|
elif current_symbol() == ".":
|
|
107
|
-
yield Token.dot()
|
|
109
|
+
yield Token.dot(cursor)
|
|
108
110
|
move()
|
|
109
111
|
elif current_symbol() == "{":
|
|
110
|
-
yield Token.lbracket()
|
|
112
|
+
yield Token.lbracket(cursor)
|
|
111
113
|
move()
|
|
112
114
|
elif current_symbol() == "}":
|
|
113
|
-
yield Token.rbracket()
|
|
115
|
+
yield Token.rbracket(cursor)
|
|
114
116
|
move()
|
|
115
117
|
elif current_symbol() == "#":
|
|
116
118
|
start = cursor
|
|
@@ -125,8 +127,8 @@ def tokenize(expression: str) -> TokenGenerator:
|
|
|
125
127
|
# `ID_{$response.body#/foo}_{$response.body#/bar}`
|
|
126
128
|
# Which is much easier if we treat `}` as a closing bracket of an embedded runtime expression
|
|
127
129
|
move_until(lambda: is_eol() or current_symbol() == "}")
|
|
128
|
-
yield Token.pointer(expression[start:cursor])
|
|
130
|
+
yield Token.pointer(expression[start:cursor], cursor - 1)
|
|
129
131
|
else:
|
|
130
132
|
start = cursor
|
|
131
133
|
move_until(lambda: is_eol() or current_symbol() in stop_symbols)
|
|
132
|
-
yield Token.string(expression[start:cursor])
|
|
134
|
+
yield Token.string(expression[start:cursor], cursor - 1)
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"""Expression nodes description and evaluation logic."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from enum import Enum, unique
|
|
5
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
6
8
|
|
|
7
9
|
from requests.structures import CaseInsensitiveDict
|
|
8
10
|
|
|
9
11
|
from .. import references
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .context import ExpressionContext
|
|
15
|
+
from .extractors import Extractor
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
@dataclass
|
|
@@ -73,6 +78,7 @@ class NonBodyRequest(Node):
|
|
|
73
78
|
|
|
74
79
|
location: str
|
|
75
80
|
parameter: str
|
|
81
|
+
extractor: Extractor | None = None
|
|
76
82
|
|
|
77
83
|
def evaluate(self, context: ExpressionContext) -> str:
|
|
78
84
|
container: dict | CaseInsensitiveDict = {
|
|
@@ -82,7 +88,12 @@ class NonBodyRequest(Node):
|
|
|
82
88
|
}[self.location] or {}
|
|
83
89
|
if self.location == "header":
|
|
84
90
|
container = CaseInsensitiveDict(container)
|
|
85
|
-
|
|
91
|
+
value = container.get(self.parameter)
|
|
92
|
+
if value is None:
|
|
93
|
+
return ""
|
|
94
|
+
if self.extractor is not None:
|
|
95
|
+
return self.extractor.extract(value) or ""
|
|
96
|
+
return value
|
|
86
97
|
|
|
87
98
|
|
|
88
99
|
@dataclass
|
|
@@ -95,7 +106,10 @@ class BodyRequest(Node):
|
|
|
95
106
|
document = context.case.body
|
|
96
107
|
if self.pointer is None:
|
|
97
108
|
return document
|
|
98
|
-
|
|
109
|
+
resolved = references.resolve_pointer(document, self.pointer[1:])
|
|
110
|
+
if resolved is references.UNRESOLVABLE:
|
|
111
|
+
return None
|
|
112
|
+
return resolved
|
|
99
113
|
|
|
100
114
|
|
|
101
115
|
@dataclass
|
|
@@ -103,9 +117,15 @@ class HeaderResponse(Node):
|
|
|
103
117
|
"""A node for `$response.header` expressions."""
|
|
104
118
|
|
|
105
119
|
parameter: str
|
|
120
|
+
extractor: Extractor | None = None
|
|
106
121
|
|
|
107
122
|
def evaluate(self, context: ExpressionContext) -> str:
|
|
108
|
-
|
|
123
|
+
value = context.response.headers.get(self.parameter)
|
|
124
|
+
if value is None:
|
|
125
|
+
return ""
|
|
126
|
+
if self.extractor is not None:
|
|
127
|
+
return self.extractor.extract(value) or ""
|
|
128
|
+
return value
|
|
109
129
|
|
|
110
130
|
|
|
111
131
|
@dataclass
|
|
@@ -124,4 +144,7 @@ class BodyResponse(Node):
|
|
|
124
144
|
if self.pointer is None:
|
|
125
145
|
# We need the parsed document - data will be serialized before sending to the application
|
|
126
146
|
return document
|
|
127
|
-
|
|
147
|
+
resolved = references.resolve_pointer(document, self.pointer[1:])
|
|
148
|
+
if resolved is references.UNRESOLVABLE:
|
|
149
|
+
return None
|
|
150
|
+
return resolved
|