schemathesis 3.25.6__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 -1760
  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/{runner → engine/phases}/probes.py +50 -67
  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 +139 -23
  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 +478 -369
  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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.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 -58
  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 -790
  156. schemathesis/cli/output/short.py +0 -44
  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 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  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 -323
  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 -199
  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.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Sequence
5
+
6
+ from schemathesis import experimental
7
+ from schemathesis.checks import CHECKS, CheckFunction, ChecksConfig
8
+
9
+
10
+ @dataclass
11
+ class CheckArguments:
12
+ included_check_names: Sequence[str]
13
+ excluded_check_names: Sequence[str]
14
+ positive_data_acceptance_allowed_statuses: list[str] | None
15
+ missing_required_header_allowed_statuses: list[str] | None
16
+ negative_data_rejection_allowed_statuses: list[str] | None
17
+ max_response_time: float | None
18
+
19
+ __slots__ = (
20
+ "included_check_names",
21
+ "excluded_check_names",
22
+ "positive_data_acceptance_allowed_statuses",
23
+ "missing_required_header_allowed_statuses",
24
+ "negative_data_rejection_allowed_statuses",
25
+ "max_response_time",
26
+ )
27
+
28
+ def into(self) -> tuple[list[CheckFunction], ChecksConfig]:
29
+ # Determine selected checks
30
+ if "all" in self.included_check_names:
31
+ selected_checks = CHECKS.get_all()
32
+ else:
33
+ selected_checks = CHECKS.get_by_names(self.included_check_names or [])
34
+
35
+ # Prepare checks configuration
36
+ checks_config: ChecksConfig = {}
37
+
38
+ if experimental.POSITIVE_DATA_ACCEPTANCE.is_enabled:
39
+ from schemathesis.openapi.checks import PositiveDataAcceptanceConfig
40
+ from schemathesis.specs.openapi.checks import positive_data_acceptance
41
+
42
+ selected_checks.append(positive_data_acceptance)
43
+ if self.positive_data_acceptance_allowed_statuses:
44
+ checks_config[positive_data_acceptance] = PositiveDataAcceptanceConfig(
45
+ allowed_statuses=self.positive_data_acceptance_allowed_statuses
46
+ )
47
+
48
+ if self.missing_required_header_allowed_statuses:
49
+ from schemathesis.openapi.checks import MissingRequiredHeaderConfig
50
+ from schemathesis.specs.openapi.checks import missing_required_header
51
+
52
+ selected_checks.append(missing_required_header)
53
+ checks_config[missing_required_header] = MissingRequiredHeaderConfig(
54
+ allowed_statuses=self.missing_required_header_allowed_statuses
55
+ )
56
+
57
+ if self.negative_data_rejection_allowed_statuses:
58
+ from schemathesis.openapi.checks import NegativeDataRejectionConfig
59
+ from schemathesis.specs.openapi.checks import negative_data_rejection
60
+
61
+ checks_config[negative_data_rejection] = NegativeDataRejectionConfig(
62
+ allowed_statuses=self.negative_data_rejection_allowed_statuses
63
+ )
64
+
65
+ if self.max_response_time is not None:
66
+ from schemathesis.checks import max_response_time as _max_response_time
67
+ from schemathesis.core.failures import MaxResponseTimeConfig
68
+
69
+ checks_config[_max_response_time] = MaxResponseTimeConfig(self.max_response_time)
70
+ selected_checks.append(_max_response_time)
71
+
72
+ if experimental.COVERAGE_PHASE.is_enabled:
73
+ from schemathesis.specs.openapi.checks import unsupported_method
74
+
75
+ selected_checks.append(unsupported_method)
76
+
77
+ # Exclude checks based on their names
78
+ selected_checks = [check for check in selected_checks if check.__name__ not in self.excluded_check_names]
79
+
80
+ return selected_checks, checks_config
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Generator
5
+
6
+ from schemathesis.core.failures import Failure
7
+ from schemathesis.core.output import OutputConfig
8
+ from schemathesis.core.transport import Response
9
+ from schemathesis.engine import Status, events
10
+ from schemathesis.engine.recorder import ScenarioRecorder
11
+
12
+
13
+ @dataclass
14
+ class Statistic:
15
+ """Running statistics about test execution."""
16
+
17
+ outcomes: dict[Status, int]
18
+ failures: dict[str, dict[str, GroupedFailures]]
19
+
20
+ tested_operations: set[str]
21
+
22
+ total_cases: int
23
+ cases_with_failures: int
24
+ cases_without_checks: int
25
+
26
+ __slots__ = (
27
+ "outcomes",
28
+ "failures",
29
+ "tested_operations",
30
+ "total_cases",
31
+ "cases_with_failures",
32
+ "cases_without_checks",
33
+ )
34
+
35
+ def __init__(self) -> None:
36
+ self.outcomes = {}
37
+ self.failures = {}
38
+ self.tested_operations = set()
39
+ self.total_cases = 0
40
+ self.cases_with_failures = 0
41
+ self.cases_without_checks = 0
42
+
43
+ def record_checks(self, recorder: ScenarioRecorder) -> None:
44
+ """Update statistics and store failures from a new batch of checks."""
45
+ failures = self.failures.get(recorder.label, {})
46
+
47
+ self.total_cases += len(recorder.cases)
48
+
49
+ for case_id, case in recorder.cases.items():
50
+ checks = recorder.checks.get(case_id, [])
51
+
52
+ if not checks:
53
+ self.cases_without_checks += 1
54
+ continue
55
+
56
+ self.tested_operations.add(case.value.operation.label)
57
+ has_failures = False
58
+ for check in checks:
59
+ response = recorder.interactions[case_id].response
60
+
61
+ # Collect failures
62
+ if check.failure_info is not None:
63
+ has_failures = True
64
+ if case_id not in failures:
65
+ failures[case_id] = GroupedFailures(
66
+ case_id=case_id,
67
+ code_sample=check.failure_info.code_sample,
68
+ failures=[],
69
+ response=response,
70
+ )
71
+ failures[case_id].failures.append(check.failure_info.failure)
72
+ if has_failures:
73
+ self.cases_with_failures += 1
74
+ if failures:
75
+ for group in failures.values():
76
+ group.failures = sorted(set(group.failures))
77
+ self.failures[recorder.label] = failures
78
+
79
+
80
+ @dataclass
81
+ class GroupedFailures:
82
+ """Represents failures grouped by case ID."""
83
+
84
+ case_id: str
85
+ code_sample: str
86
+ failures: list[Failure]
87
+ response: Response | None
88
+
89
+ __slots__ = ("case_id", "code_sample", "failures", "response")
90
+
91
+
92
+ @dataclass
93
+ class ExecutionContext:
94
+ """Storage for the current context of the execution."""
95
+
96
+ statistic: Statistic = field(default_factory=Statistic)
97
+ exit_code: int = 0
98
+ output_config: OutputConfig = field(default_factory=OutputConfig)
99
+ initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
100
+ summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
101
+ seed: int | None = None
102
+
103
+ def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
104
+ self.initialization_lines.append(line)
105
+
106
+ def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
107
+ self.summary_lines.append(line)
108
+
109
+ def on_event(self, event: events.EngineEvent) -> None:
110
+ if isinstance(event, events.ScenarioFinished):
111
+ self.statistic.record_checks(event.recorder)
112
+ elif isinstance(event, events.NonFatalError) or (
113
+ isinstance(event, events.PhaseFinished)
114
+ and event.phase.is_enabled
115
+ and event.status in (Status.FAILURE, Status.ERROR)
116
+ ):
117
+ self.exit_code = 1
@@ -0,0 +1,35 @@
1
+ import time
2
+ import uuid
3
+
4
+ from schemathesis.core import Specification
5
+ from schemathesis.engine import events
6
+ from schemathesis.schemas import ApiOperationsCount
7
+
8
+
9
+ class LoadingStarted(events.EngineEvent):
10
+ __slots__ = ("id", "timestamp", "location")
11
+
12
+ def __init__(self, *, location: str) -> None:
13
+ self.id = uuid.uuid4()
14
+ self.timestamp = time.time()
15
+ self.location = location
16
+
17
+
18
+ class LoadingFinished(events.EngineEvent):
19
+ __slots__ = ("id", "timestamp", "location", "duration", "base_url", "specification", "operations_count")
20
+
21
+ def __init__(
22
+ self,
23
+ location: str,
24
+ start_time: float,
25
+ base_url: str,
26
+ specification: Specification,
27
+ operations_count: ApiOperationsCount,
28
+ ) -> None:
29
+ self.id = uuid.uuid4()
30
+ self.timestamp = time.time()
31
+ self.location = location
32
+ self.duration = self.timestamp - start_time
33
+ self.base_url = base_url
34
+ self.specification = specification
35
+ self.operations_count = operations_count
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+ import click
8
+
9
+ from schemathesis.cli.commands.run.context import ExecutionContext
10
+ from schemathesis.cli.commands.run.events import LoadingFinished, LoadingStarted
11
+ from schemathesis.cli.commands.run.handlers import display_handler_error
12
+ from schemathesis.cli.commands.run.handlers.base import EventHandler
13
+ from schemathesis.cli.commands.run.handlers.cassettes import CassetteConfig, CassetteWriter
14
+ from schemathesis.cli.commands.run.handlers.junitxml import JunitXMLHandler
15
+ from schemathesis.cli.commands.run.handlers.output import OutputHandler
16
+ from schemathesis.cli.commands.run.loaders import AutodetectConfig, load_schema
17
+ from schemathesis.cli.ext.fs import open_file
18
+ from schemathesis.core.errors import LoaderError
19
+ from schemathesis.core.output import OutputConfig
20
+ from schemathesis.engine import from_schema
21
+ from schemathesis.engine.config import EngineConfig
22
+ from schemathesis.engine.events import EventGenerator, FatalError
23
+ from schemathesis.filters import FilterSet
24
+
25
+ CUSTOM_HANDLERS: list[type[EventHandler]] = []
26
+
27
+
28
+ def handler() -> Callable[[type], None]:
29
+ """Register a new CLI event handler."""
30
+
31
+ def _wrapper(cls: type) -> None:
32
+ CUSTOM_HANDLERS.append(cls)
33
+
34
+ return _wrapper
35
+
36
+
37
+ @dataclass
38
+ class RunConfig:
39
+ location: str
40
+ base_url: str | None
41
+ filter_set: FilterSet
42
+ engine: EngineConfig
43
+ wait_for_schema: float | None
44
+ rate_limit: str | None
45
+ output: OutputConfig
46
+ junit_xml: click.utils.LazyFile | None
47
+ cassette: CassetteConfig | None
48
+ args: list[str]
49
+ params: dict[str, Any]
50
+
51
+
52
+ def execute(config: RunConfig) -> None:
53
+ event_stream = into_event_stream(config)
54
+ _execute(event_stream, config)
55
+
56
+
57
+ def into_event_stream(config: RunConfig) -> EventGenerator:
58
+ loader_config = AutodetectConfig(
59
+ location=config.location,
60
+ network=config.engine.network,
61
+ wait_for_schema=config.wait_for_schema,
62
+ base_url=config.base_url,
63
+ rate_limit=config.rate_limit,
64
+ output=config.output,
65
+ generation=config.engine.execution.generation,
66
+ )
67
+ loading_started = LoadingStarted(location=config.location)
68
+ yield loading_started
69
+
70
+ try:
71
+ schema = load_schema(loader_config)
72
+ schema.filter_set = config.filter_set
73
+ except LoaderError as exc:
74
+ yield FatalError(exception=exc)
75
+ return
76
+
77
+ yield LoadingFinished(
78
+ location=config.location,
79
+ start_time=loading_started.timestamp,
80
+ base_url=schema.get_base_url(),
81
+ specification=schema.specification,
82
+ operations_count=schema.count_operations(),
83
+ )
84
+
85
+ try:
86
+ yield from from_schema(schema, config=config.engine).execute()
87
+ except Exception as exc:
88
+ yield FatalError(exception=exc)
89
+
90
+
91
+ def _execute(event_stream: EventGenerator, config: RunConfig) -> None:
92
+ handlers: list[EventHandler] = []
93
+ if config.junit_xml is not None:
94
+ open_file(config.junit_xml)
95
+ handlers.append(JunitXMLHandler(config.junit_xml))
96
+ if config.cassette is not None:
97
+ open_file(config.cassette.path)
98
+ handlers.append(CassetteWriter(config=config.cassette))
99
+ for custom_handler in CUSTOM_HANDLERS:
100
+ handlers.append(custom_handler(*config.args, **config.params))
101
+ handlers.append(
102
+ OutputHandler(
103
+ workers_num=config.engine.execution.workers_num,
104
+ rate_limit=config.rate_limit,
105
+ wait_for_schema=config.wait_for_schema,
106
+ cassette_config=config.cassette,
107
+ junit_xml_file=config.junit_xml.name if config.junit_xml is not None else None,
108
+ )
109
+ )
110
+
111
+ ctx = ExecutionContext(output_config=config.output, seed=config.engine.execution.seed)
112
+
113
+ def shutdown() -> None:
114
+ for _handler in handlers:
115
+ _handler.shutdown()
116
+
117
+ for handler in handlers:
118
+ handler.start(ctx)
119
+
120
+ try:
121
+ for event in event_stream:
122
+ ctx.on_event(event)
123
+ for handler in handlers:
124
+ try:
125
+ handler.handle_event(ctx, event)
126
+ except Exception as exc:
127
+ # `Abort` is used for handled errors
128
+ if not isinstance(exc, click.Abort):
129
+ display_handler_error(handler, exc)
130
+ raise
131
+ except Exception as exc:
132
+ if isinstance(exc, click.Abort):
133
+ # To avoid showing "Aborted!" message, which is the default behavior in Click
134
+ sys.exit(1)
135
+ raise
136
+ finally:
137
+ shutdown()
138
+ sys.exit(ctx.exit_code)
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, Literal, Sequence
5
+
6
+ import click
7
+
8
+ from schemathesis.cli.ext.groups import grouped_option
9
+ from schemathesis.filters import FilterSet, expression_to_filter_function, is_deprecated
10
+
11
+
12
+ def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Literal["regex"] | None) -> Callable:
13
+ """Generate a CLI option for filtering API operations."""
14
+ param = f"--{mode}-{by}"
15
+ action = "include in" if mode == "include" else "exclude from"
16
+ prop = {
17
+ "operation-id": "ID",
18
+ "name": "Operation name",
19
+ }.get(by, by.capitalize())
20
+ if modifier:
21
+ param += f"-{modifier}"
22
+ prop += " pattern"
23
+ help_text = f"{prop} to {action} testing."
24
+ return grouped_option(
25
+ param,
26
+ help=help_text,
27
+ type=str,
28
+ multiple=modifier is None,
29
+ )
30
+
31
+
32
+ _BY_VALUES = ("operation-id", "tag", "name", "method", "path")
33
+
34
+
35
+ def with_filters(command: Callable) -> Callable:
36
+ for by in _BY_VALUES:
37
+ for mode in ("exclude", "include"):
38
+ for modifier in ("regex", None):
39
+ command = _with_filter(by=by, mode=mode, modifier=modifier)(command) # type: ignore[arg-type]
40
+ return command
41
+
42
+
43
+ @dataclass
44
+ class FilterArguments:
45
+ include_path: Sequence[str]
46
+ include_method: Sequence[str]
47
+ include_name: Sequence[str]
48
+ include_tag: Sequence[str]
49
+ include_operation_id: Sequence[str]
50
+ include_path_regex: str | None
51
+ include_method_regex: str | None
52
+ include_name_regex: str | None
53
+ include_tag_regex: str | None
54
+ include_operation_id_regex: str | None
55
+
56
+ exclude_path: Sequence[str]
57
+ exclude_method: Sequence[str]
58
+ exclude_name: Sequence[str]
59
+ exclude_tag: Sequence[str]
60
+ exclude_operation_id: Sequence[str]
61
+ exclude_path_regex: str | None
62
+ exclude_method_regex: str | None
63
+ exclude_name_regex: str | None
64
+ exclude_tag_regex: str | None
65
+ exclude_operation_id_regex: str | None
66
+
67
+ include_by: str | None
68
+ exclude_by: str | None
69
+ exclude_deprecated: bool
70
+
71
+ __slots__ = (
72
+ "include_path",
73
+ "include_method",
74
+ "include_name",
75
+ "include_tag",
76
+ "include_operation_id",
77
+ "include_path_regex",
78
+ "include_method_regex",
79
+ "include_name_regex",
80
+ "include_tag_regex",
81
+ "include_operation_id_regex",
82
+ "exclude_path",
83
+ "exclude_method",
84
+ "exclude_name",
85
+ "exclude_tag",
86
+ "exclude_operation_id",
87
+ "exclude_path_regex",
88
+ "exclude_method_regex",
89
+ "exclude_name_regex",
90
+ "exclude_tag_regex",
91
+ "exclude_operation_id_regex",
92
+ "include_by",
93
+ "exclude_by",
94
+ "exclude_deprecated",
95
+ )
96
+
97
+ def into(self) -> FilterSet:
98
+ # Validate unique filter arguments
99
+ for values, arg_name in (
100
+ (self.include_path, "--include-path"),
101
+ (self.include_method, "--include-method"),
102
+ (self.include_name, "--include-name"),
103
+ (self.include_tag, "--include-tag"),
104
+ (self.include_operation_id, "--include-operation-id"),
105
+ (self.exclude_path, "--exclude-path"),
106
+ (self.exclude_method, "--exclude-method"),
107
+ (self.exclude_name, "--exclude-name"),
108
+ (self.exclude_tag, "--exclude-tag"),
109
+ (self.exclude_operation_id, "--exclude-operation-id"),
110
+ ):
111
+ validate_unique_filter(values, arg_name)
112
+
113
+ # Convert include/exclude expressions to functions
114
+ include_by_function = _filter_by_expression_to_func(self.include_by, "--include-by")
115
+ exclude_by_function = _filter_by_expression_to_func(self.exclude_by, "--exclude-by")
116
+
117
+ filter_set = FilterSet()
118
+
119
+ # Apply include filters
120
+ if include_by_function:
121
+ filter_set.include(include_by_function)
122
+ for name_ in self.include_name:
123
+ filter_set.include(name=name_)
124
+ for method in self.include_method:
125
+ filter_set.include(method=method)
126
+ for path in self.include_path:
127
+ filter_set.include(path=path)
128
+ for tag in self.include_tag:
129
+ filter_set.include(tag=tag)
130
+ for operation_id in self.include_operation_id:
131
+ filter_set.include(operation_id=operation_id)
132
+ if (
133
+ self.include_name_regex
134
+ or self.include_method_regex
135
+ or self.include_path_regex
136
+ or self.include_tag_regex
137
+ or self.include_operation_id_regex
138
+ ):
139
+ filter_set.include(
140
+ name_regex=self.include_name_regex,
141
+ method_regex=self.include_method_regex,
142
+ path_regex=self.include_path_regex,
143
+ tag_regex=self.include_tag_regex,
144
+ operation_id_regex=self.include_operation_id_regex,
145
+ )
146
+
147
+ # Apply exclude filters
148
+ if exclude_by_function:
149
+ filter_set.exclude(exclude_by_function)
150
+ for name_ in self.exclude_name:
151
+ filter_set.exclude(name=name_)
152
+ for method in self.exclude_method:
153
+ filter_set.exclude(method=method)
154
+ for path in self.exclude_path:
155
+ filter_set.exclude(path=path)
156
+ for tag in self.exclude_tag:
157
+ filter_set.exclude(tag=tag)
158
+ for operation_id in self.exclude_operation_id:
159
+ filter_set.exclude(operation_id=operation_id)
160
+ if (
161
+ self.exclude_name_regex
162
+ or self.exclude_method_regex
163
+ or self.exclude_path_regex
164
+ or self.exclude_tag_regex
165
+ or self.exclude_operation_id_regex
166
+ ):
167
+ filter_set.exclude(
168
+ name_regex=self.exclude_name_regex,
169
+ method_regex=self.exclude_method_regex,
170
+ path_regex=self.exclude_path_regex,
171
+ tag_regex=self.exclude_tag_regex,
172
+ operation_id_regex=self.exclude_operation_id_regex,
173
+ )
174
+
175
+ # Exclude deprecated operations
176
+ if self.exclude_deprecated:
177
+ filter_set.exclude(is_deprecated)
178
+
179
+ return filter_set
180
+
181
+
182
+ def validate_unique_filter(values: Sequence[str], arg_name: str) -> None:
183
+ if len(values) != len(set(values)):
184
+ duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
185
+ raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
186
+
187
+
188
+ def _filter_by_expression_to_func(value: str | None, arg_name: str) -> Callable | None:
189
+ if value:
190
+ try:
191
+ return expression_to_filter_function(value)
192
+ except ValueError:
193
+ raise click.UsageError(f"Invalid expression for {arg_name}: {value}") from None
194
+ return None
@@ -0,0 +1,46 @@
1
+ import click
2
+
3
+ from schemathesis.cli.commands.run.handlers.base import EventHandler
4
+ from schemathesis.cli.commands.run.handlers.cassettes import CassetteWriter
5
+ from schemathesis.cli.commands.run.handlers.junitxml import JunitXMLHandler
6
+ from schemathesis.cli.commands.run.handlers.output import OutputHandler
7
+ from schemathesis.cli.constants import EXTENSIONS_DOCUMENTATION_URL, ISSUE_TRACKER_URL
8
+ from schemathesis.core.errors import format_exception
9
+
10
+ __all__ = [
11
+ "EventHandler",
12
+ "CassetteWriter",
13
+ "JunitXMLHandler",
14
+ "OutputHandler",
15
+ "display_handler_error",
16
+ ]
17
+
18
+
19
+ def is_built_in_handler(handler: EventHandler) -> bool:
20
+ # Look for exact instances, not subclasses
21
+ return any(type(handler) is class_ for class_ in (CassetteWriter, JunitXMLHandler, OutputHandler))
22
+
23
+
24
+ def display_handler_error(handler: EventHandler, exc: Exception) -> None:
25
+ """Display error that happened within."""
26
+ is_built_in = is_built_in_handler(handler)
27
+ if is_built_in:
28
+ click.secho("Internal Error", fg="red", bold=True)
29
+ click.secho("\nSchemathesis encountered an unexpected issue.")
30
+ message = format_exception(exc, with_traceback=True)
31
+ else:
32
+ click.secho("CLI Handler Error", fg="red", bold=True)
33
+ click.echo(
34
+ f"\nAn error occurred within your custom CLI handler `{click.style(handler.__class__.__name__, bold=True)}`."
35
+ )
36
+ message = format_exception(exc, with_traceback=True, skip_frames=1)
37
+ click.secho(f"\n{message}", fg="red")
38
+ if is_built_in:
39
+ click.echo(
40
+ f"\nWe apologize for the inconvenience. This appears to be an internal issue.\n"
41
+ f"Please consider reporting this error to our issue tracker:\n\n {ISSUE_TRACKER_URL}."
42
+ )
43
+ else:
44
+ click.echo(
45
+ f"\nFor more information on implementing extensions for Schemathesis CLI, visit {EXTENSIONS_DOCUMENTATION_URL}"
46
+ )
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from schemathesis.cli.commands.run.context import ExecutionContext
7
+ from schemathesis.engine import events
8
+
9
+
10
+ class EventHandler:
11
+ def __init__(self, *args: Any, **params: Any) -> None: ...
12
+
13
+ def handle_event(self, ctx: ExecutionContext, event: events.EngineEvent) -> None:
14
+ raise NotImplementedError
15
+
16
+ def start(self, ctx: ExecutionContext) -> None: ...
17
+
18
+ def shutdown(self) -> None: ...