schemathesis 3.25.5__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 +793 -448
  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 +24 -4
  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 +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  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 +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  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 +60 -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 +79 -61
  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 +143 -31
  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 +368 -242
  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.5.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.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.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,67 +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
44
- from ..runner import events, prepare_hypothesis_settings
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
+ 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
- from ..specs.openapi import formats
48
47
  from ..stateful import Stateful
49
- from ..targets import Target
50
- from ..types import Filter, PathLike, RequestCert
51
- from ..internal.datetime import current_datetime
52
- from ..internal.validation import file_exists
53
- from . import callbacks, cassettes, output, probes
54
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
48
+ from ..transports import RequestConfig
49
+ from ..transports.auth import get_requests_auth
50
+ from . import callbacks, cassettes, output
51
+ from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
55
52
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
56
53
  from .debug import DebugOutputHandler
54
+ from .handlers import EventHandler
57
55
  from .junitxml import JunitXMLHandler
58
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
56
+ from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
59
57
  from .sanitization import SanitizationHandler
60
58
 
61
59
  if TYPE_CHECKING:
60
+ import io
61
+
62
62
  import hypothesis
63
63
  import requests
64
- from ..service.client import ServiceClient
64
+
65
+ from ..models import Case, CheckFunction
65
66
  from ..schemas import BaseSchema
67
+ from ..service.client import ServiceClient
66
68
  from ..specs.graphql.schemas import GraphQLSchema
67
- from .handlers import EventHandler
69
+ from ..targets import Target
70
+ from ..types import NotSet, PathLike, RequestCert
71
+
72
+
73
+ __all__ = [
74
+ "EventHandler",
75
+ ]
68
76
 
69
77
 
70
78
  def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
71
79
  return tuple(item.__name__ for item in items)
72
80
 
73
81
 
82
+ CUSTOM_HANDLERS: list[type[EventHandler]] = []
74
83
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
75
84
 
76
85
  DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
@@ -96,13 +105,6 @@ DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
96
105
  "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
97
106
  "Use `--show-trace` instead"
98
107
  )
99
- DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING = (
100
- "The `--contrib-unique-data` CLI option and the corresponding `schemathesis.contrib.unique_data` hook "
101
- "are **DEPRECATED**. The concept of this feature does not fit the core principles of Hypothesis where "
102
- "strategies are configurable on a per-example basis but this feature implies uniqueness across examples. "
103
- "This leads to cryptic error messages about external state and flaky test runs, "
104
- "therefore it will be removed in Schemathesis 4.0"
105
- )
106
108
  CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
107
109
  COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
108
110
  PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
@@ -112,21 +114,21 @@ def reset_checks() -> None:
112
114
  """Get checks list to their default state."""
113
115
  # Useful in tests
114
116
  checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
115
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
117
+ CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
116
118
 
117
119
 
118
120
  def reset_targets() -> None:
119
121
  """Get targets list to their default state."""
120
122
  # Useful in tests
121
123
  targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
122
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
124
+ TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
123
125
 
124
126
 
125
127
  @click.group(context_settings=CONTEXT_SETTINGS)
126
- @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)
127
129
  @click.version_option()
128
130
  def schemathesis(pre_run: str | None = None) -> None:
129
- """Automated API testing employing fuzzing techniques for OpenAPI and GraphQL."""
131
+ """Property-based API testing for OpenAPI and GraphQL."""
130
132
  # Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
131
133
  hooks: str | None
132
134
  if pre_run:
@@ -138,76 +140,89 @@ def schemathesis(pre_run: str | None = None) -> None:
138
140
  load_hook(hooks)
139
141
 
140
142
 
141
- class ParameterGroup(enum.Enum):
142
- filtering = "Testing scope", "Customize the scope of the API testing."
143
- validation = "Response & Schema validation", "These options specify how API responses and schemas are validated."
144
- hypothesis = "Hypothesis engine", "Configuration of the underlying Hypothesis engine."
145
- generic = "Generic", None
143
+ GROUPS: list[str] = []
146
144
 
147
145
 
148
- class CommandWithCustomHelp(click.Command):
146
+ class CommandWithGroupedOptions(click.Command):
149
147
  def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
150
- # Group options first
151
148
  groups = defaultdict(list)
152
149
  for param in self.get_params(ctx):
153
150
  rv = param.get_help_record(ctx)
154
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
+
155
159
  if isinstance(param, GroupedOption):
156
160
  group = param.group
157
161
  else:
158
- group = ParameterGroup.generic
159
- groups[group].append(rv)
160
- # Then display groups separately with optional description
161
- for group in ParameterGroup:
162
- opts = groups[group]
163
- title, description = group.value
164
- with formatter.section(title):
165
- if description:
166
- formatter.write_paragraph()
167
- formatter.write_text(description)
168
- formatter.write_paragraph()
169
- 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)
170
167
 
171
168
 
172
169
  class GroupedOption(click.Option):
173
- def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
170
+ def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
174
171
  super().__init__(*args, **kwargs)
175
172
  self.group = group
176
173
 
177
174
 
178
- 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(
179
194
  "--request-proxy",
180
- help="Set the proxy for all network requests.",
195
+ help="Set the proxy for all network requests",
181
196
  type=str,
182
197
  )
