schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,1772 +1,17 @@
1
1
  from __future__ import annotations
2
- import base64
3
- import enum
4
- import io
5
- import os
6
- import sys
7
- import traceback
8
- import warnings
9
- from collections import defaultdict
10
- from dataclasses import dataclass
11
- from enum import Enum
12
- from queue import Queue
13
- from typing import Any, Callable, Generator, Iterable, NoReturn, cast, TYPE_CHECKING
14
- from urllib.parse import urlparse
15
2
 
16
- import click
3
+ from schemathesis.cli.commands import Group, run, schemathesis
4
+ from schemathesis.cli.commands.run.executor import handler
5
+ from schemathesis.cli.commands.run.handlers import EventHandler
6
+ from schemathesis.cli.ext.groups import GROUPS
17
7
 
18
- from .. import checks as checks_module
19
- from .. import contrib, experimental, generation
20
- from .. import fixups as _fixups
21
- from .. import runner, service
22
- from .. import targets as targets_module
23
- from ..code_samples import CodeSampleStyle
24
- from .constants import HealthCheck, Phase, Verbosity
25
- from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
26
- from ..constants import (
27
- API_NAME_ENV_VAR,
28
- BASE_URL_ENV_VAR,
29
- DEFAULT_RESPONSE_TIMEOUT,
30
- DEFAULT_STATEFUL_RECURSION_LIMIT,
31
- HOOKS_MODULE_ENV_VAR,
32
- HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
33
- WAIT_FOR_SCHEMA_ENV_VAR,
34
- EXTENSIONS_DOCUMENTATION_URL,
35
- ISSUE_TRACKER_URL,
36
- )
37
- from ..exceptions import SchemaError, extract_nth_traceback, SchemaErrorType
38
- 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
42
- from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
43
- from ..models import Case, CheckFunction
44
- from ..runner import events, prepare_hypothesis_settings
45
- from ..specs.graphql import loaders as gql_loaders
46
- from ..specs.openapi import loaders as oas_loaders
47
- from ..specs.openapi import formats
48
- 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
55
- from .context import ExecutionContext, FileReportContext, ServiceReportContext
56
- from .debug import DebugOutputHandler
57
- from .junitxml import JunitXMLHandler
58
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
59
- from .sanitization import SanitizationHandler
8
+ __all__ = ["schemathesis", "run", "EventHandler", "add_group", "handler"]
60
9
 
61
- if TYPE_CHECKING:
62
- import hypothesis
63
- import requests
64
- from ..service.client import ServiceClient
65
- from ..schemas import BaseSchema
66
- from ..specs.graphql.schemas import GraphQLSchema
67
- from .handlers import EventHandler
68
10
 
