schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
-
4
- from . import ci
5
- from ..exceptions import format_exception
6
-
7
-
8
- class Event:
9
- """Signalling events coming from the Schemathesis.io worker.
10
-
11
- The purpose is to communicate with the thread that writes to stdout.
12
- """
13
-
14
- @property
15
- def status(self) -> str:
16
- return self.__class__.__name__.upper()
17
-
18
-
19
- @dataclass
20
- class Metadata(Event):
21
- """Meta-information about the report."""
22
-
23
- size: int
24
- ci_environment: ci.Environment | None
25
-
26
-
27
- @dataclass
28
- class Completed(Event):
29
- """Report uploaded successfully."""
30
-
31
- message: str
32
- next_url: str
33
-
34
-
35
- @dataclass
36
- class Error(Event):
37
- """Internal error inside the Schemathesis.io handler."""
38
-
39
- exception: Exception
40
-
41
- def get_message(self, include_traceback: bool = False) -> str:
42
- return format_exception(self.exception, include_traceback=include_traceback)
43
-
44
-
45
- @dataclass
46
- class Failed(Event):
47
- """A client-side error which should be displayed to the user."""
48
-
49
- detail: str
50
-
51
-
52
- @dataclass
53
- class Timeout(Event):
54
- """The handler did not finish its work in time.
55
-
56
- This event is not created in the handler itself, but rather in the main thread code to uniform the processing.
57
- """
@@ -1,107 +0,0 @@
1
- """Work with stored auth data."""
2
- from __future__ import annotations
3
- import enum
4
- import tempfile
5
- from dataclasses import dataclass
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- import tomli
10
- import tomli_w
11
-
12
- from ..types import PathLike
13
- from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
14
-
15
-
16
- @dataclass
17
- class HostData:
18
- """Stored data related to a host."""
19
-
20
- hostname: str
21
- hosts_file: PathLike
22
-
23
- def load(self) -> dict[str, Any]:
24
- return load(self.hosts_file).get(self.hostname, {})
25
-
26
- @property
27
- def correlation_id(self) -> str | None:
28
- return self.load().get("correlation_id")
29
-
30
- def store_correlation_id(self, correlation_id: str) -> None:
31
- """Store `correlation_id` in the hosts file."""
32
- hosts = load(self.hosts_file)
33
- data = hosts.setdefault(self.hostname, {})
34
- data["correlation_id"] = correlation_id
35
- _dump_hosts(self.hosts_file, hosts)
36
-
37
-
38
- def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> None:
39
- """Store a new token for a host."""
40
- # Don't use any file-based locking for simplicity
41
- hosts = load(hosts_file)
42
- data = hosts.setdefault(hostname, {})
43
- data.update(version=HOSTS_FORMAT_VERSION, token=token)
44
- _dump_hosts(hosts_file, hosts)
45
-
46
-
47
- def load(path: PathLike) -> dict[str, Any]:
48
- """Load the given hosts file.
49
-
50
- Return an empty dict if it doesn't exist.
51
- """
52
- from ..utils import _ensure_parent
53
-
54
- try:
55
- with open(path, "rb") as fd:
56
- return tomli.load(fd)
57
- except FileNotFoundError:
58
- _ensure_parent(path)
59
- return {}
60
- except tomli.TOMLDecodeError:
61
- return {}
62
-
63
-
64
- def load_for_host(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> dict[str, Any]:
65
- """Load all data associated with a hostname."""
66
- return load(hosts_file).get(hostname, {})
67
-
68
-
69
- @enum.unique
70
- class RemoveAuth(enum.Enum):
71
- success = 1
72
- no_match = 2
73
- no_hosts = 3
74
- error = 4
75
-
76
-
77
- def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> RemoveAuth:
78
- """Remove authentication for a Schemathesis.io host."""
79
- try:
80
- with open(hosts_file, "rb") as fd:
81
- hosts = tomli.load(fd)
82
- try:
83
- hosts.pop(hostname)
84
- _dump_hosts(hosts_file, hosts)
85
- return RemoveAuth.success
86
- except KeyError:
87
- return RemoveAuth.no_match
88
- except FileNotFoundError:
89
- return RemoveAuth.no_hosts
90
- except tomli.TOMLDecodeError:
91
- return RemoveAuth.error
92
-
93
-
94
- def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> str | None:
95
- """Load a token for a host."""
96
- return load_for_host(hostname, hosts_file).get("token")
97
-
98
-
99
- def get_temporary_hosts_file() -> str:
100
- temporary_dir = Path(tempfile.gettempdir()).resolve()
101
- return str(temporary_dir / "schemathesis-hosts.toml")
102
-
103
-
104
- def _dump_hosts(path: PathLike, hosts: dict[str, Any]) -> None:
105
- """Write hosts data to a file."""
106
- with open(path, "wb") as fd:
107
- tomli_w.dump(hosts, fd)
@@ -1,46 +0,0 @@
1
- """Useful info to collect from CLI usage."""
2
- from __future__ import annotations
3
- import os
4
- import platform
5
- from dataclasses import dataclass, field
6
-
7
- from ..constants import SCHEMATHESIS_VERSION
8
- from .constants import DOCKER_IMAGE_ENV_VAR
9
-
10
-
11
- @dataclass
12
- class PlatformMetadata:
13
- # System / OS name, e.g. "Linux" or "Windows".
14
- system: str = field(default_factory=platform.system)
15
- # System release, e.g. "5.14" or "NT".
16
- release: str = field(default_factory=platform.release)
17
- # Machine type, e.g. "i386".
18
- machine: str = field(default_factory=platform.machine)
19
-
20
-
21
- @dataclass
22
- class InterpreterMetadata:
23
- # The Python version as "major.minor.patch".
24
- version: str = field(default_factory=platform.python_version)
25
- # Python implementation, e.g. "CPython" or "PyPy".
26
- implementation: str = field(default_factory=platform.python_implementation)
27
-
28
-
29
- @dataclass
30
- class CliMetadata:
31
- # Schemathesis package version.
32
- version: str = SCHEMATHESIS_VERSION
33
-
34
-
35
- @dataclass
36
- class Metadata:
37
- """CLI environment metadata."""
38
-
39
- # Information about the host platform.
40
- platform: PlatformMetadata = field(default_factory=PlatformMetadata)
41
- # Python interpreter info.
42
- interpreter: InterpreterMetadata = field(default_factory=InterpreterMetadata)
43
- # CLI info itself.
44
- cli: CliMetadata = field(default_factory=CliMetadata)
45
- # Used Docker image if any
46
- docker_image: str | None = field(default_factory=lambda: os.getenv(DOCKER_IMAGE_ENV_VAR))
@@ -1,49 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
- from enum import Enum
4
- from typing import Any
5
-
6
-
7
- class UploadSource(str, Enum):
8
- DEFAULT = "default"
9
- UPLOAD_COMMAND = "upload_command"
10
-
11
-
12
- @dataclass
13
- class ProjectDetails:
14
- environments: list[ProjectEnvironment]
15
- specification: Specification
16
-
17
- @property
18
- def default_environment(self) -> ProjectEnvironment | None:
19
- return next((env for env in self.environments if env.is_default), None)
20
-
21
-
22
- @dataclass
23
- class ProjectEnvironment:
24
- url: str
25
- name: str
26
- description: str
27
- is_default: bool
28
-
29
-
30
- @dataclass
31
- class Specification:
32
- schema: dict[str, Any]
33
-
34
-
35
- @dataclass
36
- class AuthResponse:
37
- username: str
38
-
39
-
40
- @dataclass
41
- class UploadResponse:
42
- message: str
43
- next_url: str
44
- correlation_id: str
45
-
46
-
47
- @dataclass
48
- class FailedUploadResponse:
49
- detail: str
@@ -1,255 +0,0 @@
1
- from __future__ import annotations
2
- import enum
3
- import json
4
- import os
5
- import tarfile
6
- import threading
7
- import time
8
- from contextlib import suppress
9
- from dataclasses import asdict, dataclass, field
10
- from io import BytesIO
11
- from queue import Queue
12
- from typing import Any, TYPE_CHECKING
13
-
14
- import click
15
-
16
- from ..cli.handlers import EventHandler
17
- from ..runner.events import Initialized, InternalError, Interrupted
18
- from . import ci, events, usage
19
- from .constants import REPORT_FORMAT_VERSION, STOP_MARKER, WORKER_JOIN_TIMEOUT
20
- from .hosts import HostData
21
- from .metadata import Metadata
22
- from .models import UploadResponse
23
- from .serialization import serialize_event
24
-
25
-
26
- if TYPE_CHECKING:
27
- from .client import ServiceClient
28
- from ..cli.context import ExecutionContext
29
- from ..runner.events import ExecutionEvent
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))
@@ -1,184 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import asdict
3
- from typing import Any, Callable, Dict, Optional, TypeVar, cast
4
-
5
- from ..models import Response
6
- from ..runner import events
7
- from ..runner.serialization import SerializedCase
8
- from ..internal.transformation import merge_recursively
9
-
10
- S = TypeVar("S", bound=events.ExecutionEvent)
11
- SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
12
-
13
-
14
- def serialize_initialized(event: events.Initialized) -> dict[str, Any] | None:
15
- return {
16
- "operations_count": event.operations_count,
17
- "location": event.location or "",
18
- "base_url": event.base_url,
19
- }
20
-
21
-
22
- def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
23
- return {
24
- "correlation_id": event.correlation_id,
25
- "verbose_name": event.verbose_name,
26
- "data_generation_method": event.data_generation_method,
27
- }
28
-
29
-
30
- def _serialize_case(case: SerializedCase) -> dict[str, Any]:
31
- return {
32
- "verbose_name": case.verbose_name,
33
- "path_template": case.path_template,
34
- "path_parameters": stringify_path_parameters(case.path_parameters),
35
- "query": prepare_query(case.query),
36
- "cookies": case.cookies,
37
- "media_type": case.media_type,
38
- }
39
-
40
-
41
- def _serialize_response(response: Response) -> dict[str, Any]:
42
- return {
43
- "status_code": response.status_code,
44
- "headers": response.headers,
45
- "body": response.body,
46
- "encoding": response.encoding,
47
- "elapsed": response.elapsed,
48
- }
49
-
50
-
51
- def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
52
- return {
53
- "correlation_id": event.correlation_id,
54
- "verbose_name": event.verbose_name,
55
- "status": event.status,
56
- "elapsed_time": event.elapsed_time,
57
- "data_generation_method": event.data_generation_method,
58
- "result": {
59
- "checks": [
60
- {
61
- "name": check.name,
62
- "value": check.value,
63
- "request": {
64
- "method": check.request.method,
65
- "uri": check.request.uri,
66
- "body": check.request.body,
67
- "headers": check.request.headers,
68
- },
69
- "response": _serialize_response(check.response) if check.response is not None else None,
70
- "example": _serialize_case(check.example),
71
- "message": check.message,
72
- "context": asdict(check.context) if check.context is not None else None, # type: ignore
73
- "history": [
74
- {"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
75
- for entry in check.history
76
- ],
77
- }
78
- for check in event.result.checks
79
- ],
80
- "errors": [asdict(error) for error in event.result.errors],
81
- "skip_reason": event.result.skip_reason,
82
- },
83
- }
84
-
85
-
86
- def serialize_interrupted(_: events.Interrupted) -> dict[str, Any] | None:
87
- return None
88
-
89
-
90
- def serialize_internal_error(event: events.InternalError) -> dict[str, Any] | None:
91
- return {
92
- "type": event.type.value,
93
- "subtype": event.subtype.value if event.subtype else event.subtype,
94
- "title": event.title,
95
- "message": event.message,
96
- "extras": event.extras,
97
- "exception_type": event.exception_type,
98
- "exception": event.exception,
99
- "exception_with_traceback": event.exception_with_traceback,
100
- }
101
-
102
-
103
- def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
104
- return {
105
- "generic_errors": [
106
- {
107
- "exception": error.exception,
108
- "exception_with_traceback": error.exception_with_traceback,
109
- "title": error.title,
110
- }
111
- for error in event.generic_errors
112
- ],
113
- "running_time": event.running_time,
114
- }
115
-
116
-
117
- SERIALIZER_MAP = {
118
- events.Initialized: serialize_initialized,
119
- events.BeforeExecution: serialize_before_execution,
120
- events.AfterExecution: serialize_after_execution,
121
- events.Interrupted: serialize_interrupted,
122
- events.InternalError: serialize_internal_error,
123
- events.Finished: serialize_finished,
124
- }
125
-
126
-
127
- def serialize_event(
128
- event: events.ExecutionEvent,
129
- *,
130
- on_initialized: SerializeFunc | None = None,
131
- on_before_execution: SerializeFunc | None = None,
132
- on_after_execution: SerializeFunc | None = None,
133
- on_interrupted: SerializeFunc | None = None,
134
- on_internal_error: SerializeFunc | None = None,
135
- on_finished: SerializeFunc | None = None,
136
- extra: dict[str, Any] | None = None,
137
- ) -> dict[str, dict[str, Any] | None]:
138
- """Turn an event into JSON-serializable structure."""
139
- # Use the explicitly provided serializer for this event and fallback to default one if it is not provided
140
- serializer = {
141
- events.Initialized: on_initialized,
142
- events.BeforeExecution: on_before_execution,
143
- events.AfterExecution: on_after_execution,
144
- events.Interrupted: on_interrupted,
145
- events.InternalError: on_internal_error,
146
- events.Finished: on_finished,
147
- }.get(event.__class__)
148
- if serializer is None:
149
- serializer = cast(SerializeFunc, SERIALIZER_MAP[event.__class__])
150
- data = serializer(event)
151
- if extra is not None:
152
- # If `extra` is present, then merge it with the serialized data. If serialized data is empty, then replace it
153
- # with `extra` value
154
- if data is None:
155
- data = extra
156
- else:
157
- data = merge_recursively(data, extra)
158
- # Externally tagged structure
159
- return {event.__class__.__name__: data}
160
-
161
-
162
- def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
163
- """Cast all path parameter values to strings.
164
-
165
- Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
166
- """
167
- return {key: str(value) for key, value in (path_parameters or {}).items()}
168
-
169
-
170
- def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
171
- """Convert all query values to list of strings.
172
-
173
- Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
174
- It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
175
- """
176
-
177
- def to_list_of_strings(value: Any) -> list[str]:
178
- if isinstance(value, list):
179
- return list(map(str, value))
180
- if isinstance(value, str):
181
- return [value]
182
- return [str(value)]
183
-
184
- return {key: to_list_of_strings(value) for key, value in (query or {}).items()}