schemathesis 3.13.0__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -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 +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -1,136 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from contextlib import suppress
|
|
4
|
+
from dataclasses import dataclass
|
|
2
5
|
from functools import lru_cache
|
|
3
|
-
from
|
|
6
|
+
from itertools import cycle, islice
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast, overload
|
|
4
8
|
|
|
5
9
|
import requests
|
|
6
|
-
from
|
|
10
|
+
from hypothesis_jsonschema import from_schema
|
|
7
11
|
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from .
|
|
11
|
-
from .
|
|
12
|
+
from schemathesis.config import GenerationConfig
|
|
13
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
|
14
|
+
from schemathesis.core.errors import InfiniteRecursiveReference, UnresolvableReference
|
|
15
|
+
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
|
16
|
+
from schemathesis.core.transforms import deepclone
|
|
17
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
|
18
|
+
from schemathesis.generation.case import Case
|
|
19
|
+
from schemathesis.generation.hypothesis import examples
|
|
20
|
+
from schemathesis.generation.meta import TestPhase
|
|
21
|
+
from schemathesis.schemas import APIOperation
|
|
22
|
+
from schemathesis.specs.openapi.adapter import OpenApiResponses
|
|
23
|
+
from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameter
|
|
24
|
+
from schemathesis.specs.openapi.adapter.security import OpenApiSecurityParameters
|
|
25
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
|
12
26
|
|
|
27
|
+
from ._hypothesis import get_default_format_strategies, openapi_cases
|
|
28
|
+
from .formats import STRING_FORMATS
|
|
13
29
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
prop_name: prop["example"]
|
|
17
|
-
for prop_name, prop in object_schema.get("properties", {}).items()
|
|
18
|
-
if "example" in prop
|
|
19
|
-
}
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from hypothesis.strategies import SearchStrategy
|
|
20
32
|
|
|
33
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
21
34
|
|
|
22
|
-
@lru_cache()
|
|
23
|
-
def load_external_example(url: str) -> bytes:
|
|
24
|
-
"""Load examples the `externalValue` keyword."""
|
|
25
|
-
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
|
|
26
|
-
response.raise_for_status()
|
|
27
|
-
return response.content
|
|
28
35
|
|
|
36
|
+
@dataclass
|
|
37
|
+
class ParameterExample:
|
|
38
|
+
"""A single example for a named parameter."""
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if isinstance(example, dict):
|
|
34
|
-
if "value" in example:
|
|
35
|
-
yield example["value"]
|
|
36
|
-
elif "externalValue" in example:
|
|
37
|
-
with suppress(requests.RequestException):
|
|
38
|
-
# Report a warning if not available?
|
|
39
|
-
yield load_external_example(example["externalValue"])
|
|
40
|
+
container: str
|
|
41
|
+
name: str
|
|
42
|
+
value: Any
|
|
40
43
|
|
|
44
|
+
__slots__ = ("container", "name", "value")
|
|
41
45
|
|
|
42
|
-
def get_parameter_examples(operation_definition: Dict[str, Any], examples_field: str) -> List[Dict[str, Any]]:
|
|
43
|
-
"""Gets parameter examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
|
|
44
|
-
return [
|
|
45
|
-
{
|
|
46
|
-
"type": LOCATION_TO_CONTAINER.get(parameter["in"]),
|
|
47
|
-
"name": parameter["name"],
|
|
48
|
-
"examples": list(get_examples(parameter[examples_field])),
|
|
49
|
-
}
|
|
50
|
-
for parameter in operation_definition.get("parameters", [])
|
|
51
|
-
if examples_field in parameter
|
|
52
|
-
]
|
|
53
46
|
|
|
47
|
+
@dataclass
|
|
48
|
+
class BodyExample:
|
|
49
|
+
"""A single example for a body."""
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
else:
|
|
67
|
-
# swagger 2 body and formData parameters should not include parameter names
|
|
68
|
-
static_parameters[parameter_type] = example
|
|
69
|
-
return static_parameters
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def get_request_body_examples(operation_definition: Dict[str, Any], examples_field: str) -> Dict[str, Any]:
|
|
73
|
-
"""Gets request body examples from OAS3 `examples` keyword or `x-examples` for Swagger 2."""
|
|
74
|
-
# NOTE. `requestBody` is OAS3-specific. How should it work with OAS2?
|
|
75
|
-
request_bodies_items = operation_definition.get("requestBody", {}).get("content", {}).items()
|
|
76
|
-
if not request_bodies_items:
|
|
77
|
-
return {}
|
|
78
|
-
# first element in tuple is media type, second element is dict
|
|
79
|
-
_, schema = next(iter(request_bodies_items))
|
|
80
|
-
examples = schema.get(examples_field, {})
|
|
81
|
-
return {
|
|
82
|
-
"type": "body",
|
|
83
|
-
"examples": list(get_examples(examples)),
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def get_request_body_example_from_properties(operation_definition: Dict[str, Any]) -> Dict[str, Any]:
|
|
88
|
-
static_parameters: Dict[str, Any] = {}
|
|
89
|
-
request_bodies_items = operation_definition.get("requestBody", {}).get("content", {}).items()
|
|
90
|
-
if request_bodies_items:
|
|
91
|
-
_, request_body_schema = next(iter(request_bodies_items))
|
|
92
|
-
example = get_object_example_from_properties(request_body_schema.get("schema", {}))
|
|
93
|
-
if example:
|
|
94
|
-
static_parameters["body"] = example
|
|
95
|
-
|
|
96
|
-
return static_parameters
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def get_static_parameters_from_example(operation: APIOperation) -> Dict[str, Any]:
|
|
100
|
-
static_parameters = {}
|
|
101
|
-
for name in PARAMETERS:
|
|
102
|
-
parameters = getattr(operation, name)
|
|
103
|
-
example = parameters.example
|
|
104
|
-
if example:
|
|
105
|
-
static_parameters[name] = example
|
|
106
|
-
return static_parameters
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def get_static_parameters_from_examples(operation: APIOperation, examples_field: str) -> List[Dict[str, Any]]:
|
|
110
|
-
"""Get static parameters from OpenAPI examples keyword."""
|
|
111
|
-
operation_definition = operation.definition.resolved
|
|
112
|
-
return merge_examples(
|
|
113
|
-
get_parameter_examples(operation_definition, examples_field),
|
|
114
|
-
get_request_body_examples(operation_definition, examples_field),
|
|
115
|
-
)
|
|
51
|
+
value: Any
|
|
52
|
+
media_type: str
|
|
53
|
+
|
|
54
|
+
__slots__ = ("value", "media_type")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Example = Union[ParameterExample, BodyExample]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def merge_kwargs(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
|
|
61
|
+
mergeable_keys = {"path_parameters", "headers", "cookies", "query"}
|
|
116
62
|
|
|
63
|
+
for key, value in right.items():
|
|
64
|
+
if key in mergeable_keys and key in left:
|
|
65
|
+
if isinstance(left[key], dict) and isinstance(value, dict):
|
|
66
|
+
# kwargs takes precedence
|
|
67
|
+
left[key] = {**left[key], **value}
|
|
68
|
+
continue
|
|
69
|
+
left[key] = value
|
|
117
70
|
|
|
118
|
-
|
|
119
|
-
operation_definition = operation.definition.resolved
|
|
120
|
-
return {
|
|
121
|
-
**get_parameter_example_from_properties(operation_definition),
|
|
122
|
-
**get_request_body_example_from_properties(operation_definition),
|
|
123
|
-
}
|
|
71
|
+
return left
|
|
124
72
|
|
|
125
73
|
|
|
126
74
|
def get_strategies_from_examples(
|
|
127
|
-
operation: APIOperation,
|
|
128
|
-
) ->
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
serializer = operation.get_parameter_serializer(location)
|
|
132
|
-
if serializer is not None:
|
|
133
|
-
maps[container] = serializer
|
|
75
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters], **kwargs: Any
|
|
76
|
+
) -> list[SearchStrategy[Case]]:
|
|
77
|
+
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
|
78
|
+
maps = get_serializers_for_operation(operation)
|
|
134
79
|
|
|
135
80
|
def serialize_components(case: Case) -> Case:
|
|
136
81
|
"""Applies special serialization rules for case components.
|
|
@@ -142,64 +87,535 @@ def get_strategies_from_examples(
|
|
|
142
87
|
setattr(case, container, map_func(value))
|
|
143
88
|
return case
|
|
144
89
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
90
|
+
# Extract all top-level examples from the `examples` & `example` fields (`x-` prefixed versions in Open API 2)
|
|
91
|
+
examples = list(extract_top_level(operation))
|
|
92
|
+
# Add examples from parameter's schemas
|
|
93
|
+
examples.extend(extract_from_schemas(operation))
|
|
94
|
+
return [
|
|
95
|
+
openapi_cases(operation=operation, phase=TestPhase.EXAMPLES, **merge_kwargs(parameters, kwargs)).map(
|
|
96
|
+
serialize_components
|
|
97
|
+
)
|
|
98
|
+
for parameters in produce_combinations(examples)
|
|
149
99
|
]
|
|
150
|
-
for static_parameters in static_parameters_union(
|
|
151
|
-
get_static_parameters_from_example(operation), get_static_parameters_from_properties(operation)
|
|
152
|
-
):
|
|
153
|
-
strategies.append(get_case_strategy(operation=operation, **static_parameters).map(serialize_components))
|
|
154
|
-
return strategies
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def merge_examples(
|
|
158
|
-
parameter_examples: List[Dict[str, Any]], request_body_examples: Dict[str, Any]
|
|
159
|
-
) -> List[Dict[str, Any]]:
|
|
160
|
-
"""Create list of static parameter objects from the parameter and request body examples."""
|
|
161
|
-
static_parameter_list = []
|
|
162
|
-
for idx in range(num_examples(parameter_examples, request_body_examples)):
|
|
163
|
-
static_parameters: Dict[str, Any] = {}
|
|
164
|
-
for parameter in parameter_examples:
|
|
165
|
-
container = static_parameters.setdefault(parameter["type"], {})
|
|
166
|
-
container[parameter["name"]] = parameter["examples"][min(idx, len(parameter["examples"]) - 1)]
|
|
167
|
-
if "examples" in request_body_examples and request_body_examples["examples"]:
|
|
168
|
-
static_parameters[request_body_examples["type"]] = request_body_examples["examples"][
|
|
169
|
-
min(idx, len(request_body_examples["examples"]) - 1)
|
|
170
|
-
]
|
|
171
|
-
static_parameter_list.append(static_parameters)
|
|
172
|
-
return static_parameter_list
|
|
173
100
|
|
|
174
101
|
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
102
|
+
def extract_top_level(
|
|
103
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
|
104
|
+
) -> Generator[Example, None, None]:
|
|
105
|
+
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
|
106
|
+
from .schemas import BaseOpenAPISchema
|
|
107
|
+
|
|
108
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
|
109
|
+
|
|
110
|
+
responses = list(operation.responses.iter_examples())
|
|
111
|
+
for parameter in operation.iter_parameters():
|
|
112
|
+
if "schema" in parameter.definition:
|
|
113
|
+
schema = parameter.definition["schema"]
|
|
114
|
+
resolver = RefResolver.from_schema(schema)
|
|
115
|
+
reference_path: tuple[str, ...] = ()
|
|
116
|
+
definitions = [
|
|
117
|
+
parameter.definition,
|
|
118
|
+
*[
|
|
119
|
+
expanded_schema
|
|
120
|
+
for expanded_schema, _ in _expand_subschemas(
|
|
121
|
+
schema=schema, resolver=resolver, reference_path=reference_path
|
|
122
|
+
)
|
|
123
|
+
],
|
|
124
|
+
]
|
|
125
|
+
else:
|
|
126
|
+
definitions = [parameter.definition]
|
|
127
|
+
for definition in definitions:
|
|
128
|
+
# Open API 2 also supports `example`
|
|
129
|
+
for example_keyword in {"example", parameter.adapter.example_keyword}:
|
|
130
|
+
if isinstance(definition, dict) and example_keyword in definition:
|
|
131
|
+
yield ParameterExample(
|
|
132
|
+
container=parameter.location.container_name,
|
|
133
|
+
name=parameter.name,
|
|
134
|
+
value=definition[example_keyword],
|
|
135
|
+
)
|
|
136
|
+
if parameter.adapter.examples_container_keyword in parameter.definition:
|
|
137
|
+
for value in extract_inner_examples(
|
|
138
|
+
parameter.definition[parameter.adapter.examples_container_keyword], operation.schema
|
|
139
|
+
):
|
|
140
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
|
141
|
+
if "schema" in parameter.definition:
|
|
142
|
+
schema = parameter.definition["schema"]
|
|
143
|
+
resolver = RefResolver.from_schema(schema)
|
|
144
|
+
reference_path = ()
|
|
145
|
+
for expanded_schema, _ in _expand_subschemas(
|
|
146
|
+
schema=schema, resolver=resolver, reference_path=reference_path
|
|
147
|
+
):
|
|
148
|
+
if (
|
|
149
|
+
isinstance(expanded_schema, dict)
|
|
150
|
+
and parameter.adapter.examples_container_keyword in expanded_schema
|
|
151
|
+
):
|
|
152
|
+
for value in expanded_schema[parameter.adapter.examples_container_keyword]:
|
|
153
|
+
yield ParameterExample(
|
|
154
|
+
container=parameter.location.container_name, name=parameter.name, value=value
|
|
155
|
+
)
|
|
156
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
|
157
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
|
158
|
+
for alternative in operation.body:
|
|
159
|
+
body = cast(OpenApiBody, alternative)
|
|
160
|
+
if "schema" in body.definition:
|
|
161
|
+
schema = body.definition["schema"]
|
|
162
|
+
resolver = RefResolver.from_schema(schema)
|
|
163
|
+
reference_path = ()
|
|
164
|
+
definitions = [
|
|
165
|
+
body.definition,
|
|
166
|
+
*[
|
|
167
|
+
expanded_schema
|
|
168
|
+
for expanded_schema, _ in _expand_subschemas(
|
|
169
|
+
schema=schema, resolver=resolver, reference_path=reference_path
|
|
170
|
+
)
|
|
171
|
+
],
|
|
172
|
+
]
|
|
173
|
+
else:
|
|
174
|
+
definitions = [body.definition]
|
|
175
|
+
for definition in definitions:
|
|
176
|
+
# Open API 2 also supports `example`
|
|
177
|
+
for example_keyword in {"example", body.adapter.example_keyword}:
|
|
178
|
+
if isinstance(definition, dict) and example_keyword in definition:
|
|
179
|
+
yield BodyExample(value=definition[example_keyword], media_type=body.media_type)
|
|
180
|
+
if body.adapter.examples_container_keyword in body.definition:
|
|
181
|
+
for value in extract_inner_examples(
|
|
182
|
+
body.definition[body.adapter.examples_container_keyword], operation.schema
|
|
183
|
+
):
|
|
184
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
|
185
|
+
if "schema" in body.definition:
|
|
186
|
+
schema = body.definition["schema"]
|
|
187
|
+
resolver = RefResolver.from_schema(schema)
|
|
188
|
+
reference_path = ()
|
|
189
|
+
for expanded_schema, _ in _expand_subschemas(
|
|
190
|
+
schema=schema, resolver=resolver, reference_path=reference_path
|
|
191
|
+
):
|
|
192
|
+
if isinstance(expanded_schema, dict) and body.adapter.examples_container_keyword in expanded_schema:
|
|
193
|
+
for value in expanded_schema[body.adapter.examples_container_keyword]:
|
|
194
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def _resolve_bundled(
|
|
199
|
+
schema: dict[str, Any], resolver: RefResolver, reference_path: tuple[str, ...]
|
|
200
|
+
) -> tuple[dict[str, Any], tuple[str, ...]]: ...
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@overload
|
|
204
|
+
def _resolve_bundled(
|
|
205
|
+
schema: bool, resolver: RefResolver, reference_path: tuple[str, ...]
|
|
206
|
+
) -> tuple[bool, tuple[str, ...]]: ...
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _resolve_bundled(
|
|
210
|
+
schema: dict[str, Any] | bool, resolver: RefResolver, reference_path: tuple[str, ...]
|
|
211
|
+
) -> tuple[dict[str, Any] | bool, tuple[str, ...]]:
|
|
212
|
+
"""Resolve $ref if present."""
|
|
213
|
+
if isinstance(schema, dict):
|
|
214
|
+
reference = schema.get("$ref")
|
|
215
|
+
if isinstance(reference, str):
|
|
216
|
+
# Check if this reference is already in the current path
|
|
217
|
+
if reference in reference_path:
|
|
218
|
+
# Real infinite recursive references are caught at the bundling stage.
|
|
219
|
+
# This recursion happens because of how the example phase generates data - it explores everything,
|
|
220
|
+
# so it is the easiest way to break such cycles
|
|
221
|
+
cycle = list(reference_path[reference_path.index(reference) :])
|
|
222
|
+
raise InfiniteRecursiveReference(reference, cycle)
|
|
223
|
+
|
|
224
|
+
new_path = reference_path + (reference,)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
_, resolved_schema = resolver.resolve(reference)
|
|
228
|
+
except RefResolutionError as exc:
|
|
229
|
+
raise UnresolvableReference(reference) from exc
|
|
230
|
+
|
|
231
|
+
return resolved_schema, new_path
|
|
232
|
+
|
|
233
|
+
return schema, reference_path
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _expand_subschemas(
|
|
237
|
+
*, schema: dict[str, Any] | bool, resolver: RefResolver, reference_path: tuple[str, ...]
|
|
238
|
+
) -> Generator[tuple[dict[str, Any] | bool, tuple[str, ...]], None, None]:
|
|
239
|
+
"""Expand schema and all its subschemas."""
|
|
240
|
+
try:
|
|
241
|
+
schema, current_path = _resolve_bundled(schema, resolver, reference_path)
|
|
242
|
+
except InfiniteRecursiveReference:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
yield schema, current_path
|
|
246
|
+
|
|
247
|
+
if isinstance(schema, dict):
|
|
248
|
+
# For anyOf/oneOf, yield each alternative with the same path
|
|
249
|
+
for key in ("anyOf", "oneOf"):
|
|
250
|
+
if key in schema:
|
|
251
|
+
for subschema in schema[key]:
|
|
252
|
+
# Each alternative starts with the current path
|
|
253
|
+
yield subschema, current_path
|
|
254
|
+
|
|
255
|
+
# For allOf, merge all alternatives
|
|
256
|
+
if schema.get("allOf"):
|
|
257
|
+
subschema = deepclone(schema["allOf"][0])
|
|
258
|
+
try:
|
|
259
|
+
subschema, expanded_path = _resolve_bundled(subschema, resolver, current_path)
|
|
260
|
+
except InfiniteRecursiveReference:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
for sub in schema["allOf"][1:]:
|
|
264
|
+
if isinstance(sub, dict):
|
|
265
|
+
try:
|
|
266
|
+
sub, _ = _resolve_bundled(sub, resolver, current_path)
|
|
267
|
+
except InfiniteRecursiveReference:
|
|
268
|
+
return
|
|
269
|
+
for key, value in sub.items():
|
|
270
|
+
if key == "properties":
|
|
271
|
+
subschema.setdefault("properties", {}).update(value)
|
|
272
|
+
elif key == "required":
|
|
273
|
+
subschema.setdefault("required", []).extend(value)
|
|
274
|
+
elif key == "examples":
|
|
275
|
+
subschema.setdefault("examples", []).extend(value)
|
|
276
|
+
elif key == "example":
|
|
277
|
+
subschema.setdefault("examples", []).append(value)
|
|
278
|
+
else:
|
|
279
|
+
subschema[key] = value
|
|
280
|
+
|
|
281
|
+
# Merge parent schema's fields with the merged allOf result
|
|
282
|
+
# Parent's fields take precedence as they are more specific
|
|
283
|
+
parent_has_example = "example" in schema or "examples" in schema
|
|
284
|
+
|
|
285
|
+
# If parent has examples, remove examples from merged allOf to avoid duplicates
|
|
286
|
+
# The parent's examples were already yielded from the parent schema itself
|
|
287
|
+
if parent_has_example:
|
|
288
|
+
subschema.pop("example", None)
|
|
289
|
+
subschema.pop("examples", None)
|
|
290
|
+
|
|
291
|
+
for key, value in schema.items():
|
|
292
|
+
if key == "allOf":
|
|
293
|
+
# Skip the allOf itself, we already processed it
|
|
294
|
+
continue
|
|
295
|
+
elif key in ("example", "examples"):
|
|
296
|
+
# Skip parent's examples - they were already yielded
|
|
297
|
+
continue
|
|
298
|
+
elif key == "properties":
|
|
299
|
+
# Merge parent properties (parent overrides allOf)
|
|
300
|
+
subschema.setdefault("properties", {}).update(value)
|
|
301
|
+
elif key == "required":
|
|
302
|
+
# Extend required list
|
|
303
|
+
subschema.setdefault("required", []).extend(value)
|
|
304
|
+
else:
|
|
305
|
+
# For other fields, parent value overrides
|
|
306
|
+
subschema[key] = value
|
|
307
|
+
|
|
308
|
+
yield subschema, expanded_path
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
|
|
312
|
+
"""Extract exact examples values from the `examples` dictionary."""
|
|
313
|
+
if isinstance(examples, dict):
|
|
314
|
+
for example in examples.values():
|
|
315
|
+
if isinstance(example, dict):
|
|
316
|
+
if "$ref" in example:
|
|
317
|
+
_, example = schema.resolver.resolve(example["$ref"])
|
|
318
|
+
if "value" in example:
|
|
319
|
+
yield example["value"]
|
|
320
|
+
elif "externalValue" in example:
|
|
321
|
+
with suppress(requests.RequestException):
|
|
322
|
+
# Report a warning if not available?
|
|
323
|
+
yield load_external_example(example["externalValue"])
|
|
324
|
+
elif example:
|
|
325
|
+
yield example
|
|
326
|
+
elif isinstance(examples, list):
|
|
327
|
+
yield from examples
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@lru_cache
|
|
331
|
+
def load_external_example(url: str) -> bytes:
|
|
332
|
+
"""Load examples the `externalValue` keyword."""
|
|
333
|
+
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
|
334
|
+
response.raise_for_status()
|
|
335
|
+
return response.content
|
|
179
336
|
|
|
180
337
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
338
|
+
def extract_from_schemas(
|
|
339
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
|
340
|
+
) -> Generator[Example, None, None]:
|
|
341
|
+
"""Extract examples from parameters' schema definitions."""
|
|
342
|
+
for parameter in operation.iter_parameters():
|
|
343
|
+
schema = parameter.optimized_schema
|
|
344
|
+
if isinstance(schema, bool):
|
|
345
|
+
continue
|
|
346
|
+
resolver = RefResolver.from_schema(schema)
|
|
347
|
+
reference_path: tuple[str, ...] = ()
|
|
348
|
+
bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
|
|
349
|
+
for value in extract_from_schema(
|
|
350
|
+
operation=operation,
|
|
351
|
+
schema=schema,
|
|
352
|
+
example_keyword=parameter.adapter.example_keyword,
|
|
353
|
+
examples_container_keyword=parameter.adapter.examples_container_keyword,
|
|
354
|
+
resolver=resolver,
|
|
355
|
+
reference_path=reference_path,
|
|
356
|
+
bundle_storage=bundle_storage,
|
|
357
|
+
):
|
|
358
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
|
359
|
+
for alternative in operation.body:
|
|
360
|
+
body = cast(OpenApiBody, alternative)
|
|
361
|
+
schema = body.optimized_schema
|
|
362
|
+
if isinstance(schema, bool):
|
|
363
|
+
continue
|
|
364
|
+
resolver = RefResolver.from_schema(schema)
|
|
365
|
+
bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
|
|
366
|
+
for example_keyword, examples_container_keyword in (("example", "examples"), ("x-example", "x-examples")):
|
|
367
|
+
reference_path = ()
|
|
368
|
+
for value in extract_from_schema(
|
|
369
|
+
operation=operation,
|
|
370
|
+
schema=schema,
|
|
371
|
+
example_keyword=example_keyword,
|
|
372
|
+
examples_container_keyword=examples_container_keyword,
|
|
373
|
+
resolver=resolver,
|
|
374
|
+
reference_path=reference_path,
|
|
375
|
+
bundle_storage=bundle_storage,
|
|
376
|
+
):
|
|
377
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def extract_from_schema(
|
|
381
|
+
*,
|
|
382
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
|
383
|
+
schema: dict[str, Any],
|
|
384
|
+
example_keyword: str,
|
|
385
|
+
examples_container_keyword: str,
|
|
386
|
+
resolver: RefResolver,
|
|
387
|
+
reference_path: tuple[str, ...],
|
|
388
|
+
bundle_storage: dict[str, Any] | None,
|
|
389
|
+
) -> Generator[Any, None, None]:
|
|
390
|
+
"""Extract all examples from a single schema definition."""
|
|
391
|
+
# This implementation supports only `properties` and `items`
|
|
392
|
+
try:
|
|
393
|
+
schema, current_path = _resolve_bundled(schema, resolver, reference_path)
|
|
394
|
+
except InfiniteRecursiveReference:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# If schema has allOf, we need to get merged properties from allOf items
|
|
398
|
+
# to extract property-level examples from all schemas, not just the parent
|
|
399
|
+
properties_to_process = schema.get("properties", {})
|
|
400
|
+
if "allOf" in schema and "properties" in schema:
|
|
401
|
+
# Get the merged allOf schema which includes properties from all allOf items
|
|
402
|
+
for expanded_schema, _ in _expand_subschemas(schema=schema, resolver=resolver, reference_path=current_path):
|
|
403
|
+
if expanded_schema is not schema and isinstance(expanded_schema, dict):
|
|
404
|
+
# This is the merged allOf result with combined properties
|
|
405
|
+
if "properties" in expanded_schema:
|
|
406
|
+
properties_to_process = expanded_schema["properties"]
|
|
407
|
+
break
|
|
408
|
+
|
|
409
|
+
if properties_to_process:
|
|
410
|
+
variants = {}
|
|
411
|
+
required = schema.get("required", [])
|
|
412
|
+
to_generate: dict[str, Any] = {}
|
|
413
|
+
|
|
414
|
+
for name, subschema in list(properties_to_process.items()):
|
|
415
|
+
values = []
|
|
416
|
+
for expanded_schema, expanded_path in _expand_subschemas(
|
|
417
|
+
schema=subschema, resolver=resolver, reference_path=current_path
|
|
418
|
+
):
|
|
419
|
+
if isinstance(expanded_schema, bool):
|
|
420
|
+
to_generate[name] = expanded_schema
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
if example_keyword in expanded_schema:
|
|
424
|
+
values.append(expanded_schema[example_keyword])
|
|
425
|
+
|
|
426
|
+
if examples_container_keyword in expanded_schema and isinstance(
|
|
427
|
+
expanded_schema[examples_container_keyword], list
|
|
428
|
+
):
|
|
429
|
+
# These are JSON Schema examples, which is an array of values
|
|
430
|
+
values.extend(expanded_schema[examples_container_keyword])
|
|
431
|
+
|
|
432
|
+
# Check nested examples as well
|
|
433
|
+
values.extend(
|
|
434
|
+
extract_from_schema(
|
|
435
|
+
operation=operation,
|
|
436
|
+
schema=expanded_schema,
|
|
437
|
+
example_keyword=example_keyword,
|
|
438
|
+
examples_container_keyword=examples_container_keyword,
|
|
439
|
+
resolver=resolver,
|
|
440
|
+
reference_path=expanded_path,
|
|
441
|
+
bundle_storage=bundle_storage,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
if not values:
|
|
446
|
+
if name in required:
|
|
447
|
+
# Defer generation to only generate these variants if at least one property has examples
|
|
448
|
+
to_generate[name] = expanded_schema
|
|
449
|
+
continue
|
|
450
|
+
|
|
451
|
+
variants[name] = values
|
|
452
|
+
|
|
453
|
+
if variants:
|
|
454
|
+
config = operation.schema.config.generation_for(operation=operation, phase="examples")
|
|
455
|
+
for name, subschema in to_generate.items():
|
|
456
|
+
if name in variants:
|
|
457
|
+
# Generated by one of `anyOf` or similar sub-schemas
|
|
458
|
+
continue
|
|
459
|
+
if bundle_storage is not None:
|
|
460
|
+
subschema = dict(subschema)
|
|
461
|
+
subschema[BUNDLE_STORAGE_KEY] = bundle_storage
|
|
462
|
+
generated = _generate_single_example(subschema, config)
|
|
463
|
+
variants[name] = [generated]
|
|
464
|
+
|
|
465
|
+
# Calculate the maximum number of examples any property has
|
|
466
|
+
total_combos = max(len(examples) for examples in variants.values())
|
|
467
|
+
# Evenly distribute examples by cycling through them
|
|
468
|
+
for idx in range(total_combos):
|
|
469
|
+
yield {
|
|
470
|
+
name: next(islice(cycle(property_variants), idx, None))
|
|
471
|
+
for name, property_variants in variants.items()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
elif "items" in schema and isinstance(schema["items"], dict):
|
|
475
|
+
# Each inner value should be wrapped in an array
|
|
476
|
+
for value in extract_from_schema(
|
|
477
|
+
operation=operation,
|
|
478
|
+
schema=schema["items"],
|
|
479
|
+
example_keyword=example_keyword,
|
|
480
|
+
examples_container_keyword=examples_container_keyword,
|
|
481
|
+
resolver=resolver,
|
|
482
|
+
reference_path=current_path,
|
|
483
|
+
bundle_storage=bundle_storage,
|
|
484
|
+
):
|
|
485
|
+
yield [value]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _generate_single_example(
|
|
489
|
+
schema: dict[str, Any],
|
|
490
|
+
generation_config: GenerationConfig,
|
|
491
|
+
) -> Any:
|
|
492
|
+
strategy = from_schema(
|
|
493
|
+
schema,
|
|
494
|
+
custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
|
|
495
|
+
allow_x00=generation_config.allow_x00,
|
|
496
|
+
codec=generation_config.codec,
|
|
497
|
+
)
|
|
498
|
+
return examples.generate_one(strategy)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
|
502
|
+
"""Generate a minimal set of combinations for the given list of parameters."""
|
|
503
|
+
# Split regular parameters & body variants first
|
|
504
|
+
parameters: dict[str, dict[str, list]] = {}
|
|
505
|
+
bodies: dict[str, list] = {}
|
|
506
|
+
for example in examples:
|
|
507
|
+
if isinstance(example, ParameterExample):
|
|
508
|
+
container_examples = parameters.setdefault(example.container, {})
|
|
509
|
+
parameter_examples = container_examples.setdefault(example.name, [])
|
|
510
|
+
parameter_examples.append(example.value)
|
|
511
|
+
else:
|
|
512
|
+
values = bodies.setdefault(example.media_type, [])
|
|
513
|
+
values.append(example.value)
|
|
514
|
+
|
|
515
|
+
if bodies:
|
|
516
|
+
if parameters:
|
|
517
|
+
parameter_combos = list(_produce_parameter_combinations(parameters))
|
|
518
|
+
body_combos = [
|
|
519
|
+
{"media_type": media_type, "body": value} for media_type, values in bodies.items() for value in values
|
|
520
|
+
]
|
|
521
|
+
total_combos = max(len(parameter_combos), len(body_combos))
|
|
522
|
+
for idx in range(total_combos):
|
|
523
|
+
yield {
|
|
524
|
+
**next(islice(cycle(body_combos), idx, None)),
|
|
525
|
+
**next(islice(cycle(parameter_combos), idx, None)),
|
|
526
|
+
}
|
|
527
|
+
else:
|
|
528
|
+
for media_type, values in bodies.items():
|
|
529
|
+
for body in values:
|
|
530
|
+
yield {"media_type": media_type, "body": body}
|
|
531
|
+
elif parameters:
|
|
532
|
+
yield from _produce_parameter_combinations(parameters)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> Generator[dict[str, Any], None, None]:
|
|
536
|
+
total_combos = max(
|
|
537
|
+
len(variants) for container_variants in parameters.values() for variants in container_variants.values()
|
|
538
|
+
)
|
|
539
|
+
for idx in range(total_combos):
|
|
540
|
+
yield {
|
|
541
|
+
container: {
|
|
542
|
+
name: next(islice(cycle(parameter_variants), idx, None))
|
|
543
|
+
for name, parameter_variants in variants.items()
|
|
544
|
+
}
|
|
545
|
+
for container, variants in parameters.items()
|
|
546
|
+
}
|
|
185
547
|
|
|
186
|
-
full_static_parameters: Dict[str, Any] = {**base_obj}
|
|
187
548
|
|
|
188
|
-
|
|
189
|
-
if parameter_type not in full_static_parameters:
|
|
190
|
-
full_static_parameters[parameter_type] = examples
|
|
191
|
-
elif parameter_type != "body":
|
|
192
|
-
# copy individual parameter names.
|
|
193
|
-
# body is unnamed, single examples, so we only do this for named parameters.
|
|
194
|
-
for parameter_name, example in examples.items():
|
|
195
|
-
if parameter_name not in full_static_parameters[parameter_type]:
|
|
196
|
-
full_static_parameters[parameter_type][parameter_name] = example
|
|
197
|
-
return full_static_parameters
|
|
549
|
+
NOT_FOUND = object()
|
|
198
550
|
|
|
199
551
|
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
552
|
+
def find_matching_in_responses(examples: list[tuple[str, object]], param: str) -> Iterator[Any]:
|
|
553
|
+
"""Find matching parameter examples."""
|
|
554
|
+
normalized = param.lower()
|
|
555
|
+
is_id_param = normalized.endswith("id")
|
|
556
|
+
# Extract values from response examples that match input parameters.
|
|
557
|
+
# E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
|
|
558
|
+
# as examples for the "id" path parameter.
|
|
559
|
+
for schema_name, example in examples:
|
|
560
|
+
if not isinstance(example, dict):
|
|
561
|
+
continue
|
|
562
|
+
# Unwrapping example from `{"item": [{...}]}`
|
|
563
|
+
if isinstance(example, dict):
|
|
564
|
+
inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
|
|
565
|
+
if inner is not None:
|
|
566
|
+
if isinstance(inner, list):
|
|
567
|
+
for sub_example in inner:
|
|
568
|
+
if isinstance(sub_example, dict):
|
|
569
|
+
for found in _find_matching_in_responses(
|
|
570
|
+
sub_example, schema_name, param, normalized, is_id_param
|
|
571
|
+
):
|
|
572
|
+
if found is not NOT_FOUND:
|
|
573
|
+
yield found
|
|
574
|
+
continue
|
|
575
|
+
if isinstance(inner, dict):
|
|
576
|
+
example = inner
|
|
577
|
+
for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
|
|
578
|
+
if found is not NOT_FOUND:
|
|
579
|
+
yield found
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _find_matching_in_responses(
|
|
583
|
+
example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
|
|
584
|
+
) -> Iterator[Any]:
|
|
585
|
+
# Check for exact match
|
|
586
|
+
if param in example:
|
|
587
|
+
yield example[param]
|
|
588
|
+
return
|
|
589
|
+
if is_id_param and param[:-2] in example:
|
|
590
|
+
value = example[param[:-2]]
|
|
591
|
+
if isinstance(value, list):
|
|
592
|
+
for sub_example in value:
|
|
593
|
+
for found in _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param):
|
|
594
|
+
if found is not NOT_FOUND:
|
|
595
|
+
yield found
|
|
596
|
+
return
|
|
597
|
+
else:
|
|
598
|
+
yield value
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Check for case-insensitive match
|
|
602
|
+
for key in example:
|
|
603
|
+
if key.lower() == normalized:
|
|
604
|
+
yield example[key]
|
|
605
|
+
return
|
|
606
|
+
else:
|
|
607
|
+
# If no match found and it's an ID parameter, try additional checks
|
|
608
|
+
if is_id_param:
|
|
609
|
+
# Check for 'id' if parameter is '{something}Id'
|
|
610
|
+
if "id" in example:
|
|
611
|
+
yield example["id"]
|
|
612
|
+
return
|
|
613
|
+
# Check for '{schemaName}Id' or '{schemaName}_id'
|
|
614
|
+
if normalized == "id" or normalized.startswith(schema_name.lower()):
|
|
615
|
+
for key in (schema_name, schema_name.lower()):
|
|
616
|
+
for suffix in ("_id", "Id"):
|
|
617
|
+
with_suffix = f"{key}{suffix}"
|
|
618
|
+
if with_suffix in example:
|
|
619
|
+
yield example[with_suffix]
|
|
620
|
+
return
|
|
621
|
+
yield NOT_FOUND
|