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,222 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject
6
+
7
+
8
+ def sanitize(schema: JsonSchema) -> set[str]:
9
+ """Remove $ref from optional locations."""
10
+ if isinstance(schema, bool):
11
+ return set()
12
+
13
+ stack: list[JsonSchema] = [schema]
14
+
15
+ while stack:
16
+ current = stack.pop()
17
+ if not isinstance(current, dict):
18
+ continue
19
+
20
+ _sanitize_combinators(current)
21
+
22
+ _sanitize_properties(current)
23
+
24
+ if "items" in current:
25
+ _sanitize_items(current)
26
+
27
+ if "prefixItems" in current:
28
+ _sanitize_prefix_items(current)
29
+
30
+ if "additionalProperties" in current:
31
+ _sanitize_additional_properties(current)
32
+
33
+ if "additionalItems" in current:
34
+ _sanitize_additional_items(current)
35
+
36
+ for value in current.values():
37
+ if isinstance(value, dict):
38
+ stack.append(value)
39
+ elif isinstance(value, list):
40
+ for item in value:
41
+ if isinstance(item, dict):
42
+ stack.append(item)
43
+
44
+ remaining: set[str] = set()
45
+ _collect_all_references(schema, remaining)
46
+ return remaining
47
+
48
+
49
+ def _sanitize_combinators(schema: JsonSchemaObject) -> None:
50
+ """Sanitize anyOf/oneOf/allOf."""
51
+ for combinator_key in ("anyOf", "oneOf"):
52
+ variants = schema.get(combinator_key)
53
+ if not isinstance(variants, list):
54
+ continue
55
+
56
+ flattened = _flatten_combinator(variants, combinator_key)
57
+
58
+ cleaned = [variant for variant in flattened if not _has_ref(variant)]
59
+
60
+ # Only update if we have non-$ref variants
61
+ if cleaned:
62
+ # At least one alternative remains, which narrows the constraints
63
+ schema[combinator_key] = cleaned
64
+ elif not flattened:
65
+ schema.pop(combinator_key, None)
66
+
67
+ all_of = schema.get("allOf")
68
+ if isinstance(all_of, list):
69
+ flattened = _flatten_combinator(all_of, "allOf")
70
+
71
+ cleaned = [variant for variant in flattened if not _is_empty(variant)]
72
+ if cleaned:
73
+ schema["allOf"] = cleaned
74
+ else:
75
+ schema.pop("allOf", None)
76
+
77
+
78
+ def _flatten_combinator(variants: list, key: str) -> list:
79
+ """Flatten nested same-type combinators."""
80
+ result = []
81
+ for variant in variants:
82
+ if isinstance(variant, dict) and key in variant and isinstance(variant[key], list):
83
+ result.extend(variant[key])
84
+ else:
85
+ result.append(variant)
86
+ return result
87
+
88
+
89
+ def _is_empty(schema: JsonSchema) -> bool:
90
+ """Check if schema accepts anything."""
91
+ if schema is True:
92
+ return True
93
+
94
+ if not isinstance(schema, dict):
95
+ return False
96
+
97
+ if not schema:
98
+ return True
99
+
100
+ # Only non-validating keywords
101
+ NON_VALIDATING = {
102
+ "$id",
103
+ "$schema",
104
+ "$defs",
105
+ "definitions",
106
+ "title",
107
+ "description",
108
+ "default",
109
+ "examples",
110
+ "example",
111
+ "$comment",
112
+ "deprecated",
113
+ "readOnly",
114
+ "writeOnly",
115
+ }
116
+
117
+ return all(key in NON_VALIDATING for key in schema.keys())
118
+
119
+
120
+ def _sanitize_properties(schema: JsonSchemaObject) -> None:
121
+ """Remove OPTIONAL property schemas if they have $ref."""
122
+ if "properties" not in schema:
123
+ return
124
+
125
+ properties = schema["properties"]
126
+ if not isinstance(properties, dict):
127
+ return
128
+
129
+ required = schema.get("required", [])
130
+
131
+ for name, subschema in list(properties.items()):
132
+ if not _has_ref(subschema):
133
+ continue
134
+
135
+ if name not in required:
136
+ del properties[name]
137
+
138
+
139
+ def _sanitize_items(schema: JsonSchemaObject) -> None:
140
+ """Convert to empty array ONLY if minItems allows it."""
141
+ items = schema["items"]
142
+
143
+ has_ref = False
144
+ if isinstance(items, dict):
145
+ has_ref = _has_ref(items)
146
+ elif isinstance(items, list):
147
+ has_ref = any(_has_ref(item) for item in items)
148
+
149
+ if not has_ref:
150
+ return
151
+
152
+ min_items = schema.get("minItems", 0)
153
+
154
+ if min_items == 0:
155
+ _convert_to_empty_array(schema)
156
+
157
+
158
+ def _sanitize_prefix_items(schema: JsonSchemaObject) -> None:
159
+ """Same logic as items."""
160
+ prefix_items = schema["prefixItems"]
161
+
162
+ if not isinstance(prefix_items, list):
163
+ return
164
+
165
+ if not any(_has_ref(item) for item in prefix_items):
166
+ return
167
+
168
+ min_items = schema.get("minItems", 0)
169
+
170
+ if min_items == 0:
171
+ _convert_to_empty_array(schema)
172
+
173
+
174
+ def _convert_to_empty_array(schema: JsonSchemaObject) -> None:
175
+ schema.pop("items", None)
176
+ schema.pop("prefixItems", None)
177
+ schema["maxItems"] = 0
178
+ schema["minItems"] = 0
179
+
180
+
181
+ def _sanitize_additional_properties(schema: JsonSchemaObject) -> None:
182
+ additional = schema["additionalProperties"]
183
+ if _has_ref(additional):
184
+ schema["additionalProperties"] = False
185
+
186
+
187
+ def _sanitize_additional_items(schema: JsonSchemaObject) -> None:
188
+ additional = schema["additionalItems"]
189
+ if _has_ref(additional):
190
+ schema["additionalItems"] = False
191
+
192
+
193
+ def _has_ref(schema: Any) -> bool:
194
+ """Check if schema contains $ref at any level."""
195
+ if not isinstance(schema, dict):
196
+ return False
197
+
198
+ if "$ref" in schema:
199
+ return True
200
+ for value in schema.values():
201
+ if isinstance(value, dict):
202
+ if _has_ref(value):
203
+ return True
204
+ elif isinstance(value, list):
205
+ for item in value:
206
+ if isinstance(item, dict) and _has_ref(item):
207
+ return True
208
+
209
+ return False
210
+
211
+
212
+ def _collect_all_references(schema: JsonSchema | list[JsonSchema], remaining: set[str]) -> None:
213
+ """Collect all remaining $ref."""
214
+ if isinstance(schema, dict):
215
+ ref = schema.get("$ref")
216
+ if isinstance(ref, str):
217
+ remaining.add(ref)
218
+ for value in schema.values():
219
+ _collect_all_references(value, remaining)
220
+ elif isinstance(schema, list):
221
+ for item in schema:
222
+ _collect_all_references(item, remaining)
@@ -0,0 +1,41 @@
1
+ from typing import Any, Union
2
+
3
+ JsonSchemaObject = dict[str, Any]
4
+ JsonSchema = Union[JsonSchemaObject, bool]
5
+
6
+ ANY_TYPE = ["null", "boolean", "number", "string", "array", "object"]
7
+ ALL_TYPES = ["null", "boolean", "integer", "number", "string", "array", "object"]
8
+
9
+
10
+ def get_type(schema: JsonSchema, *, _check_type: bool = False) -> list[str]:
11
+ if isinstance(schema, bool):
12
+ return ANY_TYPE
13
+ ty = schema.get("type", ANY_TYPE)
14
+ if isinstance(ty, str):
15
+ if _check_type and ty not in ALL_TYPES:
16
+ raise AssertionError(f"Unknown type: `{ty}`. Should be one of {', '.join(ALL_TYPES)}")
17
+ return [ty]
18
+ if ty is ANY_TYPE:
19
+ return list(ty)
20
+ return [t for t in ALL_TYPES if t in ty]
21
+
22
+
23
+ def _get_type(schema: JsonSchema) -> list[str]:
24
+ # Special version to patch `hypothesis-jsonschema`
25
+ return get_type(schema, _check_type=True)
26
+
27
+
28
+ def to_json_type_name(v: Any) -> str:
29
+ if v is None:
30
+ return "null"
31
+ if isinstance(v, bool):
32
+ return "boolean"
33
+ if isinstance(v, dict):
34
+ return "object"
35
+ if isinstance(v, list):
36
+ return "array"
37
+ if isinstance(v, (int, float)):
38
+ return "number"
39
+ if isinstance(v, str):
40
+ return "string"
41
+ return type(v).__name__
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable
4
+
5
+
6
+ def lazy_import(module: str, name: str, imports: dict[str, Callable[[], Any]], _globals: dict[str, Any]) -> Any:
7
+ value = _globals.get(name)
8
+ if value is not None:
9
+ return value
10
+ loader = imports.get(name)
11
+ if loader is not None:
12
+ value = loader()
13
+ _globals[name] = value
14
+ return value
15
+ raise AttributeError(f"module {module!r} has no attribute {name!r}")
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import http.client
4
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn
5
+
6
+ from schemathesis.core.errors import LoaderError, LoaderErrorKind, get_request_error_extras, get_request_error_message
7
+ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT, USER_AGENT
8
+
9
+ if TYPE_CHECKING:
10
+ import requests
11
+
12
+
13
+ def prepare_request_kwargs(kwargs: dict[str, Any]) -> None:
14
+ """Prepare common request kwargs."""
15
+ headers = kwargs.setdefault("headers", {})
16
+ if "user-agent" not in {header.lower() for header in headers}:
17
+ kwargs["headers"]["User-Agent"] = USER_AGENT
18
+
19
+
20
+ def handle_request_error(exc: requests.RequestException) -> NoReturn:
21
+ """Handle request-level errors."""
22
+ import requests
23
+
24
+ url = exc.request.url if exc.request is not None else None
25
+ if isinstance(exc, requests.exceptions.SSLError):
26
+ kind = LoaderErrorKind.CONNECTION_SSL
27
+ elif isinstance(exc, requests.exceptions.ConnectionError):
28
+ kind = LoaderErrorKind.CONNECTION_OTHER
29
+ else:
30
+ kind = LoaderErrorKind.NETWORK_OTHER
31
+ raise LoaderError(
32
+ message=get_request_error_message(exc),
33
+ kind=kind,
34
+ url=url,
35
+ extras=get_request_error_extras(exc),
36
+ ) from exc
37
+
38
+
39
+ def raise_for_status(response: requests.Response) -> requests.Response:
40
+ """Handle response status codes."""
41
+ status_code = response.status_code
42
+ if status_code < 400:
43
+ return response
44
+
45
+ reason = http.client.responses.get(status_code, "Unknown")
46
+ if status_code >= 500:
47
+ message = f"Failed to load schema due to server error (HTTP {status_code} {reason})"
48
+ kind = LoaderErrorKind.HTTP_SERVER_ERROR
49
+ else:
50
+ message = f"Failed to load schema due to client error (HTTP {status_code} {reason})"
51
+ kind = (
52
+ LoaderErrorKind.HTTP_FORBIDDEN
53
+ if status_code == 403
54
+ else LoaderErrorKind.HTTP_NOT_FOUND
55
+ if status_code == 404
56
+ else LoaderErrorKind.HTTP_CLIENT_ERROR
57
+ )
58
+ raise LoaderError(message=message, kind=kind, url=response.request.url, extras=[])
59
+
60
+
61
+ def make_request(func: Callable[..., requests.Response], url: str, **kwargs: Any) -> requests.Response:
62
+ """Make HTTP request with error handling."""
63
+ import requests
64
+
65
+ try:
66
+ response = func(url, **kwargs)
67
+ return raise_for_status(response)
68
+ except requests.RequestException as exc:
69
+ handle_request_error(exc)
70
+ except OSError as exc:
71
+ # Possible with certificate errors
72
+ raise LoaderError(message=str(exc), kind=LoaderErrorKind.INVALID_CERTIFICATE, url=url, extras=[]) from None
73
+
74
+
75
+ WAIT_FOR_SCHEMA_INTERVAL = 0.05
76
+
77
+
78
+ def load_from_url(
79
+ func: Callable[..., requests.Response],
80
+ *,
81
+ url: str,
82
+ wait_for_schema: float | None = None,
83
+ **kwargs: Any,
84
+ ) -> requests.Response:
85
+ """Load schema from URL with retries."""
86
+ import backoff
87
+ import requests
88
+
89
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT)
90
+ prepare_request_kwargs(kwargs)
91
+
92
+ if wait_for_schema is not None:
93
+ func = backoff.on_exception(
94
+ backoff.constant,
95
+ requests.exceptions.ConnectionError,
96
+ max_time=wait_for_schema,
97
+ interval=WAIT_FOR_SCHEMA_INTERVAL,
98
+ )(func)
99
+
100
+ return make_request(func, url, **kwargs)
101
+
102
+
103
+ def require_relative_url(url: str) -> None:
104
+ """Raise an error if the URL is not relative."""
105
+ # Deliberately simplistic approach
106
+ if "://" in url or url.startswith("//"):
107
+ raise ValueError("Schema path should be relative for WSGI/ASGI loaders")
@@ -0,0 +1,66 @@
1
+ """A lightweight mechanism to attach Schemathesis-specific metadata to test functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Generic, TypeVar
7
+
8
+ from schemathesis.core import NOT_SET, NotSet
9
+
10
+ METADATA_ATTR = "_schemathesis_metadata"
11
+
12
+
13
+ @dataclass
14
+ class SchemathesisMetadata:
15
+ """Container for all Schemathesis-specific data attached to test functions."""
16
+
17
+
18
+ T = TypeVar("T")
19
+
20
+
21
+ class Mark(Generic[T]):
22
+ """Access to specific attributes in SchemathesisMetadata."""
23
+
24
+ def __init__(
25
+ self, *, attr_name: str, default: T | Callable[[], T] | None = None, check: Callable[[T], bool] | None = None
26
+ ) -> None:
27
+ self.attr_name = attr_name
28
+ self._default = default
29
+ self._check = check
30
+
31
+ def _get_default(self) -> T | None:
32
+ if callable(self._default):
33
+ return self._default()
34
+ return self._default
35
+
36
+ def _check_value(self, value: T) -> bool:
37
+ if self._check is not None:
38
+ return self._check(value)
39
+ return True
40
+
41
+ def get(self, func: Callable) -> T | None:
42
+ """Get marker value if it's set."""
43
+ metadata = getattr(func, METADATA_ATTR, None)
44
+ if metadata is None:
45
+ return self._get_default()
46
+ value = getattr(metadata, self.attr_name, NOT_SET)
47
+ if value is NOT_SET:
48
+ return self._get_default()
49
+ assert not isinstance(value, NotSet)
50
+ if self._check_value(value):
51
+ return value
52
+ return self._get_default()
53
+
54
+ def set(self, func: Callable, value: T) -> None:
55
+ """Set marker value, creating metadata if needed."""
56
+ if not hasattr(func, METADATA_ATTR):
57
+ setattr(func, METADATA_ATTR, SchemathesisMetadata())
58
+ metadata = getattr(func, METADATA_ATTR)
59
+ setattr(metadata, self.attr_name, value)
60
+
61
+ def is_set(self, func: Callable) -> bool:
62
+ """Check if function has metadata with this marker set."""
63
+ metadata = getattr(func, METADATA_ATTR, None)
64
+ if metadata is None:
65
+ return False
66
+ return hasattr(metadata, self.attr_name)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache
4
+ from typing import Generator, Tuple
5
+
6
+ from schemathesis.core.errors import MalformedMediaType
7
+
8
+ YAML_MEDIA_TYPES: Tuple[str, ...] = (
9
+ "text/yaml",
10
+ "text/x-yaml",
11
+ "application/x-yaml",
12
+ "text/vnd.yaml",
13
+ "application/yaml",
14
+ )
15
+
16
+
17
+ def _parseparam(s: str) -> Generator[str]:
18
+ while s[:1] == ";":
19
+ s = s[1:]
20
+ end = s.find(";")
21
+ while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
22
+ end = s.find(";", end + 1)
23
+ if end < 0:
24
+ end = len(s)
25
+ f = s[:end]
26
+ yield f.strip()
27
+ s = s[end:]
28
+
29
+
30
+ def _parse_header(line: str) -> tuple[str, dict]:
31
+ parts = _parseparam(";" + line)
32
+ key = parts.__next__()
33
+ pdict = {}
34
+ for p in parts:
35
+ i = p.find("=")
36
+ if i >= 0:
37
+ name = p[:i].strip().lower()
38
+ value = p[i + 1 :].strip()
39
+ if len(value) >= 2 and value[0] == value[-1] == '"':
40
+ value = value[1:-1]
41
+ value = value.replace("\\\\", "\\").replace('\\"', '"')
42
+ pdict[name] = value
43
+ return key, pdict
44
+
45
+
46
+ @lru_cache
47
+ def parse(media_type: str) -> tuple[str, str]:
48
+ """Parse Content Type and return main type and subtype."""
49
+ try:
50
+ media_type, _ = _parse_header(media_type)
51
+ main_type, sub_type = media_type.split("/", 1)
52
+ except ValueError as exc:
53
+ raise MalformedMediaType(f"Malformed media type: `{media_type}`") from exc
54
+ return main_type.lower(), sub_type.lower()
55
+
56
+
57
+ def is_json(value: str) -> bool:
58
+ """Detect whether the content type is JSON-compatible.
59
+
60
+ For example - ``application/problem+json`` matches.
61
+ """
62
+ main, sub = parse(value)
63
+ return main == "application" and (sub == "json" or sub.endswith("+json"))
64
+
65
+
66
+ def is_yaml(value: str) -> bool:
67
+ """Detect whether the content type is YAML-compatible."""
68
+ return value in YAML_MEDIA_TYPES
69
+
70
+
71
+ def is_plain_text(value: str) -> bool:
72
+ """Detect variations of the ``text/plain`` media type."""
73
+ return parse(value) == ("text", "plain")
74
+
75
+
76
+ def is_xml(value: str) -> bool:
77
+ """Detect variations of the ``application/xml`` media type."""
78
+ _, sub = parse(value)
79
+ return sub == "xml" or sub.endswith("+xml")
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from schemathesis.config import OutputConfig
8
+
9
+ TRUNCATED = "// Output truncated..."
10
+
11
+
12
+ def truncate_json(data: Any, *, config: OutputConfig, max_lines: int | None = None) -> str:
13
+ # Convert JSON to string with indentation
14
+ indent = 4
15
+ serialized = json.dumps(data, indent=indent)
16
+ if not config.truncation.enabled:
17
+ return serialized
18
+
19
+ max_lines = max_lines if max_lines is not None else config.truncation.max_lines
20
+ # Split string by lines
21
+ lines = [
22
+ line[: config.truncation.max_width - 3] + "..." if len(line) > config.truncation.max_width else line
23
+ for line in serialized.split("\n")
24
+ ]
25
+
26
+ if len(lines) <= max_lines:
27
+ return "\n".join(lines)
28
+
29
+ truncated_lines = lines[: max_lines - 1]
30
+ indentation = " " * indent
31
+ truncated_lines.append(f"{indentation}{TRUNCATED}")
32
+ truncated_lines.append(lines[-1])
33
+
34
+ return "\n".join(truncated_lines)
35
+
36
+
37
+ def prepare_response_payload(payload: str, *, config: OutputConfig) -> str:
38
+ if payload.endswith("\r\n"):
39
+ payload = payload[:-2]
40
+ elif payload.endswith("\n"):
41
+ payload = payload[:-1]
42
+ if not config.truncation.enabled:
43
+ return payload
44
+ if len(payload) > config.truncation.max_payload_size:
45
+ payload = payload[: config.truncation.max_payload_size] + f" {TRUNCATED}"
46
+ return payload
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping, MutableSequence
4
+ from typing import Any
5
+ from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
6
+
7
+ from schemathesis.config import SanitizationConfig
8
+
9
+
10
+ def sanitize_value(item: Any, *, config: SanitizationConfig) -> None:
11
+ """Sanitize sensitive values within a given item.
12
+
13
+ This function is recursive and will sanitize sensitive data within nested
14
+ dictionaries and lists as well.
15
+ """
16
+ if isinstance(item, MutableMapping):
17
+ for key in list(item.keys()):
18
+ lower_key = key.lower()
19
+ if lower_key in config.keys_to_sanitize or any(marker in lower_key for marker in config.sensitive_markers):
20
+ if isinstance(item[key], list):
21
+ item[key] = [config.replacement]
22
+ else:
23
+ item[key] = config.replacement
24
+ for value in item.values():
25
+ if isinstance(value, (MutableMapping, MutableSequence)):
26
+ sanitize_value(value, config=config)
27
+ elif isinstance(item, MutableSequence):
28
+ for value in item:
29
+ if isinstance(value, (MutableMapping, MutableSequence)):
30
+ sanitize_value(value, config=config)
31
+
32
+
33
+ def sanitize_url(url: str, *, config: SanitizationConfig) -> str:
34
+ """Sanitize sensitive parts of a given URL.
35
+
36
+ This function will sanitize the authority and query parameters in the URL.
37
+ """
38
+ parsed = urlsplit(url)
39
+
40
+ # Sanitize authority
41
+ netloc_parts = parsed.netloc.split("@")
42
+ if len(netloc_parts) > 1:
43
+ netloc = f"{config.replacement}@{netloc_parts[-1]}"
44
+ else:
45
+ netloc = parsed.netloc
46
+
47
+ # Sanitize query parameters
48
+ query = parse_qs(parsed.query, keep_blank_values=True)
49
+ sanitize_value(query, config=config)
50
+ sanitized_query = urlencode(query, doseq=True)
51
+
52
+ # Reconstruct the URL
53
+ sanitized_url_parts = parsed._replace(netloc=netloc, query=sanitized_query)
54
+ return urlunsplit(sanitized_url_parts)
@@ -0,0 +1,45 @@
1
+ from enum import Enum
2
+
3
+ LOCATION_TO_CONTAINER = {
4
+ "path": "path_parameters",
5
+ "query": "query",
6
+ "header": "headers",
7
+ "cookie": "cookies",
8
+ "body": "body",
9
+ }
10
+
11
+
12
+ class ParameterLocation(str, Enum):
13
+ """API parameter location."""
14
+
15
+ QUERY = "query"
16
+ HEADER = "header"
17
+ PATH = "path"
18
+ COOKIE = "cookie"
19
+ BODY = "body"
20
+ UNKNOWN = None
21
+
22
+ @property
23
+ def container_name(self) -> str:
24
+ return {
25
+ "path": "path_parameters",
26
+ "query": "query",
27
+ "header": "headers",
28
+ "cookie": "cookies",
29
+ "body": "body",
30
+ }[self]
31
+
32
+ @property
33
+ def is_in_header(self) -> bool:
34
+ return self in HEADER_LOCATIONS
35
+
36
+
37
+ HEADER_LOCATIONS = frozenset([ParameterLocation.HEADER, ParameterLocation.COOKIE])
38
+
39
+ CONTAINER_TO_LOCATION = {
40
+ "path_parameters": ParameterLocation.PATH,
41
+ "query": ParameterLocation.QUERY,
42
+ "headers": ParameterLocation.HEADER,
43
+ "cookies": ParameterLocation.COOKIE,
44
+ "body": ParameterLocation.BODY,
45
+ }