69
-
70
- def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
71
- return tuple(item.__name__ for item in items)
72
-
73
-
74
- CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
75
-
76
- DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
77
- ALL_CHECKS_NAMES = _get_callable_names(checks_module.ALL_CHECKS)
78
- CHECKS_TYPE = CsvChoice((*ALL_CHECKS_NAMES, "all"))
79
- EXCLUDE_CHECKS_TYPE = CsvChoice((*ALL_CHECKS_NAMES,))
80
-
81
- DEFAULT_TARGETS_NAMES = _get_callable_names(targets_module.DEFAULT_TARGETS)
82
- ALL_TARGETS_NAMES = _get_callable_names(targets_module.ALL_TARGETS)
83
- TARGETS_TYPE = click.Choice((*ALL_TARGETS_NAMES, "all"))
84
-
85
- DATA_GENERATION_METHOD_TYPE = click.Choice([item.name for item in DataGenerationMethod] + ["all"])
86
-
87
- DEPRECATED_CASSETTE_PATH_OPTION_WARNING = (
88
- "Warning: Option `--store-network-log` is deprecated and will be removed in Schemathesis 4.0. "
89
- "Use `--cassette-path` instead."
90
- )
91
- DEPRECATED_PRE_RUN_OPTION_WARNING = (
92
- "Warning: Option `--pre-run` is deprecated and will be removed in Schemathesis 4.0. "
93
- f"Use the `{HOOKS_MODULE_ENV_VAR}` environment variable instead"
94
- )
95
- DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
96
- "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
97
- "Use `--show-trace` instead"
98
- )
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
- CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
107
- COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
108
- PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
109
-
110
-
111
- def reset_checks() -> None:
112
- """Get checks list to their default state."""
113
- # Useful in tests
114
- checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
115
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
116
-
117
-
118
- def reset_targets() -> None:
119
- """Get targets list to their default state."""
120
- # Useful in tests
121
- targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
122
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
123
-
124
-
125
- @click.group(context_settings=CONTEXT_SETTINGS)
126
- @click.option("--pre-run", help="A module to execute before running the tests.", type=str, hidden=True)
127
- @click.version_option()
128
- def schemathesis(pre_run: str | None = None) -> None:
129
- """Automated API testing employing fuzzing techniques for OpenAPI and GraphQL."""
130
- # Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
131
- hooks: str | None
132
- if pre_run:
133
- click.secho(DEPRECATED_PRE_RUN_OPTION_WARNING, fg="yellow")
134
- hooks = pre_run
135
- else:
136
- hooks = os.getenv(HOOKS_MODULE_ENV_VAR)
137
- if hooks:
138
- load_hook(hooks)
139
-
140
-
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
146
-
147
-
148
- class CommandWithCustomHelp(click.Command):
149
- def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
150
- # Group options first
151
- groups = defaultdict(list)
152
- for param in self.get_params(ctx):
153
- rv = param.get_help_record(ctx)
154
- if rv is not None:
155
- if isinstance(param, GroupedOption):
156
- group = param.group
157
- 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)
170
-
171
-
172
- class GroupedOption(click.Option):
173
- def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
174
- super().__init__(*args, **kwargs)
175
- self.group = group
176
-
177
-
178
- with_request_proxy = click.option(
179
- "--request-proxy",
180
- help="Set the proxy for all network requests.",
181
- type=str,
182
- )
183
- with_request_tls_verify = click.option(
184
- "--request-tls-verify",
185
- help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs.",
186
- type=str,
187
- default="true",
188
- show_default=True,
189
- callback=callbacks.convert_boolean_string,
190
- )
191
- with_request_cert = click.option(
192
- "--request-cert",
193
- help="File path of unencrypted client certificate for authentication. "
194
- "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.",
196
- type=click.Path(exists=True),
197
- default=None,
198
- show_default=False,
199
- )
200
- with_request_cert_key = click.option(
201
- "--request-cert-key",
202
- help="Specifies the file path of the private key for the client certificate.",
203
- type=click.Path(exists=True),
204
- default=None,
205
- show_default=False,
206
- callback=callbacks.validate_request_cert_key,
207
- )
208
- with_hosts_file = click.option(
209
- "--hosts-file",
210
- help="Path to a file to store the Schemathesis.io auth configuration.",
211
- type=click.Path(dir_okay=False, writable=True),
212
- default=service.DEFAULT_HOSTS_PATH,
213
- envvar=service.HOSTS_PATH_ENV_VAR,
214
- callback=callbacks.convert_hosts_file,
215
- )
216
-
217
-
218
- class ReportToService:
219
- pass
220
-
221
-
222
- REPORT_TO_SERVICE = ReportToService()
223
-
224
-
225
- @schemathesis.command(short_help="Execute automated tests based on API specifications.", cls=CommandWithCustomHelp)
226
- @click.argument("schema", type=str)
227
- @click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
228
- @click.option(
229
- "--checks",
230
- "-c",
231
- 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)}'.",
235
- type=CHECKS_TYPE,
236
- default=DEFAULT_CHECKS_NAMES,
237
- cls=GroupedOption,
238
- group=ParameterGroup.validation,
239
- callback=callbacks.convert_checks,
240
- show_default=True,
241
- )
242
- @click.option(
243
- "--exclude-checks",
244
- multiple=True,
245
- help="Specifies the validation checks to skip during testing. "
246
- "Provide a comma-separated list of checks you wish to bypass.",
247
- type=EXCLUDE_CHECKS_TYPE,
248
- default=[],
249
- cls=GroupedOption,
250
- group=ParameterGroup.validation,
251
- callback=callbacks.convert_checks,
252
- show_default=True,
253
- )
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(
267
- "--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.",
271
- 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
- )
285
- @click.option(
286
- "-x",
287
- "--exitfirst",
288
- "exit_first",
289
- is_flag=True,
290
- default=False,
291
- help="Terminates the test suite immediately upon the first failure or error encountered.",
292
- show_default=True,
293
- )
294
- @click.option(
295
- "--max-failures",
296
- "max_failures",
297
- type=click.IntRange(min=1),
298
- help="Terminates the test suite after reaching a specified number of failures or errors.",
299
- show_default=True,
300
- )
301
- @click.option(
302
- "--dry-run",
303
- "dry_run",
304
- is_flag=True,
305
- default=False,
306
- help="Simulates test execution without making any actual requests, useful for validating data generation.",
307
- )
308
- @click.option(
309
- "--auth",
310
- "-a",
311
- help="Provides the server authentication details in the 'USER:PASSWORD' format.",
312
- type=str,
313
- callback=callbacks.validate_auth,
314
- )
315
- @click.option(
316
- "--auth-type",
317
- "-A",
318
- type=click.Choice(["basic", "digest"], case_sensitive=False),
319
- default="basic",
320
- help="Specifies the authentication method. Default is 'basic'.",
321
- show_default=True,
322
- )
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,
344
- type=str,
345
- callback=callbacks.validate_set_cookie,
346
- )
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,
352
- type=str,
353
- callback=callbacks.validate_set_path,
354
- )
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,
363
- )
364
- @click.option(
365
- "--endpoint",
366
- "-E",
367
- "endpoints",
368
- type=str,
369
- multiple=True,
370
- help=r"API operation path pattern (e.g., users/\d+).",
371
- callback=callbacks.validate_regex,
372
- cls=GroupedOption,
373
- group=ParameterGroup.filtering,
374
- )
375
- @click.option(
376
- "--method",
377
- "-M",
378
- "methods",
379
- type=str,
380
- multiple=True,
381
- help="HTTP method (e.g., GET, POST).",
382
- callback=callbacks.validate_regex,
383
- cls=GroupedOption,
384
- group=ParameterGroup.filtering,
385
- )
386
- @click.option(
387
- "--tag",
388
- "-T",
389
- "tags",
390
- type=str,
391
- multiple=True,
392
- help="Schema tag pattern.",
393
- callback=callbacks.validate_regex,
394
- cls=GroupedOption,
395
- group=ParameterGroup.filtering,
396
- )
397
- @click.option(
398
- "--operation-id",
399
- "-O",
400
- "operation_ids",
401
- type=str,
402
- multiple=True,
403
- help="OpenAPI operationId pattern.",
404
- 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,
462
- )
463
- @click.option(
464
- "--skip-deprecated-operations",
465
- help="Exclude deprecated API operations from testing.",
466
- is_flag=True,
467
- is_eager=True,
468
- default=False,
469
- show_default=True,
470
- cls=GroupedOption,
471
- group=ParameterGroup.filtering,
472
- )
473
- @click.option(
474
- "--junit-xml",
475
- help="Outputs a JUnit-XML style report at the specified file path.",
476
- type=click.File("w", encoding="utf-8"),
477
- )
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.",
493
- 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
- is_eager=True,
500
- default=False,
501
- hidden=True,
502
- show_default=True,
503
- )
504
- @click.option(
505
- "--show-trace",
506
- help="Displays complete traceback information for internal errors.",
507
- is_flag=True,
508
- is_eager=True,
509
- default=False,
510
- show_default=True,
511
- )
512
- @click.option(
513
- "--code-sample-style",
514
- help="Selects the code sample style for reproducing failures.",
515
- type=click.Choice([item.name for item in CodeSampleStyle]),
516
- default=CodeSampleStyle.default().name,
517
- callback=callbacks.convert_code_sample_style,
518
- )
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,
524
- )
525
- @click.option(
526
- "--cassette-preserve-exact-body-bytes",
527
- help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
528
- is_flag=True,
529
- callback=callbacks.validate_preserve_exact_body_bytes,
530
- )
531
- @click.option(
532
- "--store-network-log",
533
- help="Saves the test outcomes in a VCR-compatible format.",
534
- type=click.File("w", encoding="utf-8"),
535
- hidden=True,
536
- )
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"]),
542
- )
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,
549
- )
550
- @click.option(
551
- "--stateful",
552
- help="Enables or disables stateful testing features.",
553
- type=click.Choice([item.name for item in Stateful]),
554
- default=Stateful.links.name,
555
- callback=callbacks.convert_stateful,
556
- )
557
- @click.option(
558
- "--stateful-recursion-limit",
559
- help="Sets the recursion depth limit for stateful testing.",
560
- default=DEFAULT_STATEFUL_RECURSION_LIMIT,
561
- show_default=True,
562
- type=click.IntRange(1, 100),
563
- hidden=True,
564
- )
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"]),
569
- )
570
- @click.option(
571
- "--sanitize-output",
572
- type=bool,
573
- default=True,
574
- show_default=True,
575
- help="Enable or disable automatic output sanitization to obscure sensitive data.",
576
- )
577
- @click.option(
578
- "--contrib-unique-data",
579
- "contrib_unique_data",
580
- help="Forces the generation of unique test cases.",
581
- is_flag=True,
582
- default=False,
583
- show_default=True,
584
- )
585
- @click.option(
586
- "--contrib-openapi-formats-uuid",
587
- "contrib_openapi_formats_uuid",
588
- help="Enables support for the 'uuid' string format in OpenAPI.",
589
- is_flag=True,
590
- default=False,
591
- show_default=True,
592
- )
593
- @click.option(
594
- "--contrib-openapi-fill-missing-examples",
595
- "contrib_openapi_fill_missing_examples",
596
- help="Enables generation of random examples for API operations that do not have explicit examples defined.",
597
- is_flag=True,
598
- default=False,
599
- show_default=True,
600
- )
601
- @click.option(
602
- "--hypothesis-database",
603
- help="Configures storage for examples discovered by Hypothesis. "
604
- f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
605
- f"or specify a file path for persistent storage.",
606
- type=str,
607
- cls=GroupedOption,
608
- group=ParameterGroup.hypothesis,
609
- callback=callbacks.validate_hypothesis_database,
610
- )
611
- @click.option(
612
- "--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,
619
- )
620
- @click.option(
621
- "--hypothesis-derandomize",
622
- help="Enables deterministic mode in Hypothesis, which eliminates random variation between test runs.",
623
- is_flag=True,
624
- is_eager=True,
625
- default=None,
626
- show_default=True,
627
- cls=GroupedOption,
628
- group=ParameterGroup.hypothesis,
629
- )
630
- @click.option(
631
- "--hypothesis-max-examples",
632
- help="Sets the cap on the number of examples generated by Hypothesis for each API method/path pair.",
633
- type=click.IntRange(1),
634
- cls=GroupedOption,
635
- group=ParameterGroup.hypothesis,
636
- )
637
- @click.option(
638
- "--hypothesis-phases",
639
- help="Specifies which testing phases to execute.",
640
- type=CsvEnumChoice(Phase),
641
- cls=GroupedOption,
642
- group=ParameterGroup.hypothesis,
643
- )
644
- @click.option(
645
- "--hypothesis-no-phases",
646
- help="Specifies which testing phases to exclude from execution.",
647
- type=CsvEnumChoice(Phase),
648
- cls=GroupedOption,
649
- group=ParameterGroup.hypothesis,
650
- )
651
- @click.option(
652
- "--hypothesis-report-multiple-bugs",
653
- help="If set, only the most easily reproducible exception will be reported when multiple issues are found.",
654
- type=bool,
655
- cls=GroupedOption,
656
- group=ParameterGroup.hypothesis,
657
- )
658
- @click.option(
659
- "--hypothesis-seed",
660
- help="Sets a seed value for Hypothesis, ensuring reproducibility across test runs.",
661
- type=int,
662
- cls=GroupedOption,
663
- group=ParameterGroup.hypothesis,
664
- )
665
- @click.option(
666
- "--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",
669
- type=CsvEnumChoice(HealthCheck),
670
- cls=GroupedOption,
671
- group=ParameterGroup.hypothesis,
672
- )
673
- @click.option(
674
- "--hypothesis-verbosity",
675
- help="Controls the verbosity level of Hypothesis output.",
676
- type=click.Choice([item.name for item in Verbosity]),
677
- 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,
697
- )
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,
704
- )
705
- @click.option(
706
- "--schemathesis-io-token",
707
- help="Schemathesis.io authentication token.",
708
- type=str,
709
- envvar=service.TOKEN_ENV_VAR,
710
- )
711
- @click.option(
712
- "--schemathesis-io-url",
713
- help="Schemathesis.io base URL.",
714
- default=service.DEFAULT_URL,
715
- type=str,
716
- envvar=service.URL_ENV_VAR,
717
- )
718
- @click.option(
719
- "--schemathesis-io-telemetry",
720
- help="Controls whether you send anonymized CLI usage data to Schemathesis.io along with your report.",
721
- type=str,
722
- default="true",
723
- show_default=True,
724
- callback=callbacks.convert_boolean_string,
725
- envvar=service.TELEMETRY_ENV_VAR,
726
- )
727
- @with_hosts_file
728
- @click.option("--verbosity", "-v", help="Increase verbosity of the output.", count=True)
729
- @click.pass_context
730
- def run(
731
- ctx: click.Context,
732
- schema: str,
733
- api_name: str | None,
734
- auth: tuple[str, str] | None,
735
- auth_type: str,
736
- headers: dict[str, str],
737
- set_query: dict[str, str],
738
- set_header: dict[str, str],
739
- set_cookie: dict[str, str],
740
- set_path: dict[str, str],
741
- experimental: list,
742
- checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
743
- exclude_checks: Iterable[str] = (),
744
- data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
745
- max_response_time: int | None = None,
746
- targets: Iterable[str] = DEFAULT_TARGETS_NAMES,
747
- exit_first: bool = False,
748
- max_failures: int | None = None,
749
- 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,
754
- workers_num: int = DEFAULT_WORKERS,
755
- base_url: str | None = None,
756
- app: str | None = None,
757
- request_timeout: int | None = None,
758
- request_tls_verify: bool = True,
759
- request_cert: str | None = None,
760
- request_cert_key: str | None = None,
761
- request_proxy: str | None = None,
762
- validate_schema: bool = True,
763
- skip_deprecated_operations: bool = False,
764
- junit_xml: click.utils.LazyFile | None = None,
765
- debug_output_file: click.utils.LazyFile | None = None,
766
- show_errors_tracebacks: bool = False,
767
- show_trace: bool = False,
768
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
769
- cassette_path: click.utils.LazyFile | None = None,
770
- cassette_preserve_exact_body_bytes: bool = False,
771
- store_network_log: click.utils.LazyFile | None = None,
772
- wait_for_schema: float | None = None,
773
- fixups: tuple[str] = (), # type: ignore
774
- rate_limit: str | None = None,
775
- stateful: Stateful | None = None,
776
- stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
777
- force_schema_version: str | None = None,
778
- sanitize_output: bool = True,
779
- contrib_unique_data: bool = False,
780
- contrib_openapi_formats_uuid: bool = False,
781
- contrib_openapi_fill_missing_examples: bool = False,
782
- hypothesis_database: str | None = None,
783
- hypothesis_deadline: int | NotSet | None = None,
784
- hypothesis_derandomize: bool | None = None,
785
- hypothesis_max_examples: int | None = None,
786
- hypothesis_phases: list[Phase] | None = None,
787
- hypothesis_no_phases: list[Phase] | None = None,
788
- hypothesis_report_multiple_bugs: bool | None = None,
789
- hypothesis_suppress_health_check: list[HealthCheck] | None = None,
790
- hypothesis_seed: int | None = None,
791
- hypothesis_verbosity: hypothesis.Verbosity | None = None,
792
- verbosity: int = 0,
793
- no_color: bool = False,
794
- report_value: str | None = None,
795
- generation_allow_x00: bool = True,
796
- generation_codec: str = "utf-8",
797
- schemathesis_io_token: str | None = None,
798
- schemathesis_io_url: str = service.DEFAULT_URL,
799
- schemathesis_io_telemetry: bool = True,
800
- hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
801
- force_color: bool = False,
802
- ) -> None:
803
- """Run tests against an API using a specified SCHEMA.
804
-
805
- [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications.
806
-
807
- [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io.
808
- """
809
- _hypothesis_phases: list[hypothesis.Phase] | None = None
810
- if hypothesis_phases is not None:
811
- _hypothesis_phases = [phase.as_hypothesis() for phase in hypothesis_phases]
812
- if hypothesis_no_phases is not None:
813
- raise click.UsageError(PHASES_INVALID_USAGE_MESSAGE)
814
- if hypothesis_no_phases is not None:
815
- _hypothesis_phases = Phase.filter_from_all(hypothesis_no_phases)
816
- _hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
817
- if hypothesis_suppress_health_check is not None:
818
- _hypothesis_suppress_health_check = [
819
- health_check.as_hypothesis() for health_check in hypothesis_suppress_health_check
820
- ]
821
-
822
- if contrib_unique_data:
823
- click.secho(DEPRECATED_CONTRIB_UNIQUE_DATA_OPTION_WARNING, fg="yellow")
824
-
825
- if show_errors_tracebacks:
826
- click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
827
- show_trace = show_errors_tracebacks
828
-
829
- # Enable selected experiments
830
- for experiment in experimental:
831
- experiment.enable()
832
-
833
- override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
834
-
835
- generation_config = generation.GenerationConfig(allow_x00=generation_allow_x00, codec=generation_codec)
836
-
837
- report: ReportToService | click.utils.LazyFile | None
838
- if report_value is None:
839
- report = None
840
- elif report_value:
841
- report = click.utils.LazyFile(report_value, mode="wb")
11
+ def add_group(name: str, *, index: int | None = None) -> Group:
12
+ """Add a custom options group to `st run`."""
13
+ if index is not None:
14
+ GROUPS.insert(index, name)
842
15
  else:
