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,233 +0,0 @@
1
- import binascii
2
- import os
3
- from io import BytesIO
4
- from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Optional, Type
5
-
6
- import attr
7
- import yaml
8
- from typing_extensions import Protocol, runtime_checkable
9
-
10
- from .utils import is_json_media_type, is_plain_text_media_type
11
-
12
- if TYPE_CHECKING:
13
- from .models import Case
14
-
15
-
16
- try:
17
- from yaml import CSafeDumper as SafeDumper
18
- except ImportError:
19
- # pylint: disable=unused-import
20
- from yaml import SafeDumper # type: ignore
21
-
22
-
23
- SERIALIZERS: Dict[str, Type["Serializer"]] = {}
24
-
25
-
26
- @attr.s(slots=True) # pragma: no mutate
27
- class SerializerContext:
28
- """The context for serialization process.
29
-
30
- :ivar Case case: Generated example that is being processed.
31
- """
32
-
33
- case: "Case" = attr.ib() # pragma: no mutate
34
-
35
-
36
- @runtime_checkable
37
- class Serializer(Protocol):
38
- """Transform generated data to a form supported by the transport layer.
39
-
40
- For example, to handle multipart payloads, we need to serialize them differently for
41
- `requests` and `werkzeug` transports.
42
- """
43
-
44
- def as_requests(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
45
- raise NotImplementedError
46
-
47
- def as_werkzeug(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
48
- raise NotImplementedError
49
-
50
-
51
- def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Type[Serializer]], Type[Serializer]]:
52
- """Register a serializer for the given media type.
53
-
54
- Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
55
- should have two methods, ``as_requests`` and ``as_werkzeug``, providing keyword arguments that Schemathesis will
56
- pass to ``requests.request`` and ``werkzeug.Client.open`` respectively.
57
-
58
- .. code-block:: python
59
-
60
- @register("text/csv")
61
- class CSVSerializer:
62
- def as_requests(self, context, value):
63
- return {"data": to_csv(value)}
64
-
65
- def as_werkzeug(self, context, value):
66
- return {"data": to_csv(value)}
67
-
68
- The primary purpose of serializers is to transform data from its Python representation to the format suitable
69
- for making an API call. The generated data structure depends on your schema, but its type matches
70
- Python equivalents to the JSON Schema types.
71
-
72
- """
73
-
74
- def wrapper(serializer: Type[Serializer]) -> Type[Serializer]:
75
- if not issubclass(serializer, Serializer):
76
- raise TypeError(
77
- f"`{serializer.__name__}` is not a valid serializer. "
78
- f"Check `schemathesis.serializers.Serializer` documentation for examples."
79
- )
80
- SERIALIZERS[media_type] = serializer
81
- for alias in aliases:
82
- SERIALIZERS[alias] = serializer
83
- return serializer
84
-
85
- return wrapper
86
-
87
-
88
- def unregister(media_type: str) -> None:
89
- """Remove registered serializer for the given media type."""
90
- del SERIALIZERS[media_type]
91
-
92
-
93
- def _to_json(value: Any) -> Dict[str, Any]:
94
- if isinstance(value, bytes):
95
- # Possible to get via explicit examples, e.g. `externalValue`
96
- return {"data": value}
97
- if value is None:
98
- # If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
99
- # argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
100
- # Therefore we explicitly create such payload
101
- return {"data": b"null"}
102
- return {"json": value}
103
-
104
-
105
- @register("application/json")
106
- class JSONSerializer:
107
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
108
- return _to_json(value)
109
-
110
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
111
- return _to_json(value)
112
-
113
-
114
- def _to_yaml(value: Any) -> Dict[str, Any]:
115
- if isinstance(value, bytes):
116
- return {"data": value}
117
- return {"data": yaml.dump(value, Dumper=SafeDumper)}
118
-
119
-
120
- @register("text/yaml", aliases=("text/x-yaml", "application/x-yaml", "text/vnd.yaml"))
121
- class YAMLSerializer:
122
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
123
- return _to_yaml(value)
124
-
125
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
126
- return _to_yaml(value)
127
-
128
-
129
- def _should_coerce_to_bytes(item: Any) -> bool:
130
- """Whether the item should be converted to bytes."""
131
- # These types are OK in forms, others should be coerced to bytes
132
- return not isinstance(item, (bytes, str, int))
133
-
134
-
135
- def _prepare_form_data(data: Dict[str, Any]) -> Dict[str, Any]:
136
- """Make the generated data suitable for sending as multipart.
137
-
138
- If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
139
- we convert it to bytes and send it as-is, ignoring any conversion errors.
140
-
141
- NOTE. This behavior might change in the future.
142
- """
143
- for name, value in data.items():
144
- if isinstance(value, list):
145
- data[name] = [_to_bytes(item) if _should_coerce_to_bytes(item) else item for item in value]
146
- elif _should_coerce_to_bytes(value):
147
- data[name] = _to_bytes(value)
148
- return data
149
-
150
-
151
- def _to_bytes(value: Any) -> bytes:
152
- """Convert the input value to bytes and ignore any conversion errors."""
153
- if isinstance(value, bytes):
154
- return value
155
- return str(value).encode(errors="ignore")
156
-
157
-
158
- def choose_boundary() -> str:
159
- """Random boundary name."""
160
- return binascii.hexlify(os.urandom(16)).decode("ascii")
161
-
162
-
163
- def _encode_multipart(value: Any, boundary: str) -> bytes:
164
- """Encode any value as multipart.
165
-
166
- NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
167
- be used as multipart, in cases when the API schema dictates so.
168
- """
169
- # For such cases we stringify the value and wrap it to a randomly-generated boundary
170
- body = BytesIO()
171
- body.write(f"--{boundary}\r\n".encode())
172
- body.write(str(value).encode())
173
- body.write(f"--{boundary}--\r\n".encode("latin-1"))
174
- return body.getvalue()
175
-
176
-
177
- @register("multipart/form-data")
178
- class MultipartSerializer:
179
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
180
- if isinstance(value, bytes):
181
- return {"data": value}
182
- if isinstance(value, dict):
183
- multipart = _prepare_form_data(value)
184
- files, data = context.case.operation.prepare_multipart(multipart)
185
- return {"files": files, "data": data}
186
- # Uncommon schema. For example - `{"type": "string"}`
187
- boundary = choose_boundary()
188
- raw_data = _encode_multipart(value, boundary)
189
- content_type = f"multipart/form-data; boundary={boundary}"
190
- return {"data": raw_data, "headers": {"Content-Type": content_type}}
191
-
192
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
193
- return {"data": value}
194
-
195
-
196
- @register("application/x-www-form-urlencoded")
197
- class URLEncodedFormSerializer:
198
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
199
- return {"data": value}
200
-
201
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
202
- return {"data": value}
203
-
204
-
205
- @register("text/plain")
206
- class TextSerializer:
207
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
208
- if isinstance(value, bytes):
209
- return {"data": value}
210
- return {"data": str(value).encode("utf8")}
211
-
212
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
213
- if isinstance(value, bytes):
214
- return {"data": value}
215
- return {"data": str(value)}
216
-
217
-
218
- @register("application/octet-stream")
219
- class OctetStreamSerializer:
220
- def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
221
- return {"data": _to_bytes(value)}
222
-
223
- def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
224
- return {"data": _to_bytes(value)}
225
-
226
-
227
- def get(media_type: str) -> Optional[Type[Serializer]]:
228
- """Get an appropriate serializer for the given media type."""
229
- if is_json_media_type(media_type):
230
- media_type = "application/json"
231
- if is_plain_text_media_type(media_type):
232
- media_type = "text/plain"
233
- return SERIALIZERS.get(media_type)
@@ -1,3 +0,0 @@
1
- from .constants import DEFAULT_URL, WORKER_CHECK_PERIOD, WORKER_FINISH_TIMEOUT
2
- from .events import Completed, Error, Event, Timeout
3
- from .handler import ServiceReporter
@@ -1,46 +0,0 @@
1
- from typing import Any, Dict
2
- from urllib.parse import urljoin
3
-
4
- import requests
5
- from requests.adapters import HTTPAdapter, Retry
6
-
7
- from .constants import REQUEST_TIMEOUT
8
- from .models import TestRun
9
-
10
-
11
- class ServiceClient(requests.Session):
12
- """A more convenient session to send requests to Schemathesis.io."""
13
-
14
- def __init__(self, base_url: str, token: str, timeout: int = REQUEST_TIMEOUT):
15
- super().__init__()
16
- self.timeout = timeout
17
- self.base_url = base_url
18
- self.headers["Authorization"] = f"Bearer {token}"
19
- # Automatically check responses for 4XX and 5XX
20
- self.hooks["response"] = [lambda response, *args, **kwargs: response.raise_for_status()]
21
- adapter = HTTPAdapter(max_retries=Retry(5))
22
- self.mount("https://", adapter)
23
- self.mount("http://", adapter)
24
-
25
- def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: # type: ignore
26
- kwargs.setdefault("timeout", self.timeout)
27
- # All requests will be done against the base url
28
- url = urljoin(self.base_url, url)
29
- return super().request(method, url, *args, **kwargs)
30
-
31
- def create_test_run(self) -> TestRun:
32
- """Create a new test run on the Schemathesis.io side."""
33
- response = self.post("/runs/")
34
- data = response.json()
35
- return TestRun(run_id=data["run_id"], short_url=data["short_url"])
36
-
37
- def finish_test_run(self, run_id: str) -> None:
38
- """Finish a test run on the Schemathesis.io side.
39
-
40
- Only needed in corner cases when Schemathesis CLI fails with an internal error in itself, not in the runner.
41
- """
42
- self.post(f"/runs/{run_id}/finish/")
43
-
44
- def send_event(self, run_id: str, data: Dict[str, Any]) -> None:
45
- """Send a single event to Schemathesis.io."""
46
- self.post(f"/runs/{run_id}/events/", json=data)
@@ -1,12 +0,0 @@
1
- # The main Schemathesis.io API address
2
- DEFAULT_URL = "https://api.schemathesis.io/"
3
- # A sentinel to signal the worker thread to stop
4
- STOP_MARKER = object()
5
- # Timeout for each API call
6
- REQUEST_TIMEOUT = 1
7
- # The time the main thread will wait for the worker thread to finish its job before exiting
8
- WORKER_FINISH_TIMEOUT = 10.0
9
- # A period between checking the worker thread for events
10
- WORKER_CHECK_PERIOD = 0.005
11
- # Wait until the worker terminates
12
- WORKER_JOIN_TIMEOUT = 10
@@ -1,39 +0,0 @@
1
- import attr
2
-
3
- from ..utils import format_exception
4
-
5
-
6
- class Event:
7
- """Signalling events coming from the Schemathesis.io worker.
8
-
9
- The purpose is to communicate with the thread that writes to stdout.
10
- """
11
-
12
- @property
13
- def name(self) -> str:
14
- return self.__class__.__name__.upper()
15
-
16
-
17
- @attr.s(slots=True)
18
- class Completed(Event):
19
- """The handler finished successfully."""
20
-
21
- short_url: str = attr.ib()
22
-
23
-
24
- @attr.s(slots=True)
25
- class Error(Event):
26
- """Internal error inside the Schemathesis.io handler."""
27
-
28
- exception: Exception = attr.ib()
29
-
30
- def get_message(self, include_traceback: bool = False) -> str:
31
- return format_exception(self.exception, include_traceback=include_traceback)
32
-
33
-
34
- @attr.s(slots=True)
35
- class Timeout(Event):
36
- """The handler did not finish its work in time.
37
-
38
- This event is not created in the handler itself, but rather in the main thread code to uniform the processing.
39
- """
@@ -1,39 +0,0 @@
1
- import threading
2
- from queue import Queue
3
-
4
- import attr
5
-
6
- from ..cli.context import ExecutionContext
7
- from ..cli.handlers import EventHandler
8
- from ..runner import events
9
- from . import worker
10
- from .constants import DEFAULT_URL, STOP_MARKER, WORKER_JOIN_TIMEOUT
11
-
12
-
13
- @attr.s(slots=True) # pragma: no mutate
14
- class ServiceReporter(EventHandler):
15
- """Send events to the worker that communicates with Schemathesis.io."""
16
-
17
- out_queue: Queue = attr.ib() # pragma: no mutate
18
- token: str = attr.ib() # pragma: no mutate
19
- url: str = attr.ib(default=DEFAULT_URL) # pragma: no mutate
20
- in_queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
21
- worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
22
-
23
- def __attrs_post_init__(self) -> None:
24
- # A worker thread, that does all the work concurrently
25
- self.worker = threading.Thread(
26
- target=worker.start,
27
- kwargs={"token": self.token, "url": self.url, "in_queue": self.in_queue, "out_queue": self.out_queue},
28
- )
29
- self.worker.start()
30
-
31
- def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
32
- self.in_queue.put(event)
33
-
34
- def shutdown(self) -> None:
35
- self._stop_worker()
36
-
37
- def _stop_worker(self) -> None:
38
- self.in_queue.put(STOP_MARKER)
39
- self.worker.join(WORKER_JOIN_TIMEOUT)
@@ -1,7 +0,0 @@
1
- import attr
2
-
3
-
4
- @attr.s(slots=True)
5
- class TestRun:
6
- run_id: str = attr.ib()
7
- short_url: str = attr.ib()
@@ -1,153 +0,0 @@
1
- from typing import Any, Callable, Dict, List, Optional, TypeVar
2
-
3
- import attr
4
-
5
- from ..runner import events
6
- from ..runner.serialization import SerializedCase, deduplicate_checks
7
-
8
- S = TypeVar("S", bound=events.ExecutionEvent)
9
- SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
10
-
11
-
12
- def serialize_initialized(event: events.Initialized) -> Optional[Dict[str, Any]]:
13
- return {
14
- "schema": event.schema,
15
- "operations_count": event.operations_count,
16
- "location": event.location or "",
17
- "base_url": event.base_url,
18
- }
19
-
20
-
21
- def serialize_before_execution(event: events.BeforeExecution) -> Optional[Dict[str, Any]]:
22
- return {
23
- "correlation_id": event.correlation_id,
24
- "verbose_name": event.verbose_name,
25
- "data_generation_method": event.data_generation_method,
26
- }
27
-
28
-
29
- def _serialize_case(case: SerializedCase) -> Dict[str, Any]:
30
- return {
31
- "verbose_name": case.verbose_name,
32
- "path_template": case.path_template,
33
- "path_parameters": stringify_path_parameters(case.path_parameters),
34
- "query": prepare_query(case.query),
35
- "cookies": case.cookies,
36
- "media_type": case.media_type,
37
- }
38
-
39
-
40
- def serialize_after_execution(event: events.AfterExecution) -> Optional[Dict[str, Any]]:
41
- return {
42
- "correlation_id": event.correlation_id,
43
- "verbose_name": event.verbose_name,
44
- "data_generation_method": event.data_generation_method,
45
- "result": {
46
- "checks": [
47
- {
48
- "name": check.name,
49
- "value": check.value,
50
- "request": {
51
- "method": check.request.method,
52
- "uri": check.request.uri,
53
- "body": check.request.body,
54
- "headers": check.request.headers,
55
- },
56
- "response": {
57
- "status_code": check.response.status_code,
58
- "headers": check.response.headers,
59
- "body": check.response.body,
60
- "encoding": check.response.encoding,
61
- "elapsed": check.response.elapsed,
62
- }
63
- if check.response is not None
64
- else None,
65
- "example": _serialize_case(check.example),
66
- "message": check.message,
67
- "context": attr.asdict(check.context) if check.context is not None else None,
68
- }
69
- for check in deduplicate_checks(event.result.checks)
70
- ],
71
- "errors": [
72
- {
73
- "exception": error.exception,
74
- "exception_with_traceback": error.exception_with_traceback,
75
- "example": None if error.example is None else _serialize_case(error.example),
76
- }
77
- for error in event.result.errors
78
- ],
79
- },
80
- }
81
-
82
-
83
- def serialize_interrupted(_: events.Interrupted) -> Optional[Dict[str, Any]]:
84
- return None
85
-
86
-
87
- def serialize_internal_error(event: events.InternalError) -> Optional[Dict[str, Any]]:
88
- return {
89
- "message": event.message,
90
- "exception_type": event.exception_type,
91
- "exception_with_traceback": event.exception_with_traceback,
92
- }
93
-
94
-
95
- def serialize_finished(event: events.Finished) -> Optional[Dict[str, Any]]:
96
- return {
97
- "generic_errors": [
98
- {
99
- "exception": error.exception,
100
- "exception_with_traceback": error.exception_with_traceback,
101
- "title": error.title,
102
- }
103
- for error in event.generic_errors
104
- ]
105
- }
106
-
107
-
108
- def serialize_event(
109
- event: events.ExecutionEvent,
110
- on_initialized: SerializeFunc = serialize_initialized,
111
- on_before_execution: SerializeFunc = serialize_before_execution,
112
- on_after_execution: SerializeFunc = serialize_after_execution,
113
- on_interrupted: SerializeFunc = serialize_interrupted,
114
- on_internal_error: SerializeFunc = serialize_internal_error,
115
- on_finished: SerializeFunc = serialize_finished,
116
- ) -> Dict[str, Optional[Dict[str, Any]]]:
117
- """Turn an event into JSON-serializable structure."""
118
- # Due to https://github.com/python-attrs/attrs/issues/864 it is easier to implement filtration manually
119
- serializer = {
120
- events.Initialized: on_initialized,
121
- events.BeforeExecution: on_before_execution,
122
- events.AfterExecution: on_after_execution,
123
- events.Interrupted: on_interrupted,
124
- events.InternalError: on_internal_error,
125
- events.Finished: on_finished,
126
- }[event.__class__]
127
- # Externally tagged structure
128
- return {event.__class__.__name__: serializer(event)}
129
-
130
-
131
- def stringify_path_parameters(path_parameters: Optional[Dict[str, Any]]) -> Dict[str, str]:
132
- """Cast all path parameter values to strings.
133
-
134
- Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
135
- """
136
- return {key: str(value) for key, value in (path_parameters or {}).items()}
137
-
138
-
139
- def prepare_query(query: Optional[Dict[str, Any]]) -> Dict[str, List[str]]:
140
- """Convert all query values to list of strings.
141
-
142
- Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
143
- It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
144
- """
145
-
146
- def to_list_of_strings(value: Any) -> List[str]:
147
- if isinstance(value, list):
148
- return list(map(str, value))
149
- if isinstance(value, str):
150
- return [value]
151
- return [str(value)]
152
-
153
- return {key: to_list_of_strings(value) for key, value in (query or {}).items()}
@@ -1,40 +0,0 @@
1
- from queue import Queue
2
-
3
- from . import events
4
- from .client import ServiceClient
5
- from .constants import STOP_MARKER
6
- from .serialization import serialize_event
7
-
8
-
9
- def start(url: str, token: str, in_queue: Queue, out_queue: Queue) -> None:
10
- """Initialize a new run and start consuming events."""
11
- try:
12
- client = ServiceClient(url, token)
13
- response = client.create_test_run()
14
- consume_events(client, in_queue, response.run_id)
15
- # Reached a terminal event or a stop marker.
16
- # In the case of stop marker, it is still a successful result for the handler itself as the error happened in
17
- # a different handler
18
- out_queue.put(events.Completed(short_url=response.short_url))
19
- except Exception as exc:
20
- out_queue.put(events.Error(exc))
21
-
22
-
23
- def consume_events(client: ServiceClient, in_queue: Queue, run_id: str) -> None:
24
- """Main working loop that sends data to Schemathesis.io."""
25
- try:
26
- while True:
27
- event = in_queue.get()
28
- if event is STOP_MARKER:
29
- # It is an equivalent of an internal error in some other handler.
30
- # In the happy path scenario, the worker will exit on any terminal event
31
- client.finish_test_run(run_id)
32
- break
33
- data = serialize_event(event)
34
- client.send_event(run_id, data)
35
- if event.is_terminal:
36
- break
37
- except Exception:
38
- # Internal error on our side, try to finish the test run
39
- client.finish_test_run(run_id)
40
- raise