schemathesis 3.15.4__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.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from schemathesis.generation.modes import GenerationMode
6
+
7
+ __all__ = [
8
+ "GenerationMode",
9
+ "generate_random_case_id",
10
+ ]
11
+
12
+
13
+ CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
14
+ BASE = len(CASE_ID_ALPHABET)
15
+ # Separate `Random` as Hypothesis might interfere with the default one
16
+ RANDOM = random.Random()
17
+
18
+
19
+ def generate_random_case_id(length: int = 6) -> str:
20
+ number = RANDOM.randint(62 ** (length - 1), 62**length - 1)
21
+ output = ""
22
+ while number > 0:
23
+ number, rem = divmod(number, BASE)
24
+ output += CASE_ID_ALPHABET[rem]
25
+ return output
@@ -0,0 +1,478 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schemathesis import transport
8
+ from schemathesis.checks import CHECKS, CheckContext, CheckFunction, load_all_checks, run_checks
9
+ from schemathesis.core import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER, NotSet, curl
10
+ from schemathesis.core.failures import FailureGroup, failure_report_title, format_failures
11
+ from schemathesis.core.parameters import CONTAINER_TO_LOCATION, ParameterLocation
12
+ from schemathesis.core.transport import Response
13
+ from schemathesis.generation import GenerationMode, generate_random_case_id
14
+ from schemathesis.generation.meta import CaseMetadata, ComponentInfo
15
+ from schemathesis.generation.overrides import Override, store_components
16
+ from schemathesis.hooks import HookContext, dispatch
17
+ from schemathesis.transport.prepare import prepare_path, prepare_request
18
+
19
+ if TYPE_CHECKING:
20
+ import httpx
21
+ import requests
22
+ import requests.auth
23
+ from requests.structures import CaseInsensitiveDict
24
+ from werkzeug.test import TestResponse
25
+
26
+ from schemathesis.schemas import APIOperation
27
+
28
+
29
+ def _default_headers() -> CaseInsensitiveDict:
30
+ from requests.structures import CaseInsensitiveDict
31
+
32
+ return CaseInsensitiveDict()
33
+
34
+
35
+ _NOTSET_HASH = 0x7F3A9B2C
36
+
37
+
38
+ @dataclass
39
+ class Case:
40
+ """Generated test case data for a single API operation."""
41
+
42
+ operation: APIOperation
43
+ method: str
44
+ """HTTP verb (`GET`, `POST`, etc.)"""
45
+ path: str
46
+ """Path template from schema (e.g., `/users/{user_id}`)"""
47
+ id: str
48
+ """Random ID sent in headers for log correlation"""
49
+ path_parameters: dict[str, Any]
50
+ """Generated path variables (e.g., `{"user_id": "123"}`)"""
51
+ headers: CaseInsensitiveDict
52
+ """Generated HTTP headers"""
53
+ cookies: dict[str, Any]
54
+ """Generated cookies"""
55
+ query: dict[str, Any]
56
+ """Generated query parameters"""
57
+ # By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
58
+ # which is a valid payload.
59
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet
60
+ """Generated request body"""
61
+ media_type: str | None
62
+ """Media type from OpenAPI schema (e.g., "multipart/form-data")"""
63
+
64
+ _meta: CaseMetadata | None
65
+
66
+ _auth: requests.auth.AuthBase | None
67
+ _has_explicit_auth: bool
68
+ _components: dict
69
+ _freeze_metadata: bool
70
+
71
+ __slots__ = (
72
+ "operation",
73
+ "method",
74
+ "path",
75
+ "id",
76
+ "path_parameters",
77
+ "headers",
78
+ "cookies",
79
+ "query",
80
+ "body",
81
+ "media_type",
82
+ "_meta",
83
+ "_auth",
84
+ "_has_explicit_auth",
85
+ "_components",
86
+ "_freeze_metadata",
87
+ )
88
+
89
+ def __init__(
90
+ self,
91
+ operation: APIOperation,
92
+ method: str,
93
+ path: str,
94
+ *,
95
+ id: str | None = None,
96
+ path_parameters: dict[str, Any] | None = None,
97
+ headers: CaseInsensitiveDict | None = None,
98
+ cookies: dict[str, Any] | None = None,
99
+ query: dict[str, Any] | None = None,
100
+ body: list | dict[str, Any] | str | int | float | bool | bytes | "NotSet" = NOT_SET,
101
+ media_type: str | None = None,
102
+ meta: CaseMetadata | None = None,
103
+ _auth: requests.auth.AuthBase | None = None,
104
+ _has_explicit_auth: bool = False,
105
+ ) -> None:
106
+ # Use object.__setattr__ to bypass __setattr__ tracking during initialization
107
+ object.__setattr__(self, "operation", operation)
108
+ object.__setattr__(self, "method", method)
109
+ object.__setattr__(self, "path", path)
110
+ object.__setattr__(self, "id", id if id is not None else generate_random_case_id())
111
+ object.__setattr__(self, "path_parameters", path_parameters if path_parameters is not None else {})
112
+ object.__setattr__(self, "headers", headers if headers is not None else _default_headers())
113
+ object.__setattr__(self, "cookies", cookies if cookies is not None else {})
114
+ object.__setattr__(self, "query", query if query is not None else {})
115
+ object.__setattr__(self, "body", body)
116
+ object.__setattr__(self, "media_type", media_type)
117
+ object.__setattr__(self, "_meta", meta)
118
+ object.__setattr__(self, "_auth", _auth)
119
+ object.__setattr__(self, "_has_explicit_auth", _has_explicit_auth)
120
+ object.__setattr__(self, "_components", store_components(self))
121
+ object.__setattr__(self, "_freeze_metadata", False)
122
+
123
+ # Initialize hash tracking if we have metadata
124
+ if self._meta is not None:
125
+ self._init_hashes()
126
+
127
+ def __eq__(self, other: object) -> bool:
128
+ if not isinstance(other, Case):
129
+ return NotImplemented
130
+
131
+ return (
132
+ self.operation == other.operation
133
+ and self.method == other.method
134
+ and self.path == other.path
135
+ and self.path_parameters == other.path_parameters
136
+ and self.headers == other.headers
137
+ and self.cookies == other.cookies
138
+ and self.query == other.query
139
+ and self.body == other.body
140
+ and self.media_type == other.media_type
141
+ )
142
+
143
+ def __setattr__(self, name: str, value: Any) -> None:
144
+ """Track modifications to containers for metadata revalidation."""
145
+ # Set the value
146
+ object.__setattr__(self, name, value)
147
+
148
+ # Mark as dirty if we modified a tracked container and have metadata
149
+ if name in CONTAINER_TO_LOCATION and self._meta is not None:
150
+ location = CONTAINER_TO_LOCATION[name]
151
+ self._meta.mark_dirty(location)
152
+ # Update hash immediately so future in-place modifications can be detected
153
+ self._meta.update_validated_hash(location, self._hash_container(value))
154
+
155
+ @property
156
+ def _override(self) -> Override:
157
+ return Override.from_components(self._components, self)
158
+
159
+ def __repr__(self) -> str:
160
+ output = f"{self.__class__.__name__}("
161
+ first = True
162
+ for name in ("path_parameters", "headers", "cookies", "query", "body"):
163
+ value = getattr(self, name)
164
+ if name != "body" and not value:
165
+ continue
166
+ if value is not None and not isinstance(value, NotSet):
167
+ if first:
168
+ first = False
169
+ else:
170
+ output += ", "
171
+ output += f"{name}={value!r}"
172
+ return f"{output})"
173
+
174
+ def __hash__(self) -> int:
175
+ return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
176
+
177
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
178
+
179
+ def _init_hashes(self) -> None:
180
+ """Initialize hash tracking in metadata for generated components only."""
181
+ assert self._meta is not None
182
+ # Only track components that were actually generated
183
+ for location in self._meta.components.keys():
184
+ value = getattr(self, location.container_name)
185
+ hash_value = self._hash_container(value)
186
+ self._meta.update_validated_hash(location, hash_value)
187
+
188
+ def _check_modifications(self) -> None:
189
+ """Detect in-place modifications by comparing container hashes."""
190
+ if self._meta is None:
191
+ return
192
+
193
+ # Only check components that were actually generated
194
+ for location in self._meta.components.keys():
195
+ last_hash = self._meta._last_validated_hashes[location]
196
+ value = getattr(self, location.container_name)
197
+ current_hash = self._hash_container(value)
198
+
199
+ if current_hash != last_hash:
200
+ # Container was modified in-place
201
+ self._meta.mark_dirty(location)
202
+
203
+ def _revalidate_metadata(self) -> None:
204
+ """Revalidate dirty components and update metadata."""
205
+ assert self._meta and self._meta.is_dirty()
206
+
207
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
208
+
209
+ # Only works for OpenAPI schemas
210
+ if not isinstance(self.operation.schema, BaseOpenAPISchema):
211
+ # Can't validate, just clear dirty flags
212
+ for location in list(self._meta._dirty):
213
+ self._meta.clear_dirty(location)
214
+ return
215
+
216
+ validator_cls = self.operation.schema.adapter.jsonschema_validator_cls
217
+
218
+ for location in list(self._meta._dirty):
219
+ # Get current value
220
+ value = getattr(self, location.container_name)
221
+
222
+ # Validate against schema
223
+ is_valid = self._validate_component(location, value, validator_cls)
224
+
225
+ # Update component metadata
226
+ if location in self._meta.components:
227
+ new_mode = GenerationMode.POSITIVE if is_valid else GenerationMode.NEGATIVE
228
+ self._meta.components[location] = ComponentInfo(mode=new_mode)
229
+
230
+ # Update hash and clear dirty flag
231
+ self._meta.update_validated_hash(location, self._hash_container(value))
232
+ self._meta.clear_dirty(location)
233
+
234
+ # Recompute overall generation mode
235
+ if self._meta.components:
236
+ if all(info.mode.is_positive for info in self._meta.components.values()):
237
+ self._meta.generation.mode = GenerationMode.POSITIVE
238
+ else:
239
+ self._meta.generation.mode = GenerationMode.NEGATIVE
240
+
241
+ def _validate_component(
242
+ self,
243
+ location: ParameterLocation,
244
+ value: Any,
245
+ validator_cls: type,
246
+ ) -> bool:
247
+ """Validate a component value against its schema."""
248
+ if location == ParameterLocation.BODY:
249
+ # Validate body against media type schema
250
+ if isinstance(value, NotSet) or value is None:
251
+ return False
252
+ for alternative in self.operation.body:
253
+ if alternative.media_type == self.media_type:
254
+ return validator_cls(alternative.optimized_schema).is_valid(value)
255
+ # Validate other locations against container schema
256
+ container = getattr(self.operation, location.container_name)
257
+ return validator_cls(container.schema).is_valid(value)
258
+
259
+ def _hash_container(self, value: Any) -> int:
260
+ """Create a hash representing the current state of a container.
261
+
262
+ Recursively hashes nested dicts/lists/tuples and primitives to detect modifications.
263
+ """
264
+ if isinstance(value, Mapping):
265
+ return hash((type(value), tuple(sorted((k, self._hash_container(v)) for k, v in value.items()))))
266
+ elif isinstance(value, (list, tuple)):
267
+ return hash((type(value), tuple(self._hash_container(item) for item in value)))
268
+ elif isinstance(value, NotSet):
269
+ return _NOTSET_HASH
270
+ return hash((type(value), value))
271
+
272
+ @property
273
+ def meta(self) -> CaseMetadata | None:
274
+ """Get metadata, revalidating if components were modified."""
275
+ # Skip revalidation if metadata is frozen (e.g., during request preparation)
276
+ if not self._freeze_metadata:
277
+ self._check_modifications()
278
+ if self._meta and self._meta.is_dirty():
279
+ self._revalidate_metadata()
280
+ return self._meta
281
+
282
+ @property
283
+ def formatted_path(self) -> str:
284
+ """Path template with variables substituted (e.g., /users/{user_id} → /users/123)."""
285
+ return prepare_path(self.path, self.path_parameters)
286
+
287
+ def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
288
+ """Generate a curl command that reproduces this test case.
289
+
290
+ Args:
291
+ headers: Additional headers to include in the command.
292
+ verify: When False, adds `--insecure` flag to curl command.
293
+
294
+ """
295
+ request_data = prepare_request(self, headers, config=self.operation.schema.config.output.sanitization)
296
+ result = curl.generate(
297
+ method=str(request_data.method),
298
+ url=str(request_data.url),
299
+ body=request_data.body,
300
+ verify=verify,
301
+ headers=dict(request_data.headers),
302
+ known_generated_headers=dict(self.headers or {}),
303
+ )
304
+ # Include warnings if any exist
305
+ if result.warnings:
306
+ warnings_text = "\n\n".join(f"⚠️ {warning}" for warning in result.warnings)
307
+ return f"{result.command}\n\n{warnings_text}"
308
+ return result.command
309
+
310
+ def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
311
+ return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
312
+
313
+ def call(
314
+ self,
315
+ base_url: str | None = None,
316
+ session: requests.Session | None = None,
317
+ headers: dict[str, Any] | None = None,
318
+ params: dict[str, Any] | None = None,
319
+ cookies: dict[str, Any] | None = None,
320
+ **kwargs: Any,
321
+ ) -> Response:
322
+ """Make an HTTP request using this test case's data without validation.
323
+
324
+ Use when you need to validate response separately
325
+
326
+ Args:
327
+ base_url: Override the schema's base URL.
328
+ session: Reuse an existing requests session.
329
+ headers: Additional headers.
330
+ params: Additional query parameters.
331
+ cookies: Additional cookies.
332
+ **kwargs: Additional transport-level arguments.
333
+
334
+ """
335
+ hook_context = HookContext(operation=self.operation)
336
+ dispatch("before_call", hook_context, self, _with_dual_style_kwargs=True, **kwargs)
337
+
338
+ # Revalidate metadata if dirty before freezing (captures user modifications)
339
+ if self._meta and self._meta.is_dirty():
340
+ self._check_modifications()
341
+ self._revalidate_metadata()
342
+
343
+ # Freeze metadata to prevent revalidation after request preparation transforms the body
344
+ object.__setattr__(self, "_freeze_metadata", True)
345
+
346
+ if self.operation.app is not None:
347
+ kwargs.setdefault("app", self.operation.app)
348
+ if "app" in kwargs:
349
+ transport_ = transport.get(kwargs["app"])
350
+ else:
351
+ transport_ = self.operation.schema.transport
352
+ try:
353
+ response = transport_.send(
354
+ self,
355
+ session=session,
356
+ base_url=base_url,
357
+ headers=headers,
358
+ params=params,
359
+ cookies=cookies,
360
+ **kwargs,
361
+ )
362
+ except Exception as exc:
363
+ # May happen in ASGI / WSGI apps
364
+ if not hasattr(exc, "__notes__"):
365
+ exc.__notes__ = [] # type: ignore[attr-defined]
366
+ verify = kwargs.get("verify", True)
367
+ curl = self.as_curl_command(headers=headers, verify=verify)
368
+ exc.__notes__.append(f"\nReproduce with: \n\n {curl}") # type: ignore[attr-defined]
369
+ raise
370
+ dispatch("after_call", hook_context, self, response)
371
+ return response
372
+
373
+ def validate_response(
374
+ self,
375
+ response: Response | httpx.Response | requests.Response | TestResponse,
376
+ checks: list[CheckFunction] | None = None,
377
+ additional_checks: list[CheckFunction] | None = None,
378
+ excluded_checks: list[CheckFunction] | None = None,
379
+ headers: dict[str, Any] | None = None,
380
+ transport_kwargs: dict[str, Any] | None = None,
381
+ ) -> None:
382
+ """Validate a response against the API schema and built-in checks.
383
+
384
+ Args:
385
+ response: Response to validate.
386
+ checks: Explicit set of checks to run.
387
+ additional_checks: Additional custom checks to run.
388
+ excluded_checks: Built-in checks to skip.
389
+ headers: Headers used in the original request.
390
+ transport_kwargs: Transport arguments used in the original request.
391
+
392
+ """
393
+ __tracebackhide__ = True
394
+ from requests.structures import CaseInsensitiveDict
395
+
396
+ # In some cases checks may not be loaded.
397
+ # For example - non-Schemathesis tests that manually construct `Case` instances
398
+ load_all_checks()
399
+
400
+ response = Response.from_any(response)
401
+
402
+ config = self.operation.schema.config.checks_config_for(
403
+ operation=self.operation, phase=self.meta.phase.name.value if self.meta is not None else None
404
+ )
405
+ if not checks:
406
+ # Checks are not specified explicitly, derive from the config
407
+ checks = []
408
+ for check in CHECKS.get_all():
409
+ name = check.__name__
410
+ if config.get_by_name(name=name).enabled:
411
+ checks.append(check)
412
+ checks = [
413
+ check for check in list(checks) + list(additional_checks or []) if check not in set(excluded_checks or [])
414
+ ]
415
+
416
+ ctx = CheckContext(
417
+ override=self._override,
418
+ auth=None,
419
+ headers=CaseInsensitiveDict(headers) if headers else None,
420
+ config=config,
421
+ transport_kwargs=transport_kwargs,
422
+ recorder=None,
423
+ )
424
+ failures = run_checks(
425
+ case=self,
426
+ response=response,
427
+ ctx=ctx,
428
+ checks=checks,
429
+ on_failure=lambda _, collected, failure: collected.add(failure),
430
+ )
431
+ if failures:
432
+ _failures = list(failures)
433
+ message = failure_report_title(_failures) + "\n"
434
+ verify = getattr(response, "verify", True)
435
+ curl = self.as_curl_command(headers=dict(response.request.headers), verify=verify)
436
+ message += format_failures(
437
+ case_id=None,
438
+ response=response,
439
+ failures=_failures,
440
+ curl=curl,
441
+ config=self.operation.schema.config.output,
442
+ )
443
+ message += "\n\n"
444
+ raise FailureGroup(_failures, message) from None
445
+
446
+ def call_and_validate(
447
+ self,
448
+ base_url: str | None = None,
449
+ session: requests.Session | None = None,
450
+ headers: dict[str, Any] | None = None,
451
+ checks: list[CheckFunction] | None = None,
452
+ additional_checks: list[CheckFunction] | None = None,
453
+ excluded_checks: list[CheckFunction] | None = None,
454
+ **kwargs: Any,
455
+ ) -> Response:
456
+ """Make an HTTP request and validates the response automatically.
457
+
458
+ Args:
459
+ base_url: Override the schema's base URL.
460
+ session: Reuse an existing requests session.
461
+ headers: Additional headers to send.
462
+ checks: Explicit set of checks to run.
463
+ additional_checks: Additional custom checks to run.
464
+ excluded_checks: Built-in checks to skip.
465
+ **kwargs: Additional transport-level arguments.
466
+
467
+ """
468
+ __tracebackhide__ = True
469
+ response = self.call(base_url, session, headers, **kwargs)
470
+ self.validate_response(
471
+ response,
472
+ checks,
473
+ headers=headers,
474
+ additional_checks=additional_checks,
475
+ excluded_checks=excluded_checks,
476
+ transport_kwargs=kwargs,
477
+ )
478
+ return response