843
- report = REPORT_TO_SERVICE
844
- started_at = current_datetime()
845
-
846
- if no_color and force_color:
847
- raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
848
- decide_color_output(ctx, no_color, force_color)
849
-
850
- check_auth(auth, headers, override)
851
- selected_targets = tuple(target for target in targets_module.ALL_TARGETS if target.__name__ in targets)
852
-
853
- if store_network_log and cassette_path:
854
- raise click.UsageError(CASSETTES_PATH_INVALID_USAGE_MESSAGE)
855
- if store_network_log is not None:
856
- click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
857
- cassette_path = store_network_log
858
-
859
- schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
860
- token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
861
- schema_kind = callbacks.parse_schema_kind(schema, app)
862
- callbacks.validate_schema(schema, schema_kind, base_url=base_url, dry_run=dry_run, app=app, api_name=api_name)
863
- client = None
864
- schema_or_location: str | dict[str, Any] = schema
865
- if schema_kind == callbacks.SchemaInputKind.NAME:
866
- api_name = schema
867
- if (
868
- not isinstance(report, click.utils.LazyFile)
869
- and api_name is not None
870
- and schema_kind == callbacks.SchemaInputKind.NAME
871
- ):
872
- from ..service.client import ServiceClient
873
-
874
- client = ServiceClient(base_url=schemathesis_io_url, token=token)
875
- # It is assigned above
876
- if token is not None or schema_kind == callbacks.SchemaInputKind.NAME:
877
- if token is None:
878
- hostname = (
879
- "Schemathesis.io"
880
- if schemathesis_io_hostname == service.DEFAULT_HOSTNAME
881
- else schemathesis_io_hostname
882
- )
883
- click.secho(f"Missing authentication for {hostname} upload", bold=True, fg="red")
884
- click.echo(
885
- f"\nYou've specified an API name, suggesting you want to upload data to {bold(hostname)}. "
886
- "However, your CLI is not currently authenticated."
887
- )
888
- output.default.display_service_unauthorized(hostname)
889
- raise click.exceptions.Exit(1) from None
890
- name: str = cast(str, api_name)
891
- import requests
892
-
893
- try:
894
- details = client.get_api_details(name)
895
- # Replace config values with ones loaded from the service
896
- schema_or_location = details.specification.schema
897
- default_environment = details.default_environment
898
- base_url = base_url or (default_environment.url if default_environment else None)
899
- except requests.HTTPError as exc:
900
- handle_service_error(exc, name)
901
- if report is REPORT_TO_SERVICE and not client:
902
- from ..service.client import ServiceClient
903
-
904
- # Upload without connecting data to a certain API
905
- client = ServiceClient(base_url=schemathesis_io_url, token=token)
906
- host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
907
-
908
- if "all" in checks:
909
- selected_checks = checks_module.ALL_CHECKS
910
- else:
911
- selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
912
-
913
- selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
914
-
915
- if fixups:
916
- if "all" in fixups:
917
- _fixups.install()
918
- else:
919
- _fixups.install(fixups)
920
-
921
- if contrib_unique_data:
922
- contrib.unique_data.install()
923
- if contrib_openapi_formats_uuid:
924
- contrib.openapi.formats.uuid.install()
925
- if contrib_openapi_fill_missing_examples:
926
- contrib.openapi.fill_missing_examples.install()
927
-
928
- hypothesis_settings = prepare_hypothesis_settings(
929
- database=hypothesis_database,
930
- deadline=hypothesis_deadline,
931
- derandomize=hypothesis_derandomize,
932
- max_examples=hypothesis_max_examples,
933
- phases=_hypothesis_phases,
934
- report_multiple_bugs=hypothesis_report_multiple_bugs,
935
- suppress_health_check=_hypothesis_suppress_health_check,
936
- verbosity=hypothesis_verbosity,
937
- )
938
- event_stream = into_event_stream(
939
- schema_or_location,
940
- app=app,
941
- base_url=base_url,
942
- started_at=started_at,
943
- validate_schema=validate_schema,
944
- skip_deprecated_operations=skip_deprecated_operations,
945
- data_generation_methods=data_generation_methods,
946
- force_schema_version=force_schema_version,
947
- request_tls_verify=request_tls_verify,
948
- request_proxy=request_proxy,
949
- request_cert=prepare_request_cert(request_cert, request_cert_key),
950
- wait_for_schema=wait_for_schema,
951
- auth=auth,
952
- auth_type=auth_type,
953
- override=override,
954
- 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
- request_timeout=request_timeout,
960
- seed=hypothesis_seed,
961
- exit_first=exit_first,
962
- max_failures=max_failures,
963
- dry_run=dry_run,
964
- store_interactions=cassette_path is not None,
965
- checks=selected_checks,
966
- max_response_time=max_response_time,
967
- targets=selected_targets,
968
- workers_num=workers_num,
969
- rate_limit=rate_limit,
970
- stateful=stateful,
971
- stateful_recursion_limit=stateful_recursion_limit,
972
- hypothesis_settings=hypothesis_settings,
973
- generation_config=generation_config,
974
- )
975
- execute(
976
- event_stream,
977
- hypothesis_settings=hypothesis_settings,
978
- workers_num=workers_num,
979
- rate_limit=rate_limit,
980
- show_trace=show_trace,
981
- wait_for_schema=wait_for_schema,
982
- validate_schema=validate_schema,
983
- cassette_path=cassette_path,
984
- cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
985
- junit_xml=junit_xml,
986
- verbosity=verbosity,
987
- code_sample_style=code_sample_style,
988
- data_generation_methods=data_generation_methods,
989
- debug_output_file=debug_output_file,
990
- sanitize_output=sanitize_output,
991
- host_data=host_data,
992
- client=client,
993
- report=report,
994
- telemetry=schemathesis_io_telemetry,
995
- api_name=api_name,
996
- location=schema,
997
- base_url=base_url,
998
- started_at=started_at,
999
- )
1000
-
1001
-
1002
- def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
1003
- if cert is not None and key is not None:
1004
- return cert, key
1005
- return cert
1006
-
1007
-
1008
- @dataclass
1009
- class LoaderConfig:
1010
- """Container for API loader parameters.
1011
-
1012
- The main goal is to avoid too many parameters in function signatures.
1013
- """
1014
-
1015
- schema_or_location: str | dict[str, Any]
1016
- app: Any
1017
- base_url: str | None
1018
- validate_schema: bool
1019
- skip_deprecated_operations: bool
1020
- data_generation_methods: tuple[DataGenerationMethod, ...]
1021
- force_schema_version: str | None
1022
- request_tls_verify: bool | str
1023
- request_proxy: str | None
1024
- request_cert: RequestCert | None
1025
- wait_for_schema: float | None
1026
- rate_limit: str | None
1027
- # Network request parameters
1028
- auth: tuple[str, str] | None
1029
- auth_type: str | None
1030
- 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
-
1037
-
1038
- def into_event_stream(
1039
- schema_or_location: str | dict[str, Any],
1040
- *,
1041
- app: Any,
1042
- base_url: str | None,
1043
- started_at: str,
1044
- validate_schema: bool,
1045
- skip_deprecated_operations: bool,
1046
- data_generation_methods: tuple[DataGenerationMethod, ...],
1047
- force_schema_version: str | None,
1048
- request_tls_verify: bool | str,
1049
- request_proxy: str | None,
1050
- request_cert: RequestCert | None,
1051
- # Network request parameters
1052
- auth: tuple[str, str] | None,
1053
- auth_type: str | None,
1054
- override: CaseOverride,
1055
- headers: dict[str, str] | None,
1056
- request_timeout: int | None,
1057
- 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,
1063
- # Runtime behavior
1064
- checks: Iterable[CheckFunction],
1065
- max_response_time: int | None,
1066
- targets: Iterable[Target],
1067
- workers_num: int,
1068
- hypothesis_settings: hypothesis.settings | None,
1069
- generation_config: generation.GenerationConfig,
1070
- seed: int | None,
1071
- exit_first: bool,
1072
- max_failures: int | None,
1073
- rate_limit: str | None,
1074
- dry_run: bool,
1075
- store_interactions: bool,
1076
- stateful: Stateful | None,
1077
- stateful_recursion_limit: int,
1078
- ) -> Generator[events.ExecutionEvent, None, None]:
1079
- try:
1080
- if app is not None:
1081
- app = load_app(app)
1082
- config = LoaderConfig(
1083
- schema_or_location=schema_or_location,
1084
- app=app,
1085
- base_url=base_url,
1086
- validate_schema=validate_schema,
1087
- skip_deprecated_operations=skip_deprecated_operations,
1088
- data_generation_methods=data_generation_methods,
1089
- force_schema_version=force_schema_version,
1090
- request_proxy=request_proxy,
1091
- request_tls_verify=request_tls_verify,
1092
- request_cert=request_cert,
1093
- wait_for_schema=wait_for_schema,
1094
- rate_limit=rate_limit,
1095
- auth=auth,
1096
- auth_type=auth_type,
1097
- headers=headers,
1098
- endpoint=endpoint or None,
1099
- method=method or None,
1100
- tag=tag or None,
1101
- operation_id=operation_id or None,
1102
- )
1103
- loaded_schema = load_schema(config)
1104
- run_probes(loaded_schema, config)
1105
- yield from runner.from_schema(
1106
- loaded_schema,
1107
- auth=auth,
1108
- auth_type=auth_type,
1109
- override=override,
1110
- headers=headers,
1111
- request_timeout=request_timeout,
1112
- request_tls_verify=request_tls_verify,
1113
- request_proxy=request_proxy,
1114
- request_cert=request_cert,
1115
- seed=seed,
1116
- exit_first=exit_first,
1117
- max_failures=max_failures,
1118
- started_at=started_at,
1119
- dry_run=dry_run,
1120
- store_interactions=store_interactions,
1121
- checks=checks,
1122
- max_response_time=max_response_time,
1123
- targets=targets,
1124
- workers_num=workers_num,
1125
- stateful=stateful,
1126
- stateful_recursion_limit=stateful_recursion_limit,
1127
- hypothesis_settings=hypothesis_settings,
1128
- generation_config=generation_config,
1129
- ).execute()
1130
- except SchemaError as error:
1131
- yield events.InternalError.from_schema_error(error)
1132
- except Exception as exc:
1133
- yield events.InternalError.from_exc(exc)
1134
-
1135
-
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
- def load_schema(config: LoaderConfig) -> BaseSchema:
1150
- """Automatically load API schema."""
1151
- first: Callable[[LoaderConfig], BaseSchema]
1152
- second: Callable[[LoaderConfig], BaseSchema]
1153
- if is_probably_graphql(config.schema_or_location):
1154
- # Try GraphQL first, then fallback to Open API
1155
- first, second = (_load_graphql_schema, _load_openapi_schema)
1156
- else:
1157
- # Try Open API first, then fallback to GraphQL
1158
- first, second = (_load_openapi_schema, _load_graphql_schema)
1159
- return _try_load_schema(config, first, second)
1160
-
1161
-
1162
- def should_try_more(exc: SchemaError) -> bool:
1163
- import requests
1164
- from yaml.reader import ReaderError
1165
-
1166
- if isinstance(exc.__cause__, ReaderError) and "characters are not allowed" in str(exc.__cause__):
1167
- return False
1168
-
1169
- # We should not try other loaders for cases when we can't even establish connection
1170
- return not isinstance(exc.__cause__, requests.exceptions.ConnectionError) and exc.type not in (
1171
- SchemaErrorType.OPEN_API_INVALID_SCHEMA,
1172
- SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
1173
- SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
1174
- SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION,
1175
- )
1176
-
1177
-
1178
- Loader = Callable[[LoaderConfig], "BaseSchema"]
1179
-
1180
-
1181
- def _try_load_schema(config: LoaderConfig, first: Loader, second: Loader) -> BaseSchema:
1182
- from urllib3.exceptions import InsecureRequestWarning
1183
-
1184
- with warnings.catch_warnings():
1185
- warnings.simplefilter("ignore", InsecureRequestWarning)
1186
- try:
1187
- return first(config)
1188
- except SchemaError as exc:
1189
- if config.force_schema_version is None and should_try_more(exc):
1190
- try:
1191
- return second(config)
1192
- except Exception as second_exc:
1193
- if is_specific_exception(second, second_exc):
1194
- raise second_exc
1195
- # Re-raise the original error
1196
- raise exc
1197
-
1198
-
1199
- def is_specific_exception(loader: Loader, exc: Exception) -> bool:
1200
- return (
1201
- loader is _load_graphql_schema
1202
- and isinstance(exc, SchemaError)
1203
- and exc.type == SchemaErrorType.GRAPHQL_INVALID_SCHEMA
1204
- # In some cases it is not clear that the schema is even supposed to be GraphQL, e.g. an empty input
1205
- and "Syntax Error: Unexpected <EOF>." not in exc.extras
1206
- )
1207
-
1208
-
1209
- def _load_graphql_schema(config: LoaderConfig) -> GraphQLSchema:
1210
- loader = detect_loader(config.schema_or_location, config.app, is_openapi=False)
1211
- kwargs = get_graphql_loader_kwargs(loader, config)
1212
- return loader(config.schema_or_location, **kwargs)
1213
-
1214
-
1215
- def _load_openapi_schema(config: LoaderConfig) -> BaseSchema:
1216
- loader = detect_loader(config.schema_or_location, config.app, is_openapi=True)
1217
- kwargs = get_loader_kwargs(loader, config)
1218
- return loader(config.schema_or_location, **kwargs)
1219
-
1220
-
1221
- def detect_loader(schema_or_location: str | dict[str, Any], app: Any, is_openapi: bool) -> Callable:
1222
- """Detect API schema loader."""
1223
- if isinstance(schema_or_location, str):
1224
- if file_exists(schema_or_location):
1225
- # If there is an existing file with the given name,
1226
- # then it is likely that the user wants to load API schema from there
1227
- return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
1228
- if app is not None and not urlparse(schema_or_location).netloc:
1229
- # App is passed & location is relative
1230
- return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
1231
- # Default behavior
1232
- return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
1233
- return oas_loaders.from_dict if is_openapi else gql_loaders.from_dict # type: ignore
1234
-
1235
-
1236
- def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
1237
- """Detect the proper set of parameters for a loader."""
1238
- # These kwargs are shared by all loaders
1239
- kwargs = {
1240
- "app": config.app,
1241
- "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
- "validate_schema": config.validate_schema,
1248
- "force_schema_version": config.force_schema_version,
1249
- "data_generation_methods": config.data_generation_methods,
1250
- "rate_limit": config.rate_limit,
1251
- }
1252
- if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
1253
- kwargs["headers"] = config.headers
1254
- if loader in (oas_loaders.from_uri, oas_loaders.from_aiohttp):
1255
- _add_requests_kwargs(kwargs, config)
1256
- return kwargs
1257
-
1258
-
1259
- def get_graphql_loader_kwargs(
1260
- loader: Callable,
1261
- config: LoaderConfig,
1262
- ) -> dict[str, Any]:
1263
- """Detect the proper set of parameters for a loader."""
1264
- # These kwargs are shared by all loaders
1265
- kwargs = {
1266
- "app": config.app,
1267
- "base_url": config.base_url,
1268
- "data_generation_methods": config.data_generation_methods,
1269
- "rate_limit": config.rate_limit,
1270
- }
1271
- if loader not in (gql_loaders.from_path, gql_loaders.from_dict):
1272
- kwargs["headers"] = config.headers
1273
- if loader is gql_loaders.from_url:
1274
- _add_requests_kwargs(kwargs, config)
1275
- return kwargs
1276
-
1277
-
1278
- def _add_requests_kwargs(kwargs: dict[str, Any], config: LoaderConfig) -> None:
1279
- kwargs["verify"] = config.request_tls_verify
1280
- if config.request_cert is not None:
1281
- kwargs["cert"] = config.request_cert
1282
- if config.auth is not None:
1283
- kwargs["auth"] = get_requests_auth(config.auth, config.auth_type)
1284
- if config.wait_for_schema is not None:
1285
- kwargs["wait_for_schema"] = config.wait_for_schema
1286
-
1287
-
1288
- def is_probably_graphql(schema_or_location: str | dict[str, Any]) -> bool:
1289
- """Detect whether it is likely that the given location is a GraphQL endpoint."""
1290
- if isinstance(schema_or_location, str):
1291
- return schema_or_location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
1292
- return "__schema" in schema_or_location or (
1293
- "data" in schema_or_location and "__schema" in schema_or_location["data"]
1294
- )
1295
-
1296
-
1297
- def check_auth(auth: tuple[str, str] | None, headers: dict[str, str], override: CaseOverride) -> None:
1298
- auth_is_set = auth is not None
1299
- header_is_set = "authorization" in {header.lower() for header in headers}
1300
- override_is_set = "authorization" in {header.lower() for header in override.headers}
1301
- if len([is_set for is_set in (auth_is_set, header_is_set, override_is_set) if is_set]) > 1:
1302
- message = "The "
1303
- used = []
1304
- if auth_is_set:
1305
- used.append("`--auth`")
1306
- if header_is_set:
1307
- used.append("`--header`")
1308
- if override_is_set:
1309
- used.append("`--set-header`")
1310
- message += " and ".join(used)
1311
- message += " options were both used to set the 'Authorization' header, which is not permitted."
1312
- raise click.BadParameter(message)
1313
-
1314
-
1315
- def get_output_handler(workers_num: int) -> EventHandler:
1316
- if workers_num > 1:
1317
- output_style = OutputStyle.short
1318
- else:
1319
- output_style = OutputStyle.default
1320
- return output_style.value()
1321
-
1322
-
1323
- def load_hook(module_name: str) -> None:
1324
- """Load the given hook by importing it."""
1325
- try:
1326
- sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
1327
- __import__(module_name)
1328
- except Exception as exc:
1329
- click.secho("Unable to load Schemathesis extension hooks", fg="red", bold=True)
1330
- formatted_module_name = bold(f"'{module_name}'")
1331
- if isinstance(exc, ModuleNotFoundError) and exc.name == module_name:
1332
- click.echo(
1333
- f"\nAn attempt to import the module {formatted_module_name} failed because it could not be found."
1334
- )
1335
- click.echo("\nEnsure the module name is correctly spelled and reachable from the current directory.")
1336
- else:
1337
- click.echo(f"\nAn error occurred while importing the module {formatted_module_name}. Traceback:")
1338
- trace = extract_nth_traceback(exc.__traceback__, 1)
1339
- lines = traceback.format_exception(type(exc), exc, trace)
1340
- message = "".join(lines).strip()
1341
- click.secho(f"\n{message}", fg="red")
1342
- click.echo(f"\nFor more information on how to work with hooks, visit {EXTENSIONS_DOCUMENTATION_URL}")
1343
- raise click.exceptions.Exit(1) from None
1344
-
1345
-
1346
- class OutputStyle(Enum):
1347
- """Provide different output styles."""
1348
-
1349
- default = output.default.DefaultOutputStyleHandler
1350
- short = output.short.ShortOutputStyleHandler
1351
-
1352
-
1353
- def execute(
1354
- event_stream: Generator[events.ExecutionEvent, None, None],
1355
- *,
1356
- hypothesis_settings: hypothesis.settings,
1357
- workers_num: int,
1358
- rate_limit: str | None,
1359
- show_trace: bool,
1360
- wait_for_schema: float | None,
1361
- validate_schema: bool,
1362
- cassette_path: click.utils.LazyFile | None,
1363
- cassette_preserve_exact_body_bytes: bool,
1364
- junit_xml: click.utils.LazyFile | None,
1365
- verbosity: int,
1366
- code_sample_style: CodeSampleStyle,
1367
- data_generation_methods: tuple[DataGenerationMethod, ...],
1368
- debug_output_file: click.utils.LazyFile | None,
1369
- sanitize_output: bool,
1370
- host_data: service.hosts.HostData,
1371
- client: ServiceClient | None,
1372
- report: ReportToService | click.utils.LazyFile | None,
1373
- telemetry: bool,
1374
- api_name: str | None,
1375
- location: str,
1376
- base_url: str | None,
1377
- started_at: str,
1378
- ) -> None:
1379
- """Execute a prepared runner by drawing events from it and passing to a proper handler."""
1380
- handlers: list[EventHandler] = []
1381
- report_context: ServiceReportContext | FileReportContext | None = None
1382
- report_queue: Queue
1383
- if client:
1384
- # If API name is specified, validate it
1385
- report_queue = Queue()
1386
- report_context = ServiceReportContext(queue=report_queue, service_base_url=client.base_url)
1387
- handlers.append(
1388
- service.ServiceReportHandler(
1389
- client=client,
1390
- host_data=host_data,
1391
- api_name=api_name,
1392
- location=location,
1393
- base_url=base_url,
1394
- started_at=started_at,
1395
- out_queue=report_queue,
1396
- telemetry=telemetry,
1397
- )
1398
- )
1399
- elif isinstance(report, click.utils.LazyFile):
1400
- _open_file(report)
1401
- report_queue = Queue()
1402
- report_context = FileReportContext(queue=report_queue, filename=report.name)
1403
- handlers.append(
1404
- service.FileReportHandler(
1405
- file_handle=report,
1406
- api_name=api_name,
1407
- location=location,
1408
- base_url=base_url,
1409
- started_at=started_at,
1410
- out_queue=report_queue,
1411
- telemetry=telemetry,
1412
- )
1413
- )
1414
- if junit_xml is not None:
1415
- _open_file(junit_xml)
1416
- handlers.append(JunitXMLHandler(junit_xml))
1417
- if debug_output_file is not None:
1418
- _open_file(debug_output_file)
1419
- handlers.append(DebugOutputHandler(debug_output_file))
1420
- if cassette_path is not None:
1421
- # This handler should be first to have logs writing completed when the output handler will display statistic
1422
- _open_file(cassette_path)
1423
- handlers.append(
1424
- cassettes.CassetteWriter(cassette_path, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes)
1425
- )
1426
- handlers.append(get_output_handler(workers_num))
1427
- if sanitize_output:
1428
- handlers.insert(0, SanitizationHandler())
1429
- execution_context = ExecutionContext(
1430
- hypothesis_settings=hypothesis_settings,
1431
- workers_num=workers_num,
1432
- rate_limit=rate_limit,
1433
- show_trace=show_trace,
1434
- wait_for_schema=wait_for_schema,
1435
- validate_schema=validate_schema,
1436
- cassette_path=cassette_path.name if cassette_path is not None else None,
1437
- junit_xml_file=junit_xml.name if junit_xml is not None else None,
1438
- verbosity=verbosity,
1439
- code_sample_style=code_sample_style,
1440
- report=report_context,
1441
- )
1442
-
1443
- def shutdown() -> None:
1444
- for _handler in handlers:
1445
- _handler.shutdown()
1446
-
1447
- GLOBAL_HOOK_DISPATCHER.dispatch("after_init_cli_run_handlers", HookContext(), handlers, execution_context)
1448
- event = None
1449
- try:
1450
- for event in event_stream:
1451
- for handler in handlers:
1452
- try:
1453
- handler.handle_event(execution_context, event)
1454
- except Exception as exc:
1455
- # `Abort` is used for handled errors
1456
- if not isinstance(exc, click.Abort):
1457
- display_handler_error(handler, exc)
1458
- raise
1459
- except Exception as exc:
1460
- if isinstance(exc, click.Abort):
1461
- # To avoid showing "Aborted!" message, which is the default behavior in Click
1462
- sys.exit(1)
1463
- raise
1464
- finally:
1465
- shutdown()
1466
- if event is not None and event.is_terminal:
1467
- exit_code = get_exit_code(event)
1468
- sys.exit(exit_code)
1469
- # Event stream did not finish with a terminal event. Only possible if the handler is broken
1470
- click.secho("Unexpected error", fg="red")
1471
- sys.exit(1)
1472
-
1473
-
1474
- def _open_file(file: click.utils.LazyFile) -> None:
1475
- from ..utils import _ensure_parent
1476
-
1477
- try:
1478
- _ensure_parent(file.name, fail_silently=False)
1479
- except OSError as exc:
1480
- raise click.BadParameter(f"'{file.name}': {exc.strerror}") from exc
1481
- try:
1482
- file.open()
1483
- except click.FileError as exc:
1484
- raise click.BadParameter(exc.format_message()) from exc
1485
-
1486
-
1487
- def is_built_in_handler(handler: EventHandler) -> bool:
1488
- # Look for exact instances, not subclasses
1489
- return any(
1490
- type(handler) is class_
1491
- for class_ in (
1492
- output.default.DefaultOutputStyleHandler,
1493
- output.short.ShortOutputStyleHandler,
1494
- service.FileReportHandler,
1495
- service.ServiceReportHandler,
1496
- DebugOutputHandler,
1497
- cassettes.CassetteWriter,
1498
- JunitXMLHandler,
1499
- SanitizationHandler,
1500
- )
1501
- )
1502
-
1503
-
1504
- def display_handler_error(handler: EventHandler, exc: Exception) -> None:
1505
- """Display error that happened within."""
1506
- is_built_in = is_built_in_handler(handler)
1507
- if is_built_in:
1508
- click.secho("Internal Error", fg="red", bold=True)
1509
- click.secho("\nSchemathesis encountered an unexpected issue.")
1510
- trace = exc.__traceback__
1511
- else:
1512
- click.secho("CLI Handler Error", fg="red", bold=True)
1513
- click.echo(f"\nAn error occurred within your custom CLI handler `{bold(handler.__class__.__name__)}`.")
1514
- trace = extract_nth_traceback(exc.__traceback__, 1)
1515
- lines = traceback.format_exception(type(exc), exc, trace)
1516
- message = "".join(lines).strip()
1517
- click.secho(f"\n{message}", fg="red")
1518
- if is_built_in:
1519
- click.echo(
1520
- f"\nWe apologize for the inconvenience. This appears to be an internal issue.\n"
1521
- f"Please consider reporting this error to our issue tracker:\n\n {ISSUE_TRACKER_URL}."
1522
- )
1523
- else:
1524
- click.echo(
1525
- f"\nFor more information on implementing extensions for Schemathesis CLI, visit {EXTENSIONS_DOCUMENTATION_URL}"
1526
- )
1527
-
1528
-
1529
- def handle_service_error(exc: requests.HTTPError, api_name: str) -> NoReturn:
1530
- import requests
1531
-
1532
- response = cast(requests.Response, exc.response)
1533
- if response.status_code == 403:
1534
- error_message(response.json()["detail"])
1535
- elif response.status_code == 404:
1536
- error_message(f"API with name `{api_name}` not found!")
1537
- else:
1538
- output.default.display_service_error(service.Error(exc), message_prefix="❌ ")
1539
- sys.exit(1)
1540
-
1541
-
1542
- def get_exit_code(event: events.ExecutionEvent) -> int:
1543
- if isinstance(event, events.Finished):
1544
- if event.has_failures or event.has_errors:
1545
- return 1
1546
- return 0
1547
- # Practically not possible. May occur only if the output handler is broken - in this case we still will have the
1548
- # right exit code.
1549
- return 1
1550
-
1551
-
1552
- @schemathesis.command(short_help="Replay requests from a saved cassette.")
1553
- @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)
1561
- @with_request_tls_verify
1562
- @with_request_proxy
1563
- @with_request_cert
1564
- @with_request_cert_key
1565
- @click.pass_context
1566
- def replay(
1567
- ctx: click.Context,
1568
- cassette_path: str,
1569
- id_: str | None,
1570
- status: str | None = None,
1571
- uri: str | None = None,
1572
- method: str | None = None,
1573
- no_color: bool = False,
1574
- verbosity: int = 0,
1575
- request_tls_verify: bool = True,
1576
- request_cert: str | None = None,
1577
- request_cert_key: str | None = None,
1578
- request_proxy: str | None = None,
1579
- force_color: bool = False,
1580
- ) -> None:
1581
- """Replay a cassette.
1582
-
1583
- Cassettes in VCR-compatible format can be replayed.
1584
- For example, ones that are recorded with the ``--cassette-path`` option of the `st run` command.
1585
- """
1586
- if no_color and force_color:
1587
- raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
1588
- decide_color_output(ctx, no_color, force_color)
1589
-
1590
- click.secho(f"{bold('Replaying cassette')}: {cassette_path}")
1591
- with open(cassette_path, "rb") as fd:
1592
- cassette = load_yaml(fd)
1593
- click.secho(f"{bold('Total interactions')}: {len(cassette['http_interactions'])}\n")
1594
- for replayed in cassettes.replay(
1595
- cassette,
1596
- id_=id_,
1597
- status=status,
1598
- uri=uri,
1599
- method=method,
1600
- request_tls_verify=request_tls_verify,
1601
- request_cert=prepare_request_cert(request_cert, request_cert_key),
1602
- request_proxy=request_proxy,
1603
- ):
1604
- click.secho(f" {bold('ID')} : {replayed.interaction['id']}")
1605
- click.secho(f" {bold('URI')} : {replayed.interaction['request']['uri']}")
1606
- click.secho(f" {bold('Old status code')} : {replayed.interaction['response']['status']['code']}")
1607
- click.secho(f" {bold('New status code')} : {replayed.response.status_code}")
1608
- if verbosity > 0:
1609
- data = replayed.interaction["response"]
1610
- old_body = ""
1611
- # Body may be missing for 204 responses
1612
- if "body" in data:
1613
- if "base64_string" in data["body"]:
1614
- content = data["body"]["base64_string"]
1615
- if content:
1616
- old_body = base64.b64decode(content).decode(errors="replace")
1617
- else:
1618
- old_body = data["body"]["string"]
1619
- click.secho(f" {bold('Old payload')} : {old_body}")
1620
- click.secho(f" {bold('New payload')} : {replayed.response.text}")
1621
- click.echo()
1622
-
1623
-
1624
- @schemathesis.command(short_help="Upload report to Schemathesis.io.")
1625
- @click.argument("report", type=click.File(mode="rb"))
1626
- @click.option(
1627
- "--schemathesis-io-token",
1628
- help="Schemathesis.io authentication token.",
1629
- type=str,
1630
- envvar=service.TOKEN_ENV_VAR,
1631
- )
1632
- @click.option(
1633
- "--schemathesis-io-url",
1634
- help="Schemathesis.io base URL.",
1635
- default=service.DEFAULT_URL,
1636
- type=str,
1637
- envvar=service.URL_ENV_VAR,
1638
- )
1639
- @with_request_tls_verify
1640
- @with_hosts_file
1641
- def upload(
1642
- report: io.BufferedReader,
1643
- hosts_file: str,
1644
- request_tls_verify: bool = True,
1645
- schemathesis_io_url: str = service.DEFAULT_URL,
1646
- schemathesis_io_token: str | None = None,
1647
- ) -> None:
1648
- """Upload report to Schemathesis.io."""
1649
- from ..service.client import ServiceClient
1650
- from ..service.models import UploadResponse, UploadSource
1651
-
1652
- schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
1653
- host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
1654
- token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
1655
- client = ServiceClient(base_url=schemathesis_io_url, token=token, verify=request_tls_verify)
1656
- ci_environment = service.ci.environment()
1657
- provider = ci_environment.provider if ci_environment is not None else None
1658
- response = client.upload_report(
1659
- report=report.read(),
1660
- correlation_id=host_data.correlation_id,
1661
- ci_provider=provider,
1662
- source=UploadSource.UPLOAD_COMMAND,
1663
- )
1664
- if isinstance(response, UploadResponse):
1665
- host_data.store_correlation_id(response.correlation_id)
1666
- click.echo(f"{response.message}\n{response.next_url}")
1667
- else:
1668
- error_message(f"Failed to upload report to {schemathesis_io_hostname}: " + bold(response.detail))
1669
- sys.exit(1)
1670
-
1671
-
1672
- @schemathesis.group(short_help="Authenticate with Schemathesis.io.")
1673
- def auth() -> None:
1674
- pass
1675
-
1676
-
1677
- @auth.command(short_help="Authenticate with a Schemathesis.io host.")
1678
- @click.argument("token", type=str, envvar=service.TOKEN_ENV_VAR)
1679
- @click.option(
1680
- "--hostname",
1681
- help="The hostname of the Schemathesis.io instance to authenticate with",
1682
- type=str,
1683
- default=service.DEFAULT_HOSTNAME,
1684
- envvar=service.HOSTNAME_ENV_VAR,
1685
- )
1686
- @click.option(
1687
- "--protocol",
1688
- type=click.Choice(["https", "http"]),
1689
- default=service.DEFAULT_PROTOCOL,
1690
- envvar=service.PROTOCOL_ENV_VAR,
1691
- )
1692
- @with_request_tls_verify
1693
- @with_hosts_file
1694
- def login(token: str, hostname: str, hosts_file: str, protocol: str, request_tls_verify: bool = True) -> None:
1695
- """Authenticate with a Schemathesis.io host."""
1696
- import requests
1697
-
1698
- try:
1699
- username = service.auth.login(token, hostname, protocol, request_tls_verify)
1700
- service.hosts.store(token, hostname, hosts_file)
1701
- success_message(f"Logged in into {hostname} as " + bold(username))
1702
- except requests.HTTPError as exc:
1703
- response = cast(requests.Response, exc.response)
1704
- detail = response.json()["detail"]
1705
- error_message(f"Failed to login into {hostname}: " + bold(detail))
1706
- sys.exit(1)
1707
-
1708
-
1709
- @auth.command(short_help="Remove authentication for a Schemathesis.io host.")
1710
- @click.option(
1711
- "--hostname",
1712
- help="The hostname of the Schemathesis.io instance to authenticate with",
1713
- type=str,
1714
- default=service.DEFAULT_HOSTNAME,
1715
- envvar=service.HOSTNAME_ENV_VAR,
1716
- )
1717
- @with_hosts_file
1718
- def logout(hostname: str, hosts_file: str) -> None:
1719
- """Remove authentication for a Schemathesis.io host."""
1720
- result = service.hosts.remove(hostname, hosts_file)
1721
- if result == service.hosts.RemoveAuth.success:
1722
- success_message(f"Logged out of {hostname} account")
1723
- else:
1724
- if result == service.hosts.RemoveAuth.no_match:
1725
- warning_message(f"Not logged in to {hostname}")
1726
- if result == service.hosts.RemoveAuth.no_hosts:
1727
- warning_message("Not logged in to any hosts")
1728
- if result == service.hosts.RemoveAuth.error:
1729
- error_message(f"Failed to read the hosts file. Try to remove {hosts_file}")
1730
- sys.exit(1)
1731
-
1732
-
1733
- def success_message(message: str) -> None:
1734
- click.secho(click.style("✔️", fg="green") + f" {message}")
1735
-
1736
-
1737
- def warning_message(message: str) -> None:
1738
- click.secho(click.style("🟡️", fg="yellow") + f" {message}")
1739
-
1740
-
1741
- def error_message(message: str) -> None:
1742
- click.secho(f"❌ {message}")
1743
-
1744
-
1745
- def bold(message: str) -> str:
1746
- return click.style(message, bold=True)
1747
-
1748
-
1749
- def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -> None:
1750
- if force_color:
1751
- ctx.color = True
1752
- elif no_color or "NO_COLOR" in os.environ:
1753
- ctx.color = False
1754
-
1755
-
1756
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1757
- def after_init_cli_run_handlers(
1758
- context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
1759
- ) -> None:
1760
- """Called after CLI hooks are initialized.
1761
-
1762
- Might be used to add extra event handlers.
1763
- """
1764
-
1765
-
1766
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1767
- def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
1768
- """Called before every network call in CLI tests.
1769
-
1770
- Aims to modify the argument passed to `case.call` / `case.call_wsgi` / `case.call_asgi`.
1771
- Note that you need to modify `kwargs` in-place.
1772
- """
16
+ GROUPS.append(name)
17
+ return Group(name)