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,111 +0,0 @@
1
- """Work with stored auth data."""
2
-
3
- from __future__ import annotations
4
-
5
- import enum
6
- import tempfile
7
- from dataclasses import dataclass
8
- from pathlib import Path
9
- from typing import TYPE_CHECKING, Any
10
-
11
- import tomli
12
- import tomli_w
13
-
14
- from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
15
-
16
- if TYPE_CHECKING:
17
- from ..types import PathLike
18
-
19
-
20
- @dataclass
21
- class HostData:
22
- """Stored data related to a host."""
23
-
24
- hostname: str
25
- hosts_file: PathLike
26
-
27
- def load(self) -> dict[str, Any]:
28
- return load(self.hosts_file).get(self.hostname, {})
29
-
30
- @property
31
- def correlation_id(self) -> str | None:
32
- return self.load().get("correlation_id")
33
-
34
- def store_correlation_id(self, correlation_id: str) -> None:
35
- """Store `correlation_id` in the hosts file."""
36
- hosts = load(self.hosts_file)
37
- data = hosts.setdefault(self.hostname, {})
38
- data["correlation_id"] = correlation_id
39
- _dump_hosts(self.hosts_file, hosts)
40
-
41
-
42
- def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> None:
43
- """Store a new token for a host."""
44
- # Don't use any file-based locking for simplicity
45
- hosts = load(hosts_file)
46
- data = hosts.setdefault(hostname, {})
47
- data.update(version=HOSTS_FORMAT_VERSION, token=token)
48
- _dump_hosts(hosts_file, hosts)
49
-
50
-
51
- def load(path: PathLike) -> dict[str, Any]:
52
- """Load the given hosts file.
53
-
54
- Return an empty dict if it doesn't exist.
55
- """
56
- from ..utils import _ensure_parent
57
-
58
- try:
59
- with open(path, "rb") as fd:
60
- return tomli.load(fd)
61
- except FileNotFoundError:
62
- _ensure_parent(path)
63
- return {}
64
- except tomli.TOMLDecodeError:
65
- return {}
66
-
67
-
68
- def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
69
- """Load all data associated with a hostname."""
70
- return load(hosts_file).get(hostname, {})
71
-
72
-
73
- @enum.unique
74
- class RemoveAuth(enum.Enum):
75
- success = 1
76
- no_match = 2
77
- no_hosts = 3
78
- error = 4
79
-
80
-
81
- def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> RemoveAuth:
82
- """Remove authentication for a Schemathesis.io host."""
83
- try:
84
- with open(hosts_file, "rb") as fd:
85
- hosts = tomli.load(fd)
86
- try:
87
- hosts.pop(hostname)
88
- _dump_hosts(hosts_file, hosts)
89
- return RemoveAuth.success
90
- except KeyError:
91
- return RemoveAuth.no_match
92
- except FileNotFoundError:
93
- return RemoveAuth.no_hosts
94
- except tomli.TOMLDecodeError:
95
- return RemoveAuth.error
96
-
97
-
98
- def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
99
- """Load a token for a host."""
100
- return load_for_host(hostname, hosts_file).get("token")
101
-
102
-
103
- def get_temporary_hosts_file() -> str:
104
- temporary_dir = Path(tempfile.gettempdir()).resolve()
105
- return str(temporary_dir / "schemathesis-hosts.toml")
106
-
107
-
108
- def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
109
- """Write hosts data to a file."""
110
- with open(path, "wb") as fd:
111
- tomli_w.dump(hosts, fd)
@@ -1,71 +0,0 @@
1
- """Useful info to collect from CLI usage."""
2
-
3
- from __future__ import annotations
4
-
5
- import os
6
- import platform
7
- from dataclasses import dataclass, field
8
- from importlib import metadata
9
-
10
- from ..constants import SCHEMATHESIS_VERSION
11
- from .constants import DOCKER_IMAGE_ENV_VAR
12
-
13
-
14
- @dataclass
15
- class PlatformMetadata:
16
- # System / OS name, e.g. "Linux" or "Windows".
17
- system: str = field(default_factory=platform.system)
18
- # System release, e.g. "5.14" or "NT".
19
- release: str = field(default_factory=platform.release)
20
- # Machine type, e.g. "i386".
21
- machine: str = field(default_factory=platform.machine)
22
-
23
-
24
- @dataclass
25
- class InterpreterMetadata:
26
- # The Python version as "major.minor.patch".
27
- version: str = field(default_factory=platform.python_version)
28
- # Python implementation, e.g. "CPython" or "PyPy".
29
- implementation: str = field(default_factory=platform.python_implementation)
30
-
31
-
32
- @dataclass
33
- class CliMetadata:
34
- # Schemathesis package version.
35
- version: str = SCHEMATHESIS_VERSION
36
-
37
-
38
- DEPENDENCY_NAMES = ["hypothesis", "hypothesis-jsonschema", "hypothesis-graphql"]
39
-
40
-
41
- @dataclass
42
- class Dependency:
43
- """A single dependency."""
44
-
45
- # Name of the package.
46
- name: str
47
- # Version of the package.
48
- version: str
49
-
50
- @classmethod
51
- def from_name(cls, name: str) -> Dependency:
52
- return cls(name=name, version=metadata.version(name))
53
-
54
-
55
- def collect_dependency_versions() -> list[Dependency]:
56
- return [Dependency.from_name(name) for name in DEPENDENCY_NAMES]
57
-
58
-
59
- @dataclass
60
- class Metadata:
61
- """CLI environment metadata."""
62
-
63
- # Information about the host platform.
64
- platform: PlatformMetadata = field(default_factory=PlatformMetadata)
65
- # Python interpreter info.
66
- interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
67
- # CLI info itself.
68
- cli: CliMetadata = field(default_factory=CliMetadata)
69
- # Used Docker image if any
70
- docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
71
- depedenencies: list[Dependency] = field(default_factory=collect_dependency_versions)
@@ -1,258 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from enum import Enum
5
- from typing import Any, Iterable, Literal, TypedDict, Union
6
-
7
-
8
- class UploadSource(str, Enum):
9
- DEFAULT = "default"
10
- UPLOAD_COMMAND = "upload_command"
11
-
12
-
13
- @dataclass
14
- class ProjectDetails:
15
- environments: list[ProjectEnvironment]
16
- specification: Specification
17
-
18
- @property
19
- def default_environment(self) -> ProjectEnvironment | None:
20
- return next((env for env in self.environments if env.is_default), None)
21
-
22
-
23
- @dataclass
24
- class ProjectEnvironment:
25
- url: str
26
- name: str
27
- description: str
28
- is_default: bool
29
-
30
-
31
- @dataclass
32
- class Specification:
33
- schema: dict[str, Any]
34
-
35
-
36
- @dataclass
37
- class AuthResponse:
38
- username: str
39
-
40
-
41
- @dataclass
42
- class UploadResponse:
43
- message: str
44
- next_url: str
45
- correlation_id: str
46
-
47
-
48
- @dataclass
49
- class FailedUploadResponse:
50
- detail: str
51
-
52
-
53
- @dataclass
54
- class NotAppliedState:
55
- """The extension was not applied."""
56
-
57
- def __str__(self) -> str:
58
- return "Not Applied"
59
-
60
-
61
- @dataclass
62
- class SuccessState:
63
- """The extension was applied successfully."""
64
-
65
- def __str__(self) -> str:
66
- return "Success"
67
-
68
-
69
- @dataclass
70
- class ErrorState:
71
- """An error occurred during the extension application."""
72
-
73
- errors: list[str] = field(default_factory=list)
74
- exceptions: list[Exception] = field(default_factory=list)
75
-
76
- def __str__(self) -> str:
77
- return "Error"
78
-
79
-
80
- ExtensionState = Union[NotAppliedState, SuccessState, ErrorState]
81
-
82
-
83
- @dataclass
84
- class BaseExtension:
85
- def set_state(self, state: ExtensionState) -> None:
86
- self.state = state
87
-
88
- def set_success(self) -> None:
89
- self.set_state(SuccessState())
90
-
91
- def set_error(self, errors: list[str] | None = None, exceptions: list[Exception] | None = None) -> None:
92
- self.set_state(ErrorState(errors=errors or [], exceptions=exceptions or []))
93
-
94
-
95
- @dataclass
96
- class UnknownExtension(BaseExtension):
97
- """An unknown extension.
98
-
99
- Likely the CLI should be updated.
100
- """
101
-
102
- type: str
103
- state: ExtensionState = field(default_factory=NotAppliedState)
104
-
105
- @property
106
- def summary(self) -> str:
107
- return f"`{self.type}`"
108
-
109
-
110
- class AddPatch(TypedDict):
111
- operation: Literal["add"]
112
- path: list[str | int]
113
- value: Any
114
-
115
-
116
- class RemovePatch(TypedDict):
117
- operation: Literal["remove"]
118
- path: list[str | int]
119
-
120
-
121
- Patch = Union[AddPatch, RemovePatch]
122
-
123
-
124
- @dataclass
125
- class SchemaPatchesExtension(BaseExtension):
126
- """Update the schema with its optimized version."""
127
-
128
- patches: list[Patch]
129
- state: ExtensionState = field(default_factory=NotAppliedState)
130
-
131
- @property
132
- def summary(self) -> str:
133
- count = len(self.patches)
134
- plural = "es" if count > 1 else ""
135
- return f"{count} schema patch{plural}"
136
-
137
-
138
- class TransformFunctionDefinition(TypedDict):
139
- kind: Literal["map", "filter"]
140
- name: str
141
- arguments: dict[str, Any]
142
-
143
-
144
- @dataclass
145
- class StrategyDefinition:
146
- name: str
147
- transforms: list[TransformFunctionDefinition] | None = None
148
- arguments: dict[str, Any] | None = None
149
-
150
-
151
- def _strategies_from_definition(items: dict[str, list[dict[str, Any]]]) -> dict[str, list[StrategyDefinition]]:
152
- return {name: [StrategyDefinition(**item) for item in value] for name, value in items.items()}
153
-
154
-
155
- def _format_items(items: Iterable[str]) -> str:
156
- return ", ".join([f"`{item}`" for item in items])
157
-
158
-
159
- @dataclass
160
- class OpenApiStringFormatsExtension(BaseExtension):
161
- """Custom string formats."""
162
-
163
- formats: dict[str, list[StrategyDefinition]]
164
- state: ExtensionState = field(default_factory=NotAppliedState)
165
-
166
- @classmethod
167
- def from_dict(cls, formats: dict[str, list[dict[str, Any]]]) -> OpenApiStringFormatsExtension:
168
- return cls(formats=_strategies_from_definition(formats))
169
-
170
- @property
171
- def summary(self) -> str:
172
- count = len(self.formats)
173
- plural = "s" if count > 1 else ""
174
- formats = _format_items(self.formats)
175
- return f"Data generator{plural} for {formats} Open API format{plural}"
176
-
177
-
178
- @dataclass
179
- class GraphQLScalarsExtension(BaseExtension):
180
- """Custom scalars."""
181
-
182
- scalars: dict[str, list[StrategyDefinition]]
183
- state: ExtensionState = field(default_factory=NotAppliedState)
184
-
185
- @classmethod
186
- def from_dict(cls, scalars: dict[str, list[dict[str, Any]]]) -> GraphQLScalarsExtension:
187
- return cls(scalars=_strategies_from_definition(scalars))
188
-
189
- @property
190
- def summary(self) -> str:
191
- count = len(self.scalars)
192
- plural = "s" if count > 1 else ""
193
- scalars = _format_items(self.scalars)
194
- return f"Data generator{plural} for {scalars} GraphQL scalar{plural}"
195
-
196
-
197
- @dataclass
198
- class MediaTypesExtension(BaseExtension):
199
- media_types: dict[str, list[StrategyDefinition]]
200
- state: ExtensionState = field(default_factory=NotAppliedState)
201
-
202
- @classmethod
203
- def from_dict(cls, media_types: dict[str, list[dict[str, Any]]]) -> MediaTypesExtension:
204
- return cls(media_types=_strategies_from_definition(media_types))
205
-
206
- @property
207
- def summary(self) -> str:
208
- count = len(self.media_types)
209
- plural = "s" if count > 1 else ""
210
- media_types = _format_items(self.media_types)
211
- return f"Data generator{plural} for {media_types} media type{plural}"
212
-
213
-
214
- # A CLI extension that can be used to adjust the behavior of Schemathesis.
215
- Extension = Union[
216
- SchemaPatchesExtension,
217
- OpenApiStringFormatsExtension,
218
- GraphQLScalarsExtension,
219
- MediaTypesExtension,
220
- UnknownExtension,
221
- ]
222
-
223
-
224
- def extension_from_dict(data: dict[str, Any]) -> Extension:
225
- if data["type"] == "schema_patches":
226
- return SchemaPatchesExtension(patches=data["patches"])
227
- if data["type"] == "string_formats":
228
- return OpenApiStringFormatsExtension.from_dict(formats=data["items"])
229
- if data["type"] == "scalars":
230
- return GraphQLScalarsExtension.from_dict(scalars=data["items"])
231
- if data["type"] == "media_types":
232
- return MediaTypesExtension.from_dict(media_types=data["items"])
233
- return UnknownExtension(type=data["type"])
234
-
235
-
236
- @dataclass
237
- class AnalysisSuccess:
238
- id: str
239
- elapsed: float
240
- message: str
241
- extensions: list[Extension]
242
-
243
- @classmethod
244
- def from_dict(cls, data: dict[str, Any]) -> AnalysisSuccess:
245
- return cls(
246
- id=data["id"],
247
- elapsed=data["elapsed"],
248
- message=data["message"],
249
- extensions=[extension_from_dict(ext) for ext in data["extensions"]],
250
- )
251
-
252
-
253
- @dataclass
254
- class AnalysisError:
255
- message: str
256
-
257
-
258
- AnalysisResult = Union[AnalysisSuccess, AnalysisError]
@@ -1,255 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import json
5
- import os
6
- import tarfile
7
- import threading
8
- import time
9
- from contextlib import suppress
10
- from dataclasses import asdict, dataclass, field
11
- from io import BytesIO
12
- from queue import Queue
13
- from typing import TYPE_CHECKING, Any
14
-
15
- from ..cli.handlers import EventHandler
16
- from ..runner.events import Initialized, InternalError, Interrupted
17
- from . import ci, events, usage
18
- from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
19
- from .metadata import Metadata
20
- from .models import UploadResponse
21
- from .serialization import serialize_event
22
-
23
- if TYPE_CHECKING:
24
- import click
25
-
26
- from ..cli.context import ExecutionContext
27
- from ..runner.events import ExecutionEvent
28
- from .client import ServiceClient
29
- from .hosts import HostData
30
-
31
-
32
- @dataclass
33
- class ReportWriter:
34
- """Schemathesis.io test run report.
35
-
36
- Simplifies adding new files to the archive.
37
- """
38
-
39
- _tar: tarfile.TarFile
40
- _events_count: int = 0
41
-
42
- def add_json_file(self, name: str, data: Any) -> None:
43
- buffer = BytesIO()
44
- buffer.write(json.dumps(data, separators=(",", ":")).encode())
45
- buffer.seek(0)
46
- info = tarfile.TarInfo(name=name)
47
- info.size = len(buffer.getbuffer())
48
- info.mtime = int(time.time())
49
- self._tar.addfile(info, buffer)
50
-
51
- def add_metadata(
52
- self,
53
- *,
54
- api_name: str | None,
55
- location: str,
56
- base_url: str | None,
57
- started_at: str,
58
- metadata: Metadata,
59
- ci_environment: ci.Environment | None,
60
- usage_data: dict[str, Any] | None,
61
- ) -> None:
62
- data = {
63
- # API identifier on the Schemathesis.io side (optional)
64
- "api_name": api_name,
65
- # The place, where the API schema is located
66
- "location": location,
67
- # The base URL against which the tests are running
68
- "base_url": base_url,
69
- # The time that the test run began
70
- "started_at": started_at,
71
- # Metadata about CLI environment
72
- "environment": asdict(metadata),
73
- # Environment variables specific for CI providers
74
- "ci": ci_environment.asdict() if ci_environment is not None else None,
75
- # CLI usage statistic
76
- "usage": usage_data,
77
- # Report format version
78
- "version": REPORT_FORMAT_VERSION,
79
- }
80
- self.add_json_file("metadata.json", data)
81
-
82
- def add_event(self, event: ExecutionEvent) -> None:
83
- """Add an execution event to the report."""
84
- self._events_count += 1
85
- filename = f"events/{self._events_count}-{event.__class__.__name__}.json"
86
- self.add_json_file(filename, serialize_event(event))
87
-
88
-
89
- class BaseReportHandler(EventHandler):
90
- in_queue: Queue
91
- worker: threading.Thread
92
-
93
- def handle_event(self, context: ExecutionContext, event: ExecutionEvent) -> None:
94
- self.in_queue.put(event)
95
-
96
- def shutdown(self) -> None:
97
- self._stop_worker()
98
-
99
- def _stop_worker(self) -> None:
100
- self.in_queue.put(STOP_MARKER)
101
- self.worker.join(WORKER_JOIN_TIMEOUT)
102
-
103
-
104
- @dataclass
105
- class ServiceReportHandler(BaseReportHandler):
106
- client: ServiceClient
107
- host_data: HostData
108
- api_name: str | None
109
- location: str
110
- base_url: str | None
111
- started_at: str
112
- telemetry: bool
113
- out_queue: Queue
114
- in_queue: Queue = field(default_factory=Queue)
115
- worker: threading.Thread = field(init=False)
116
-
117
- def __post_init__(self) -> None:
118
- self.worker = threading.Thread(
119
- target=write_remote,
120
- kwargs={
121
- "client": self.client,
122
- "host_data": self.host_data,
123
- "api_name": self.api_name,
124
- "location": self.location,
125
- "base_url": self.base_url,
126
- "started_at": self.started_at,
127
- "in_queue": self.in_queue,
128
- "out_queue": self.out_queue,
129
- "usage_data": usage.collect() if self.telemetry else None,
130
- },
131
- )
132
- self.worker.start()
133
-
134
-
135
- @enum.unique
136
- class ConsumeResult(enum.Enum):
137
- NORMAL = 1
138
- INTERRUPT = 2
139
-
140
-
141
- def consume_events(writer: ReportWriter, in_queue: Queue) -> ConsumeResult:
142
- while True:
143
- event = in_queue.get()
144
- if event is STOP_MARKER or isinstance(event, (Interrupted, InternalError)):
145
- # If the run is interrupted, or there is an internal error - do not send the report
146
- return ConsumeResult.INTERRUPT
147
- # Add every event to the report
148
- if isinstance(event, Initialized):
149
- writer.add_json_file("schema.json", event.schema)
150
- writer.add_event(event)
151
- if event.is_terminal:
152
- break
153
- return ConsumeResult.NORMAL
154
-
155
-
156
- def write_remote(
157
- client: ServiceClient,
158
- host_data: HostData,
159
- api_name: str | None,
160
- location: str,
161
- base_url: str | None,
162
- started_at: str,
163
- in_queue: Queue,
164
- out_queue: Queue,
165
- usage_data: dict[str, Any] | None,
166
- ) -> None:
167
- """Create a compressed ``tar.gz`` file during the run & upload it to Schemathesis.io when the run is finished."""
168
- payload = BytesIO()
169
- try:
170
- with tarfile.open(mode="w:gz", fileobj=payload) as tar:
171
- writer = ReportWriter(tar)
172
- ci_environment = ci.environment()
173
- writer.add_metadata(
174
- api_name=api_name,
175
- location=location,
176
- base_url=base_url,
177
- started_at=started_at,
178
- metadata=Metadata(),
179
- ci_environment=ci_environment,
180
- usage_data=usage_data,
181
- )
182
- if consume_events(writer, in_queue) == ConsumeResult.INTERRUPT:
183
- return
184
- data = payload.getvalue()
185
- out_queue.put(events.Metadata(size=len(data), ci_environment=ci_environment))
186
- provider = ci_environment.provider if ci_environment is not None else None
187
- response = client.upload_report(data, host_data.correlation_id, provider)
188
- event: events.Event
189
- if isinstance(response, UploadResponse):
190
- host_data.store_correlation_id(response.correlation_id)
191
- event = events.Completed(message=response.message, next_url=response.next_url)
192
- else:
193
- event = events.Failed(detail=response.detail)
194
- out_queue.put(event)
195
- except Exception as exc:
196
- out_queue.put(events.Error(exc))
197
-
198
-
199
- @dataclass
200
- class FileReportHandler(BaseReportHandler):
201
- file_handle: click.utils.LazyFile
202
- api_name: str | None
203
- location: str
204
- base_url: str | None
205
- started_at: str
206
- telemetry: bool
207
- out_queue: Queue
208
- in_queue: Queue = field(default_factory=Queue)
209
- worker: threading.Thread = field(init=False)
210
-
211
- def __post_init__(self) -> None:
212
- self.worker = threading.Thread(
213
- target=write_file,
214
- kwargs={
215
- "file_handle": self.file_handle,
216
- "api_name": self.api_name,
217
- "location": self.location,
218
- "base_url": self.base_url,
219
- "started_at": self.started_at,
220
- "in_queue": self.in_queue,
221
- "out_queue": self.out_queue,
222
- "usage_data": usage.collect() if self.telemetry else None,
223
- },
224
- )
225
- self.worker.start()
226
-
227
-
228
- def write_file(
229
- file_handle: click.utils.LazyFile,
230
- api_name: str | None,
231
- location: str,
232
- base_url: str | None,
233
- started_at: str,
234
- in_queue: Queue,
235
- out_queue: Queue,
236
- usage_data: dict[str, Any] | None,
237
- ) -> None:
238
- with file_handle.open() as fileobj, tarfile.open(mode="w:gz", fileobj=fileobj) as tar:
239
- writer = ReportWriter(tar)
240
- ci_environment = ci.environment()
241
- writer.add_metadata(
242
- api_name=api_name,
243
- location=location,
244
- base_url=base_url,
245
- started_at=started_at,
246
- metadata=Metadata(),
247
- ci_environment=ci_environment,
248
- usage_data=usage_data,
249
- )
250
- result = consume_events(writer, in_queue)
251
- if result == ConsumeResult.INTERRUPT:
252
- with suppress(OSError):
253
- os.remove(file_handle.name)
254
- else:
255
- out_queue.put(events.Metadata(size=os.path.getsize(file_handle.name), ci_environment=ci_environment))