schemathesis 3.39.15__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.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.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