183
- with_request_tls_verify = click.option(
198
+ with_request_tls_verify = grouped_option(
184
199
  "--request-tls-verify",
185
- 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",
186
201
  type=str,
187
202
  default="true",
188
203
  show_default=True,
189
204
  callback=callbacks.convert_boolean_string,
190
205
  )
191
- with_request_cert = click.option(
206
+ with_request_cert = grouped_option(
192
207
  "--request-cert",
193
208
  help="File path of unencrypted client certificate for authentication. "
194
209
  "The certificate can be bundled with a private key (e.g. PEM) or the private "
195
- "key can be provided with the --request-cert-key argument.",
210
+ "key can be provided with the --request-cert-key argument",
196
211
  type=click.Path(exists=True),
197
212
  default=None,
198
213
  show_default=False,
199
214
  )
200
- with_request_cert_key = click.option(
215
+ with_request_cert_key = grouped_option(
201
216
  "--request-cert-key",
202
- 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",
203
218
  type=click.Path(exists=True),
204
219
  default=None,
205
220
  show_default=False,
206
221
  callback=callbacks.validate_request_cert_key,
207
222
  )
208
- with_hosts_file = click.option(
223
+ with_hosts_file = grouped_option(
209
224
  "--hosts-file",
210
- 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",
211
226
  type=click.Path(dir_okay=False, writable=True),
212
227
  default=service.DEFAULT_HOSTS_PATH,
213
228
  envvar=service.HOSTS_PATH_ENV_VAR,
@@ -215,6 +230,37 @@ with_hosts_file = click.option(
215
230
  )
216
231
 
217
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
+
218
264
  class ReportToService:
219
265
  pass
220
266
 
@@ -222,502 +268,587 @@ class ReportToService:
222
268
  REPORT_TO_SERVICE = ReportToService()
223
269
 
224
270
 
225
- @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
+ )
226
276
  @click.argument("schema", type=str)
227
277
  @click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
228
- @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(
229
364
  "--checks",
230
365
  "-c",
231
366
  multiple=True,
232
- help="Specifies the validation checks to apply to API responses. "
233
- "Provide a comma-separated list of checks such as 'not_a_server_error,status_code_conformance', etc. "
234
- f"Default is '{','.join(DEFAULT_CHECKS_NAMES)}'.",
367
+ help="Comma-separated list of checks to run against API responses",
235
368
  type=CHECKS_TYPE,
236
369
  default=DEFAULT_CHECKS_NAMES,
237
- cls=GroupedOption,
238
- group=ParameterGroup.validation,
239
370
  callback=callbacks.convert_checks,
240
371
  show_default=True,
372
+ metavar="",
241
373
  )
242
- @click.option(
374
+ @grouped_option(
243
375
  "--exclude-checks",
244
376
  multiple=True,
245
- help="Specifies the validation checks to skip during testing. "
246
- "Provide a comma-separated list of checks you wish to bypass.",
377
+ help="Comma-separated list of checks to skip during testing",
247
378
  type=EXCLUDE_CHECKS_TYPE,
248
379
  default=[],
249
- cls=GroupedOption,
250
- group=ParameterGroup.validation,
251
380
  callback=callbacks.convert_checks,
252
381
  show_default=True,
382
+ metavar="",
253
383
  )
254
- @click.option(
255
- "--data-generation-method",
256
- "-D",
257
- "data_generation_methods",
258
- help="Specifies the approach Schemathesis uses to generate test data. "
259
- "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both. "
260
- "Default is 'positive'.",
261
- type=DATA_GENERATION_METHOD_TYPE,
262
- default=DataGenerationMethod.default().name,
263
- callback=callbacks.convert_data_generation_method,
264
- show_default=True,
265
- )
266
- @click.option(
384
+ @grouped_option(
267
385
  "--max-response-time",
268
- help="Sets a custom time limit for API response times. "
269
- "The test will fail if a response time exceeds this limit. "
270
- "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. ",
271
388
  type=click.IntRange(min=1),
272
- cls=GroupedOption,
273
- group=ParameterGroup.validation,
274
- )
275
- @click.option(
276
- "--target",
277
- "-t",
278
- "targets",
279
- multiple=True,
280
- help="Guides input generation to values more likely to expose bugs via targeted property-based testing.",
281
- type=TARGETS_TYPE,
282
- default=DEFAULT_TARGETS_NAMES,
283
- show_default=True,
284
389
  )
285
- @click.option(
390
+ @grouped_option(
286
391
  "-x",
287
392
  "--exitfirst",
288
393
  "exit_first",
289
394
  is_flag=True,
290
395
  default=False,
291
- 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",
292
397
  show_default=True,
293
398
  )
294
- @click.option(
399
+ @grouped_option(
295
400
  "--max-failures",
296
401
  "max_failures",
297
402
  type=click.IntRange(min=1),
298
- 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",
299
404
  show_default=True,
300
405
  )
301
- @click.option(
302
- "--dry-run",
303
- "dry_run",
304
- 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,
305
424
  default=False,
306
- help="Simulates test execution without making any actual requests, useful for validating data generation.",
425
+ show_default=True,
307
426
  )
308
- @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(
309
463
  "--auth",
310
464
  "-a",
311
- help="Provides the server authentication details in the 'USER:PASSWORD' format.",
465
+ help="Provide the server authentication details in the 'USER:PASSWORD' format",
312
466
  type=str,
313
467
  callback=callbacks.validate_auth,
314
468
  )
315
- @click.option(
469
+ @grouped_option(
316
470
  "--auth-type",
317
471
  "-A",
318
472
  type=click.Choice(["basic", "digest"], case_sensitive=False),
319
473
  default="basic",
320
- 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",
321
475
  show_default=True,
476
+ metavar="",
322
477
  )
323
- @click.option(
324
- "--set-query",
325
- "set_query",
326
- help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
327
- multiple=True,
328
- type=str,
329
- callback=callbacks.validate_set_query,
330
- )
331
- @click.option(
332
- "--set-header",
333
- "set_header",
334
- help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
335
- multiple=True,
336
- type=str,
337
- callback=callbacks.validate_set_header,
338
- )
339
- @click.option(
340
- "--set-cookie",
341
- "set_cookie",
342
- help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
343
- multiple=True,
478
+ @group("Filtering options")
479
+ @with_filters
480
+ @grouped_option(
481
+ "--include-by",
482
+ "include_by",
344
483
  type=str,
345
- callback=callbacks.validate_set_cookie,
484
+ help="Include API operations by expression",
346
485
  )
347
- @click.option(
348
- "--set-path",
349
- "set_path",
350
- help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
351
- multiple=True,
486
+ @grouped_option(
487
+ "--exclude-by",
488
+ "exclude_by",
352
489
  type=str,
353
- callback=callbacks.validate_set_path,
490
+ help="Exclude API operations by expression",
354
491
  )
355
- @click.option(
356
- "--header",
357
- "-H",
358
- "headers",
359
- help=r"Adds a custom HTTP header to all API requests. Format: 'Header-Name: Value'.",
360
- multiple=True,
361
- type=str,
362
- 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,
363
499
  )
364
- @click.option(
500
+ @grouped_option(
365
501
  "--endpoint",
366
502
  "-E",
367
503
  "endpoints",
368
504
  type=str,
369
505
  multiple=True,
370
- help=r"API operation path pattern (e.g., users/\d+).",
506
+ help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+)",
371
507
  callback=callbacks.validate_regex,
372
- cls=GroupedOption,
373
- group=ParameterGroup.filtering,
508
+ hidden=True,
374
509
  )
375
- @click.option(
510
+ @grouped_option(
376
511
  "--method",
377
512
  "-M",
378
513
  "methods",
379
514
  type=str,
380
515
  multiple=True,
381
- help="HTTP method (e.g., GET, POST).",
516
+ help="[DEPRECATED] HTTP method (e.g., GET, POST)",
382
517
  callback=callbacks.validate_regex,
383
- cls=GroupedOption,
384
- group=ParameterGroup.filtering,
518
+ hidden=True,
385
519
  )
386
- @click.option(
520
+ @grouped_option(
387
521
  "--tag",
388
522
  "-T",
389
523
  "tags",
390
524
  type=str,
391
525
  multiple=True,
392
- help="Schema tag pattern.",
526
+ help="[DEPRECATED] Schema tag pattern",
393
527
  callback=callbacks.validate_regex,
394
- cls=GroupedOption,
395
- group=ParameterGroup.filtering,
528
+ hidden=True,
396
529
  )
397
- @click.option(
530
+ @grouped_option(
398
531
  "--operation-id",
399
532
  "-O",
400
533
  "operation_ids",
401
534
  type=str,
402
535
  multiple=True,
403
- help="OpenAPI operationId pattern.",
536
+ help="[DEPRECATED] OpenAPI operationId pattern",
404
537
  callback=callbacks.validate_regex,
405
- cls=GroupedOption,
406
- group=ParameterGroup.filtering,
407
- )
408
- @click.option(
409
- "--workers",
410
- "-w",
411
- "workers_num",
412
- help="Sets the number of concurrent workers for testing. Auto-adjusts if 'auto' is specified.",
413
- type=CustomHelpMessageChoice(
414
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
415
- choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
416
- ),
417
- default=str(DEFAULT_WORKERS),
418
- show_default=True,
419
- callback=callbacks.convert_workers,
420
- )
421
- @click.option(
422
- "--base-url",
423
- "-b",
424
- help="Provides the base URL of the API, required when schema is provided as a file.",
425
- type=str,
426
- callback=callbacks.validate_base_url,
427
- envvar=BASE_URL_ENV_VAR,
428
- )
429
- @click.option(
430
- "--app",
431
- help="Specifies the WSGI/ASGI application under test, provided as an importable Python path.",
432
- type=str,
433
- callback=callbacks.validate_app,
434
- )
435
- @click.option(
436
- "--wait-for-schema",
437
- help="Maximum duration, in seconds, to wait for the API schema to become available.",
438
- type=click.FloatRange(1.0),
439
- default=None,
440
- envvar=WAIT_FOR_SCHEMA_ENV_VAR,
441
- )
442
- @click.option(
443
- "--request-timeout",
444
- help="Sets a timeout limit, in milliseconds, for each network request during tests.",
445
- type=click.IntRange(1),
446
- default=DEFAULT_RESPONSE_TIMEOUT,
447
- )
448
- @with_request_proxy
449
- @with_request_tls_verify
450
- @with_request_cert
451
- @with_request_cert_key
452
- @click.option(
453
- "--validate-schema",
454
- help="Toggles validation of incoming payloads against the defined API schema. "
455
- "Set to 'True' to enable or 'False' to disable. "
456
- "Default is 'False'.",
457
- type=bool,
458
- default=False,
459
- show_default=True,
460
- cls=GroupedOption,
461
- group=ParameterGroup.validation,
538
+ hidden=True,
462
539
  )
463
- @click.option(
540
+ @grouped_option(
464
541
  "--skip-deprecated-operations",
465
- help="Exclude deprecated API operations from testing.",
542
+ help="[DEPRECATED] Exclude deprecated API operations from testing",
466
543
  is_flag=True,
467
544
  is_eager=True,
468
545
  default=False,
469
546
  show_default=True,
470
- cls=GroupedOption,
471
- group=ParameterGroup.filtering,
547
+ hidden=True,
472
548
  )
473
- @click.option(
549
+ @group("Output options")
550
+ @grouped_option(
474
551
  "--junit-xml",
475
- 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",
476
553
  type=click.File("w", encoding="utf-8"),
477
554
  )
478
- @click.option(
479
- "--report",
480
- "report_value",
481
- help="""Specifies how the generated report should be handled.
482
- If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
483
- If a file name is provided, the report will be stored in that file.
484
- The report data, consisting of a tar gz file with multiple JSON files, is subject to change.""",
485
- is_flag=False,
486
- flag_value="",
487
- envvar=service.REPORT_ENV_VAR,
488
- callback=callbacks.convert_report, # type: ignore
489
- )
490
- @click.option(
491
- "--debug-output-file",
492
- 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",
493
558
  type=click.File("w", encoding="utf-8"),
494
- )
495
- @click.option(
496
- "--show-errors-tracebacks",
497
- help="Displays complete traceback information for internal errors.",
498
- is_flag=True,
499
559
  is_eager=True,
500
- default=False,
501
- hidden=True,
502
- show_default=True,
503
560
  )
504
- @click.option(
505
- "--show-trace",
506
- 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",
507
572
  is_flag=True,
508
- is_eager=True,
509
- default=False,
510
- show_default=True,
573
+ callback=callbacks.validate_preserve_exact_body_bytes,
511
574
  )
512
- @click.option(
575
+ @grouped_option(
513
576
  "--code-sample-style",
514
- help="Selects the code sample style for reproducing failures.",
577
+ help="Code sample style for reproducing failures",
515
578
  type=click.Choice([item.name for item in CodeSampleStyle]),
516
579
  default=CodeSampleStyle.default().name,
517
580
  callback=callbacks.convert_code_sample_style,
581
+ metavar="",
518
582
  )
519
- @click.option(
520
- "--cassette-path",
521
- help="Saves the test outcomes in a VCR-compatible format.",
522
- type=click.File("w", encoding="utf-8"),
523
- 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",
524
589
  )
525
- @click.option(
526
- "--cassette-preserve-exact-body-bytes",
527
- 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",
528
601
  is_flag=True,
529
- callback=callbacks.validate_preserve_exact_body_bytes,
602
+ is_eager=True,
603
+ default=False,
604
+ show_default=True,
530
605
  )
531
- @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(
532
612
  "--store-network-log",
533
- help="Saves the test outcomes in a VCR-compatible format.",
613
+ help="[DEPRECATED] Save the test outcomes in a VCR-compatible format",
534
614
  type=click.File("w", encoding="utf-8"),
535
615
  hidden=True,
536
616
  )
537
- @click.option(
538
- "--fixups",
539
- help="Applies compatibility adjustments like 'fast_api', 'utf8_bom'.",
540
- multiple=True,
541
- 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,
542
625
  )
543
- @click.option(
544
- "--rate-limit",
545
- help="Specifies a rate limit for test requests in '<limit>/<duration>' format. "
546
- "Example - `100/m` for 100 requests per minute.",
547
- type=str,
548
- 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="",
549
638
  )
550
- @click.option(
639
+ @grouped_option(
551
640
  "--stateful",
552
- help="Enables or disables stateful testing features.",
641
+ help="Enable or disable stateful testing",
553
642
  type=click.Choice([item.name for item in Stateful]),
554
643
  default=Stateful.links.name,
555
644
  callback=callbacks.convert_stateful,
645
+ metavar="",
556
646
  )
557
- @click.option(
647
+ @grouped_option(
558
648
  "--stateful-recursion-limit",
559
- help="Sets the recursion depth limit for stateful testing.",
649
+ help="Recursion depth limit for stateful testing",
560
650
  default=DEFAULT_STATEFUL_RECURSION_LIMIT,
561
651
  show_default=True,
562
652
  type=click.IntRange(1, 100),
563
653
  hidden=True,
564
654
  )
565
- @click.option(
566
- "--force-schema-version",
567
- help="Forces the schema to be interpreted as a particular OpenAPI version.",
568
- 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,
569
662
  )
570
- @click.option(
571
- "--sanitize-output",
572
- type=bool,
573
- 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",
574
675
  show_default=True,
575
- help="Enable or disable automatic output sanitization to obscure sensitive data.",
676
+ callback=callbacks.convert_boolean_string,
576
677
  )
577
- @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(
578
687
  "--contrib-unique-data",
579
688
  "contrib_unique_data",
580
- help="Forces the generation of unique test cases.",
689
+ help="Force the generation of unique test cases",
581
690
  is_flag=True,
582
691
  default=False,
583
692
  show_default=True,
584
693
  )
585
- @click.option(
694
+ @grouped_option(
586
695
  "--contrib-openapi-formats-uuid",
587
696
  "contrib_openapi_formats_uuid",
588
- help="Enables support for the 'uuid' string format in OpenAPI.",
697
+ help="Enable support for the 'uuid' string format in OpenAPI",
589
698
  is_flag=True,
590
699
  default=False,
591
700
  show_default=True,
592
701
  )
593
- @click.option(
702
+ @grouped_option(
594
703
  "--contrib-openapi-fill-missing-examples",
595
704
  "contrib_openapi_fill_missing_examples",
596
- 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",
597
706
  is_flag=True,
598
707
  default=False,
599
708
  show_default=True,
600
709
  )
601
- @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(
602
762
  "--hypothesis-database",
603
- help="Configures storage for examples discovered by Hypothesis. "
763
+ help="Storage for examples discovered by Hypothesis. "
604
764
  f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
605
- f"or specify a file path for persistent storage.",
765
+ f"or specify a file path for persistent storage",
606
766
  type=str,
607
- cls=GroupedOption,
608
- group=ParameterGroup.hypothesis,
609
767
  callback=callbacks.validate_hypothesis_database,
610
768
  )
611
- @click.option(
769
+ @grouped_option(
612
770
  "--hypothesis-deadline",
613
- help="Sets a time limit for each test case generated by Hypothesis, in milliseconds. "
614
- "Exceeding this limit will cause the test to fail.",
615
- # max value to avoid overflow. It is the maximum amount of days in milliseconds
616
- type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
617
- cls=GroupedOption,
618
- 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),
619
774
  )
620
- @click.option(
775
+ @grouped_option(
621
776
  "--hypothesis-derandomize",
622
- 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",
623
778
  is_flag=True,
624
779
  is_eager=True,
625
780
  default=None,
626
781
  show_default=True,
627
- cls=GroupedOption,
628
- group=ParameterGroup.hypothesis,
629
782
  )
630
- @click.option(
783
+ @grouped_option(
631
784
  "--hypothesis-max-examples",
632
- 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",
633
786
  type=click.IntRange(1),
634
- cls=GroupedOption,
635
- group=ParameterGroup.hypothesis,
636
787
  )
637
- @click.option(
788
+ @grouped_option(
638
789
  "--hypothesis-phases",
639
- help="Specifies which testing phases to execute.",
790
+ help="Testing phases to execute",
640
791
  type=CsvEnumChoice(Phase),
641
- cls=GroupedOption,
642
- group=ParameterGroup.hypothesis,
792
+ metavar="",
643
793
  )
644
- @click.option(
794
+ @grouped_option(
645
795
  "--hypothesis-no-phases",
646
- help="Specifies which testing phases to exclude from execution.",
796
+ help="Testing phases to exclude from execution",
647
797
  type=CsvEnumChoice(Phase),
648
- cls=GroupedOption,
649
- group=ParameterGroup.hypothesis,
798
+ metavar="",
650
799
  )
651
- @click.option(
800
+ @grouped_option(
652
801
  "--hypothesis-report-multiple-bugs",
653
- 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",
654
803
  type=bool,
655
- cls=GroupedOption,
656
- group=ParameterGroup.hypothesis,
657
804
  )
658
- @click.option(
805
+ @grouped_option(
659
806
  "--hypothesis-seed",
660
- help="Sets a seed value for Hypothesis, ensuring reproducibility across test runs.",
807
+ help="Seed value for Hypothesis, ensuring reproducibility across test runs",
661
808
  type=int,
662
- cls=GroupedOption,
663
- group=ParameterGroup.hypothesis,
664
809
  )
665
- @click.option(
810
+ @grouped_option(
666
811
  "--hypothesis-suppress-health-check",
667
- help="Disables specified health checks from Hypothesis like 'data_too_large', 'filter_too_much', etc. "
668
- "Provide a comma-separated list",
812
+ help="A comma-separated list of Hypothesis health checks to disable",
669
813
  type=CsvEnumChoice(HealthCheck),
670
- cls=GroupedOption,
671
- group=ParameterGroup.hypothesis,
814
+ metavar="",
672
815
  )
673
- @click.option(
816
+ @grouped_option(
674
817
  "--hypothesis-verbosity",
675
- help="Controls the verbosity level of Hypothesis output.",
818
+ help="Verbosity level of Hypothesis output",
676
819
  type=click.Choice([item.name for item in Verbosity]),
677
820
  callback=callbacks.convert_verbosity,
678
- cls=GroupedOption,
679
- group=ParameterGroup.hypothesis,
680
- )
681
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
682
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
683
- @click.option(
684
- "--experimental",
685
- help="Enable experimental support for specific features.",
686
- type=click.Choice([experimental.OPEN_API_3_1.name]),
687
- callback=callbacks.convert_experimental,
688
- multiple=True,
689
- )
690
- @click.option(
691
- "--generation-allow-x00",
692
- help="Determines whether to allow the generation of `\x00` bytes within strings.",
693
- type=str,
694
- default="true",
695
- show_default=True,
696
- callback=callbacks.convert_boolean_string,
821
+ metavar="",
697
822
  )
698
- @click.option(
699
- "--generation-codec",
700
- help="Specifies the codec used for generating strings.",
701
- type=str,
702
- default="utf-8",
703
- 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
704
835
  )
705
- @click.option(
836
+ @grouped_option(
706
837
  "--schemathesis-io-token",
707
- help="Schemathesis.io authentication token.",
838
+ help="Schemathesis.io authentication token",
708
839
  type=str,
709
840
  envvar=service.TOKEN_ENV_VAR,
710
841
  )
711
- @click.option(
842
+ @grouped_option(
712
843
  "--schemathesis-io-url",
713
- help="Schemathesis.io base URL.",
844
+ help="Schemathesis.io base URL",
714
845
  default=service.DEFAULT_URL,
715
846
  type=str,
716
847
  envvar=service.URL_ENV_VAR,
717
848
  )
718
- @click.option(
849
+ @grouped_option(
719
850
  "--schemathesis-io-telemetry",
720
- 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",
721
852
  type=str,
722
853
  default="true",
723
854
  show_default=True,
@@ -725,7 +856,10 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
725
856
  envvar=service.TELEMETRY_ENV_VAR,
726
857
  )
727
858
  @with_hosts_file
728
- @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)
729
863
  @click.pass_context
730
864
  def run(
731
865
  ctx: click.Context,
@@ -738,7 +872,11 @@ def run(
738
872
  set_header: dict[str, str],
739
873
  set_cookie: dict[str, str],
740
874
  set_path: dict[str, str],
741
- 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],
742
880
  checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
743
881
  exclude_checks: Iterable[str] = (),
744
882
  data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
@@ -747,10 +885,33 @@ def run(
747
885
  exit_first: bool = False,
748
886
  max_failures: int | None = None,
749
887
  dry_run: bool = False,
750
- endpoints: Filter | None = None,
751
- methods: Filter | None = None,
752
- tags: Filter | None = None,
753
- 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, ...] = (),
754
915
  workers_num: int = DEFAULT_WORKERS,
755
916
  base_url: str | None = None,
756
917
  app: str | None = None,
@@ -767,6 +928,7 @@ def run(
767
928
  show_trace: bool = False,
768
929
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
769
930
  cassette_path: click.utils.LazyFile | None = None,
931
+ cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
770
932
  cassette_preserve_exact_body_bytes: bool = False,
771
933
  store_network_log: click.utils.LazyFile | None = None,
772
934
  wait_for_schema: float | None = None,
@@ -776,6 +938,7 @@ def run(
776
938
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
777
939
  force_schema_version: str | None = None,
778
940
  sanitize_output: bool = True,
941
+ output_truncate: bool = True,
779
942
  contrib_unique_data: bool = False,
780
943
  contrib_openapi_formats_uuid: bool = False,
781
944
  contrib_openapi_fill_missing_examples: bool = False,
@@ -793,18 +956,21 @@ def run(
793
956
  no_color: bool = False,
794
957
  report_value: str | None = None,
795
958
  generation_allow_x00: bool = True,
959
+ generation_graphql_allow_null: bool = True,
960
+ generation_with_security_parameters: bool = True,
796
961
  generation_codec: str = "utf-8",
797
962
  schemathesis_io_token: str | None = None,
798
963
  schemathesis_io_url: str = service.DEFAULT_URL,
799
964
  schemathesis_io_telemetry: bool = True,
800
965
  hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
801
966
  force_color: bool = False,
967
+ **__kwargs,
802
968
  ) -> None:
803
969
  """Run tests against an API using a specified SCHEMA.
804
970
 
805
- [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
806
972
 
807
- [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io.
973
+ [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
808
974
  """
809
975
  _hypothesis_phases: list[hypothesis.Phase] | None = None
810
976
  if hypothesis_phases is not None:
@@ -816,23 +982,25 @@ def run(
816
982
  _hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
817
983
  if hypothesis_suppress_health_check is not None:
818
984
  _hypothesis_suppress_health_check = [
819
- 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()
820
986
  ]
821
987
 
822
- if contrib_unique_data:
823
- click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
824
-
825
988
  if show_errors_tracebacks:
826
989
  click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
827
990
  show_trace = show_errors_tracebacks
828
991
 
829
992
  # Enable selected experiments
830
- for experiment in experimental:
993
+ for experiment in experiments:
831
994
  experiment.enable()
832
995
 
833
996
  override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
834
997
 
835
- 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
+ )
836
1004
 
837
1005
  report: ReportToService | click.utils.LazyFile | None
838
1006
  if report_value is None:
@@ -856,6 +1024,109 @@ def run(
856
1024
  click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
857
1025
  cassette_path = store_network_log
858
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
+
859
1130
  schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
860
1131
  token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
861
1132
  schema_kind = callbacks.parse_schema_kind(schema, app)
@@ -902,6 +1173,10 @@ def run(
902
1173
  from ..service.client import ServiceClient
903
1174
 
904
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
+
905
1180
  client = ServiceClient(base_url=schemathesis_io_url, token=token)
906
1181
  host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
907
1182
 
@@ -910,6 +1185,25 @@ def run(
910
1185
  else:
911
1186
  selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
912
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
+
913
1207
  selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
914
1208
 
915
1209
  if fixups:
@@ -918,8 +1212,6 @@ def run(
918
1212
  else:
919
1213
  _fixups.install(fixups)
920
1214
 
921
- if contrib_unique_data:
922
- contrib.unique_data.install()
923
1215
  if contrib_openapi_formats_uuid:
924
1216
  contrib.openapi.formats.uuid.install()
925
1217
  if contrib_openapi_fill_missing_examples:
@@ -941,7 +1233,6 @@ def run(
941
1233
  base_url=base_url,
942
1234
  started_at=started_at,
943
1235
  validate_schema=validate_schema,
944
- skip_deprecated_operations=skip_deprecated_operations,
945
1236
  data_generation_methods=data_generation_methods,
946
1237
  force_schema_version=force_schema_version,
947
1238
  request_tls_verify=request_tls_verify,
@@ -952,14 +1243,12 @@ def run(
952
1243
  auth_type=auth_type,
953
1244
  override=override,
954
1245
  headers=headers,
955
- endpoint=endpoints or None,
956
- method=methods or None,
957
- tag=tags or None,
958
- operation_id=operation_ids or None,
959
1246
  request_timeout=request_timeout,
960
1247
  seed=hypothesis_seed,
961
1248
  exit_first=exit_first,
1249
+ no_failfast=no_failfast,
962
1250
  max_failures=max_failures,
1251
+ unique_data=contrib_unique_data,
963
1252
  dry_run=dry_run,
964
1253
  store_interactions=cassette_path is not None,
965
1254
  checks=selected_checks,
@@ -971,9 +1260,14 @@ def run(
971
1260
  stateful_recursion_limit=stateful_recursion_limit,
972
1261
  hypothesis_settings=hypothesis_settings,
973
1262
  generation_config=generation_config,
1263
+ checks_config=checks_config,
1264
+ output_config=output_config,
1265
+ service_client=client,
1266
+ filter_set=filter_set,
974
1267
  )
975
1268
  execute(
976
1269
  event_stream,
1270
+ ctx=ctx,
977
1271
  hypothesis_settings=hypothesis_settings,
978
1272
  workers_num=workers_num,
979
1273
  rate_limit=rate_limit,
@@ -981,6 +1275,7 @@ def run(
981
1275
  wait_for_schema=wait_for_schema,
982
1276
  validate_schema=validate_schema,
983
1277
  cassette_path=cassette_path,
1278
+ cassette_format=cassette_format,
984
1279
  cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
985
1280
  junit_xml=junit_xml,
986
1281
  verbosity=verbosity,
@@ -996,9 +1291,25 @@ def run(
996
1291
  location=schema,
997
1292
  base_url=base_url,
998
1293
  started_at=started_at,
1294
+ output_config=output_config,
999
1295
  )
1000
1296
 
1001
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
+
1002
1313
  def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
1003
1314
  if cert is not None and key is not None:
1004
1315
  return cert, key
@@ -1016,7 +1327,6 @@ class LoaderConfig:
1016
1327
  app: Any
1017
1328
  base_url: str | None
1018
1329
  validate_schema: bool
1019
- skip_deprecated_operations: bool
1020
1330
  data_generation_methods: tuple[DataGenerationMethod, ...]
1021
1331
  force_schema_version: str | None
1022
1332
  request_tls_verify: bool | str
@@ -1024,15 +1334,12 @@ class LoaderConfig:
1024
1334
  request_cert: RequestCert | None
1025
1335
  wait_for_schema: float | None
1026
1336
  rate_limit: str | None
1337
+ output_config: OutputConfig
1338
+ generation_config: generation.GenerationConfig
1027
1339
  # Network request parameters
1028
1340
  auth: tuple[str, str] | None
1029
1341
  auth_type: str | None
1030
1342
  headers: dict[str, str] | None
1031
- # Schema filters
1032
- endpoint: Filter | None
1033
- method: Filter | None
1034
- tag: Filter | None
1035
- operation_id: Filter | None
1036
1343
 
1037
1344
 
1038
1345
  def into_event_stream(
@@ -1042,7 +1349,6 @@ def into_event_stream(
1042
1349
  base_url: str | None,
1043
1350
  started_at: str,
1044
1351
  validate_schema: bool,
1045
- skip_deprecated_operations: bool,
1046
1352
  data_generation_methods: tuple[DataGenerationMethod, ...],
1047
1353
  force_schema_version: str | None,
1048
1354
  request_tls_verify: bool | str,
@@ -1055,26 +1361,27 @@ def into_event_stream(
1055
1361
  headers: dict[str, str] | None,
1056
1362
  request_timeout: int | None,
1057
1363
  wait_for_schema: float | None,
1058
- # Schema filters
1059
- endpoint: Filter | None,
1060
- method: Filter | None,
1061
- tag: Filter | None,
1062
- operation_id: Filter | None,
1364
+ filter_set: FilterSet,
1063
1365
  # Runtime behavior
1064
1366
  checks: Iterable[CheckFunction],
1367
+ checks_config: CheckConfig,
1065
1368
  max_response_time: int | None,
1066
1369
  targets: Iterable[Target],
1067
1370
  workers_num: int,
1068
1371
  hypothesis_settings: hypothesis.settings | None,
1069
1372
  generation_config: generation.GenerationConfig,
1373
+ output_config: OutputConfig,
1070
1374
  seed: int | None,
1071
1375
  exit_first: bool,
1376
+ no_failfast: bool,
1072
1377
  max_failures: int | None,
1073
1378
  rate_limit: str | None,
1379
+ unique_data: bool,
1074
1380
  dry_run: bool,
1075
1381
  store_interactions: bool,
1076
1382
  stateful: Stateful | None,
1077
1383
  stateful_recursion_limit: int,
1384
+ service_client: ServiceClient | None,
1078
1385
  ) -> Generator[events.ExecutionEvent, None, None]:
1079
1386
  try:
1080
1387
  if app is not None:
@@ -1084,7 +1391,6 @@ def into_event_stream(
1084
1391
  app=app,
1085
1392
  base_url=base_url,
1086
1393
  validate_schema=validate_schema,
1087
- skip_deprecated_operations=skip_deprecated_operations,
1088
1394
  data_generation_methods=data_generation_methods,
1089
1395
  force_schema_version=force_schema_version,
1090
1396
  request_proxy=request_proxy,
@@ -1095,15 +1401,13 @@ def into_event_stream(
1095
1401
  auth=auth,
1096
1402
  auth_type=auth_type,
1097
1403
  headers=headers,
1098
- endpoint=endpoint or None,
1099
- method=method or None,
1100
- tag=tag or None,
1101
- operation_id=operation_id or None,
1404
+ output_config=output_config,
1405
+ generation_config=generation_config,
1102
1406
  )
1103
- loaded_schema = load_schema(config)
1104
- run_probes(loaded_schema, config)
1407
+ schema = load_schema(config)
1408
+ schema.filter_set = filter_set
1105
1409
  yield from runner.from_schema(
1106
- loaded_schema,
1410
+ schema,
1107
1411
  auth=auth,
1108
1412
  auth_type=auth_type,
1109
1413
  override=override,
@@ -1114,11 +1418,14 @@ def into_event_stream(
1114
1418
  request_cert=request_cert,
1115
1419
  seed=seed,
1116
1420
  exit_first=exit_first,
1421
+ no_failfast=no_failfast,
1117
1422
  max_failures=max_failures,
1118
1423
  started_at=started_at,
1424
+ unique_data=unique_data,
1119
1425
  dry_run=dry_run,
1120
1426
  store_interactions=store_interactions,
1121
1427
  checks=checks,
1428
+ checks_config=checks_config,
1122
1429
  max_response_time=max_response_time,
1123
1430
  targets=targets,
1124
1431
  workers_num=workers_num,
@@ -1126,6 +1433,19 @@ def into_event_stream(
1126
1433
  stateful_recursion_limit=stateful_recursion_limit,
1127
1434
  hypothesis_settings=hypothesis_settings,
1128
1435
  generation_config=generation_config,
1436
+ probe_config=probes.ProbeConfig(
1437
+ base_url=config.base_url,
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
+ ),
1444
+ auth=config.auth,
1445
+ auth_type=config.auth_type,
1446
+ headers=config.headers,
1447
+ ),
1448
+ service_client=service_client,
1129
1449
  ).execute()
1130
1450
  except SchemaError as error:
1131
1451
  yield events.InternalError.from_schema_error(error)
@@ -1133,19 +1453,6 @@ def into_event_stream(
1133
1453
  yield events.InternalError.from_exc(exc)
1134
1454
 
1135
1455
 
1136
- def run_probes(schema: BaseSchema, config: LoaderConfig) -> None:
1137
- """Discover capabilities of the tested app."""
1138
- probe_results = probes.run(schema, config)
1139
- for result in probe_results:
1140
- if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
1141
- from ..specs.openapi._hypothesis import HEADER_FORMAT, header_values
1142
-
1143
- formats.register(
1144
- HEADER_FORMAT,
1145
- header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
1146
- )
1147
-
1148
-
1149
1456
  def load_schema(config: LoaderConfig) -> BaseSchema:
1150
1457
  """Automatically load API schema."""
1151
1458
  first: Callable[[LoaderConfig], BaseSchema]
@@ -1239,15 +1546,12 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
1239
1546
  kwargs = {
1240
1547
  "app": config.app,
1241
1548
  "base_url": config.base_url,
1242
- "method": config.method,
1243
- "endpoint": config.endpoint,
1244
- "tag": config.tag,
1245
- "operation_id": config.operation_id,
1246
- "skip_deprecated_operations": config.skip_deprecated_operations,
1247
1549
  "validate_schema": config.validate_schema,
1248
1550
  "force_schema_version": config.force_schema_version,
1249
1551
  "data_generation_methods": config.data_generation_methods,
1250
1552
  "rate_limit": config.rate_limit,
1553
+ "output_config": config.output_config,
1554
+ "generation_config": config.generation_config,
1251
1555
  }
1252
1556
  if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
1253
1557
  kwargs["headers"] = config.headers
@@ -1353,6 +1657,7 @@ class OutputStyle(Enum):
1353
1657
  def execute(
1354
1658
  event_stream: Generator[events.ExecutionEvent, None, None],
1355
1659
  *,
1660
+ ctx: click.Context,
1356
1661
  hypothesis_settings: hypothesis.settings,
1357
1662
  workers_num: int,
1358
1663
  rate_limit: str | None,
@@ -1360,6 +1665,7 @@ def execute(
1360
1665
  wait_for_schema: float | None,
1361
1666
  validate_schema: bool,
1362
1667
  cassette_path: click.utils.LazyFile | None,
1668
+ cassette_format: cassettes.CassetteFormat,
1363
1669
  cassette_preserve_exact_body_bytes: bool,
1364
1670
  junit_xml: click.utils.LazyFile | None,
1365
1671
  verbosity: int,
@@ -1375,6 +1681,7 @@ def execute(
1375
1681
  location: str,
1376
1682
  base_url: str | None,
1377
1683
  started_at: str,
1684
+ output_config: OutputConfig,
1378
1685
  ) -> None:
1379
1686
  """Execute a prepared runner by drawing events from it and passing to a proper handler."""
1380
1687
  handlers: list[EventHandler] = []
@@ -1421,8 +1728,12 @@ def execute(
1421
1728
  # This handler should be first to have logs writing completed when the output handler will display statistic
1422
1729
  _open_file(cassette_path)
1423
1730
  handlers.append(
1424
- 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
+ )
1425
1734
  )
1735
+ for custom_handler in CUSTOM_HANDLERS:
1736
+ handlers.append(custom_handler(*ctx.args, **ctx.params))
1426
1737
  handlers.append(get_output_handler(workers_num))
1427
1738
  if sanitize_output:
1428
1739
  handlers.insert(0, SanitizationHandler())
@@ -1438,6 +1749,7 @@ def execute(
1438
1749
  verbosity=verbosity,
1439
1750
  code_sample_style=code_sample_style,
1440
1751
  report=report_context,
1752
+ output_config=output_config,
1441
1753
  )
1442
1754
 
1443
1755
  def shutdown() -> None:
@@ -1551,13 +1863,13 @@ def get_exit_code(event: events.ExecutionEvent) -> int:
1551
1863
 
1552
1864
  @schemathesis.command(short_help="Replay requests from a saved cassette.")
1553
1865
  @click.argument("cassette_path", type=click.Path(exists=True))
1554
- @click.option("--id", "id_", help="ID of interaction to replay.", type=str)
1555
- @click.option("--status", help="Status of interactions to replay.", type=str)
1556
- @click.option("--uri", help="A regexp that filters interactions by their request URI.", type=str)
1557
- @click.option("--method", help="A regexp that filters interactions by their request method.", type=str)
1558
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
1559
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
1560
- @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)
1561
1873
  @with_request_tls_verify
1562
1874
  @with_request_proxy
1563
1875
  @with_request_cert
@@ -1625,13 +1937,13 @@ def replay(
1625
1937
  @click.argument("report", type=click.File(mode="rb"))
1626
1938
  @click.option(
1627
1939
  "--schemathesis-io-token",
1628
- help="Schemathesis.io authentication token.",
1940
+ help="Schemathesis.io authentication token",
1629
1941
  type=str,
1630
1942
  envvar=service.TOKEN_ENV_VAR,
1631
1943
  )
1632
1944
  @click.option(
1633
1945
  "--schemathesis-io-url",
1634
- help="Schemathesis.io base URL.",
1946
+ help="Schemathesis.io base URL",
1635
1947
  default=service.DEFAULT_URL,
1636
1948
  type=str,
1637
1949
  envvar=service.URL_ENV_VAR,
@@ -1753,6 +2065,39 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
1753
2065
  ctx.color = False
1754
2066
 
1755
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
+
1756
2101
  @HookDispatcher.register_spec([HookScope.GLOBAL])
1757
2102
  def after_init_cli_run_handlers(
1758
2103
  context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
@@ -1767,6 +2112,6 @@ def after_init_cli_run_handlers(
1767
2112
  def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
1768
2113
  """Called before every network call in CLI tests.
1769
2114
 
1770
- 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`.
1771
2116
  Note that you need to modify `kwargs` in-place.
1772
2117
  """