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,561 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import enum
5
- import json
6
- import re
7
- import sys
8
- import threading
9
- from dataclasses import dataclass, field
10
- from http.cookies import SimpleCookie
11
- from queue import Queue
12
- from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
13
- from urllib.parse import parse_qsl, urlparse
14
-
15
- import harfile
16
-
17
- from ..constants import SCHEMATHESIS_VERSION
18
- from ..runner import events
19
- from .handlers import EventHandler
20
-
21
- if TYPE_CHECKING:
22
- import click
23
- import requests
24
-
25
- from ..models import Request, Response
26
- from ..runner.serialization import SerializedCheck, SerializedInteraction
27
- from ..types import RequestCert
28
- from .context import ExecutionContext
29
-
30
- # Wait until the worker terminates
31
- WRITER_WORKER_JOIN_TIMEOUT = 1
32
-
33
-
34
- class CassetteFormat(str, enum.Enum):
35
- """Type of the cassette."""
36
-
37
- VCR = "vcr"
38
- HAR = "har"
39
-
40
- @classmethod
41
- def from_str(cls, value: str) -> CassetteFormat:
42
- try:
43
- return cls[value.upper()]
44
- except KeyError:
45
- available_formats = ", ".join(cls)
46
- raise ValueError(
47
- f"Invalid value for cassette format: {value}. Available formats: {available_formats}"
48
- ) from None
49
-
50
-
51
- @dataclass
52
- class CassetteWriter(EventHandler):
53
- """Write interactions in a YAML cassette.
54
-
55
- A low-level interface is used to write data to YAML file during the test run and reduce the delay at
56
- the end of the test run.
57
- """
58
-
59
- file_handle: click.utils.LazyFile
60
- format: CassetteFormat
61
- preserve_exact_body_bytes: bool
62
- queue: Queue = field(default_factory=Queue)
63
- worker: threading.Thread = field(init=False)
64
-
65
- def __post_init__(self) -> None:
66
- kwargs = {
67
- "file_handle": self.file_handle,
68
- "queue": self.queue,
69
- "preserve_exact_body_bytes": self.preserve_exact_body_bytes,
70
- }
71
- writer: Callable
72
- if self.format == CassetteFormat.HAR:
73
- writer = har_writer
74
- else:
75
- writer = vcr_writer
76
- self.worker = threading.Thread(name="SchemathesisCassetteWriter", target=writer, kwargs=kwargs)
77
- self.worker.start()
78
-
79
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
80
- if isinstance(event, events.Initialized):
81
- # In the beginning we write metadata and start `http_interactions` list
82
- self.queue.put(Initialize(seed=event.seed))
83
- elif isinstance(event, events.AfterExecution):
84
- self.queue.put(
85
- Process(
86
- correlation_id=event.correlation_id,
87
- thread_id=event.thread_id,
88
- interactions=event.result.interactions,
89
- )
90
- )
91
- elif isinstance(event, events.AfterStatefulExecution):
92
- self.queue.put(
93
- Process(
94
- # Correlation ID is not used in stateful testing
95
- correlation_id="",
96
- thread_id=event.thread_id,
97
- interactions=event.result.interactions,
98
- )
99
- )
100
- elif isinstance(event, events.Finished):
101
- self.shutdown()
102
-
103
- def shutdown(self) -> None:
104
- self.queue.put(Finalize())
105
- self._stop_worker()
106
-
107
- def _stop_worker(self) -> None:
108
- self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
109
-
110
-
111
- @dataclass
112
- class Initialize:
113
- """Start up, the first message to make preparations before proceeding the input data."""
114
-
115
- seed: int | None
116
-
117
-
118
- @dataclass
119
- class Process:
120
- """A new chunk of data should be processed."""
121
-
122
- correlation_id: str
123
- thread_id: int
124
- interactions: list[SerializedInteraction]
125
-
126
-
127
- @dataclass
128
- class Finalize:
129
- """The work is done and there will be no more messages to process."""
130
-
131
-
132
- def get_command_representation() -> str:
133
- """Get how Schemathesis was run."""
134
- # It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
135
- if not sys.argv[0].endswith(("schemathesis", "st")):
136
- return "<unknown entrypoint>"
137
- args = " ".join(sys.argv[1:])
138
- return f"st {args}"
139
-
140
-
141
- def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
142
- """Write YAML to a file in an incremental manner.
143
-
144
- This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
145
- - It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
146
- - Implementation complexity. We have a quite simple format where almost all values are strings, and it is much
147
- simpler to implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit
148
- types. Another point is that with `pyyaml` we need to emit events and handle some low-level details like
149
- providing tags, anchors to have incremental writing, with primitive types it is much simpler.
150
- """
151
- current_id = 1
152
- stream = file_handle.open()
153
-
154
- def format_header_values(values: list[str]) -> str:
155
- return "\n".join(f" - {json.dumps(v)}" for v in values)
156
-
157
- def format_headers(headers: dict[str, list[str]]) -> str:
158
- return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
159
-
160
- def format_check_message(message: str | None) -> str:
161
- return "~" if message is None else f"{message!r}"
162
-
163
- def format_checks(checks: list[SerializedCheck]) -> str:
164
- if not checks:
165
- return " checks: []"
166
- items = "\n".join(
167
- f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
168
- for check in checks
169
- )
170
- return f"""
171
- checks:
172
- {items}"""
173
-
174
- if preserve_exact_body_bytes:
175
-
176
- def format_request_body(output: IO, request: Request) -> None:
177
- if request.body is not None:
178
- output.write(
179
- f"""
180
- body:
181
- encoding: 'utf-8'
182
- base64_string: '{request.body}'"""
183
- )
184
-
185
- def format_response_body(output: IO, response: Response) -> None:
186
- if response.body is not None:
187
- output.write(
188
- f""" body:
189
- encoding: '{response.encoding}'
190
- base64_string: '{response.body}'"""
191
- )
192
-
193
- else:
194
-
195
- def format_request_body(output: IO, request: Request) -> None:
196
- if request.body is not None:
197
- string = _safe_decode(request.body, "utf8")
198
- output.write(
199
- """
200
- body:
201
- encoding: 'utf-8'
202
- string: """
203
- )
204
- write_double_quoted(output, string)
205
-
206
- def format_response_body(output: IO, response: Response) -> None:
207
- if response.body is not None:
208
- encoding = response.encoding or "utf8"
209
- string = _safe_decode(response.body, encoding)
210
- output.write(
211
- f""" body:
212
- encoding: '{encoding}'
213
- string: """
214
- )
215
- write_double_quoted(output, string)
216
-
217
- seed = "null"
218
- while True:
219
- item = queue.get()
220
- if isinstance(item, Initialize):
221
- seed = f"'{item.seed}'"
222
- stream.write(
223
- f"""command: '{get_command_representation()}'
224
- recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
225
- http_interactions:"""
226
- )
227
- elif isinstance(item, Process):
228
- for interaction in item.interactions:
229
- status = interaction.status.name.upper()
230
- # Body payloads are handled via separate `stream.write` calls to avoid some allocations
231
- phase = f"'{interaction.phase.value}'" if interaction.phase is not None else "null"
232
- stream.write(
233
- f"""\n- id: '{current_id}'
234
- status: '{status}'
235
- seed: {seed}
236
- thread_id: {item.thread_id}
237
- correlation_id: '{item.correlation_id}'
238
- data_generation_method: '{interaction.data_generation_method.value}'
239
- meta:
240
- description: """
241
- )
242
-
243
- if interaction.description is not None:
244
- write_double_quoted(stream, interaction.description)
245
- else:
246
- stream.write("null")
247
-
248
- stream.write("\n location: ")
249
- if interaction.location is not None:
250
- write_double_quoted(stream, interaction.location)
251
- else:
252
- stream.write("null")
253
-
254
- stream.write("\n parameter: ")
255
- if interaction.parameter is not None:
256
- write_double_quoted(stream, interaction.parameter)
257
- else:
258
- stream.write("null")
259
-
260
- stream.write("\n parameter_location: ")
261
- if interaction.parameter_location is not None:
262
- write_double_quoted(stream, interaction.parameter_location)
263
- else:
264
- stream.write("null")
265
- stream.write(
266
- f"""
267
- phase: {phase}
268
- elapsed: '{interaction.response.elapsed if interaction.response else 0}'
269
- recorded_at: '{interaction.recorded_at}'
270
- {format_checks(interaction.checks)}
271
- request:
272
- uri: '{interaction.request.uri}'
273
- method: '{interaction.request.method}'
274
- headers:
275
- {format_headers(interaction.request.headers)}"""
276
- )
277
- format_request_body(stream, interaction.request)
278
- if interaction.response is not None:
279
- stream.write(
280
- f"""
281
- response:
282
- status:
283
- code: '{interaction.response.status_code}'
284
- message: {json.dumps(interaction.response.message)}
285
- headers:
286
- {format_headers(interaction.response.headers)}
287
- """
288
- )
289
- format_response_body(stream, interaction.response)
290
- stream.write(
291
- f"""
292
- http_version: '{interaction.response.http_version}'"""
293
- )
294
- else:
295
- stream.write("""
296
- response: null
297
- """)
298
- current_id += 1
299
- else:
300
- break
301
- file_handle.close()
302
-
303
-
304
- def _safe_decode(value: str, encoding: str) -> str:
305
- """Decode base64-encoded body bytes as a string."""
306
- return base64.b64decode(value).decode(encoding, "replace")
307
-
308
-
309
- def write_double_quoted(stream: IO, text: str) -> None:
310
- """Writes a valid YAML string enclosed in double quotes."""
311
- from yaml.emitter import Emitter
312
-
313
- # Adapted from `yaml.Emitter.write_double_quoted`:
314
- # - Doesn't split the string, therefore doesn't track the current column
315
- # - Doesn't encode the input
316
- # - Allows Unicode unconditionally
317
- stream.write('"')
318
- start = end = 0
319
- length = len(text)
320
- while end <= length:
321
- ch = None
322
- if end < length:
323
- ch = text[end]
324
- if (
325
- ch is None
326
- or ch in '"\\\x85\u2028\u2029\ufeff'
327
- or not ("\x20" <= ch <= "\x7e" or ("\xa0" <= ch <= "\ud7ff" or "\ue000" <= ch <= "\ufffd"))
328
- ):
329
- if start < end:
330
- stream.write(text[start:end])
331
- start = end
332
- if ch is not None:
333
- # Escape character
334
- if ch in Emitter.ESCAPE_REPLACEMENTS:
335
- data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
336
- elif ch <= "\xff":
337
- data = f"\\x{ord(ch):02X}"
338
- elif ch <= "\uffff":
339
- data = f"\\u{ord(ch):04X}"
340
- else:
341
- data = f"\\U{ord(ch):08X}"
342
- stream.write(data)
343
- start = end + 1
344
- end += 1
345
- stream.write('"')
346
-
347
-
348
- def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
349
- if preserve_exact_body_bytes:
350
-
351
- def get_body(body: str) -> str:
352
- return body
353
- else:
354
-
355
- def get_body(body: str) -> str:
356
- return base64.b64decode(body).decode("utf-8", errors="replace")
357
-
358
- with harfile.open(file_handle) as har:
359
- while True:
360
- item = queue.get()
361
- if isinstance(item, Process):
362
- for interaction in item.interactions:
363
- query_params = urlparse(interaction.request.uri).query
364
- if interaction.request.body is not None:
365
- post_data = harfile.PostData(
366
- mimeType=interaction.request.headers.get("Content-Type", [""])[0],
367
- text=get_body(interaction.request.body),
368
- )
369
- else:
370
- post_data = None
371
- if interaction.response is not None:
372
- content_type = interaction.response.headers.get("Content-Type", [""])[0]
373
- content = harfile.Content(
374
- size=interaction.response.body_size or 0,
375
- mimeType=content_type,
376
- text=get_body(interaction.response.body) if interaction.response.body is not None else None,
377
- encoding="base64"
378
- if interaction.response.body is not None and preserve_exact_body_bytes
379
- else None,
380
- )
381
- http_version = f"HTTP/{interaction.response.http_version}"
382
- response = harfile.Response(
383
- status=interaction.response.status_code,
384
- httpVersion=http_version,
385
- statusText=interaction.response.message,
386
- headers=[
387
- harfile.Record(name=name, value=values[0])
388
- for name, values in interaction.response.headers.items()
389
- ],
390
- cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
391
- content=content,
392
- headersSize=_headers_size(interaction.response.headers),
393
- bodySize=interaction.response.body_size or 0,
394
- redirectURL=interaction.response.headers.get("Location", [""])[0],
395
- )
396
- time = round(interaction.response.elapsed * 1000, 2)
397
- else:
398
- response = HARFILE_NO_RESPONSE
399
- time = 0
400
- http_version = ""
401
-
402
- har.add_entry(
403
- startedDateTime=interaction.recorded_at,
404
- time=time,
405
- request=harfile.Request(
406
- method=interaction.request.method.upper(),
407
- url=interaction.request.uri,
408
- httpVersion=http_version,
409
- headers=[
410
- harfile.Record(name=name, value=values[0])
411
- for name, values in interaction.request.headers.items()
412
- ],
413
- queryString=[
414
- harfile.Record(name=name, value=value)
415
- for name, value in parse_qsl(query_params, keep_blank_values=True)
416
- ],
417
- cookies=_extract_cookies(interaction.request.headers.get("Cookie", [])),
418
- headersSize=_headers_size(interaction.request.headers),
419
- bodySize=interaction.request.body_size or 0,
420
- postData=post_data,
421
- ),
422
- response=response,
423
- timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
424
- )
425
- elif isinstance(item, Finalize):
426
- break
427
-
428
-
429
- HARFILE_NO_RESPONSE = harfile.Response(
430
- status=0,
431
- httpVersion="",
432
- statusText="",
433
- headers=[],
434
- cookies=[],
435
- content=harfile.Content(),
436
- )
437
-
438
-
439
- def _headers_size(headers: dict[str, list[str]]) -> int:
440
- size = 0
441
- for name, values in headers.items():
442
- # 4 is for ": " and "\r\n"
443
- size += len(name) + 4 + len(values[0])
444
- return size
445
-
446
-
447
- def _extract_cookies(headers: list[str]) -> list[harfile.Cookie]:
448
- return [cookie for items in headers for item in items for cookie in _cookie_to_har(item)]
449
-
450
-
451
- def _cookie_to_har(cookie: str) -> Iterator[harfile.Cookie]:
452
- parsed = SimpleCookie(cookie)
453
- for name, data in parsed.items():
454
- yield harfile.Cookie(
455
- name=name,
456
- value=data.value,
457
- path=data["path"] or None,
458
- domain=data["domain"] or None,
459
- expires=data["expires"] or None,
460
- httpOnly=data["httponly"] or None,
461
- secure=data["secure"] or None,
462
- )
463
-
464
-
465
- @dataclass
466
- class Replayed:
467
- interaction: dict[str, Any]
468
- response: requests.Response
469
-
470
-
471
- def replay(
472
- cassette: dict[str, Any],
473
- id_: str | None = None,
474
- status: str | None = None,
475
- uri: str | None = None,
476
- method: str | None = None,
477
- request_tls_verify: bool = True,
478
- request_cert: RequestCert | None = None,
479
- request_proxy: str | None = None,
480
- ) -> Generator[Replayed, None, None]:
481
- """Replay saved interactions."""
482
- import requests
483
-
484
- session = requests.Session()
485
- session.verify = request_tls_verify
486
- session.cert = request_cert
487
- kwargs = {}
488
- if request_proxy is not None:
489
- kwargs["proxies"] = {"all": request_proxy}
490
- for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
491
- request = get_prepared_request(interaction["request"])
492
- response = session.send(request, **kwargs) # type: ignore
493
- yield Replayed(interaction, response)
494
-
495
-
496
- def filter_cassette(
497
- interactions: list[dict[str, Any]],
498
- id_: str | None = None,
499
- status: str | None = None,
500
- uri: str | None = None,
501
- method: str | None = None,
502
- ) -> Iterator[dict[str, Any]]:
503
- filters = []
504
-
505
- def id_filter(item: dict[str, Any]) -> bool:
506
- return item["id"] == id_
507
-
508
- def status_filter(item: dict[str, Any]) -> bool:
509
- status_ = cast(str, status)
510
- return item["status"].upper() == status_.upper()
511
-
512
- def uri_filter(item: dict[str, Any]) -> bool:
513
- uri_ = cast(str, uri)
514
- return bool(re.search(uri_, item["request"]["uri"]))
515
-
516
- def method_filter(item: dict[str, Any]) -> bool:
517
- method_ = cast(str, method)
518
- return bool(re.search(method_, item["request"]["method"]))
519
-
520
- if id_ is not None:
521
- filters.append(id_filter)
522
-
523
- if status is not None:
524
- filters.append(status_filter)
525
-
526
- if uri is not None:
527
- filters.append(uri_filter)
528
-
529
- if method is not None:
530
- filters.append(method_filter)
531
-
532
- def is_match(interaction: dict[str, Any]) -> bool:
533
- return all(filter_(interaction) for filter_ in filters)
534
-
535
- return filter(is_match, interactions)
536
-
537
-
538
- def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
539
- """Create a `requests.PreparedRequest` from a serialized one."""
540
- import requests
541
- from requests.cookies import RequestsCookieJar
542
- from requests.structures import CaseInsensitiveDict
543
-
544
- prepared = requests.PreparedRequest()
545
- prepared.method = data["method"]
546
- prepared.url = data["uri"]
547
- prepared._cookies = RequestsCookieJar() # type: ignore
548
- if "body" in data:
549
- body = data["body"]
550
- if "base64_string" in body:
551
- content = body["base64_string"]
552
- if content:
553
- prepared.body = base64.b64decode(content)
554
- else:
555
- content = body["string"]
556
- if content:
557
- prepared.body = content.encode("utf8")
558
- # There is always 1 value in a request
559
- headers = [(key, value[0]) for key, value in data["headers"].items()]
560
- prepared.headers = CaseInsensitiveDict(headers)
561
- return prepared
@@ -1,75 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import shutil
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Generator
6
-
7
- from ..code_samples import CodeSampleStyle
8
- from ..internal.deprecation import deprecated_property
9
- from ..internal.output import OutputConfig
10
-
11
- if TYPE_CHECKING:
12
- import os
13
- from queue import Queue
14
-
15
- import hypothesis
16
-
17
- from ..internal.result import Result
18
- from ..runner.probes import ProbeRun
19
- from ..runner.serialization import SerializedTestResult
20
- from ..service.models import AnalysisResult
21
- from ..stateful.sink import StateMachineSink
22
-
23
-
24
- @dataclass
25
- class ServiceReportContext:
26
- queue: Queue
27
- service_base_url: str
28
-
29
-
30
- @dataclass
31
- class FileReportContext:
32
- queue: Queue
33
- filename: str | None = None
34
-
35
-
36
- @dataclass
37
- class ExecutionContext:
38
- """Storage for the current context of the execution."""
39
-
40
- hypothesis_settings: hypothesis.settings
41
- hypothesis_output: list[str] = field(default_factory=list)
42
- workers_num: int = 1
43
- rate_limit: str | None = None
44
- show_trace: bool = False
45
- wait_for_schema: float | None = None
46
- validate_schema: bool = True
47
- operations_processed: int = 0
48
- # It is set in runtime, from the `Initialized` event
49
- operations_count: int | None = None
50
- seed: int | None = None
51
- current_line_length: int = 0
52
- terminal_size: os.terminal_size = field(default_factory=shutil.get_terminal_size)
53
- results: list[SerializedTestResult] = field(default_factory=list)
54
- cassette_path: str | None = None
55
- junit_xml_file: str | None = None
56
- is_interrupted: bool = False
57
- verbosity: int = 0
58
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
59
- report: ServiceReportContext | FileReportContext | None = None
60
- probes: list[ProbeRun] | None = None
61
- analysis: Result[AnalysisResult, Exception] | None = None
62
- output_config: OutputConfig = field(default_factory=OutputConfig)
63
- state_machine_sink: StateMachineSink | None = None
64
- initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
65
- summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
66
-
67
- @deprecated_property(removed_in="4.0", replacement="show_trace")
68
- def show_errors_tracebacks(self) -> bool:
69
- return self.show_trace
70
-
71
- def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
72
- self.initialization_lines.append(line)
73
-
74
- def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
75
- self.summary_lines.append(line)
schemathesis/cli/debug.py DELETED
@@ -1,27 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- from dataclasses import dataclass
5
- from typing import TYPE_CHECKING
6
-
7
- from .handlers import EventHandler
8
-
9
- if TYPE_CHECKING:
10
- from click.utils import LazyFile
11
-
12
- from ..runner import events
13
- from .context import ExecutionContext
14
-
15
-
16
- @dataclass
17
- class DebugOutputHandler(EventHandler):
18
- file_handle: LazyFile
19
-
20
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
21
- stream = self.file_handle.open()
22
- data = event.asdict()
23
- stream.write(json.dumps(data))
24
- stream.write("\n")
25
-
26
- def shutdown(self) -> None:
27
- self.file_handle.close()
@@ -1,19 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any
4
-
5
- if TYPE_CHECKING:
6
- from ..runner import events
7
- from .context import ExecutionContext
8
-
9
-
10
- class EventHandler:
11
- def __init__(self, *args: Any, **params: Any) -> None:
12
- pass
13
-
14
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
15
- raise NotImplementedError
16
-
17
- def shutdown(self) -> None:
18
- # Do nothing by default
19
- pass