schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
+
2
3
  import base64
3
- import enum
4
- import io
5
4
  import os
6
5
  import sys
7
6
  import traceback
@@ -10,66 +9,77 @@ from collections import defaultdict
10
9
  from dataclasses import dataclass
11
10
  from enum import Enum
12
11
  from queue import Queue
13
- from typing import Any, Callable, Generator, Iterable, NoReturn, cast, TYPE_CHECKING
12
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, Type, cast
14
13
  from urllib.parse import urlparse
15
14
 
16
15
  import click
17
16
 
18
17
  from .. import checks as checks_module
19
- from .. import contrib, experimental, generation
18
+ from .. import contrib, experimental, generation, runner, service
20
19
  from .. import fixups as _fixups
21
- from .. import runner, service
22
20
  from .. import targets as targets_module
21
+ from .._override import CaseOverride
23
22
  from ..code_samples import CodeSampleStyle
24
- from .constants import HealthCheck, Phase, Verbosity
25
- from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
26
23
  from ..constants import (
27
24
  API_NAME_ENV_VAR,
28
25
  BASE_URL_ENV_VAR,
29
26
  DEFAULT_RESPONSE_TIMEOUT,
30
27
  DEFAULT_STATEFUL_RECURSION_LIMIT,
28
+ EXTENSIONS_DOCUMENTATION_URL,
31
29
  HOOKS_MODULE_ENV_VAR,
32
30
  HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
33
- WAIT_FOR_SCHEMA_ENV_VAR,
34
- EXTENSIONS_DOCUMENTATION_URL,
35
31
  ISSUE_TRACKER_URL,
32
+ WAIT_FOR_SCHEMA_ENV_VAR,
36
33
  )
37
- from ..exceptions import SchemaError, extract_nth_traceback, SchemaErrorType
34
+ from ..exceptions import SchemaError, SchemaErrorType, extract_nth_traceback
35
+ from ..filters import FilterSet, expression_to_filter_function, is_deprecated
38
36
  from ..fixups import ALL_FIXUPS
39
- from ..loaders import load_app, load_yaml
40
- from .._override import CaseOverride
41
- from ..transports.auth import get_requests_auth
37
+ from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
42
38
  from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
43
- from ..models import Case, CheckFunction
39
+ from ..internal.checks import CheckConfig
40
+ from ..internal.datetime import current_datetime
41
+ from ..internal.output import OutputConfig
42
+ from ..internal.validation import file_exists
43
+ from ..loaders import load_app, load_yaml
44
44
  from ..runner import events, prepare_hypothesis_settings, probes
45
45
  from ..specs.graphql import loaders as gql_loaders
46
46
  from ..specs.openapi import loaders as oas_loaders
47
47
  from ..stateful import Stateful
48
- from ..targets import Target
49
- from ..types import Filter, PathLike, RequestCert
50
- from ..internal.datetime import current_datetime
51
- from ..internal.validation import file_exists
48
+ from ..transports import RequestConfig
49
+ from ..transports.auth import get_requests_auth
52
50
  from . import callbacks, cassettes, output
53
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
51
+ from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
54
52
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
55
53
  from .debug import DebugOutputHandler
54
+ from .handlers import EventHandler
56
55
  from .junitxml import JunitXMLHandler
57
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
56
+ from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
58
57
  from .sanitization import SanitizationHandler
59
58
 
60
59
  if TYPE_CHECKING:
60
+ import io
61
+
61
62
  import hypothesis
62
63
  import requests
63
- from ..service.client import ServiceClient
64
+
65
+ from ..models import Case, CheckFunction
64
66
  from ..schemas import BaseSchema
67
+ from ..service.client import ServiceClient
65
68
  from ..specs.graphql.schemas import GraphQLSchema
66
- from .handlers import EventHandler
69
+ from ..targets import Target
70
+ from ..types import NotSet, PathLike, RequestCert
71
+
72
+
73
+ __all__ = [
74
+ "EventHandler",
75
+ ]
67
76
 
68
77
 
69
78
  def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
70
79
  return tuple(item.__name__ for item in items)
71
80
 
72
81
 
82
+ CUSTOM_HANDLERS: list[type[EventHandler]] = []
73
83
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
74
84
 
75
85
  DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
@@ -95,13 +105,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
95
105
  "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
96
106
  "Use `--show-trace` instead"
97
107
  )
98
- DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
99
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
100
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
101
- "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
102
- "This leads to cryptic error messages about external state and flaky test runs, "
103
- "therefore it will be removed in Schemathesis 4.0"
104
- )
105
108
  CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
106
109
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
107
110
  PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
@@ -111,21 +114,21 @@ def reset_checks() -> None:
111
114
  """Get checks list to their default state."""
112
115
  # Useful in tests
113
116
  checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
114
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
117
+ CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
115
118
 
116
119
 
117
120
  def reset_targets() -> None:
118
121
  """Get targets list to their default state."""
119
122
  # Useful in tests
120
123
  targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
121
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
124
+ TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
122
125
 
123
126
 
124
127
  @click.group(context_settings=CONTEXT_SETTINGS)
125
- @click.option("--pre-run", help="A module to execute before running the tests.", type=str, hidden=True)
128
+ @click.option("--pre-run", help="[DEPRECATED] A module to execute before running the tests", type=str, hidden=True)
126
129
  @click.version_option()
127
130
  def schemathesis(pre_run: str | None = None) -> None:
128
- """Automated API testing employing fuzzing techniques for OpenAPI and GraphQL."""
131
+ """Property-based API testing for OpenAPI and GraphQL."""
129
132
  # Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
130
133
  hooks: str | None
131
134
  if pre_run:
@@ -137,76 +140,89 @@ def schemathesis(pre_run: str | None = None) -> None:
137
140
  load_hook(hooks)
138
141
 
139
142
 
140
- class ParameterGroup(enum.Enum):
141
- filtering = "Testing scope", "Customize the scope of the API testing."
142
- validation = "Response & Schema validation", "These options specify how API responses and schemas are validated."
143
- hypothesis = "Hypothesis engine", "Configuration of the underlying Hypothesis engine."
144
- generic = "Generic", None
143
+ GROUPS: list[str] = []
145
144
 
146
145
 
147
- class CommandWithCustomHelp(click.Command):
146
+ class CommandWithGroupedOptions(click.Command):
148
147
  def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
149
- # Group options first
150
148
  groups = defaultdict(list)
151
149
  for param in self.get_params(ctx):
152
150
  rv = param.get_help_record(ctx)
153
151
  if rv is not None:
152
+ (option_repr, message) = rv
153
+ if isinstance(param.type, click.Choice):
154
+ message += (
155
+ getattr(param.type, "choices_repr", None)
156
+ or f" [possible values: {', '.join(param.type.choices)}]"
157
+ )
158
+
154
159
  if isinstance(param, GroupedOption):
155
160
  group = param.group
156
161
  else:
157
- group = ParameterGroup.generic
158
- groups[group].append(rv)
159
- # Then display groups separately with optional description
160
- for group in ParameterGroup:
161
- opts = groups[group]
162
- title, description = group.value
163
- with formatter.section(title):
164
- if description:
165
- formatter.write_paragraph()
166
- formatter.write_text(description)
167
- formatter.write_paragraph()
168
- formatter.write_dl(opts)
162
+ group = "Global options"
163
+ groups[group].append((option_repr, message))
164
+ for group in GROUPS:
165
+ with formatter.section(group or "Options"):
166
+ formatter.write_dl(groups[group], col_max=40)
169
167
 
170
168
 
171
169
  class GroupedOption(click.Option):
172
- def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
170
+ def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
173
171
  super().__init__(*args, **kwargs)
174
172
  self.group = group
175
173
 
176
174
 
