schemathesis 3.39.16__py3-none-any.whl → 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +233 -307
  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.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.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 -717
  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.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.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.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,252 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import threading
4
- from collections.abc import MutableMapping, MutableSequence
5
- from dataclasses import dataclass, replace
6
- from typing import TYPE_CHECKING, Any, cast
7
- from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
8
-
9
- from .constants import NOT_SET
10
-
11
- if TYPE_CHECKING:
12
- from requests import PreparedRequest
13
-
14
- from .models import Case, CaseSource, Request
15
- from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
16
- from .transports.responses import GenericResponse
17
-
18
- # Exact keys to sanitize
19
- DEFAULT_KEYS_TO_SANITIZE = frozenset(
20
- (
21
- "phpsessid",
22
- "xsrf-token",
23
- "_csrf",
24
- "_csrf_token",
25
- "_session",
26
- "_xsrf",
27
- "aiohttp_session",
28
- "api_key",
29
- "api-key",
30
- "apikey",
31
- "auth",
32
- "authorization",
33
- "connect.sid",
34
- "cookie",
35
- "credentials",
36
- "csrf",
37
- "csrf_token",
38
- "csrf-token",
39
- "csrftoken",
40
- "ip_address",
41
- "mysql_pwd",
42
- "passwd",
43
- "password",
44
- "private_key",
45
- "private-key",
46
- "privatekey",
47
- "remote_addr",
48
- "remote-addr",
49
- "secret",
50
- "session",
51
- "sessionid",
52
- "set_cookie",
53
- "set-cookie",
54
- "token",
55
- "x_api_key",
56
- "x-api-key",
57
- "x_csrftoken",
58
- "x-csrftoken",
59
- "x_forwarded_for",
60
- "x-forwarded-for",
61
- "x_real_ip",
62
- "x-real-ip",
63
- )
64
- )
65
-
66
- # Markers indicating potentially sensitive keys
67
- DEFAULT_SENSITIVE_MARKERS = frozenset(
68
- (
69
- "token",
70
- "key",
71
- "secret",
72
- "password",
73
- "auth",
74
- "session",
75
- "passwd",
76
- "credential",
77
- )
78
- )
79
-
80
- DEFAULT_REPLACEMENT = "[Filtered]"
81
-
82
-
83
- @dataclass
84
- class Config:
85
- """Configuration class for sanitizing sensitive data.
86
-
87
- :param FrozenSet[str] keys_to_sanitize: The exact keys to sanitize (case-insensitive).
88
- :param FrozenSet[str] sensitive_markers: Markers indicating potentially sensitive keys (case-insensitive).
89
- :param str replacement: The replacement string for sanitized values.
90
- """
91
-
92
- keys_to_sanitize: frozenset[str] = DEFAULT_KEYS_TO_SANITIZE
93
- sensitive_markers: frozenset[str] = DEFAULT_SENSITIVE_MARKERS
94
- replacement: str = DEFAULT_REPLACEMENT
95
-
96
- def with_keys_to_sanitize(self, *keys: str) -> Config:
97
- """Create a new configuration with additional keys to sanitize."""
98
- new_keys_to_sanitize = self.keys_to_sanitize.union([key.lower() for key in keys])
99
- return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
100
-
101
- def without_keys_to_sanitize(self, *keys: str) -> Config:
102
- """Create a new configuration without certain keys to sanitize."""
103
- new_keys_to_sanitize = self.keys_to_sanitize.difference([key.lower() for key in keys])
104
- return replace(self, keys_to_sanitize=frozenset(new_keys_to_sanitize))
105
-
106
- def with_sensitive_markers(self, *markers: str) -> Config:
107
- """Create a new configuration with additional sensitive markers."""
108
- new_sensitive_markers = self.sensitive_markers.union([key.lower() for key in markers])
109
- return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
110
-
111
- def without_sensitive_markers(self, *markers: str) -> Config:
112
- """Create a new configuration without certain sensitive markers."""
113
- new_sensitive_markers = self.sensitive_markers.difference([key.lower() for key in markers])
114
- return replace(self, sensitive_markers=frozenset(new_sensitive_markers))
115
-
116
-
117
- _thread_local = threading.local()
118
-
119
-
120
- def _get_default_sanitization_config() -> Config:
121
- # Initialize the thread-local default sanitization config if not already set
122
- if not hasattr(_thread_local, "default_sanitization_config"):
123
- _thread_local.default_sanitization_config = Config()
124
- return _thread_local.default_sanitization_config
125
-
126
-
127
- def configure(config: Config) -> None:
128
- _thread_local.default_sanitization_config = config
129
-
130
-
131
- def sanitize_value(item: Any, *, config: Config | None = None) -> None:
132
- """Sanitize sensitive values within a given item.
133
-
134
- This function is recursive and will sanitize sensitive data within nested
135
- dictionaries and lists as well.
136
- """
137
- config = config or _get_default_sanitization_config()
138
- if isinstance(item, MutableMapping):
139
- for key in list(item.keys()):
140
- lower_key = key.lower()
141
- if lower_key in config.keys_to_sanitize or any(marker in lower_key for marker in config.sensitive_markers):
142
- if isinstance(item[key], list):
143
- item[key] = [config.replacement]
144
- else:
145
- item[key] = config.replacement
146
- for value in item.values():
147
- if isinstance(value, (MutableMapping, MutableSequence)):
148
- sanitize_value(value, config=config)
149
- elif isinstance(item, MutableSequence):
150
- for value in item:
151
- if isinstance(value, (MutableMapping, MutableSequence)):
152
- sanitize_value(value, config=config)
153
-
154
-
155
- def sanitize_case(case: Case, *, config: Config | None = None) -> None:
156
- """Sanitize sensitive values within a given case."""
157
- if case.path_parameters is not None:
158
- sanitize_value(case.path_parameters, config=config)
159
- if case.headers is not None:
160
- sanitize_value(case.headers, config=config)
161
- if case.cookies is not None:
162
- sanitize_value(case.cookies, config=config)
163
- if case.query is not None:
164
- sanitize_value(case.query, config=config)
165
- if case.body not in (None, NOT_SET):
166
- sanitize_value(case.body, config=config)
167
- if case.source is not None:
168
- sanitize_history(case.source, config=config)
169
-
170
-
171
- def sanitize_history(source: CaseSource, *, config: Config | None = None) -> None:
172
- """Recursively sanitize history of case/response pairs."""
173
- current: CaseSource | None = source
174
- while current is not None:
175
- sanitize_case(current.case, config=config)
176
- sanitize_response(current.response, config=config)
177
- current = current.case.source
178
-
179
-
180
- def sanitize_response(response: GenericResponse, *, config: Config | None = None) -> None:
181
- # Sanitize headers
182
- sanitize_value(response.headers, config=config)
183
-
184
-
185
- def sanitize_request(request: PreparedRequest | Request, *, config: Config | None = None) -> None:
186
- from requests import PreparedRequest
187
-
188
- if isinstance(request, PreparedRequest) and request.url:
189
- request.url = sanitize_url(request.url, config=config)
190
- else:
191
- request = cast("Request", request)
192
- request.uri = sanitize_url(request.uri, config=config)
193
- # Sanitize headers
194
- sanitize_value(request.headers, config=config)
195
-
196
-
197
- def sanitize_output(case: Case, response: GenericResponse | None = None, *, config: Config | None = None) -> None:
198
- sanitize_case(case, config=config)
199
- if response is not None:
200
- sanitize_response(response, config=config)
201
- sanitize_request(response.request, config=config)
202
-
203
-
204
- def sanitize_url(url: str, *, config: Config | None = None) -> str:
205
- """Sanitize sensitive parts of a given URL.
206
-
207
- This function will sanitize the authority and query parameters in the URL.
208
- """
209
- config = config or _get_default_sanitization_config()
210
- parsed = urlsplit(url)
211
-
212
- # Sanitize authority
213
- netloc_parts = parsed.netloc.split("@")
214
- if len(netloc_parts) > 1:
215
- netloc = f"{config.replacement}@{netloc_parts[-1]}"
216
- else:
217
- netloc = parsed.netloc
218
-
219
- # Sanitize query parameters
220
- query = parse_qs(parsed.query, keep_blank_values=True)
221
- sanitize_value(query, config=config)
222
- sanitized_query = urlencode(query, doseq=True)
223
-
224
- # Reconstruct the URL
225
- sanitized_url_parts = parsed._replace(netloc=netloc, query=sanitized_query)
226
- return urlunsplit(sanitized_url_parts)
227
-
228
-
229
- def sanitize_serialized_check(check: SerializedCheck, *, config: Config | None = None) -> None:
230
- sanitize_request(check.request, config=config)
231
- response = check.response
232
- if response:
233
- sanitize_value(response.headers, config=config)
234
- sanitize_serialized_case(check.example, config=config)
235
- for entry in check.history:
236
- sanitize_serialized_case(entry.case, config=config)
237
- sanitize_value(entry.response.headers, config=config)
238
-
239
-
240
- def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = None) -> None:
241
- case.url = sanitize_url(case.url, config=config)
242
- for value in (case.path_parameters, case.headers, case.cookies, case.query, case.extra_headers):
243
- if value is not None:
244
- sanitize_value(value, config=config)
245
-
246
-
247
- def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
248
- sanitize_request(interaction.request, config=config)
249
- if interaction.response is not None:
250
- sanitize_value(interaction.response.headers, config=config)
251
- for check in interaction.checks:
252
- sanitize_serialized_check(check, config=config)
@@ -1,328 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import binascii
4
- import os
5
- from dataclasses import dataclass
6
- from io import BytesIO
7
- from typing import (
8
- TYPE_CHECKING,
9
- Any,
10
- Callable,
11
- Collection,
12
- Dict,
13
- Generator,
14
- Protocol,
15
- cast,
16
- runtime_checkable,
17
- )
18
-
19
- from ._xml import _to_xml
20
- from .internal.copy import fast_deepcopy
21
- from .internal.jsonschema import traverse_schema
22
- from .transports.content_types import (
23
- is_json_media_type,
24
- is_plain_text_media_type,
25
- is_xml_media_type,
26
- parse_content_type,
27
- )
28
-
29
- if TYPE_CHECKING:
30
- from .models import Case
31
-
32
-
33
- SERIALIZERS: dict[str, type[Serializer]] = {}
34
-
35
-
36
- @dataclass
37
- class Binary(str):
38
- """A wrapper around `bytes` to resolve OpenAPI and JSON Schema `format` discrepancies.
39
-
40
- Treat `bytes` as a valid type, allowing generation of bytes for OpenAPI `format` values like `binary` or `file`
41
- that JSON Schema expects to be strings.
42
- """
43
-
44
- data: bytes
45
-
46
- __slots__ = ("data",)
47
-
48
- def __hash__(self) -> int:
49
- return hash(self.data)
50
-
51
-
52
- @dataclass
53
- class SerializerContext:
54
- """The context for serialization process.
55
-
56
- :ivar Case case: Generated example that is being processed.
57
- """
58
-
59
- case: Case
60
-
61
- @property
62
- def media_type(self) -> str:
63
- # `media_type` is a string, otherwise we won't serialize anything
64
- return cast(str, self.case.media_type)
65
-
66
- # Note on type casting below.
67
- # If we serialize data, then there should be non-empty definition for it in the first place
68
- # Therefore `schema` is never `None` if called from here. However, `APIOperation.get_raw_payload_schema` is
69
- # generic and can be called from other places where it may return `None`
70
-
71
- def get_raw_payload_schema(self) -> dict[str, Any]:
72
- schema = self.case.operation.get_raw_payload_schema(self.media_type)
73
- return cast(Dict[str, Any], schema)
74
-
75
- def get_resolved_payload_schema(self) -> dict[str, Any]:
76
- schema = self.case.operation.get_resolved_payload_schema(self.media_type)
77
- return cast(Dict[str, Any], schema)
78
-
79
-
80
- @runtime_checkable
81
- class Serializer(Protocol):
82
- """Transform generated data to a form supported by the transport layer.
83
-
84
- For example, to handle multipart payloads, we need to serialize them differently for
85
- `requests` and `werkzeug` transports.
86
- """
87
-
88
- def as_requests(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
89
- raise NotImplementedError
90
-
91
- def as_werkzeug(self, context: SerializerContext, payload: Any) -> dict[str, Any]:
92
- raise NotImplementedError
93
-
94
-
95
- def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[type[Serializer]], type[Serializer]]:
96
- """Register a serializer for the given media type.
97
-
98
- Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
99
- should have two methods, ``as_requests`` and ``as_werkzeug``, providing keyword arguments that Schemathesis will
100
- pass to ``requests.request`` and ``werkzeug.Client.open`` respectively.
101
-
102
- .. code-block:: python
103
-
104
- @register("text/csv")
105
- class CSVSerializer:
106
- def as_requests(self, context, value):
107
- return {"data": to_csv(value)}
108
-
109
- def as_werkzeug(self, context, value):
110
- return {"data": to_csv(value)}
111
-
112
- The primary purpose of serializers is to transform data from its Python representation to the format suitable
113
- for making an API call. The generated data structure depends on your schema, but its type matches
114
- Python equivalents to the JSON Schema types.
115
-
116
- """
117
-
118
- def wrapper(serializer: type[Serializer]) -> type[Serializer]:
119
- if not issubclass(serializer, Serializer):
120
- raise TypeError(
121
- f"`{serializer.__name__}` is not a valid serializer. "
122
- f"Check `schemathesis.serializers.Serializer` documentation for examples."
123
- )
124
- SERIALIZERS[media_type] = serializer
125
- for alias in aliases:
126
- SERIALIZERS[alias] = serializer
127
- return serializer
128
-
129
- return wrapper
130
-
131
-
132
- def unregister(media_type: str) -> None:
133
- """Remove registered serializer for the given media type."""
134
- del SERIALIZERS[media_type]
135
-
136
-
137
- def _to_json(value: Any) -> dict[str, Any]:
138
- if isinstance(value, bytes):
139
- # Possible to get via explicit examples, e.g. `externalValue`
140
- return {"data": value}
141
- if isinstance(value, Binary):
142
- return {"data": value.data}
143
- if value is None:
144
- # If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
145
- # argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
146
- # Therefore we explicitly create such payload
147
- return {"data": b"null"}
148
- return {"json": value}
149
-
150
-
151
- @register("application/json", aliases=("text/json",))
152
- class JSONSerializer:
153
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
154
- return _to_json(value)
155
-
156
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
157
- return _to_json(value)
158
-
159
-
160
- def _replace_binary(value: dict) -> dict:
161
- return {key: value.data if isinstance(value, Binary) else value for key, value in value.items()}
162
-
163
-
164
- def _to_yaml(value: Any) -> dict[str, Any]:
165
- import yaml
166
-
167
- try:
168
- from yaml import CSafeDumper as SafeDumper
169
- except ImportError:
170
- from yaml import SafeDumper # type: ignore
171
-
172
- if isinstance(value, bytes):
173
- return {"data": value}
174
- if isinstance(value, Binary):
175
- return {"data": value.data}
176
- if isinstance(value, (list, dict)):
177
- value = traverse_schema(value, _replace_binary)
178
- return {"data": yaml.dump(value, Dumper=SafeDumper)}
179
-
180
-
181
- @register("text/yaml", aliases=("text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"))
182
- class YAMLSerializer:
183
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
184
- return _to_yaml(value)
185
-
186
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
187
- return _to_yaml(value)
188
-
189
-
190
- @register("application/xml", aliases=("text/xml",))
191
- class XMLSerializer:
192
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
193
- return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
194
-
195
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
196
- return _to_xml(value, context.get_raw_payload_schema(), context.get_resolved_payload_schema())
197
-
198
-
199
- def _should_coerce_to_bytes(item: Any) -> bool:
200
- """Whether the item should be converted to bytes."""
201
- # These types are OK in forms, others should be coerced to bytes
202
- return isinstance(item, Binary) or not isinstance(item, (bytes, str, int))
203
-
204
-
205
- def _prepare_form_data(data: dict[str, Any]) -> dict[str, Any]:
206
- """Make the generated data suitable for sending as multipart.
207
-
208
- If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
209
- we convert it to bytes and send it as-is, ignoring any conversion errors.
210
-
211
- NOTE. This behavior might change in the future.
212
- """
213
- for name, value in data.items():
214
- if isinstance(value, list):
215
- data[name] = [_to_bytes(item) if _should_coerce_to_bytes(item) else item for item in value]
216
- elif _should_coerce_to_bytes(value):
217
- data[name] = _to_bytes(value)
218
- return data
219
-
220
-
221
- def _to_bytes(value: Any) -> bytes:
222
- """Convert the input value to bytes and ignore any conversion errors."""
223
- if isinstance(value, bytes):
224
- return value
225
- if isinstance(value, Binary):
226
- return value.data
227
- return str(value).encode(errors="ignore")
228
-
229
-
230
- def choose_boundary() -> str:
231
- """Random boundary name."""
232
- return binascii.hexlify(os.urandom(16)).decode("ascii")
233
-
234
-
235
- def _encode_multipart(value: Any, boundary: str) -> bytes:
236
- """Encode any value as multipart.
237
-
238
- NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
239
- be used as multipart, in cases when the API schema dictates so.
240
- """
241
- # For such cases we stringify the value and wrap it to a randomly-generated boundary
242
- body = BytesIO()
243
- body.write(f"--{boundary}\r\n".encode())
244
- body.write(str(value).encode())
245
- body.write(f"--{boundary}--\r\n".encode("latin-1"))
246
- return body.getvalue()
247
-
248
-
249
- @register("multipart/form-data", aliases=("multipart/mixed",))
250
- class MultipartSerializer:
251
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
252
- if isinstance(value, bytes):
253
- return {"data": value}
254
- if isinstance(value, dict):
255
- value = fast_deepcopy(value)
256
- multipart = _prepare_form_data(value)
257
- files, data = context.case.operation.prepare_multipart(multipart)
258
- return {"files": files, "data": data}
259
- # Uncommon schema. For example - `{"type": "string"}`
260
- boundary = choose_boundary()
261
- raw_data = _encode_multipart(value, boundary)
262
- content_type = f"multipart/form-data; boundary={boundary}"
263
- return {"data": raw_data, "headers": {"Content-Type": content_type}}
264
-
265
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
266
- return {"data": value}
267
-
268
-
269
- @register("application/x-www-form-urlencoded")
270
- class URLEncodedFormSerializer:
271
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
272
- return {"data": value}
273
-
274
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
275
- return {"data": value}
276
-
277
-
278
- @register("text/plain")
279
- class TextSerializer:
280
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
281
- if isinstance(value, bytes):
282
- return {"data": value}
283
- return {"data": str(value).encode("utf8")}
284
-
285
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
286
- if isinstance(value, bytes):
287
- return {"data": value}
288
- return {"data": str(value)}
289
-
290
-
291
- @register("application/octet-stream")
292
- class OctetStreamSerializer:
293
- def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
294
- return {"data": _to_bytes(value)}
295
-
296
- def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
297
- return {"data": _to_bytes(value)}
298
-
299
-
300
- def get_matching_media_types(media_type: str) -> Generator[str, None, None]:
301
- """Get all registered media types matching the given media type."""
302
- if media_type == "*/*":
303
- # Shortcut to avoid comparing all values
304
- yield from iter(SERIALIZERS)
305
- else:
306
- main, sub = parse_content_type(media_type)
307
- if main == "application" and (sub == "json" or sub.endswith("+json")):
308
- yield media_type
309
- else:
310
- for registered_media_type in SERIALIZERS:
311
- target_main, target_sub = parse_content_type(registered_media_type)
312
- if main in ("*", target_main) and sub in ("*", target_sub):
313
- yield registered_media_type
314
-
315
-
316
- def get_first_matching_media_type(media_type: str) -> str | None:
317
- return next(get_matching_media_types(media_type), None)
318
-
319
-
320
- def get(media_type: str) -> type[Serializer] | None:
321
- """Get an appropriate serializer for the given media type."""
322
- if is_json_media_type(media_type):
323
- media_type = "application/json"
324
- if is_plain_text_media_type(media_type):
325
- media_type = "text/plain"
326
- if is_xml_media_type(media_type):
327
- media_type = "application/xml"
328
- return SERIALIZERS.get(media_type)
@@ -1,18 +0,0 @@
1
- from . import auth, ci, hosts
2
- from .constants import (
3
- DEFAULT_HOSTNAME,
4
- DEFAULT_HOSTS_PATH,
5
- DEFAULT_PROTOCOL,
6
- DEFAULT_URL,
7
- HOSTNAME_ENV_VAR,
8
- HOSTS_PATH_ENV_VAR,
9
- PROTOCOL_ENV_VAR,
10
- REPORT_ENV_VAR,
11
- TELEMETRY_ENV_VAR,
12
- TOKEN_ENV_VAR,
13
- URL_ENV_VAR,
14
- WORKER_CHECK_PERIOD,
15
- WORKER_FINISH_TIMEOUT,
16
- )
17
- from .events import Completed, Error, Event, Failed, Metadata, Timeout
18
- from .report import FileReportHandler, ServiceReportHandler
@@ -1,11 +0,0 @@
1
- from . import metadata
2
- from .constants import DEFAULT_HOSTNAME, DEFAULT_PROTOCOL
3
-
4
-
5
- def login(token: str, hostname: str = DEFAULT_HOSTNAME, protocol: str = DEFAULT_PROTOCOL, verify: bool = True) -> str:
6
- from .client import ServiceClient
7
-
8
- """Make a login request to SaaS."""
9
- client = ServiceClient(f"{protocol}://{hostname}", token, verify=verify)
10
- response = client.login(metadata=metadata.Metadata())
11
- return response.username