schemathesis 3.13.0__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 (245) 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 -1016
  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 +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  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 +753 -74
  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 +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  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.13.0.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.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,188 +0,0 @@
1
- import os
2
- import re
3
- from contextlib import contextmanager
4
- from typing import Dict, Generator, List, Optional, Tuple, Union
5
- from urllib.parse import urlparse
6
-
7
- import click
8
- import hypothesis
9
- from requests import PreparedRequest, RequestException
10
-
11
- from .. import utils
12
- from ..constants import CodeSampleStyle, DataGenerationMethod
13
- from ..stateful import Stateful
14
- from .constants import DEFAULT_WORKERS
15
-
16
-
17
- def validate_schema(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
18
- if "app" not in ctx.params:
19
- try:
20
- netloc = urlparse(raw_value).netloc
21
- except ValueError as exc:
22
- raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
23
- if not netloc:
24
- if "\x00" in raw_value or not utils.file_exists(raw_value):
25
- raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.")
26
- if "base_url" not in ctx.params and not ctx.params.get("dry_run", False):
27
- raise click.UsageError('Missing argument, "--base-url" is required for SCHEMA specified by file.')
28
- else:
29
- _validate_url(raw_value)
30
- return raw_value
31
-
32
-
33
- def _validate_url(value: str) -> None:
34
- try:
35
- PreparedRequest().prepare_url(value, {}) # type: ignore
36
- except RequestException as exc:
37
- raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
38
-
39
-
40
- def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
41
- try:
42
- netloc = urlparse(raw_value).netloc
43
- except ValueError as exc:
44
- raise click.UsageError("Invalid base URL") from exc
45
- if raw_value and not netloc:
46
- raise click.UsageError("Invalid base URL")
47
- return raw_value
48
-
49
-
50
- APPLICATION_FORMAT_MESSAGE = (
51
- "Can not import application from the given module!\n"
52
- "The `--app` option value should be in format:\n\n path:variable\n\n"
53
- "where `path` is an importable path to a Python module,\n"
54
- "and `variable` is a variable name inside that module."
55
- )
56
-
57
-
58
- def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]) -> Optional[str]:
59
- if raw_value is None:
60
- return raw_value
61
- try:
62
- utils.import_app(raw_value)
63
- # String is returned instead of an app because it might be passed to a subprocess
64
- # Since most app instances are not-transferable to another process, they are passed as strings and
65
- # imported in a subprocess
66
- return raw_value
67
- except Exception as exc:
68
- show_errors_tracebacks = ctx.params["show_errors_tracebacks"]
69
- message = utils.format_exception(exc, show_errors_tracebacks).strip()
70
- click.secho(f"{APPLICATION_FORMAT_MESSAGE}\n\nException:\n\n{message}", fg="red")
71
- if not show_errors_tracebacks:
72
- click.secho(
73
- "\nAdd this option to your command line parameters to see full tracebacks: --show-errors-tracebacks",
74
- fg="red",
75
- )
76
- raise click.exceptions.Exit(1)
77
-
78
-
79
- def validate_auth(
80
- ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
81
- ) -> Optional[Tuple[str, str]]:
82
- if raw_value is not None:
83
- with reraise_format_error(raw_value):
84
- user, password = tuple(raw_value.split(":"))
85
- if not user:
86
- raise click.BadParameter("Username should not be empty")
87
- if not utils.is_latin_1_encodable(user):
88
- raise click.BadParameter("Username should be latin-1 encodable")
89
- if not utils.is_latin_1_encodable(password):
90
- raise click.BadParameter("Password should be latin-1 encodable")
91
- return user, password
92
- return None
93
-
94
-
95
- def validate_headers(
96
- ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]
97
- ) -> Dict[str, str]:
98
- headers = {}
99
- for header in raw_value:
100
- with reraise_format_error(header):
101
- key, value = header.split(":", maxsplit=1)
102
- value = value.lstrip()
103
- key = key.strip()
104
- if not key:
105
- raise click.BadParameter("Header name should not be empty")
106
- if not utils.is_latin_1_encodable(key):
107
- raise click.BadParameter("Header name should be latin-1 encodable")
108
- if not utils.is_latin_1_encodable(value):
109
- raise click.BadParameter("Header value should be latin-1 encodable")
110
- if utils.has_invalid_characters(key, value):
111
- raise click.BadParameter("Invalid return character or leading space in header")
112
- headers[key] = value
113
- return headers
114
-
115
-
116
- def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]) -> Tuple[str, ...]:
117
- for value in raw_value:
118
- try:
119
- re.compile(value)
120
- except (re.error, OverflowError, RuntimeError) as exc:
121
- raise click.BadParameter(f"Invalid regex: {exc.args[0]}")
122
- return raw_value
123
-
124
-
125
- def validate_request_cert_key(
126
- ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
127
- ) -> Optional[str]:
128
- if raw_value is not None and "request_cert" not in ctx.params:
129
- raise click.UsageError('Missing argument, "--request-cert" should be specified as well.')
130
- return raw_value
131
-
132
-
133
- def convert_verbosity(
134
- ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]
135
- ) -> Optional[hypothesis.Verbosity]:
136
- if value is None:
137
- return value
138
- return hypothesis.Verbosity[value]
139
-
140
-
141
- def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]) -> Optional[Stateful]:
142
- if value is None:
143
- return value
144
- return Stateful[value]
145
-
146
-
147
- def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
148
- return CodeSampleStyle.from_str(value)
149
-
150
-
151
- def convert_data_generation_method(
152
- ctx: click.core.Context, param: click.core.Parameter, value: str
153
- ) -> List[DataGenerationMethod]:
154
- return [DataGenerationMethod[value]]
155
-
156
-
157
- def convert_request_tls_verify(ctx: click.core.Context, param: click.core.Parameter, value: str) -> Union[str, bool]:
158
- if value.lower() in ("y", "yes", "t", "true", "on", "1"):
159
- return True
160
- if value.lower() in ("n", "no", "f", "false", "off", "0"):
161
- return False
162
- return value
163
-
164
-
165
- @contextmanager
166
- def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
167
- try:
168
- yield
169
- except ValueError as exc:
170
- raise click.BadParameter(f"Should be in KEY:VALUE format. Got: {raw_value}") from exc
171
-
172
-
173
- def get_workers_count() -> int:
174
- """Detect the number of available CPUs for the current process, if possible.
175
-
176
- Use ``DEFAULT_WORKERS`` if not possible to detect.
177
- """
178
- if hasattr(os, "sched_getaffinity"):
179
- # In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
180
- return len(os.sched_getaffinity(0))
181
- # Number of CPUs in the system, or 1 if undetermined
182
- return os.cpu_count() or DEFAULT_WORKERS
183
-
184
-
185
- def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
186
- if value == "auto":
187
- return get_workers_count()
188
- return int(value)
@@ -1,253 +0,0 @@
1
- import base64
2
- import json
3
- import re
4
- import sys
5
- import threading
6
- from queue import Queue
7
- from typing import Any, Dict, Generator, Iterator, List, Optional, cast
8
-
9
- import attr
10
- import click
11
- import requests
12
- from requests.cookies import RequestsCookieJar
13
- from requests.structures import CaseInsensitiveDict
14
-
15
- from .. import constants
16
- from ..models import Request, Response
17
- from ..runner import events
18
- from ..runner.serialization import SerializedCheck, SerializedInteraction
19
- from .context import ExecutionContext
20
- from .handlers import EventHandler
21
-
22
- # Wait until the worker terminates
23
- WRITER_WORKER_JOIN_TIMEOUT = 1
24
-
25
-
26
- @attr.s(slots=True) # pragma: no mutate
27
- class CassetteWriter(EventHandler):
28
- """Write interactions in a YAML cassette.
29
-
30
- A low-level interface is used to write data to YAML file during the test run and reduce the delay at
31
- the end of the test run.
32
- """
33
-
34
- file_handle: click.utils.LazyFile = attr.ib() # pragma: no mutate
35
- queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
36
- worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
37
-
38
- def __attrs_post_init__(self) -> None:
39
- self.worker = threading.Thread(target=worker, kwargs={"file_handle": self.file_handle, "queue": self.queue})
40
- self.worker.start()
41
-
42
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
43
- if isinstance(event, events.Initialized):
44
- # In the beginning we write metadata and start `http_interactions` list
45
- self.queue.put(Initialize())
46
- if isinstance(event, events.AfterExecution):
47
- # Seed is always present at this point, the original Optional[int] type is there because `TestResult`
48
- # instance is created before `seed` is generated on the hypothesis side
49
- seed = cast(int, event.result.seed)
50
- self.queue.put(
51
- Process(
52
- seed=seed,
53
- interactions=event.result.interactions,
54
- )
55
- )
56
- if isinstance(event, events.Finished):
57
- self.shutdown()
58
-
59
- def shutdown(self) -> None:
60
- self.queue.put(Finalize())
61
- self._stop_worker()
62
-
63
- def _stop_worker(self) -> None:
64
- self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
65
-
66
-
67
- @attr.s(slots=True) # pragma: no mutate
68
- class Initialize:
69
- """Start up, the first message to make preparations before proceeding the input data."""
70
-
71
-
72
- @attr.s(slots=True) # pragma: no mutate
73
- class Process:
74
- """A new chunk of data should be processed."""
75
-
76
- seed: int = attr.ib() # pragma: no mutate
77
- interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
78
-
79
-
80
- @attr.s(slots=True) # pragma: no mutate
81
- class Finalize:
82
- """The work is done and there will be no more messages to process."""
83
-
84
-
85
- def get_command_representation() -> str:
86
- """Get how Schemathesis was run."""
87
- # It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
88
- if not sys.argv[0].endswith("schemathesis"):
89
- return "<unknown entrypoint>"
90
- args = " ".join(sys.argv[1:])
91
- return f"schemathesis {args}"
92
-
93
-
94
- def worker(file_handle: click.utils.LazyFile, queue: Queue) -> None:
95
- """Write YAML to a file in an incremental manner.
96
-
97
- This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
98
- - It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
99
- - Implementation complexity. We have a quite simple format where all values are strings, and it is much simpler to
100
- implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit types.
101
- Another point is that with `pyyaml` we need to emit events and handle some low-level details like providing
102
- tags, anchors to have incremental writing, with strings it is much simpler.
103
- """
104
- current_id = 1
105
- stream = file_handle.open()
106
-
107
- def format_header_values(values: List[str]) -> str:
108
- return "\n".join(f" - {json.dumps(v)}" for v in values)
109
-
110
- def format_headers(headers: Dict[str, List[str]]) -> str:
111
- return "\n".join(f" {name}:\n{format_header_values(values)}" for name, values in headers.items())
112
-
113
- def format_check_message(message: Optional[str]) -> str:
114
- return "~" if message is None else f"{repr(message)}"
115
-
116
- def format_checks(checks: List[SerializedCheck]) -> str:
117
- return "\n".join(
118
- f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
119
- for check in checks
120
- )
121
-
122
- def format_request_body(request: Request) -> str:
123
- if request.body is not None:
124
- return f""" body:
125
- encoding: 'utf-8'
126
- base64_string: '{request.body}'"""
127
- return ""
128
-
129
- def format_response_body(response: Response) -> str:
130
- if response.body is not None:
131
- return f""" body:
132
- encoding: '{response.encoding}'
133
- base64_string: '{response.body}'"""
134
- return ""
135
-
136
- while True:
137
- item = queue.get()
138
- if isinstance(item, Initialize):
139
- stream.write(
140
- f"""command: '{get_command_representation()}'
141
- recorded_with: 'Schemathesis {constants.__version__}'
142
- http_interactions:"""
143
- )
144
- elif isinstance(item, Process):
145
- for interaction in item.interactions:
146
- status = interaction.status.name.upper()
147
- stream.write(
148
- f"""\n- id: '{current_id}'
149
- status: '{status}'
150
- seed: '{item.seed}'
151
- elapsed: '{interaction.response.elapsed}'
152
- recorded_at: '{interaction.recorded_at}'
153
- checks:
154
- {format_checks(interaction.checks)}
155
- request:
156
- uri: '{interaction.request.uri}'
157
- method: '{interaction.request.method}'
158
- headers:
159
- {format_headers(interaction.request.headers)}
160
- {format_request_body(interaction.request)}
161
- response:
162
- status:
163
- code: '{interaction.response.status_code}'
164
- message: {json.dumps(interaction.response.message)}
165
- headers:
166
- {format_headers(interaction.response.headers)}
167
- {format_response_body(interaction.response)}
168
- http_version: '{interaction.response.http_version}'"""
169
- )
170
- current_id += 1
171
- else:
172
- break
173
- file_handle.close()
174
-
175
-
176
- @attr.s(slots=True) # pragma: no mutate
177
- class Replayed:
178
- interaction: Dict[str, Any] = attr.ib() # pragma: no mutate
179
- response: requests.Response = attr.ib() # pragma: no mutate
180
-
181
-
182
- def replay(
183
- cassette: Dict[str, Any],
184
- id_: Optional[str] = None,
185
- status: Optional[str] = None,
186
- uri: Optional[str] = None,
187
- method: Optional[str] = None,
188
- ) -> Generator[Replayed, None, None]:
189
- """Replay saved interactions."""
190
- session = requests.Session()
191
- for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
192
- request = get_prepared_request(interaction["request"])
193
- response = session.send(request) # type: ignore
194
- yield Replayed(interaction, response)
195
-
196
-
197
- def filter_cassette(
198
- interactions: List[Dict[str, Any]],
199
- id_: Optional[str] = None,
200
- status: Optional[str] = None,
201
- uri: Optional[str] = None,
202
- method: Optional[str] = None,
203
- ) -> Iterator[Dict[str, Any]]:
204
-
205
- filters = []
206
-
207
- def id_filter(item: Dict[str, Any]) -> bool:
208
- return item["id"] == id_
209
-
210
- def status_filter(item: Dict[str, Any]) -> bool:
211
- status_ = cast(str, status)
212
- return item["status"].upper() == status_.upper()
213
-
214
- def uri_filter(item: Dict[str, Any]) -> bool:
215
- uri_ = cast(str, uri)
216
- return bool(re.search(uri_, item["request"]["uri"]))
217
-
218
- def method_filter(item: Dict[str, Any]) -> bool:
219
- method_ = cast(str, method)
220
- return bool(re.search(method_, item["request"]["method"]))
221
-
222
- if id_ is not None:
223
- filters.append(id_filter)
224
-
225
- if status is not None:
226
- filters.append(status_filter)
227
-
228
- if uri is not None:
229
- filters.append(uri_filter)
230
-
231
- if method is not None:
232
- filters.append(method_filter)
233
-
234
- def is_match(interaction: Dict[str, Any]) -> bool:
235
- return all(filter_(interaction) for filter_ in filters)
236
-
237
- return filter(is_match, interactions)
238
-
239
-
240
- def get_prepared_request(data: Dict[str, Any]) -> requests.PreparedRequest:
241
- """Create a `requests.PreparedRequest` from a serialized one."""
242
- prepared = requests.PreparedRequest()
243
- prepared.method = data["method"]
244
- prepared.url = data["uri"]
245
- prepared._cookies = RequestsCookieJar() # type: ignore
246
- if "body" in data:
247
- encoded = data["body"]["base64_string"]
248
- if encoded:
249
- prepared.body = base64.b64decode(encoded)
250
- # There is always 1 value in a request
251
- headers = [(key, value[0]) for key, value in data["headers"].items()]
252
- prepared.headers = CaseInsensitiveDict(headers)
253
- return prepared
@@ -1,36 +0,0 @@
1
- import os
2
- import shutil
3
- from queue import Queue
4
- from typing import List, Optional
5
-
6
- import attr
7
-
8
- from ..constants import CodeSampleStyle
9
- from ..runner.serialization import SerializedTestResult
10
-
11
-
12
- @attr.s(slots=True) # pragma: no mutate
13
- class ServiceContext:
14
- url: str = attr.ib() # pragma: no mutate
15
- queue: Queue = attr.ib() # pragma: no mutate
16
-
17
-
18
- @attr.s(slots=True) # pragma: no mutate
19
- class ExecutionContext:
20
- """Storage for the current context of the execution."""
21
-
22
- hypothesis_output: List[str] = attr.ib(factory=list) # pragma: no mutate
23
- workers_num: int = attr.ib(default=1) # pragma: no mutate
24
- show_errors_tracebacks: bool = attr.ib(default=False) # pragma: no mutate
25
- validate_schema: bool = attr.ib(default=True) # pragma: no mutate
26
- operations_processed: int = attr.ib(default=0) # pragma: no mutate
27
- # It is set in runtime, from a `Initialized` event
28
- operations_count: Optional[int] = attr.ib(default=None) # pragma: no mutate
29
- current_line_length: int = attr.ib(default=0) # pragma: no mutate
30
- terminal_size: os.terminal_size = attr.ib(factory=shutil.get_terminal_size) # pragma: no mutate
31
- results: List[SerializedTestResult] = attr.ib(factory=list) # pragma: no mutate
32
- cassette_file_name: Optional[str] = attr.ib(default=None) # pragma: no mutate
33
- junit_xml_file: Optional[str] = attr.ib(default=None) # pragma: no mutate
34
- verbosity: int = attr.ib(default=0) # pragma: no mutate
35
- code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
36
- service: Optional[ServiceContext] = attr.ib(default=None) # pragma: no mutate
schemathesis/cli/debug.py DELETED
@@ -1,21 +0,0 @@
1
- import json
2
-
3
- import attr
4
- from click.utils import LazyFile
5
-
6
- from ..runner import events
7
- from .handlers import EventHandler, ExecutionContext
8
-
9
-
10
- @attr.s(slots=True) # pragma: no mutate
11
- class DebugOutputHandler(EventHandler):
12
- file_handle: LazyFile = attr.ib() # pragma: no mutate
13
-
14
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
15
- stream = self.file_handle.open()
16
- data = event.asdict()
17
- stream.write(json.dumps(data))
18
- stream.write("\n")
19
-
20
- def shutdown(self) -> None:
21
- self.file_handle.close()
@@ -1,11 +0,0 @@
1
- from ..runner import events
2
- from .context import ExecutionContext
3
-
4
-
5
- class EventHandler:
6
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
7
- raise NotImplementedError
8
-
9
- def shutdown(self) -> None:
10
- # Do nothing by default
11
- pass
@@ -1,41 +0,0 @@
1
- import platform
2
- from typing import List, Optional
3
-
4
- import attr
5
- from click.utils import LazyFile
6
- from junit_xml import TestCase, TestSuite, to_xml_report_file
7
-
8
- from ..models import Status
9
- from ..runner import events
10
- from ..runner.serialization import deduplicate_failures
11
- from .handlers import EventHandler, ExecutionContext
12
-
13
-
14
- @attr.s(slots=True) # pragma: no mutate
15
- class JunitXMLHandler(EventHandler):
16
- file_handle: LazyFile = attr.ib() # pragma: no mutate
17
- test_cases: List = attr.ib(factory=list) # pragma: no mutate
18
- start_time: Optional[float] = attr.ib(default=None) # pragma: no mutate
19
-
20
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
21
- if isinstance(event, events.Initialized):
22
- self.start_time = event.start_time
23
- if isinstance(event, events.AfterExecution):
24
- test_case = TestCase(
25
- f"{event.result.method} {event.result.path}",
26
- elapsed_sec=event.elapsed_time,
27
- allow_multiple_subelements=True,
28
- )
29
- if event.status == Status.failure:
30
- checks = deduplicate_failures(event.result.checks)
31
- for idx, check in enumerate(checks, 1):
32
- # `check.message` is always not empty for events with `failure` status
33
- test_case.add_failure_info(message=f"{idx}. {check.message}")
34
- if event.status == Status.error:
35
- test_case.add_error_info(
36
- message=event.result.errors[-1].exception, output=event.result.errors[-1].exception_with_traceback
37
- )
38
- self.test_cases.append(test_case)
39
- if isinstance(event, events.Finished):
40
- test_suites = [TestSuite("schemathesis", test_cases=self.test_cases, hostname=platform.node())]
41
- to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
@@ -1,51 +0,0 @@
1
- from enum import Enum
2
- from typing import Any, List, Optional, Type, Union
3
-
4
- import click
5
-
6
- from ..types import NotSet
7
-
8
-
9
- class CustomHelpMessageChoice(click.Choice):
10
- """Allows you to customize how choices are displayed in the help message."""
11
-
12
- def __init__(self, *args: Any, choices_repr: str, **kwargs: Any):
13
- super().__init__(*args, **kwargs)
14
- self.choices_repr = choices_repr
15
-
16
- def get_metavar(self, param: click.Parameter) -> str:
17
- return self.choices_repr
18
-
19
-
20
- class CSVOption(click.Choice):
21
- def __init__(self, choices: Type[Enum]):
22
- self.enum = choices
23
- super().__init__(tuple(choices.__members__))
24
-
25
- def convert( # type: ignore[return]
26
- self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
27
- ) -> List[Enum]:
28
- items = [item for item in value.split(",") if item]
29
- invalid_options = set(items) - set(self.choices)
30
- if not invalid_options and items:
31
- return [self.enum[item] for item in items]
32
- # Sort to keep the error output consistent with the passed values
33
- sorted_options = ", ".join(sorted(invalid_options, key=items.index))
34
- available_options = ", ".join(self.choices)
35
- self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}")
36
-
37
-
38
- not_set = NotSet()
39
-
40
-
41
- class OptionalInt(click.types.IntRange):
42
- def convert( # type: ignore
43
- self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
44
- ) -> Union[int, NotSet]:
45
- if value == "None":
46
- return not_set
47
- try:
48
- int(value)
49
- return super().convert(value, param, ctx)
50
- except ValueError:
51
- self.fail("%s is not a valid integer or None" % value, param, ctx)
@@ -1 +0,0 @@
1
- from . import default, short