177
- with_request_proxy = click.option(
175
+ def group(name: str) -> Callable:
176
+ GROUPS.append(name)
177
+
178
+ def _inner(cmd: Callable) -> Callable:
179
+ for param in reversed(cmd.__click_params__): # type: ignore[attr-defined]
180
+ if not isinstance(param, GroupedOption) or param.group is not None:
181
+ break
182
+ param.group = name
183
+ return cmd
184
+
185
+ return _inner
186
+
187
+
188
+ def grouped_option(*args: Any, **kwargs: Any) -> Callable:
189
+ kwargs.setdefault("cls", GroupedOption)
190
+ return click.option(*args, **kwargs)
191
+
192
+
193
+ with_request_proxy = grouped_option(
178
194
  "--request-proxy",
179
- help="Set the proxy for all network requests.",
195
+ help="Set the proxy for all network requests",
180
196
  type=str,
181
197
  )
182
- with_request_tls_verify = click.option(
198
+ with_request_tls_verify = grouped_option(
183
199
  "--request-tls-verify",
184
- help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs.",
200
+ help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs",
185
201
  type=str,
186
202
  default="true",
187
203
  show_default=True,
188
204
  callback=callbacks.convert_boolean_string,
189
205
  )
190
- with_request_cert = click.option(
206
+ with_request_cert = grouped_option(
191
207
  "--request-cert",
192
208
  help="File path of unencrypted client certificate for authentication. "
193
209
  "The certificate can be bundled with a private key (e.g. PEM) or the private "
194
- "key can be provided with the --request-cert-key argument.",
210
+ "key can be provided with the --request-cert-key argument",
195
211
  type=click.Path(exists=True),
196
212
  default=None,
197
213
  show_default=False,
198
214
  )
199
- with_request_cert_key = click.option(
215
+ with_request_cert_key = grouped_option(
200
216
  "--request-cert-key",
201
- help="Specifies the file path of the private key for the client certificate.",
217
+ help="Specify the file path of the private key for the client certificate",
202
218
  type=click.Path(exists=True),
203
219
  default=None,
204
220
  show_default=False,
205
221
  callback=callbacks.validate_request_cert_key,
206
222
  )
207
- with_hosts_file = click.option(
223
+ with_hosts_file = grouped_option(
208
224
  "--hosts-file",
209
- help="Path to a file to store the Schemathesis.io auth configuration.",
225
+ help="Path to a file to store the Schemathesis.io auth configuration",
210
226
  type=click.Path(dir_okay=False, writable=True),
211
227
  default=service.DEFAULT_HOSTS_PATH,
212
228
  envvar=service.HOSTS_PATH_ENV_VAR,
@@ -214,6 +230,37 @@ with_hosts_file = click.option(
214
230
  )
215
231
 
216
232
 
233
+ def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Literal["regex"] | None = None) -> Callable:
234
+ """Generate a CLI option for filtering API operations."""
235
+ param = f"--{mode}-{by}"
236
+ action = "include in" if mode == "include" else "exclude from"
237
+ prop = {
238
+ "operation-id": "ID",
239
+ "name": "Operation name",
240
+ }.get(by, by.capitalize())
241
+ if modifier:
242
+ param += f"-{modifier}"
243
+ prop += " pattern"
244
+ help_text = f"{prop} to {action} testing."
245
+ return grouped_option(
246
+ param,
247
+ help=help_text,
248
+ type=str,
249
+ multiple=modifier is None,
250
+ )
251
+
252
+
253
+ _BY_VALUES = ("operation-id", "tag", "name", "method", "path")
254
+
255
+
256
+ def with_filters(command: Callable) -> Callable:
257
+ for by in _BY_VALUES:
258
+ for mode in ("exclude", "include"):
259
+ for modifier in ("regex", None):
260
+ command = _with_filter(by=by, mode=mode, modifier=modifier)(command) # type: ignore[arg-type]
261
+ return command
262
+
263
+
217
264
  class ReportToService:
218
265
  pass
219
266
 
@@ -221,502 +268,587 @@ class ReportToService:
221
268
  REPORT_TO_SERVICE = ReportToService()
222
269
 
223
270
 
224
- @schemathesis.command(short_help="Execute automated tests based on API specifications.", cls=CommandWithCustomHelp)
271
+ @schemathesis.command(
272
+ short_help="Execute automated tests based on API specifications",
273
+ cls=CommandWithGroupedOptions,
274
+ context_settings={"terminal_width": output.default.get_terminal_width(), **CONTEXT_SETTINGS},
275
+ )
225
276
  @click.argument("schema", type=str)
226
277
  @click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
227
- @click.option(
278
+ @group("Options")
279
+ @grouped_option(
280
+ "--workers",
281
+ "-w",
282
+ "workers_num",
283
+ help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
284
+ type=CustomHelpMessageChoice(
285
+ ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
286
+ choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
287
+ ),
288
+ default=str(DEFAULT_WORKERS),
289
+ show_default=True,
290
+ callback=callbacks.convert_workers,
291
+ metavar="",
292
+ )
293
+ @grouped_option(
294
+ "--dry-run",
295
+ "dry_run",
296
+ is_flag=True,
297
+ default=False,
298
+ help="Simulate test execution without making any actual requests, useful for validating data generation",
299
+ )
300
+ @grouped_option(
301
+ "--fixups",
302
+ help="Apply compatibility adjustments",
303
+ multiple=True,
304
+ type=click.Choice([*ALL_FIXUPS, "all"]),
305
+ metavar="",
306
+ )
307
+ @group("Experimental options")
308
+ @grouped_option(
309
+ "--experimental",
310
+ "experiments",
311
+ help="Enable experimental features",
312
+ type=click.Choice(
313
+ [
314
+ experimental.OPEN_API_3_1.name,
315
+ experimental.SCHEMA_ANALYSIS.name,
316
+ experimental.STATEFUL_TEST_RUNNER.name,
317
+ experimental.STATEFUL_ONLY.name,
318
+ experimental.COVERAGE_PHASE.name,
319
+ experimental.POSITIVE_DATA_ACCEPTANCE.name,
320
+ ]
321
+ ),
322
+ callback=callbacks.convert_experimental,
323
+ multiple=True,
324
+ metavar="",
325
+ )
326
+ @grouped_option(
327
+ "--experimental-no-failfast",
328
+ "no_failfast",
329
+ help="Continue testing an API operation after a failure is found",
330
+ is_flag=True,
331
+ default=False,
332
+ metavar="",
333
+ envvar="SCHEMATHESIS_EXPERIMENTAL_NO_FAILFAST",
334
+ )
335
+ @grouped_option(
336
+ "--experimental-missing-required-header-allowed-statuses",
337
+ "missing_required_header_allowed_statuses",
338
+ help="Comma-separated list of status codes expected for test cases with a missing required header",
339
+ type=CsvListChoice(),
340
+ callback=callbacks.convert_status_codes,
341
+ metavar="",
342
+ envvar="SCHEMATHESIS_EXPERIMENTAL_MISSING_REQUIRED_HEADER_ALLOWED_STATUSES",
343
+ )
344
+ @grouped_option(
345
+ "--experimental-positive-data-acceptance-allowed-statuses",
346
+ "positive_data_acceptance_allowed_statuses",
347
+ help="Comma-separated list of status codes considered as successful responses",
348
+ type=CsvListChoice(),
349
+ callback=callbacks.convert_status_codes,
350
+ metavar="",
351
+ envvar="SCHEMATHESIS_EXPERIMENTAL_POSITIVE_DATA_ACCEPTANCE_ALLOWED_STATUSES",
352
+ )
353
+ @grouped_option(
354
+ "--experimental-negative-data-rejection-allowed-statuses",
355
+ "negative_data_rejection_allowed_statuses",
356
+ help="Comma-separated list of status codes expected for rejected negative data",
357
+ type=CsvListChoice(),
358
+ callback=callbacks.convert_status_codes,
359
+ metavar="",
360
+ envvar="SCHEMATHESIS_EXPERIMENTAL_NEGATIVE_DATA_REJECTION_ALLOWED_STATUSES",
361
+ )
362
+ @group("API validation options")
363
+ @grouped_option(
228
364
  "--checks",
229
365
  "-c",
230
366
  multiple=True,
231
- help="Specifies the validation checks to apply to API responses. "
232
- "Provide a comma-separated list of checks such as 'not_a_server_error,status_code_conformance', etc. "
233
- f"Default is '{','.join(DEFAULT_CHECKS_NAMES)}'.",
367
+ help="Comma-separated list of checks to run against API responses",
234
368
  type=CHECKS_TYPE,
235
369
  default=DEFAULT_CHECKS_NAMES,
236
- cls=GroupedOption,
237
- group=ParameterGroup.validation,
238
370
  callback=callbacks.convert_checks,
239
371
  show_default=True,
372
+ metavar="",
240
373
  )
241
- @click.option(
374
+ @grouped_option(
242
375
  "--exclude-checks",
243
376
  multiple=True,
244
- help="Specifies the validation checks to skip during testing. "
245
- "Provide a comma-separated list of checks you wish to bypass.",
377
+ help="Comma-separated list of checks to skip during testing",
246
378
  type=EXCLUDE_CHECKS_TYPE,
247
379
  default=[],
248
- cls=GroupedOption,
249
- group=ParameterGroup.validation,
250
380
  callback=callbacks.convert_checks,
251
381
  show_default=True,
382
+ metavar="",
252
383
  )
253
- @click.option(
254
- "--data-generation-method",
255
- "-D",
256
- "data_generation_methods",
257
- help="Specifies the approach Schemathesis uses to generate test data. "
258
- "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
259
- "Default is 'positive'.",
260
- type=DATA_GENERATION_METHOD_TYPE,
261
- default=DataGenerationMethod.default().name,
262
- callback=callbacks.convert_data_generation_method,
263
- show_default=True,
264
- )
265
- @click.option(
384
+ @grouped_option(
266
385
  "--max-response-time",
267
- help="Sets a custom time limit for API response times. "
268
- "The test will fail if a response time exceeds this limit. "
269
- "Provide the time in milliseconds.",
386
+ help="Time limit in milliseconds for API response times. "
387
+ "The test will fail if a response time exceeds this limit. ",
270
388
  type=click.IntRange(min=1),
271
- cls=GroupedOption,
272
- group=ParameterGroup.validation,
273
389
  )
274
- @click.option(
275
- "--target",
276
- "-t",
277
- "targets",
278
- multiple=True,
279
- help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
280
- type=TARGETS_TYPE,
281
- default=DEFAULT_TARGETS_NAMES,
282
- show_default=True,
283
- )
284
- @click.option(
390
+ @grouped_option(
285
391
  "-x",
286
392
  "--exitfirst",
287
393
  "exit_first",
288
394
  is_flag=True,
289
395
  default=False,
290
- help="Terminates the test suite immediately upon the first failure or error encountered.",
396
+ help="Terminate the test suite immediately upon the first failure or error encountered",
291
397
  show_default=True,
292
398
  )
293
- @click.option(
399
+ @grouped_option(
294
400
  "--max-failures",
295
401
  "max_failures",
296
402
  type=click.IntRange(min=1),
297
- help="Terminates the test suite after reaching a specified number of failures or errors.",
403
+ help="Terminate the test suite after reaching a specified number of failures or errors",
298
404
  show_default=True,
299
405
  )
300
- @click.option(
301
- "--dry-run",
302
- "dry_run",
303
- is_flag=True,
406
+ @group("Loader options")
407
+ @grouped_option(
408
+ "--app",
409
+ help="Specify the WSGI/ASGI application under test, provided as an importable Python path",
410
+ type=str,
411
+ callback=callbacks.validate_app,
412
+ )
413
+ @grouped_option(
414
+ "--wait-for-schema",
415
+ help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default",
416
+ type=click.FloatRange(1.0),
417
+ default=None,
418
+ envvar=WAIT_FOR_SCHEMA_ENV_VAR,
419
+ )
420
+ @grouped_option(
421
+ "--validate-schema",
422
+ help="Validate input API schema. Set to 'true' to enable or 'false' to disable",
423
+ type=bool,
304
424
  default=False,
305
- help="Simulates test execution without making any actual requests, useful for validating data generation.",
425
+ show_default=True,
306
426
  )
307
- @click.option(
427
+ @group("Network requests options")
428
+ @grouped_option(
429
+ "--base-url",
430
+ "-b",
431
+ help="Base URL of the API, required when schema is provided as a file",
432
+ type=str,
433
+ callback=callbacks.validate_base_url,
434
+ envvar=BASE_URL_ENV_VAR,
435
+ )
436
+ @grouped_option(
437
+ "--request-timeout",
438
+ help="Timeout limit, in milliseconds, for each network request during tests",
439
+ type=click.IntRange(1),
440
+ default=DEFAULT_RESPONSE_TIMEOUT,
441
+ )
442
+ @with_request_proxy
443
+ @with_request_tls_verify
444
+ @with_request_cert
445
+ @with_request_cert_key
446
+ @grouped_option(
447
+ "--rate-limit",
448
+ help="Specify a rate limit for test requests in '<limit>/<duration>' format. "
449
+ "Example - `100/m` for 100 requests per minute",
450
+ type=str,
451
+ callback=callbacks.validate_rate_limit,
452
+ )
453
+ @grouped_option(
454
+ "--header",
455
+ "-H",
456
+ "headers",
457
+ help=r"Add a custom HTTP header to all API requests. Format: 'Header-Name: Value'",
458
+ multiple=True,
459
+ type=str,
460
+ callback=callbacks.validate_headers,
461
+ )
462
+ @grouped_option(
308
463
  "--auth",
309
464
  "-a",
310
- help="Provides the server authentication details in the 'USER:PASSWORD' format.",
465
+ help="Provide the server authentication details in the 'USER:PASSWORD' format",
311
466
  type=str,
312
467
  callback=callbacks.validate_auth,
313
468
  )
314
- @click.option(
469
+ @grouped_option(
315
470
  "--auth-type",
316
471
  "-A",
317
472
  type=click.Choice(["basic", "digest"], case_sensitive=False),
318
473
  default="basic",
319
- help="Specifies the authentication method. Default is 'basic'.",
474
+ help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
320
475
  show_default=True,
476
+ metavar="",
321
477
  )
322
- @click.option(
323
- "--set-query",
324
- "set_query",
325
- help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
326
- multiple=True,
327
- type=str,
328
- callback=callbacks.validate_set_query,
329
- )
330
- @click.option(
331
- "--set-header",
332
- "set_header",
333
- help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
334
- multiple=True,
335
- type=str,
336
- callback=callbacks.validate_set_header,
337
- )
338
- @click.option(
339
- "--set-cookie",
340
- "set_cookie",
341
- help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
342
- multiple=True,
478
+ @group("Filtering options")
479
+ @with_filters
480
+ @grouped_option(
481
+ "--include-by",
482
+ "include_by",
343
483
  type=str,
344
- callback=callbacks.validate_set_cookie,
484
+ help="Include API operations by expression",
345
485
  )
346
- @click.option(
347
- "--set-path",
348
- "set_path",
349
- help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
350
- multiple=True,
486
+ @grouped_option(
487
+ "--exclude-by",
488
+ "exclude_by",
351
489
  type=str,
352
- callback=callbacks.validate_set_path,
490
+ help="Exclude API operations by expression",
353
491
  )
354
- @click.option(
355
- "--header",
356
- "-H",
357
- "headers",
358
- help=r"Adds a custom HTTP header to all API requests. Format: 'Header-Name: Value'.",
359
- multiple=True,
360
- type=str,
361
- callback=callbacks.validate_headers,
492
+ @grouped_option(
493
+ "--exclude-deprecated",
494
+ help="Exclude deprecated API operations from testing",
495
+ is_flag=True,
496
+ is_eager=True,
497
+ default=False,
498
+ show_default=True,
362
499
  )
363
- @click.option(
500
+ @grouped_option(
364
501
  "--endpoint",
365
502
  "-E",
366
503
  "endpoints",
367
504
  type=str,
368
505
  multiple=True,
369
- help=r"API operation path pattern (e.g., users/\d+).",
506
+ help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+)",
370
507
  callback=callbacks.validate_regex,
371
- cls=GroupedOption,
372
- group=ParameterGroup.filtering,
508
+ hidden=True,
373
509
  )
374
- @click.option(
510
+ @grouped_option(
375
511
  "--method",
376
512
  "-M",
377
513
  "methods",
378
514
  type=str,
379
515
  multiple=True,
380
- help="HTTP method (e.g., GET, POST).",
516
+ help="[DEPRECATED] HTTP method (e.g., GET, POST)",
381
517
  callback=callbacks.validate_regex,
382
- cls=GroupedOption,
383
- group=ParameterGroup.filtering,
518
+ hidden=True,
384
519
  )
385
- @click.option(
520
+ @grouped_option(
386
521
  "--tag",
387
522
  "-T",
388
523
  "tags",
389
524
  type=str,
390
525
  multiple=True,
391
- help="Schema tag pattern.",
526
+ help="[DEPRECATED] Schema tag pattern",
392
527
  callback=callbacks.validate_regex,
393
- cls=GroupedOption,
394
- group=ParameterGroup.filtering,
528
+ hidden=True,
395
529
  )
396
- @click.option(
530
+ @grouped_option(
397
531
  "--operation-id",
398
532
  "-O",
399
533
  "operation_ids",
400
534
  type=str,
401
535
  multiple=True,
402
- help="OpenAPI operationId pattern.",
536
+ help="[DEPRECATED] OpenAPI operationId pattern",
403
537
  callback=callbacks.validate_regex,
404
- cls=GroupedOption,
405
- group=ParameterGroup.filtering,
406
- )
407
- @click.option(
408
- "--workers",
409
- "-w",
410
- "workers_num",
411
- help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
412
- type=CustomHelpMessageChoice(
413
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
414
- choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
415
- ),
416
- default=str(DEFAULT_WORKERS),
417
- show_default=True,
418
- callback=callbacks.convert_workers,
419
- )
420
- @click.option(
421
- "--base-url",
422
- "-b",
423
- help="Provides the base URL of the API, required when schema is provided as a file.",
424
- type=str,
425
- callback=callbacks.validate_base_url,
426
- envvar=BASE_URL_ENV_VAR,
427
- )
428
- @click.option(
429
- "--app",
430
- help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
431
- type=str,
432
- callback=callbacks.validate_app,
433
- )
434
- @click.option(
435
- "--wait-for-schema",
436
- help="Maximum duration, in seconds, to wait for the API schema to become available.",
437
- type=click.FloatRange(1.0),
438
- default=None,
439
- envvar=WAIT_FOR_SCHEMA_ENV_VAR,
440
- )
441
- @click.option(
442
- "--request-timeout",
443
- help="Sets a timeout limit, in milliseconds, for each network request during tests.",
444
- type=click.IntRange(1),
445
- default=DEFAULT_RESPONSE_TIMEOUT,
446
- )
447
- @with_request_proxy
448
- @with_request_tls_verify
449
- @with_request_cert
450
- @with_request_cert_key
451
- @click.option(
452
- "--validate-schema",
453
- help="Toggles validation of incoming payloads against the defined API schema. "
454
- "Set to 'True' to enable or 'False' to disable. "
455
- "Default is 'False'.",
456
- type=bool,
457
- default=False,
458
- show_default=True,
459
- cls=GroupedOption,
460
- group=ParameterGroup.validation,
538
+ hidden=True,
461
539
  )
462
- @click.option(
540
+ @grouped_option(
463
541
  "--skip-deprecated-operations",
464
- help="Exclude deprecated API operations from testing.",
542
+ help="[DEPRECATED] Exclude deprecated API operations from testing",
465
543
  is_flag=True,
466
544
  is_eager=True,
467
545
  default=False,
468
546
  show_default=True,
469
- cls=GroupedOption,
470
- group=ParameterGroup.filtering,
547
+ hidden=True,
471
548
  )
472
- @click.option(
549
+ @group("Output options")
550
+ @grouped_option(
473
551
  "--junit-xml",
474
- help="Outputs a JUnit-XML style report at the specified file path.",
552
+ help="Output a JUnit-XML style report at the specified file path",
475
553
  type=click.File("w", encoding="utf-8"),
476
554
  )
477
- @click.option(
478
- "--report",
479
- "report_value",
480
- help="""Specifies how the generated report should be handled.
481
- If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
482
- If a file name is provided, the report will be stored in that file.
483
- The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
484
- is_flag=False,
485
- flag_value="",
486
- envvar=service.REPORT_ENV_VAR,
487
- callback=callbacks.convert_report, # type: ignore
488
- )
489
- @click.option(
490
- "--debug-output-file",
491
- help="Saves debugging information in a JSONL format at the specified file path.",
555
+ @grouped_option(
556
+ "--cassette-path",
557
+ help="Save the test outcomes in a VCR-compatible format",
492
558
  type=click.File("w", encoding="utf-8"),
493
- )
494
- @click.option(
495
- "--show-errors-tracebacks",
496
- help="Displays complete traceback information for internal errors.",
497
- is_flag=True,
498
559
  is_eager=True,
499
- default=False,
500
- hidden=True,
501
- show_default=True,
502
560
  )
503
- @click.option(
504
- "--show-trace",
505
- help="Displays complete traceback information for internal errors.",
561
+ @grouped_option(
562
+ "--cassette-format",
563
+ help="Format of the saved cassettes",
564
+ type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
565
+ default=cassettes.CassetteFormat.VCR.name.lower(),
566
+ callback=callbacks.convert_cassette_format,
567
+ metavar="",
568
+ )
569
+ @grouped_option(
570
+ "--cassette-preserve-exact-body-bytes",
571
+ help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
506
572
  is_flag=True,
507
- is_eager=True,
508
- default=False,
509
- show_default=True,
573
+ callback=callbacks.validate_preserve_exact_body_bytes,
510
574
  )
511
- @click.option(
575
+ @grouped_option(
512
576
  "--code-sample-style",
513
- help="Selects the code sample style for reproducing failures.",
577
+ help="Code sample style for reproducing failures",
514
578
  type=click.Choice([item.name for item in CodeSampleStyle]),
515
579
  default=CodeSampleStyle.default().name,
516
580
  callback=callbacks.convert_code_sample_style,
581
+ metavar="",
517
582
  )
518
- @click.option(
519
- "--cassette-path",
520
- help="Saves the test outcomes in a VCR-compatible format.",
521
- type=click.File("w", encoding="utf-8"),
522
- is_eager=True,
583
+ @grouped_option(
584
+ "--sanitize-output",
585
+ type=bool,
586
+ default=True,
587
+ show_default=True,
588
+ help="Enable or disable automatic output sanitization to obscure sensitive data",
523
589
  )
524
- @click.option(
525
- "--cassette-preserve-exact-body-bytes",
526
- help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
590
+ @grouped_option(
591
+ "--output-truncate",
592
+ help="Truncate schemas and responses in error messages",
593
+ type=str,
594
+ default="true",
595
+ show_default=True,
596
+ callback=callbacks.convert_boolean_string,
597
+ )
598
+ @grouped_option(
599
+ "--show-trace",
600
+ help="Display complete traceback information for internal errors",
527
601
  is_flag=True,
528
- callback=callbacks.validate_preserve_exact_body_bytes,
602
+ is_eager=True,
603
+ default=False,
604
+ show_default=True,
529
605
  )
530
- @click.option(
606
+ @grouped_option(
607
+ "--debug-output-file",
608
+ help="Save debugging information in a JSONL format at the specified file path",
609
+ type=click.File("w", encoding="utf-8"),
610
+ )
611
+ @grouped_option(
531
612
  "--store-network-log",
532
- help="Saves the test outcomes in a VCR-compatible format.",
613
+ help="[DEPRECATED] Save the test outcomes in a VCR-compatible format",
533
614
  type=click.File("w", encoding="utf-8"),
534
615
  hidden=True,
535
616
  )
536
- @click.option(
537
- "--fixups",
538
- help="Applies compatibility adjustments like 'fast_api', 'utf8_bom'.",
539
- multiple=True,
540
- type=click.Choice(list(ALL_FIXUPS) + ["all"]),
617
+ @grouped_option(
618
+ "--show-errors-tracebacks",
619
+ help="[DEPRECATED] Display complete traceback information for internal errors",
620
+ is_flag=True,
621
+ is_eager=True,
622
+ default=False,
623
+ hidden=True,
624
+ show_default=True,
541
625
  )
542
- @click.option(
543
- "--rate-limit",
544
- help="Specifies a rate limit for test requests in '<limit>/<duration>' format. "
545
- "Example - `100/m` for 100 requests per minute.",
546
- type=str,
547
- callback=callbacks.validate_rate_limit,
626
+ @group("Data generation options")
627
+ @grouped_option(
628
+ "--data-generation-method",
629
+ "-D",
630
+ "data_generation_methods",
631
+ help="Specify the approach Schemathesis uses to generate test data. "
632
+ "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both",
633
+ type=DATA_GENERATION_METHOD_TYPE,
634
+ default=DataGenerationMethod.default().name,
635
+ callback=callbacks.convert_data_generation_method,
636
+ show_default=True,
637
+ metavar="",
548
638
  )
549
- @click.option(
639
+ @grouped_option(
550
640
  "--stateful",
551
- help="Enables or disables stateful testing features.",
641
+ help="Enable or disable stateful testing",
552
642
  type=click.Choice([item.name for item in Stateful]),
553
643
  default=Stateful.links.name,
554
644
  callback=callbacks.convert_stateful,
645
+ metavar="",
555
646
  )
556
- @click.option(
647
+ @grouped_option(
557
648
  "--stateful-recursion-limit",
558
- help="Sets the recursion depth limit for stateful testing.",
649
+ help="Recursion depth limit for stateful testing",
559
650
  default=DEFAULT_STATEFUL_RECURSION_LIMIT,
560
651
  show_default=True,
561
652
  type=click.IntRange(1, 100),
562
653
  hidden=True,
563
654
  )
564
- @click.option(
565
- "--force-schema-version",
566
- help="Forces the schema to be interpreted as a particular OpenAPI version.",
567
- type=click.Choice(["20", "30"]),
655
+ @grouped_option(
656
+ "--generation-allow-x00",
657
+ help="Whether to allow the generation of `\x00` bytes within strings",
658
+ type=str,
659
+ default="true",
660
+ show_default=True,
661
+ callback=callbacks.convert_boolean_string,
568
662
  )
569
- @click.option(
570
- "--sanitize-output",
571
- type=bool,
572
- default=True,
663
+ @grouped_option(
664
+ "--generation-codec",
665
+ help="The codec used for generating strings",
666
+ type=str,
667
+ default="utf-8",
668
+ callback=callbacks.validate_generation_codec,
669
+ )
670
+ @grouped_option(
671
+ "--generation-with-security-parameters",
672
+ help="Whether to generate security parameters",
673
+ type=str,
674
+ default="true",
573
675
  show_default=True,
574
- help="Enable or disable automatic output sanitization to obscure sensitive data.",
676
+ callback=callbacks.convert_boolean_string,
575
677
  )
576
- @click.option(
678
+ @grouped_option(
679
+ "--generation-graphql-allow-null",
680
+ help="Whether to use `null` values for optional arguments in GraphQL queries",
681
+ type=str,
682
+ default="true",
683
+ show_default=True,
684
+ callback=callbacks.convert_boolean_string,
685
+ )
686
+ @grouped_option(
577
687
  "--contrib-unique-data",
578
688
  "contrib_unique_data",
579
- help="Forces the generation of unique test cases.",
689
+ help="Force the generation of unique test cases",
580
690
  is_flag=True,
581
691
  default=False,
582
692
  show_default=True,
583
693
  )
584
- @click.option(
694
+ @grouped_option(
585
695
  "--contrib-openapi-formats-uuid",
586
696
  "contrib_openapi_formats_uuid",
587
- help="Enables support for the 'uuid' string format in OpenAPI.",
697
+ help="Enable support for the 'uuid' string format in OpenAPI",
588
698
  is_flag=True,
589
699
  default=False,
590
700
  show_default=True,
591
701
  )
592
- @click.option(
702
+ @grouped_option(
593
703
  "--contrib-openapi-fill-missing-examples",
594
704
  "contrib_openapi_fill_missing_examples",
595
- help="Enables generation of random examples for API operations that do not have explicit examples defined.",
705
+ help="Enable generation of random examples for API operations that do not have explicit examples",
596
706
  is_flag=True,
597
707
  default=False,
598
708
  show_default=True,
599
709
  )
600
- @click.option(
710
+ @grouped_option(
711
+ "--target",
712
+ "-t",
713
+ "targets",
714
+ multiple=True,
715
+ help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
716
+ type=TARGETS_TYPE,
717
+ default=DEFAULT_TARGETS_NAMES,
718
+ show_default=True,
719
+ metavar="",
720
+ )
721
+ @group("Open API options")
722
+ @grouped_option(
723
+ "--force-schema-version",
724
+ help="Force the schema to be interpreted as a particular OpenAPI version",
725
+ type=click.Choice(["20", "30"]),
726
+ metavar="",
727
+ )
728
+ @grouped_option(
729
+ "--set-query",
730
+ "set_query",
731
+ help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
732
+ multiple=True,
733
+ type=str,
734
+ callback=callbacks.validate_set_query,
735
+ )
736
+ @grouped_option(
737
+ "--set-header",
738
+ "set_header",
739
+ help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
740
+ multiple=True,
741
+ type=str,
742
+ callback=callbacks.validate_set_header,
743
+ )
744
+ @grouped_option(
745
+ "--set-cookie",
746
+ "set_cookie",
747
+ help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
748
+ multiple=True,
749
+ type=str,
750
+ callback=callbacks.validate_set_cookie,
751
+ )
752
+ @grouped_option(
753
+ "--set-path",
754
+ "set_path",
755
+ help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
756
+ multiple=True,
757
+ type=str,
758
+ callback=callbacks.validate_set_path,
759
+ )
760
+ @group("Hypothesis engine options")
761
+ @grouped_option(
601
762
  "--hypothesis-database",
602
- help="Configures storage for examples discovered by Hypothesis. "
763
+ help="Storage for examples discovered by Hypothesis. "
603
764
  f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
604
- f"or specify a file path for persistent storage.",
765
+ f"or specify a file path for persistent storage",
605
766
  type=str,
606
- cls=GroupedOption,
607
- group=ParameterGroup.hypothesis,
608
767
  callback=callbacks.validate_hypothesis_database,
609
768
  )
610
- @click.option(
769
+ @grouped_option(
611
770
  "--hypothesis-deadline",
612
- help="Sets a time limit for each test case generated by Hypothesis, in milliseconds. "
613
- "Exceeding this limit will cause the test to fail.",
614
- # max value to avoid overflow. It is the maximum amount of days in milliseconds
615
- type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
616
- cls=GroupedOption,
617
- group=ParameterGroup.hypothesis,
771
+ help="Time limit for each test case generated by Hypothesis, in milliseconds. "
772
+ "Exceeding this limit will cause the test to fail",
773
+ type=OptionalInt(1, 5 * 60 * 1000),
618
774
  )
619
- @click.option(
775
+ @grouped_option(
620
776
  "--hypothesis-derandomize",
621
- help="Enables deterministic mode in Hypothesis, which eliminates random variation between test runs.",
777
+ help="Enables deterministic mode in Hypothesis, which eliminates random variation between tests",
622
778
  is_flag=True,
623
779
  is_eager=True,
624
780
  default=None,
625
781
  show_default=True,
626
- cls=GroupedOption,
627
- group=ParameterGroup.hypothesis,
628
782
  )
629
- @click.option(
783
+ @grouped_option(
630
784
  "--hypothesis-max-examples",
631
- help="Sets the cap on the number of examples generated by Hypothesis for each API method/path pair.",
785
+ help="The cap on the number of examples generated by Hypothesis for each API operation",
632
786
  type=click.IntRange(1),
633
- cls=GroupedOption,
634
- group=ParameterGroup.hypothesis,
635
787
  )
636
- @click.option(
788
+ @grouped_option(
637
789
  "--hypothesis-phases",
638
- help="Specifies which testing phases to execute.",
790
+ help="Testing phases to execute",
639
791
  type=CsvEnumChoice(Phase),
640
- cls=GroupedOption,
641
- group=ParameterGroup.hypothesis,
792
+ metavar="",
642
793
  )
643
- @click.option(
794
+ @grouped_option(
644
795
  "--hypothesis-no-phases",
645
- help="Specifies which testing phases to exclude from execution.",
796
+ help="Testing phases to exclude from execution",
646
797
  type=CsvEnumChoice(Phase),
647
- cls=GroupedOption,
648
- group=ParameterGroup.hypothesis,
798
+ metavar="",
649
799
  )
650
- @click.option(
800
+ @grouped_option(
651
801
  "--hypothesis-report-multiple-bugs",
652
- help="If set, only the most easily reproducible exception will be reported when multiple issues are found.",
802
+ help="Report only the most easily reproducible error when multiple issues are found",
653
803
  type=bool,
654
- cls=GroupedOption,
655
- group=ParameterGroup.hypothesis,
656
804
  )
657
- @click.option(
805
+ @grouped_option(
658
806
  "--hypothesis-seed",
659
- help="Sets a seed value for Hypothesis, ensuring reproducibility across test runs.",
807
+ help="Seed value for Hypothesis, ensuring reproducibility across test runs",
660
808
  type=int,
661
- cls=GroupedOption,
662
- group=ParameterGroup.hypothesis,
663
809
  )
664
- @click.option(
810
+ @grouped_option(
665
811
  "--hypothesis-suppress-health-check",
666
- help="Disables specified health checks from Hypothesis like 'data_too_large', 'filter_too_much', etc. "
667
- "Provide a comma-separated list",
812
+ help="A comma-separated list of Hypothesis health checks to disable",
668
813
  type=CsvEnumChoice(HealthCheck),
669
- cls=GroupedOption,
670
- group=ParameterGroup.hypothesis,
814
+ metavar="",
671
815
  )
672
- @click.option(
816
+ @grouped_option(
673
817
  "--hypothesis-verbosity",
674
- help="Controls the verbosity level of Hypothesis output.",
818
+ help="Verbosity level of Hypothesis output",
675
819
  type=click.Choice([item.name for item in Verbosity]),
676
820
  callback=callbacks.convert_verbosity,
677
- cls=GroupedOption,
678
- group=ParameterGroup.hypothesis,
821
+ metavar="",
679
822
  )
680
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
681
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
682
- @click.option(
683
- "--experimental",
684
- help="Enable experimental support for specific features.",
685
- type=click.Choice([experimental.OPEN_API_3_1.name]),
686
- callback=callbacks.convert_experimental,
687
- multiple=True,
688
- )
689
- @click.option(
690
- "--generation-allow-x00",
691
- help="Determines whether to allow the generation of `\x00` bytes within strings.",
692
- type=str,
693
- default="true",
694
- show_default=True,
695
- callback=callbacks.convert_boolean_string,
696
- )
697
- @click.option(
698
- "--generation-codec",
699
- help="Specifies the codec used for generating strings.",
700
- type=str,
701
- default="utf-8",
702
- callback=callbacks.validate_generation_codec,
823
+ @group("Schemathesis.io options")
824
+ @grouped_option(
825
+ "--report",
826
+ "report_value",
827
+ help="""Specify how the generated report should be handled.
828
+ If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
829
+ If a file name is provided, the report will be stored in that file.
830
+ The report data, consisting of a tar gz file with multiple JSON files, is subject to change""",
831
+ is_flag=False,
832
+ flag_value="",
833
+ envvar=service.REPORT_ENV_VAR,
834
+ callback=callbacks.convert_report, # type: ignore
703
835
  )
704
- @click.option(
836
+ @grouped_option(
705
837
  "--schemathesis-io-token",
706
- help="Schemathesis.io authentication token.",
838
+ help="Schemathesis.io authentication token",
707
839
  type=str,
708
840
  envvar=service.TOKEN_ENV_VAR,
709
841
  )
710
- @click.option(
842
+ @grouped_option(
711
843
  "--schemathesis-io-url",
712
- help="Schemathesis.io base URL.",
844
+ help="Schemathesis.io base URL",
713
845
  default=service.DEFAULT_URL,
714
846
  type=str,
715
847
  envvar=service.URL_ENV_VAR,
716
848
  )
717
- @click.option(
849
+ @grouped_option(
718
850
  "--schemathesis-io-telemetry",
719
- help="Controls whether you send anonymized CLI usage data to Schemathesis.io along with your report.",
851
+ help="Whether to send anonymized usage data to Schemathesis.io along with your report",
720
852
  type=str,
721
853
  default="true",
722
854
  show_default=True,
@@ -724,7 +856,10 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
724
856
  envvar=service.TELEMETRY_ENV_VAR,
725
857
  )
726
858
  @with_hosts_file
727
- @click.option("--verbosity", "-v", help="Increase verbosity of the output.", count=True)
859
+ @group("Global options")
860
+ @grouped_option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
861
+ @grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
862
+ @grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
728
863
  @click.pass_context
729
864
  def run(
730
865
  ctx: click.Context,
@@ -737,7 +872,11 @@ def run(
737
872
  set_header: dict[str, str],
738
873
  set_cookie: dict[str, str],
739
874
  set_path: dict[str, str],
740
- experimental: list,
875
+ experiments: list,
876
+ no_failfast: bool,
877
+ missing_required_header_allowed_statuses: list[str],
878
+ positive_data_acceptance_allowed_statuses: list[str],
879
+ negative_data_rejection_allowed_statuses: list[str],
741
880
  checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
742
881
  exclude_checks: Iterable[str] = (),
743
882
  data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
@@ -746,10 +885,33 @@ def run(
746
885
  exit_first: bool = False,
747
886
  max_failures: int | None = None,
748
887
  dry_run: bool = False,
749
- endpoints: Filter | None = None,
750
- methods: Filter | None = None,
751
- tags: Filter | None = None,
752
- operation_ids: Filter | None = None,
888
+ include_path: Sequence[str] = (),
889
+ include_path_regex: str | None = None,
890
+ include_method: Sequence[str] = (),
891
+ include_method_regex: str | None = None,
892
+ include_name: Sequence[str] = (),
893
+ include_name_regex: str | None = None,
894
+ include_tag: Sequence[str] = (),
895
+ include_tag_regex: str | None = None,
896
+ include_operation_id: Sequence[str] = (),
897
+ include_operation_id_regex: str | None = None,
898
+ exclude_path: Sequence[str] = (),
899
+ exclude_path_regex: str | None = None,
900
+ exclude_method: Sequence[str] = (),
901
+ exclude_method_regex: str | None = None,
902
+ exclude_name: Sequence[str] = (),
903
+ exclude_name_regex: str | None = None,
904
+ exclude_tag: Sequence[str] = (),
905
+ exclude_tag_regex: str | None = None,
906
+ exclude_operation_id: Sequence[str] = (),
907
+ exclude_operation_id_regex: str | None = None,
908
+ include_by: str | None = None,
909
+ exclude_by: str | None = None,
910
+ exclude_deprecated: bool = False,
911
+ endpoints: tuple[str, ...] = (),
912
+ methods: tuple[str, ...] = (),
913
+ tags: tuple[str, ...] = (),
914
+ operation_ids: tuple[str, ...] = (),
753
915
  workers_num: int = DEFAULT_WORKERS,
754
916
  base_url: str | None = None,
755
917
  app: str | None = None,
@@ -766,6 +928,7 @@ def run(
766
928
  show_trace: bool = False,
767
929
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
768
930
  cassette_path: click.utils.LazyFile | None = None,
931
+ cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
769
932
  cassette_preserve_exact_body_bytes: bool = False,
770
933
  store_network_log: click.utils.LazyFile | None = None,
771
934
  wait_for_schema: float | None = None,
@@ -775,6 +938,7 @@ def run(
775
938
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
776
939
  force_schema_version: str | None = None,
777
940
  sanitize_output: bool = True,
941
+ output_truncate: bool = True,
778
942
  contrib_unique_data: bool = False,
779
943
  contrib_openapi_formats_uuid: bool = False,
780
944
  contrib_openapi_fill_missing_examples: bool = False,
@@ -792,18 +956,21 @@ def run(
792
956
  no_color: bool = False,
793
957
  report_value: str | None = None,
794
958
  generation_allow_x00: bool = True,
959
+ generation_graphql_allow_null: bool = True,
960
+ generation_with_security_parameters: bool = True,
795
961
  generation_codec: str = "utf-8",
796
962
  schemathesis_io_token: str | None = None,
797
963
  schemathesis_io_url: str = service.DEFAULT_URL,
798
964
  schemathesis_io_telemetry: bool = True,
799
965
  hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
800
966
  force_color: bool = False,
967
+ **__kwargs,
801
968
  ) -> None:
802
969
  """Run tests against an API using a specified SCHEMA.
803
970
 
804
- [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications.
971
+ [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
805
972
 
806
- [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io.
973
+ [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
807
974
  """
808
975
  _hypothesis_phases: list[hypothesis.Phase] | None = None
809
976
  if hypothesis_phases is not None:
@@ -815,23 +982,25 @@ def run(
815
982
  _hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
816
983
  if hypothesis_suppress_health_check is not None:
817
984
  _hypothesis_suppress_health_check = [
818
- health_check.as_hypothesis() for health_check in hypothesis_suppress_health_check
985
+ entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
819
986
  ]
820
987
 
821
- if contrib_unique_data:
822
- click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
823
-
824
988
  if show_errors_tracebacks:
825
989
  click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
826
990
  show_trace = show_errors_tracebacks
827
991
 
828
992
  # Enable selected experiments
829
- for experiment in experimental:
993
+ for experiment in experiments:
830
994
  experiment.enable()
831
995
 
832
996
  override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
833
997
 
834
- generation_config = generation.GenerationConfig(allow_x00=generation_allow_x00, codec=generation_codec)
998
+ generation_config = generation.GenerationConfig(
999
+ allow_x00=generation_allow_x00,
1000
+ graphql_allow_null=generation_graphql_allow_null,
1001
+ codec=generation_codec,
1002
+ with_security_parameters=generation_with_security_parameters,
1003
+ )
835
1004
 
836
1005
  report: ReportToService | click.utils.LazyFile | None
837
1006
  if report_value is None:
@@ -855,6 +1024,109 @@ def run(
855
1024
  click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
856
1025
  cassette_path = store_network_log
857
1026
 
1027
+ output_config = OutputConfig(truncate=output_truncate)
1028
+
1029
+ deprecated_filters = {
1030
+ "--method": "--include-method",
1031
+ "--endpoint": "--include-path",
1032
+ "--tag": "--include-tag",
1033
+ "--operation-id": "--include-operation-id",
1034
+ }
1035
+ for values, arg_name in (
1036
+ (include_path, "--include-path"),
1037
+ (include_method, "--include-method"),
1038
+ (include_name, "--include-name"),
1039
+ (include_tag, "--include-tag"),
1040
+ (include_operation_id, "--include-operation-id"),
1041
+ (exclude_path, "--exclude-path"),
1042
+ (exclude_method, "--exclude-method"),
1043
+ (exclude_name, "--exclude-name"),
1044
+ (exclude_tag, "--exclude-tag"),
1045
+ (exclude_operation_id, "--exclude-operation-id"),
1046
+ (methods, "--method"),
1047
+ (endpoints, "--endpoint"),
1048
+ (tags, "--tag"),
1049
+ (operation_ids, "--operation-id"),
1050
+ ):
1051
+ if values and arg_name in deprecated_filters:
1052
+ replacement = deprecated_filters[arg_name]
1053
+ click.secho(
1054
+ f"Warning: Option `{arg_name}` is deprecated and will be removed in Schemathesis 4.0. "
1055
+ f"Use `{replacement}` instead",
1056
+ fg="yellow",
1057
+ )
1058
+ _ensure_unique_filter(values, arg_name)
1059
+ include_by_function = _filter_by_expression_to_func(include_by, "--include-by")
1060
+ exclude_by_function = _filter_by_expression_to_func(exclude_by, "--exclude-by")
1061
+
1062
+ filter_set = FilterSet()
1063
+ if include_by_function:
1064
+ filter_set.include(include_by_function)
1065
+ for name_ in include_name:
1066
+ filter_set.include(name=name_)
1067
+ for method in include_method:
1068
+ filter_set.include(method=method)
1069
+ if methods:
1070
+ for method in methods:
1071
+ filter_set.include(method_regex=method)
1072
+ for path in include_path:
1073
+ filter_set.include(path=path)
1074
+ if endpoints:
1075
+ for endpoint in endpoints:
1076
+ filter_set.include(path_regex=endpoint)
1077
+ for tag in include_tag:
1078
+ filter_set.include(tag=tag)
1079
+ if tags:
1080
+ for tag in tags:
1081
+ filter_set.include(tag_regex=tag)
1082
+ for operation_id in include_operation_id:
1083
+ filter_set.include(operation_id=operation_id)
1084
+ if operation_ids:
1085
+ for operation_id in operation_ids:
1086
+ filter_set.include(operation_id_regex=operation_id)
1087
+ if (
1088
+ include_name_regex
1089
+ or include_method_regex
1090
+ or include_path_regex
1091
+ or include_tag_regex
1092
+ or include_operation_id_regex
1093
+ ):
1094
+ filter_set.include(
1095
+ name_regex=include_name_regex,
1096
+ method_regex=include_method_regex,
1097
+ path_regex=include_path_regex,
1098
+ tag_regex=include_tag_regex,
1099
+ operation_id_regex=include_operation_id_regex,
1100
+ )
1101
+ if exclude_by_function:
1102
+ filter_set.exclude(exclude_by_function)
1103
+ for name_ in exclude_name:
1104
+ filter_set.exclude(name=name_)
1105
+ for method in exclude_method:
1106
+ filter_set.exclude(method=method)
1107
+ for path in exclude_path:
1108
+ filter_set.exclude(path=path)
1109
+ for tag in exclude_tag:
1110
+ filter_set.exclude(tag=tag)
1111
+ for operation_id in exclude_operation_id:
1112
+ filter_set.exclude(operation_id=operation_id)
1113
+ if (
1114
+ exclude_name_regex
1115
+ or exclude_method_regex
1116
+ or exclude_path_regex
1117
+ or exclude_tag_regex
1118
+ or exclude_operation_id_regex
1119
+ ):
1120
+ filter_set.exclude(
1121
+ name_regex=exclude_name_regex,
1122
+ method_regex=exclude_method_regex,
1123
+ path_regex=exclude_path_regex,
1124
+ tag_regex=exclude_tag_regex,
1125
+ operation_id_regex=exclude_operation_id_regex,
1126
+ )
1127
+ if exclude_deprecated or skip_deprecated_operations:
1128
+ filter_set.exclude(is_deprecated)
1129
+
858
1130
  schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
859
1131
  token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
860
1132
  schema_kind = callbacks.parse_schema_kind(schema, app)
@@ -901,6 +1173,10 @@ def run(
901
1173
  from ..service.client import ServiceClient
902
1174
 
903
1175
  # Upload without connecting data to a certain API
1176
+ client = ServiceClient(base_url=schemathesis_io_url, token=token)
1177
+ if experimental.SCHEMA_ANALYSIS.is_enabled and not client:
1178
+ from ..service.client import ServiceClient
1179
+
904
1180
  client = ServiceClient(base_url=schemathesis_io_url, token=token)
905
1181
  host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
906
1182
 
@@ -909,6 +1185,25 @@ def run(
909
1185
  else:
910
1186
  selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
911
1187
 
1188
+ checks_config = CheckConfig()
1189
+ if experimental.POSITIVE_DATA_ACCEPTANCE.is_enabled:
1190
+ from ..specs.openapi.checks import positive_data_acceptance
1191
+
1192
+ selected_checks += (positive_data_acceptance,)
1193
+ if positive_data_acceptance_allowed_statuses:
1194
+ checks_config.positive_data_acceptance.allowed_statuses = positive_data_acceptance_allowed_statuses
1195
+ if missing_required_header_allowed_statuses:
1196
+ from ..specs.openapi.checks import missing_required_header
1197
+
1198
+ selected_checks += (missing_required_header,)
1199
+ checks_config.missing_required_header.allowed_statuses = missing_required_header_allowed_statuses
1200
+ if negative_data_rejection_allowed_statuses:
1201
+ checks_config.negative_data_rejection.allowed_statuses = negative_data_rejection_allowed_statuses
1202
+ if experimental.COVERAGE_PHASE.is_enabled:
1203
+ from ..specs.openapi.checks import unsupported_method
1204
+
1205
+ selected_checks += (unsupported_method,)
1206
+
912
1207
  selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
913
1208
 
914
1209
  if fixups:
@@ -917,8 +1212,6 @@ def run(
917
1212
  else:
918
1213
  _fixups.install(fixups)
919
1214
 
920
- if contrib_unique_data:
921
- contrib.unique_data.install()
922
1215
  if contrib_openapi_formats_uuid:
923
1216
  contrib.openapi.formats.uuid.install()
924
1217
  if contrib_openapi_fill_missing_examples:
@@ -940,7 +1233,6 @@ def run(
940
1233
  base_url=base_url,
941
1234
  started_at=started_at,
942
1235
  validate_schema=validate_schema,
943
- skip_deprecated_operations=skip_deprecated_operations,
944
1236
  data_generation_methods=data_generation_methods,
945
1237
  force_schema_version=force_schema_version,
946
1238
  request_tls_verify=request_tls_verify,
@@ -951,14 +1243,12 @@ def run(
951
1243
  auth_type=auth_type,
952
1244
  override=override,
953
1245
  headers=headers,
954
- endpoint=endpoints or None,
955
- method=methods or None,
956
- tag=tags or None,
957
- operation_id=operation_ids or None,
958
1246
  request_timeout=request_timeout,
959
1247
  seed=hypothesis_seed,
960
1248
  exit_first=exit_first,
1249
+ no_failfast=no_failfast,
961
1250
  max_failures=max_failures,
1251
+ unique_data=contrib_unique_data,
962
1252
  dry_run=dry_run,
963
1253
  store_interactions=cassette_path is not None,
964
1254
  checks=selected_checks,
@@ -970,9 +1260,14 @@ def run(
970
1260
  stateful_recursion_limit=stateful_recursion_limit,
971
1261
  hypothesis_settings=hypothesis_settings,
972
1262
  generation_config=generation_config,
1263
+ checks_config=checks_config,
1264
+ output_config=output_config,
1265
+ service_client=client,
1266
+ filter_set=filter_set,
973
1267
  )
974
1268
  execute(
975
1269
  event_stream,
1270
+ ctx=ctx,
976
1271
  hypothesis_settings=hypothesis_settings,
977
1272
  workers_num=workers_num,
978
1273
  rate_limit=rate_limit,
@@ -980,6 +1275,7 @@ def run(
980
1275
  wait_for_schema=wait_for_schema,
981
1276
  validate_schema=validate_schema,
982
1277
  cassette_path=cassette_path,
1278
+ cassette_format=cassette_format,
983
1279
  cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
984
1280
  junit_xml=junit_xml,
985
1281
  verbosity=verbosity,
@@ -995,9 +1291,25 @@ def run(
995
1291
  location=schema,
996
1292
  base_url=base_url,
997
1293
  started_at=started_at,
1294
+ output_config=output_config,
998
1295
  )
999
1296
 
1000
1297
 
1298
+ def _ensure_unique_filter(values: Sequence[str], arg_name: str) -> None:
1299
+ if len(values) != len(set(values)):
1300
+ duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
1301
+ raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
1302
+
1303
+
1304
+ def _filter_by_expression_to_func(value: str | None, arg_name: str) -> Callable | None:
1305
+ if value:
1306
+ try:
1307
+ return expression_to_filter_function(value)
1308
+ except ValueError:
1309
+ raise click.UsageError(f"Invalid expression for {arg_name}: {value}") from None
1310
+ return None
1311
+
1312
+
1001
1313
  def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
1002
1314
  if cert is not None and key is not None:
1003
1315
  return cert, key
@@ -1015,7 +1327,6 @@ class LoaderConfig:
1015
1327
  app: Any
1016
1328
  base_url: str | None
1017
1329
  validate_schema: bool
1018
- skip_deprecated_operations: bool
1019
1330
  data_generation_methods: tuple[DataGenerationMethod, ...]
1020
1331
  force_schema_version: str | None
1021
1332
  request_tls_verify: bool | str
@@ -1023,15 +1334,12 @@ class LoaderConfig:
1023
1334
  request_cert: RequestCert | None
1024
1335
  wait_for_schema: float | None
1025
1336
  rate_limit: str | None
1337
+ output_config: OutputConfig
1338
+ generation_config: generation.GenerationConfig
1026
1339
  # Network request parameters
1027
1340
  auth: tuple[str, str] | None
1028
1341
  auth_type: str | None
1029
1342
  headers: dict[str, str] | None
1030
- # Schema filters
1031
- endpoint: Filter | None
1032
- method: Filter | None
1033
- tag: Filter | None
1034
- operation_id: Filter | None
1035
1343
 
1036
1344
 
1037
1345
  def into_event_stream(
@@ -1041,7 +1349,6 @@ def into_event_stream(
1041
1349
  base_url: str | None,
1042
1350
  started_at: str,
1043
1351
  validate_schema: bool,
1044
- skip_deprecated_operations: bool,
1045
1352
  data_generation_methods: tuple[DataGenerationMethod, ...],
1046
1353
  force_schema_version: str | None,
1047
1354
  request_tls_verify: bool | str,
@@ -1054,26 +1361,27 @@ def into_event_stream(
1054
1361
  headers: dict[str, str] | None,
1055
1362
  request_timeout: int | None,
1056
1363
  wait_for_schema: float | None,
1057
- # Schema filters
1058
- endpoint: Filter | None,
1059
- method: Filter | None,
1060
- tag: Filter | None,
1061
- operation_id: Filter | None,
1364
+ filter_set: FilterSet,
1062
1365
  # Runtime behavior
1063
1366
  checks: Iterable[CheckFunction],
1367
+ checks_config: CheckConfig,
1064
1368
  max_response_time: int | None,
1065
1369
  targets: Iterable[Target],
1066
1370
  workers_num: int,
1067
1371
  hypothesis_settings: hypothesis.settings | None,
1068
1372
  generation_config: generation.GenerationConfig,
1373
+ output_config: OutputConfig,
1069
1374
  seed: int | None,
1070
1375
  exit_first: bool,
1376
+ no_failfast: bool,
1071
1377
  max_failures: int | None,
1072
1378
  rate_limit: str | None,
1379
+ unique_data: bool,
1073
1380
  dry_run: bool,
1074
1381
  store_interactions: bool,
1075
1382
  stateful: Stateful | None,
1076
1383
  stateful_recursion_limit: int,
1384
+ service_client: ServiceClient | None,
1077
1385
  ) -> Generator[events.ExecutionEvent, None, None]:
1078
1386
  try:
1079
1387
  if app is not None:
@@ -1083,7 +1391,6 @@ def into_event_stream(
1083
1391
  app=app,
1084
1392
  base_url=base_url,
1085
1393
  validate_schema=validate_schema,
1086
- skip_deprecated_operations=skip_deprecated_operations,
1087
1394
  data_generation_methods=data_generation_methods,
1088
1395
  force_schema_version=force_schema_version,
1089
1396
  request_proxy=request_proxy,
@@ -1094,12 +1401,11 @@ def into_event_stream(
1094
1401
  auth=auth,
1095
1402
  auth_type=auth_type,
1096
1403
  headers=headers,
1097
- endpoint=endpoint or None,
1098
- method=method or None,
1099
- tag=tag or None,
1100
- operation_id=operation_id or None,
1404
+ output_config=output_config,
1405
+ generation_config=generation_config,
1101
1406
  )
1102
1407
  schema = load_schema(config)
1408
+ schema.filter_set = filter_set
1103
1409
  yield from runner.from_schema(
1104
1410
  schema,
1105
1411
  auth=auth,
@@ -1112,11 +1418,14 @@ def into_event_stream(
1112
1418
  request_cert=request_cert,
1113
1419
  seed=seed,
1114
1420
  exit_first=exit_first,
1421
+ no_failfast=no_failfast,
1115
1422
  max_failures=max_failures,
1116
1423
  started_at=started_at,
1424
+ unique_data=unique_data,
1117
1425
  dry_run=dry_run,
1118
1426
  store_interactions=store_interactions,
1119
1427
  checks=checks,
1428
+ checks_config=checks_config,
1120
1429
  max_response_time=max_response_time,
1121
1430
  targets=targets,
1122
1431
  workers_num=workers_num,
@@ -1126,13 +1435,17 @@ def into_event_stream(
1126
1435
  generation_config=generation_config,
1127
1436
  probe_config=probes.ProbeConfig(
1128
1437
  base_url=config.base_url,
1129
- request_tls_verify=config.request_tls_verify,
1130
- request_proxy=config.request_proxy,
1131
- request_cert=config.request_cert,
1438
+ request=RequestConfig(
1439
+ timeout=request_timeout,
1440
+ tls_verify=config.request_tls_verify,
1441
+ proxy=config.request_proxy,
1442
+ cert=config.request_cert,
1443
+ ),
1132
1444
  auth=config.auth,
1133
1445
  auth_type=config.auth_type,
1134
1446
  headers=config.headers,
1135
1447
  ),
1448
+ service_client=service_client,
1136
1449
  ).execute()
1137
1450
  except SchemaError as error:
1138
1451
  yield events.InternalError.from_schema_error(error)
@@ -1233,15 +1546,12 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
1233
1546
  kwargs = {
1234
1547
  "app": config.app,
1235
1548
  "base_url": config.base_url,
1236
- "method": config.method,
1237
- "endpoint": config.endpoint,
1238
- "tag": config.tag,
1239
- "operation_id": config.operation_id,
1240
- "skip_deprecated_operations": config.skip_deprecated_operations,
1241
1549
  "validate_schema": config.validate_schema,
1242
1550
  "force_schema_version": config.force_schema_version,
1243
1551
  "data_generation_methods": config.data_generation_methods,
1244
1552
  "rate_limit": config.rate_limit,
1553
+ "output_config": config.output_config,
1554
+ "generation_config": config.generation_config,
1245
1555
  }
1246
1556
  if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
1247
1557
  kwargs["headers"] = config.headers
@@ -1347,6 +1657,7 @@ class OutputStyle(Enum):
1347
1657
  def execute(
1348
1658
  event_stream: Generator[events.ExecutionEvent, None, None],
1349
1659
  *,
1660
+ ctx: click.Context,
1350
1661
  hypothesis_settings: hypothesis.settings,
1351
1662
  workers_num: int,
1352
1663
  rate_limit: str | None,
@@ -1354,6 +1665,7 @@ def execute(
1354
1665
  wait_for_schema: float | None,
1355
1666
  validate_schema: bool,
1356
1667
  cassette_path: click.utils.LazyFile | None,
1668
+ cassette_format: cassettes.CassetteFormat,
1357
1669
  cassette_preserve_exact_body_bytes: bool,
1358
1670
  junit_xml: click.utils.LazyFile | None,
1359
1671
  verbosity: int,
@@ -1369,6 +1681,7 @@ def execute(
1369
1681
  location: str,
1370
1682
  base_url: str | None,
1371
1683
  started_at: str,
1684
+ output_config: OutputConfig,
1372
1685
  ) -> None:
1373
1686
  """Execute a prepared runner by drawing events from it and passing to a proper handler."""
1374
1687
  handlers: list[EventHandler] = []
@@ -1415,8 +1728,12 @@ def execute(
1415
1728
  # This handler should be first to have logs writing completed when the output handler will display statistic
1416
1729
  _open_file(cassette_path)
1417
1730
  handlers.append(
1418
- cassettes.CassetteWriter(cassette_path, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes)
1731
+ cassettes.CassetteWriter(
1732
+ cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
1733
+ )
1419
1734
  )
1735
+ for custom_handler in CUSTOM_HANDLERS:
1736
+ handlers.append(custom_handler(*ctx.args, **ctx.params))
1420
1737
  handlers.append(get_output_handler(workers_num))
1421
1738
  if sanitize_output:
1422
1739
  handlers.insert(0, SanitizationHandler())
@@ -1432,6 +1749,7 @@ def execute(
1432
1749
  verbosity=verbosity,
1433
1750
  code_sample_style=code_sample_style,
1434
1751
  report=report_context,
1752
+ output_config=output_config,
1435
1753
  )
1436
1754
 
1437
1755
  def shutdown() -> None:
@@ -1545,13 +1863,13 @@ def get_exit_code(event: events.ExecutionEvent) -> int:
1545
1863
 
1546
1864
  @schemathesis.command(short_help="Replay requests from a saved cassette.")
1547
1865
  @click.argument("cassette_path", type=click.Path(exists=True))
1548
- @click.option("--id", "id_", help="ID of interaction to replay.", type=str)
1549
- @click.option("--status", help="Status of interactions to replay.", type=str)
1550
- @click.option("--uri", help="A regexp that filters interactions by their request URI.", type=str)
1551
- @click.option("--method", help="A regexp that filters interactions by their request method.", type=str)
1552
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
1553
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
1554
- @click.option("--verbosity", "-v", help="Increase verbosity of the output.", count=True)
1866
+ @click.option("--id", "id_", help="ID of interaction to replay", type=str)
1867
+ @click.option("--status", help="Status of interactions to replay", type=str)
1868
+ @click.option("--uri", help="A regexp that filters interactions by their request URI", type=str)
1869
+ @click.option("--method", help="A regexp that filters interactions by their request method", type=str)
1870
+ @click.option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
1871
+ @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
1872
+ @click.option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
1555
1873
  @with_request_tls_verify
1556
1874
  @with_request_proxy
1557
1875
  @with_request_cert
@@ -1619,13 +1937,13 @@ def replay(
1619
1937
  @click.argument("report", type=click.File(mode="rb"))
1620
1938
  @click.option(
1621
1939
  "--schemathesis-io-token",
1622
- help="Schemathesis.io authentication token.",
1940
+ help="Schemathesis.io authentication token",
1623
1941
  type=str,
1624
1942
  envvar=service.TOKEN_ENV_VAR,
1625
1943
  )
1626
1944
  @click.option(
1627
1945
  "--schemathesis-io-url",
1628
- help="Schemathesis.io base URL.",
1946
+ help="Schemathesis.io base URL",
1629
1947
  default=service.DEFAULT_URL,
1630
1948
  type=str,
1631
1949
  envvar=service.URL_ENV_VAR,
@@ -1747,6 +2065,39 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
1747
2065
  ctx.color = False
1748
2066
 
1749
2067
 
2068
+ def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
2069
+ """Add a new CLI option to `st run`."""
2070
+ run.params.append(cls(args, **kwargs))
2071
+
2072
+
2073
+ @dataclass
2074
+ class Group:
2075
+ name: str
2076
+
2077
+ def add_option(self, *args: Any, **kwargs: Any) -> None:
2078
+ kwargs["cls"] = GroupedOption
2079
+ kwargs["group"] = self.name
2080
+ add_option(*args, **kwargs)
2081
+
2082
+
2083
+ def add_group(name: str, *, index: int | None = None) -> Group:
2084
+ """Add a custom options group to `st run`."""
2085
+ if index is not None:
2086
+ GROUPS.insert(index, name)
2087
+ else:
2088
+ GROUPS.append(name)
2089
+ return Group(name)
2090
+
2091
+
2092
+ def handler() -> Callable[[type], None]:
2093
+ """Register a new CLI event handler."""
2094
+
2095
+ def _wrapper(cls: type) -> None:
2096
+ CUSTOM_HANDLERS.append(cls)
2097
+
2098
+ return _wrapper
2099
+
2100
+
1750
2101
  @HookDispatcher.register_spec([HookScope.GLOBAL])
1751
2102
  def after_init_cli_run_handlers(
1752
2103
  context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
@@ -1761,6 +2112,6 @@ def after_init_cli_run_handlers(
1761
2112
  def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
1762
2113
  """Called before every network call in CLI tests.
1763
2114
 
1764
- Aims to modify the argument passed to `case.call` / `case.call_wsgi` / `case.call_asgi`.
2115
+ Aims to modify the argument passed to `case.call`.
1765
2116
  Note that you need to modify `kwargs` in-place.
1766
2117
  """