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
@@ -1,389 +0,0 @@
|
|
1
|
-
"""Open API links support.
|
2
|
-
|
3
|
-
Based on https://swagger.io/docs/specification/links/
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
from dataclasses import dataclass, field
|
9
|
-
from difflib import get_close_matches
|
10
|
-
from types import SimpleNamespace
|
11
|
-
from typing import TYPE_CHECKING, Any, Generator, Literal, NoReturn, Sequence, TypedDict, Union, cast
|
12
|
-
|
13
|
-
from ...constants import NOT_SET
|
14
|
-
from ...internal.copy import fast_deepcopy
|
15
|
-
from ...models import APIOperation, Case, TransitionId
|
16
|
-
from ...stateful import ParsedData, StatefulTest, UnresolvableLink
|
17
|
-
from ...stateful.state_machine import Direction
|
18
|
-
from . import expressions
|
19
|
-
from .constants import LOCATION_TO_CONTAINER
|
20
|
-
from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
|
21
|
-
from .references import RECURSION_DEPTH_LIMIT, Unresolvable
|
22
|
-
|
23
|
-
if TYPE_CHECKING:
|
24
|
-
from hypothesis.vendor.pretty import RepresentationPrinter
|
25
|
-
from jsonschema import RefResolver
|
26
|
-
|
27
|
-
from ...parameters import ParameterSet
|
28
|
-
from ...transports.responses import GenericResponse
|
29
|
-
from ...types import NotSet
|
30
|
-
|
31
|
-
|
32
|
-
@dataclass(repr=False)
|
33
|
-
class Link(StatefulTest):
|
34
|
-
operation: APIOperation
|
35
|
-
parameters: dict[str, Any]
|
36
|
-
request_body: Any = NOT_SET
|
37
|
-
merge_body: bool = True
|
38
|
-
|
39
|
-
def __post_init__(self) -> None:
|
40
|
-
if self.request_body is not NOT_SET and not self.operation.body:
|
41
|
-
# Link defines `requestBody` for a parameter that does not accept one
|
42
|
-
raise ValueError(
|
43
|
-
f"Request body is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
|
44
|
-
)
|
45
|
-
|
46
|
-
@classmethod
|
47
|
-
def from_definition(cls, name: str, definition: dict[str, dict[str, Any]], source_operation: APIOperation) -> Link:
|
48
|
-
# Links can be behind a reference
|
49
|
-
_, definition = source_operation.schema.resolver.resolve_in_scope( # type: ignore
|
50
|
-
definition, source_operation.definition.scope
|
51
|
-
)
|
52
|
-
if "operationId" in definition:
|
53
|
-
# source_operation.schema is `BaseOpenAPISchema` and has this method
|
54
|
-
operation = source_operation.schema.get_operation_by_id(definition["operationId"]) # type: ignore
|
55
|
-
else:
|
56
|
-
operation = source_operation.schema.get_operation_by_reference(definition["operationRef"]) # type: ignore
|
57
|
-
extension = definition.get(SCHEMATHESIS_LINK_EXTENSION)
|
58
|
-
return cls(
|
59
|
-
# Pylint can't detect that the API operation is always defined at this point
|
60
|
-
# E.g. if there is no matching operation or no operations at all, then a ValueError will be risen
|
61
|
-
name=name,
|
62
|
-
operation=operation,
|
63
|
-
parameters=definition.get("parameters", {}),
|
64
|
-
request_body=definition.get("requestBody", NOT_SET), # `None` might be a valid value - `null`
|
65
|
-
merge_body=extension.get("merge_body", True) if extension is not None else True,
|
66
|
-
)
|
67
|
-
|
68
|
-
def parse(self, case: Case, response: GenericResponse) -> ParsedData:
|
69
|
-
"""Parse data into a structure expected by links definition."""
|
70
|
-
context = expressions.ExpressionContext(case=case, response=response)
|
71
|
-
parameters = {}
|
72
|
-
for parameter, expression in self.parameters.items():
|
73
|
-
evaluated = expressions.evaluate(expression, context)
|
74
|
-
if isinstance(evaluated, Unresolvable):
|
75
|
-
raise UnresolvableLink(f"Unresolvable reference in the link: {expression}")
|
76
|
-
parameters[parameter] = evaluated
|
77
|
-
body = expressions.evaluate(self.request_body, context, evaluate_nested=True)
|
78
|
-
if self.merge_body:
|
79
|
-
body = merge_body(case.body, body)
|
80
|
-
return ParsedData(parameters=parameters, body=body)
|
81
|
-
|
82
|
-
def is_match(self) -> bool:
|
83
|
-
return self.operation.schema.filter_set.match(SimpleNamespace(operation=self.operation))
|
84
|
-
|
85
|
-
def make_operation(self, collected: list[ParsedData]) -> APIOperation:
|
86
|
-
"""Create a modified version of the original API operation with additional data merged in."""
|
87
|
-
# We split the gathered data among all locations & store the original parameter
|
88
|
-
containers = {
|
89
|
-
location: {
|
90
|
-
parameter.name: {"options": [], "parameter": parameter}
|
91
|
-
for parameter in getattr(self.operation, container_name)
|
92
|
-
}
|
93
|
-
for location, container_name in LOCATION_TO_CONTAINER.items()
|
94
|
-
}
|
95
|
-
# There might be duplicates in the data
|
96
|
-
for item in set(collected):
|
97
|
-
for name, value in item.parameters.items():
|
98
|
-
container = self._get_container_by_parameter_name(name, containers)
|
99
|
-
container.append(value)
|
100
|
-
if "body" in containers["body"] and item.body is not NOT_SET:
|
101
|
-
containers["body"]["body"]["options"].append(item.body)
|
102
|
-
# These are the final `path_parameters`, `query`, and other API operation components
|
103
|
-
components: dict[str, ParameterSet] = {
|
104
|
-
container_name: getattr(self.operation, container_name).__class__()
|
105
|
-
for location, container_name in LOCATION_TO_CONTAINER.items()
|
106
|
-
}
|
107
|
-
# Here are all components that are filled with parameters
|
108
|
-
for location, parameters in containers.items():
|
109
|
-
for parameter_data in parameters.values():
|
110
|
-
parameter = parameter_data["parameter"]
|
111
|
-
if parameter_data["options"]:
|
112
|
-
definition = fast_deepcopy(parameter.definition)
|
113
|
-
if "schema" in definition:
|
114
|
-
# The actual schema doesn't matter since we have a list of allowed values
|
115
|
-
definition["schema"] = {"enum": parameter_data["options"]}
|
116
|
-
else:
|
117
|
-
# Other schema-related keywords will be ignored later, during the canonicalisation step
|
118
|
-
# inside `hypothesis-jsonschema`
|
119
|
-
definition["enum"] = parameter_data["options"]
|
120
|
-
new_parameter: OpenAPIParameter
|
121
|
-
if isinstance(parameter, OpenAPI30Body):
|
122
|
-
new_parameter = parameter.__class__(
|
123
|
-
definition, media_type=parameter.media_type, required=parameter.required
|
124
|
-
)
|
125
|
-
elif isinstance(parameter, OpenAPI20Body):
|
126
|
-
new_parameter = parameter.__class__(definition, media_type=parameter.media_type)
|
127
|
-
else:
|
128
|
-
new_parameter = parameter.__class__(definition)
|
129
|
-
components[LOCATION_TO_CONTAINER[location]].add(new_parameter)
|
130
|
-
else:
|
131
|
-
# No options were gathered for this parameter - use the original one
|
132
|
-
components[LOCATION_TO_CONTAINER[location]].add(parameter)
|
133
|
-
return self.operation.clone(**components)
|
134
|
-
|
135
|
-
def _get_container_by_parameter_name(self, full_name: str, templates: dict[str, dict[str, dict[str, Any]]]) -> list:
|
136
|
-
"""Detect in what request part the parameters is defined."""
|
137
|
-
location: str | None
|
138
|
-
try:
|
139
|
-
# The parameter name is prefixed with its location. Example: `path.id`
|
140
|
-
location, name = full_name.split(".")
|
141
|
-
except ValueError:
|
142
|
-
location, name = None, full_name
|
143
|
-
if location:
|
144
|
-
try:
|
145
|
-
parameters = templates[location]
|
146
|
-
except KeyError:
|
147
|
-
self._unknown_parameter(full_name)
|
148
|
-
else:
|
149
|
-
for parameters in templates.values():
|
150
|
-
if name in parameters:
|
151
|
-
break
|
152
|
-
else:
|
153
|
-
self._unknown_parameter(full_name)
|
154
|
-
if not parameters:
|
155
|
-
self._unknown_parameter(full_name)
|
156
|
-
return parameters[name]["options"]
|
157
|
-
|
158
|
-
def _unknown_parameter(self, name: str) -> NoReturn:
|
159
|
-
raise ValueError(
|
160
|
-
f"Parameter `{name}` is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
|
161
|
-
)
|
162
|
-
|
163
|
-
|
164
|
-
def get_links(response: GenericResponse, operation: APIOperation, field: str) -> Sequence[Link]:
|
165
|
-
"""Get `x-links` / `links` definitions from the schema."""
|
166
|
-
responses = operation.definition.raw["responses"]
|
167
|
-
if str(response.status_code) in responses:
|
168
|
-
definition = responses[str(response.status_code)]
|
169
|
-
elif response.status_code in responses:
|
170
|
-
definition = responses[response.status_code]
|
171
|
-
else:
|
172
|
-
definition = responses.get("default", {})
|
173
|
-
if not definition:
|
174
|
-
return []
|
175
|
-
_, definition = operation.schema.resolver.resolve_in_scope(definition, operation.definition.scope) # type: ignore[attr-defined]
|
176
|
-
links = definition.get(field, {})
|
177
|
-
return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
|
178
|
-
|
179
|
-
|
180
|
-
SCHEMATHESIS_LINK_EXTENSION = "x-schemathesis"
|
181
|
-
|
182
|
-
|
183
|
-
class SchemathesisLink(TypedDict):
|
184
|
-
merge_body: bool
|
185
|
-
|
186
|
-
|
187
|
-
@dataclass(repr=False)
|
188
|
-
class OpenAPILink(Direction):
|
189
|
-
"""Alternative approach to link processing.
|
190
|
-
|
191
|
-
NOTE. This class will replace `Link` in the future.
|
192
|
-
"""
|
193
|
-
|
194
|
-
name: str
|
195
|
-
status_code: str
|
196
|
-
definition: dict[str, Any]
|
197
|
-
operation: APIOperation
|
198
|
-
parameters: list[tuple[Literal["path", "query", "header", "cookie", "body"] | None, str, str]] = field(init=False)
|
199
|
-
body: dict[str, Any] | NotSet = field(init=False)
|
200
|
-
merge_body: bool = True
|
201
|
-
|
202
|
-
def __repr__(self) -> str:
|
203
|
-
path = self.operation.path
|
204
|
-
method = self.operation.method
|
205
|
-
return f"state.schema['{path}']['{method}'].links['{self.status_code}']['{self.name}']"
|
206
|
-
|
207
|
-
def _repr_pretty_(self, printer: RepresentationPrinter, cycle: bool) -> None:
|
208
|
-
return printer.text(repr(self))
|
209
|
-
|
210
|
-
def __post_init__(self) -> None:
|
211
|
-
extension = self.definition.get(SCHEMATHESIS_LINK_EXTENSION)
|
212
|
-
self.parameters = [
|
213
|
-
normalize_parameter(parameter, expression)
|
214
|
-
for parameter, expression in self.definition.get("parameters", {}).items()
|
215
|
-
]
|
216
|
-
self.body = self.definition.get("requestBody", NOT_SET)
|
217
|
-
if extension is not None:
|
218
|
-
self.merge_body = extension.get("merge_body", True)
|
219
|
-
|
220
|
-
def set_data(self, case: Case, elapsed: float, **kwargs: Any) -> None:
|
221
|
-
"""Assign all linked definitions to the new case instance."""
|
222
|
-
context = kwargs["context"]
|
223
|
-
overrides = self.set_parameters(case, context)
|
224
|
-
self.set_body(case, context, overrides)
|
225
|
-
overrides_all_parameters = True
|
226
|
-
if case.operation.body and "body" not in overrides.get("body", []):
|
227
|
-
overrides_all_parameters = False
|
228
|
-
if overrides_all_parameters:
|
229
|
-
for parameter in case.operation.iter_parameters():
|
230
|
-
if parameter.name not in overrides.get(parameter.location, []):
|
231
|
-
overrides_all_parameters = False
|
232
|
-
break
|
233
|
-
case.set_source(
|
234
|
-
context.response,
|
235
|
-
context.case,
|
236
|
-
elapsed,
|
237
|
-
overrides_all_parameters,
|
238
|
-
transition_id=TransitionId(
|
239
|
-
name=self.name,
|
240
|
-
status_code=self.status_code,
|
241
|
-
),
|
242
|
-
)
|
243
|
-
|
244
|
-
def set_parameters(
|
245
|
-
self, case: Case, context: expressions.ExpressionContext
|
246
|
-
) -> dict[Literal["path", "query", "header", "cookie", "body"], list[str]]:
|
247
|
-
overrides: dict[Literal["path", "query", "header", "cookie", "body"], list[str]] = {}
|
248
|
-
for location, name, expression in self.parameters:
|
249
|
-
location, container = get_container(case, location, name)
|
250
|
-
# Might happen if there is directly specified container,
|
251
|
-
# but the schema has no parameters of such type at all.
|
252
|
-
# Therefore the container is empty, otherwise it will be at least an empty object
|
253
|
-
if container is None:
|
254
|
-
message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
|
255
|
-
possibilities = [param.name for param in case.operation.iter_parameters()]
|
256
|
-
matches = get_close_matches(name, possibilities)
|
257
|
-
if matches:
|
258
|
-
message += f" Did you mean `{matches[0]}`?"
|
259
|
-
raise ValueError(message)
|
260
|
-
value = expressions.evaluate(expression, context)
|
261
|
-
if value is not None:
|
262
|
-
container[name] = value
|
263
|
-
overrides.setdefault(location, []).append(name)
|
264
|
-
return overrides
|
265
|
-
|
266
|
-
def set_body(
|
267
|
-
self,
|
268
|
-
case: Case,
|
269
|
-
context: expressions.ExpressionContext,
|
270
|
-
overrides: dict[Literal["path", "query", "header", "cookie", "body"], list[str]],
|
271
|
-
) -> None:
|
272
|
-
if self.body is not NOT_SET:
|
273
|
-
evaluated = expressions.evaluate(self.body, context, evaluate_nested=True)
|
274
|
-
overrides["body"] = ["body"]
|
275
|
-
if self.merge_body:
|
276
|
-
case.body = merge_body(case.body, evaluated)
|
277
|
-
else:
|
278
|
-
case.body = evaluated
|
279
|
-
|
280
|
-
def get_target_operation(self) -> APIOperation:
|
281
|
-
if "operationId" in self.definition:
|
282
|
-
return self.operation.schema.get_operation_by_id(self.definition["operationId"]) # type: ignore
|
283
|
-
return self.operation.schema.get_operation_by_reference(self.definition["operationRef"]) # type: ignore
|
284
|
-
|
285
|
-
|
286
|
-
def merge_body(old: Any, new: Any) -> Any:
|
287
|
-
if isinstance(old, dict) and isinstance(new, dict):
|
288
|
-
return {**old, **new}
|
289
|
-
return new
|
290
|
-
|
291
|
-
|
292
|
-
def get_container(
|
293
|
-
case: Case, location: Literal["path", "query", "header", "cookie", "body"] | None, name: str
|
294
|
-
) -> tuple[Literal["path", "query", "header", "cookie", "body"], dict[str, Any] | None]:
|
295
|
-
"""Get a container that suppose to store the given parameter."""
|
296
|
-
if location:
|
297
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
298
|
-
else:
|
299
|
-
for param in case.operation.iter_parameters():
|
300
|
-
if param.name == name:
|
301
|
-
location = param.location
|
302
|
-
container_name = LOCATION_TO_CONTAINER[param.location]
|
303
|
-
break
|
304
|
-
else:
|
305
|
-
raise ValueError(f"Parameter `{name}` is not defined in API operation `{case.operation.verbose_name}`")
|
306
|
-
return location, getattr(case, container_name)
|
307
|
-
|
308
|
-
|
309
|
-
def normalize_parameter(
|
310
|
-
parameter: str, expression: str
|
311
|
-
) -> tuple[Literal["path", "query", "header", "cookie", "body"] | None, str, str]:
|
312
|
-
"""Normalize runtime expressions.
|
313
|
-
|
314
|
-
Runtime expressions may have parameter names prefixed with their location - `path.id`.
|
315
|
-
At the same time, parameters could be defined without a prefix - `id`.
|
316
|
-
We need to normalize all parameters to the same form to simplify working with them.
|
317
|
-
"""
|
318
|
-
try:
|
319
|
-
# The parameter name is prefixed with its location. Example: `path.id`
|
320
|
-
location, name = tuple(parameter.split("."))
|
321
|
-
_location = cast(Literal["path", "query", "header", "cookie", "body"], location)
|
322
|
-
return _location, name, expression
|
323
|
-
except ValueError:
|
324
|
-
return None, parameter, expression
|
325
|
-
|
326
|
-
|
327
|
-
def get_all_links(operation: APIOperation) -> Generator[tuple[str, OpenAPILink], None, None]:
|
328
|
-
for status_code, definition in operation.definition.raw["responses"].items():
|
329
|
-
definition = operation.schema.resolver.resolve_all(definition, RECURSION_DEPTH_LIMIT - 8) # type: ignore[attr-defined]
|
330
|
-
for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
|
331
|
-
yield status_code, OpenAPILink(name, status_code, link_definition, operation)
|
332
|
-
|
333
|
-
|
334
|
-
StatusCode = Union[str, int]
|
335
|
-
|
336
|
-
|
337
|
-
def _get_response_by_status_code(responses: dict[StatusCode, dict[str, Any]], status_code: str | int) -> dict:
|
338
|
-
if isinstance(status_code, int):
|
339
|
-
# Invalid schemas may contain status codes as integers
|
340
|
-
if status_code in responses:
|
341
|
-
return responses[status_code]
|
342
|
-
# Passed here as an integer, but there is no such status code as int
|
343
|
-
# We cast it to a string because it is either there already and we'll get relevant responses, otherwise
|
344
|
-
# a new dict will be created because there is no such status code in the schema (as an int or a string)
|
345
|
-
return responses.setdefault(str(status_code), {})
|
346
|
-
if status_code.isnumeric():
|
347
|
-
# Invalid schema but the status code is passed as a string
|
348
|
-
numeric_status_code = int(status_code)
|
349
|
-
if numeric_status_code in responses:
|
350
|
-
return responses[numeric_status_code]
|
351
|
-
# All status codes as strings, including `default` and patterned values like `5XX`
|
352
|
-
return responses.setdefault(status_code, {})
|
353
|
-
|
354
|
-
|
355
|
-
def add_link(
|
356
|
-
resolver: RefResolver,
|
357
|
-
responses: dict[StatusCode, dict[str, Any]],
|
358
|
-
links_field: str,
|
359
|
-
parameters: dict[str, str] | None,
|
360
|
-
request_body: Any,
|
361
|
-
status_code: StatusCode,
|
362
|
-
target: str | APIOperation,
|
363
|
-
name: str | None = None,
|
364
|
-
) -> None:
|
365
|
-
response = _get_response_by_status_code(responses, status_code)
|
366
|
-
if "$ref" in response:
|
367
|
-
_, response = resolver.resolve(response["$ref"])
|
368
|
-
links_definition = response.setdefault(links_field, {})
|
369
|
-
new_link: dict[str, str | dict[str, str]] = {}
|
370
|
-
if parameters is not None:
|
371
|
-
new_link["parameters"] = parameters
|
372
|
-
if request_body is not None:
|
373
|
-
new_link["requestBody"] = request_body
|
374
|
-
if isinstance(target, str):
|
375
|
-
name = name or target
|
376
|
-
new_link["operationRef"] = target
|
377
|
-
else:
|
378
|
-
name = name or f"{target.method.upper()} {target.path}"
|
379
|
-
# operationId is a dict lookup which is more efficient than using `operationRef`, since it
|
380
|
-
# doesn't involve reference resolving when we will look up for this target during testing.
|
381
|
-
if "operationId" in target.definition.raw:
|
382
|
-
new_link["operationId"] = target.definition.raw["operationId"]
|
383
|
-
else:
|
384
|
-
new_link["operationRef"] = target.operation_reference
|
385
|
-
# The name is arbitrary, so we don't really case what it is,
|
386
|
-
# but it should not override existing links
|
387
|
-
while name in links_definition:
|
388
|
-
name += "_new"
|
389
|
-
links_definition[name] = new_link
|