schemathesis 3.39.16__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +233 -307
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -717
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,2130 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
- import os
5
- import sys
6
- import traceback
7
- import warnings
8
- from collections import defaultdict
9
- from dataclasses import dataclass
10
- from enum import Enum
11
- from queue import Queue
12
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, cast
13
- from urllib.parse import urlparse
14
-
15
- import click
16
-
17
- from .. import checks as checks_module
18
- from .. import contrib, experimental, generation, runner, service
19
- from .. import fixups as _fixups
20
- from .. import targets as targets_module
21
- from .._override import CaseOverride
22
- from ..code_samples import CodeSampleStyle
23
- from ..constants import (
24
- API_NAME_ENV_VAR,
25
- BASE_URL_ENV_VAR,
26
- DEFAULT_RESPONSE_TIMEOUT,
27
- DEFAULT_STATEFUL_RECURSION_LIMIT,
28
- EXTENSIONS_DOCUMENTATION_URL,
29
- HOOKS_MODULE_ENV_VAR,
30
- HTTP_METHODS,
31
- HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
32
- ISSUE_TRACKER_URL,
33
- WAIT_FOR_SCHEMA_ENV_VAR,
34
- )
35
- from ..exceptions import SchemaError, SchemaErrorType, extract_nth_traceback
36
- from ..filters import FilterSet, expression_to_filter_function, is_deprecated
37
- from ..fixups import ALL_FIXUPS
38
- from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
39
- from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
40
- from ..internal.checks import CheckConfig
41
- from ..internal.datetime import current_datetime
42
- from ..internal.output import OutputConfig
43
- from ..internal.validation import file_exists
44
- from ..loaders import load_app, load_yaml
45
- from ..runner import events, prepare_hypothesis_settings, probes
46
- from ..specs.graphql import loaders as gql_loaders
47
- from ..specs.openapi import loaders as oas_loaders
48
- from ..stateful import Stateful
49
- from ..transports import RequestConfig
50
- from ..transports.auth import get_requests_auth
51
- from . import callbacks, cassettes, output
52
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
53
- from .context import ExecutionContext, FileReportContext, ServiceReportContext
54
- from .debug import DebugOutputHandler
55
- from .handlers import EventHandler
56
- from .junitxml import JunitXMLHandler
57
- from .options import CsvChoice, CsvEnumChoice, CsvListChoice, CustomHelpMessageChoice, OptionalInt
58
- from .sanitization import SanitizationHandler
59
-
60
- if TYPE_CHECKING:
61
- import io
62
-
63
- import hypothesis
64
- import requests
65
-
66
- from ..models import Case, CheckFunction
67
- from ..schemas import BaseSchema
68
- from ..service.client import ServiceClient
69
- from ..specs.graphql.schemas import GraphQLSchema
70
- from ..targets import Target
71
- from ..types import NotSet, PathLike, RequestCert
72
-
3
+ from schemathesis.cli.commands import Group, run, schemathesis
4
+ from schemathesis.cli.commands.run.context import ExecutionContext
5
+ from schemathesis.cli.commands.run.events import LoadingFinished, LoadingStarted
6
+ from schemathesis.cli.commands.run.executor import handler
7
+ from schemathesis.cli.commands.run.handlers import EventHandler
8
+ from schemathesis.cli.ext.groups import GROUPS, OptionGroup
73
9
 
74
10
  __all__ = [
11
+ "schemathesis",
12
+ "run",
75
13
  "EventHandler",
14
+ "ExecutionContext",
15
+ "LoadingStarted",
16
+ "LoadingFinished",
17
+ "add_group",
18
+ "handler",
76
19
  ]
77
20
 
78
21
 
79
- def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
80
- return tuple(item.__name__ for item in items)
81
-
82
-
83
- CUSTOM_HANDLERS: list[type[EventHandler]] = []
84
- CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
85
-
86
- DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
87
- ALL_CHECKS_NAMES = _get_callable_names(checks_module.ALL_CHECKS)
88
- CHECKS_TYPE = CsvChoice((*ALL_CHECKS_NAMES, "all"))
89
- EXCLUDE_CHECKS_TYPE = CsvChoice((*ALL_CHECKS_NAMES,))
90
-
91
- DEFAULT_TARGETS_NAMES = _get_callable_names(targets_module.DEFAULT_TARGETS)
92
- ALL_TARGETS_NAMES = _get_callable_names(targets_module.ALL_TARGETS)
93
- TARGETS_TYPE = click.Choice((*ALL_TARGETS_NAMES, "all"))
94
-
95
- DATA_GENERATION_METHOD_TYPE = click.Choice([item.name for item in DataGenerationMethod] + ["all"])
96
-
97
- DEPRECATED_CASSETTE_PATH_OPTION_WARNING = (
98
- "Warning: Option `--store-network-log` is deprecated and will be removed in Schemathesis 4.0. "
99
- "Use `--cassette-path` instead."
100
- )
101
- DEPRECATED_PRE_RUN_OPTION_WARNING = (
102
- "Warning: Option `--pre-run` is deprecated and will be removed in Schemathesis 4.0. "
103
- f"Use the `{HOOKS_MODULE_ENV_VAR}` environment variable instead"
104
- )
105
- DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
106
- "Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
107
- "Use `--show-trace` instead"
108
- )
109
- CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
110
- COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
111
- PHASES_INVALID_USAGE_MESSAGE = "Can't use `--hypothesis-phases` and `--hypothesis-no-phases` simultaneously"
112
-
113
-
114
- def reset_checks() -> None:
115
- """Get checks list to their default state."""
116
- # Useful in tests
117
- checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
118
- CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
119
-
120
-
121
- def reset_targets() -> None:
122
- """Get targets list to their default state."""
123
- # Useful in tests
124
- targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
125
- TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
126
-
127
-
128
- @click.group(context_settings=CONTEXT_SETTINGS)
129
- @click.option("--pre-run", help="[DEPRECATED] A module to execute before running the tests", type=str, hidden=True)
130
- @click.version_option()
131
- def schemathesis(pre_run: str | None = None) -> None:
132
- """Property-based API testing for OpenAPI and GraphQL."""
133
- # Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
134
- hooks: str | None
135
- if pre_run:
136
- click.secho(DEPRECATED_PRE_RUN_OPTION_WARNING, fg="yellow")
137
- hooks = pre_run
138
- else:
139
- hooks = os.getenv(HOOKS_MODULE_ENV_VAR)
140
- if hooks:
141
- load_hook(hooks)
142
-
143
-
144
- GROUPS: list[str] = []
145
-
146
-
147
- class CommandWithGroupedOptions(click.Command):
148
- def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
149
- groups = defaultdict(list)
150
- for param in self.get_params(ctx):
151
- rv = param.get_help_record(ctx)
152
- if rv is not None:
153
- (option_repr, message) = rv
154
- if isinstance(param.type, click.Choice):
155
- message += (
156
- getattr(param.type, "choices_repr", None)
157
- or f" [possible values: {', '.join(param.type.choices)}]"
158
- )
159
-
160
- if isinstance(param, GroupedOption):
161
- group = param.group
162
- else:
163
- group = "Global options"
164
- groups[group].append((option_repr, message))
165
- for group in GROUPS:
166
- with formatter.section(group or "Options"):
167
- formatter.write_dl(groups[group], col_max=40)
168
-
169
-
170
- class GroupedOption(click.Option):
171
- def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
172
- super().__init__(*args, **kwargs)
173
- self.group = group
174
-
175
-
176
- def group(name: str) -> Callable:
177
- GROUPS.append(name)
178
-
179
- def _inner(cmd: Callable) -> Callable:
180
- for param in reversed(cmd.__click_params__): # type: ignore[attr-defined]
181
- if not isinstance(param, GroupedOption) or param.group is not None:
182
- break
183
- param.group = name
184
- return cmd
185
-
186
- return _inner
187
-
188
-
189
- def grouped_option(*args: Any, **kwargs: Any) -> Callable:
190
- kwargs.setdefault("cls", GroupedOption)
191
- return click.option(*args, **kwargs)
192
-
193
-
194
- with_request_proxy = grouped_option(
195
- "--request-proxy",
196
- help="Set the proxy for all network requests",
197
- type=str,
198
- )
199
- with_request_tls_verify = grouped_option(
200
- "--request-tls-verify",
201
- help="Configures TLS certificate verification for server requests. Can specify path to CA_BUNDLE for custom certs",
202
- type=str,
203
- default="true",
204
- show_default=True,
205
- callback=callbacks.convert_boolean_string,
206
- )
207
- with_request_cert = grouped_option(
208
- "--request-cert",
209
- help="File path of unencrypted client certificate for authentication. "
210
- "The certificate can be bundled with a private key (e.g. PEM) or the private "
211
- "key can be provided with the --request-cert-key argument",
212
- type=click.Path(exists=True),
213
- default=None,
214
- show_default=False,
215
- )
216
- with_request_cert_key = grouped_option(
217
- "--request-cert-key",
218
- help="Specify the file path of the private key for the client certificate",
219
- type=click.Path(exists=True),
220
- default=None,
221
- show_default=False,
222
- callback=callbacks.validate_request_cert_key,
223
- )
224
- with_hosts_file = grouped_option(
225
- "--hosts-file",
226
- help="Path to a file to store the Schemathesis.io auth configuration",
227
- type=click.Path(dir_okay=False, writable=True),
228
- default=service.DEFAULT_HOSTS_PATH,
229
- envvar=service.HOSTS_PATH_ENV_VAR,
230
- callback=callbacks.convert_hosts_file,
231
- )
232
-
233
-
234
- def _with_filter(*, by: str, mode: Literal["include", "exclude"], modifier: Literal["regex"] | None = None) -> Callable:
235
- """Generate a CLI option for filtering API operations."""
236
- param = f"--{mode}-{by}"
237
- action = "include in" if mode == "include" else "exclude from"
238
- prop = {
239
- "operation-id": "ID",
240
- "name": "Operation name",
241
- }.get(by, by.capitalize())
242
- if modifier:
243
- param += f"-{modifier}"
244
- prop += " pattern"
245
- help_text = f"{prop} to {action} testing."
246
- return grouped_option(
247
- param,
248
- help=help_text,
249
- type=str,
250
- multiple=modifier is None,
251
- )
252
-
253
-
254
- _BY_VALUES = ("operation-id", "tag", "name", "method", "path")
255
-
256
-
257
- def with_filters(command: Callable) -> Callable:
258
- for by in _BY_VALUES:
259
- for mode in ("exclude", "include"):
260
- for modifier in ("regex", None):
261
- command = _with_filter(by=by, mode=mode, modifier=modifier)(command) # type: ignore[arg-type]
262
- return command
263
-
264
-
265
- class ReportToService:
266
- pass
267
-
268
-
269
- REPORT_TO_SERVICE = ReportToService()
270
-
271
-
272
- @schemathesis.command(
273
- short_help="Execute automated tests based on API specifications",
274
- cls=CommandWithGroupedOptions,
275
- context_settings={"terminal_width": output.default.get_terminal_width(), **CONTEXT_SETTINGS},
276
- )
277
- @click.argument("schema", type=str)
278
- @click.argument("api_name", type=str, required=False, envvar=API_NAME_ENV_VAR)
279
- @group("Options")
280
- @grouped_option(
281
- "--workers",
282
- "-w",
283
- "workers_num",
284
- help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
285
- type=CustomHelpMessageChoice(
286
- ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
287
- choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
288
- ),
289
- default=str(DEFAULT_WORKERS),
290
- show_default=True,
291
- callback=callbacks.convert_workers,
292
- metavar="",
293
- )
294
- @grouped_option(
295
- "--dry-run",
296
- "dry_run",
297
- is_flag=True,
298
- default=False,
299
- help="Simulate test execution without making any actual requests, useful for validating data generation",
300
- )
301
- @grouped_option(
302
- "--fixups",
303
- help="Apply compatibility adjustments",
304
- multiple=True,
305
- type=click.Choice([*ALL_FIXUPS, "all"]),
306
- metavar="",
307
- )
308
- @group("Experimental options")
309
- @grouped_option(
310
- "--experimental",
311
- "experiments",
312
- help="Enable experimental features",
313
- type=click.Choice(
314
- [
315
- experimental.OPEN_API_3_1.name,
316
- experimental.SCHEMA_ANALYSIS.name,
317
- experimental.STATEFUL_TEST_RUNNER.name,
318
- experimental.STATEFUL_ONLY.name,
319
- experimental.COVERAGE_PHASE.name,
320
- experimental.POSITIVE_DATA_ACCEPTANCE.name,
321
- ]
322
- ),
323
- callback=callbacks.convert_experimental,
324
- multiple=True,
325
- metavar="",
326
- )
327
- @grouped_option(
328
- "--experimental-coverage-unexpected-methods",
329
- "coverage_unexpected_methods",
330
- help="HTTP methods to use when generating test cases with methods not specified in the API during the coverage phase.",
331
- type=CsvChoice(sorted(HTTP_METHODS), case_sensitive=False),
332
- callback=callbacks.convert_http_methods,
333
- metavar="",
334
- default=None,
335
- envvar="SCHEMATHESIS_EXPERIMENTAL_COVERAGE_UNEXPECTED_METHODS",
336
- )
337
- @grouped_option(
338
- "--experimental-no-failfast",
339
- "no_failfast",
340
- help="Continue testing an API operation after a failure is found",
341
- is_flag=True,
342
- default=False,
343
- metavar="",
344
- envvar="SCHEMATHESIS_EXPERIMENTAL_NO_FAILFAST",
345
- )
346
- @grouped_option(
347
- "--experimental-missing-required-header-allowed-statuses",
348
- "missing_required_header_allowed_statuses",
349
- help="Comma-separated list of status codes expected for test cases with a missing required header",
350
- type=CsvListChoice(),
351
- callback=callbacks.convert_status_codes,
352
- metavar="",
353
- envvar="SCHEMATHESIS_EXPERIMENTAL_MISSING_REQUIRED_HEADER_ALLOWED_STATUSES",
354
- )
355
- @grouped_option(
356
- "--experimental-positive-data-acceptance-allowed-statuses",
357
- "positive_data_acceptance_allowed_statuses",
358
- help="Comma-separated list of status codes considered as successful responses",
359
- type=CsvListChoice(),
360
- callback=callbacks.convert_status_codes,
361
- metavar="",
362
- envvar="SCHEMATHESIS_EXPERIMENTAL_POSITIVE_DATA_ACCEPTANCE_ALLOWED_STATUSES",
363
- )
364
- @grouped_option(
365
- "--experimental-negative-data-rejection-allowed-statuses",
366
- "negative_data_rejection_allowed_statuses",
367
- help="Comma-separated list of status codes expected for rejected negative data",
368
- type=CsvListChoice(),
369
- callback=callbacks.convert_status_codes,
370
- metavar="",
371
- envvar="SCHEMATHESIS_EXPERIMENTAL_NEGATIVE_DATA_REJECTION_ALLOWED_STATUSES",
372
- )
373
- @group("API validation options")
374
- @grouped_option(
375
- "--checks",
376
- "-c",
377
- multiple=True,
378
- help="Comma-separated list of checks to run against API responses",
379
- type=CHECKS_TYPE,
380
- default=DEFAULT_CHECKS_NAMES,
381
- callback=callbacks.convert_checks,
382
- show_default=True,
383
- metavar="",
384
- )
385
- @grouped_option(
386
- "--exclude-checks",
387
- multiple=True,
388
- help="Comma-separated list of checks to skip during testing",
389
- type=EXCLUDE_CHECKS_TYPE,
390
- default=[],
391
- callback=callbacks.convert_checks,
392
- show_default=True,
393
- metavar="",
394
- )
395
- @grouped_option(
396
- "--max-response-time",
397
- help="Time limit in milliseconds for API response times. "
398
- "The test will fail if a response time exceeds this limit. ",
399
- type=click.IntRange(min=1),
400
- )
401
- @grouped_option(
402
- "-x",
403
- "--exitfirst",
404
- "exit_first",
405
- is_flag=True,
406
- default=False,
407
- help="Terminate the test suite immediately upon the first failure or error encountered",
408
- show_default=True,
409
- )
410
- @grouped_option(
411
- "--max-failures",
412
- "max_failures",
413
- type=click.IntRange(min=1),
414
- help="Terminate the test suite after reaching a specified number of failures or errors",
415
- show_default=True,
416
- )
417
- @group("Loader options")
418
- @grouped_option(
419
- "--app",
420
- help="Specify the WSGI/ASGI application under test, provided as an importable Python path",
421
- type=str,
422
- callback=callbacks.validate_app,
423
- )
424
- @grouped_option(
425
- "--wait-for-schema",
426
- help="Maximum duration, in seconds, to wait for the API schema to become available. Disabled by default",
427
- type=click.FloatRange(1.0),
428
- default=None,
429
- envvar=WAIT_FOR_SCHEMA_ENV_VAR,
430
- )
431
- @grouped_option(
432
- "--validate-schema",
433
- help="Validate input API schema. Set to 'true' to enable or 'false' to disable",
434
- type=bool,
435
- default=False,
436
- show_default=True,
437
- )
438
- @group("Network requests options")
439
- @grouped_option(
440
- "--base-url",
441
- "-b",
442
- help="Base URL of the API, required when schema is provided as a file",
443
- type=str,
444
- callback=callbacks.validate_base_url,
445
- envvar=BASE_URL_ENV_VAR,
446
- )
447
- @grouped_option(
448
- "--request-timeout",
449
- help="Timeout limit, in milliseconds, for each network request during tests",
450
- type=click.IntRange(1),
451
- default=DEFAULT_RESPONSE_TIMEOUT,
452
- )
453
- @with_request_proxy
454
- @with_request_tls_verify
455
- @with_request_cert
456
- @with_request_cert_key
457
- @grouped_option(
458
- "--rate-limit",
459
- help="Specify a rate limit for test requests in '<limit>/<duration>' format. "
460
- "Example - `100/m` for 100 requests per minute",
461
- type=str,
462
- callback=callbacks.validate_rate_limit,
463
- )
464
- @grouped_option(
465
- "--header",
466
- "-H",
467
- "headers",
468
- help=r"Add a custom HTTP header to all API requests. Format: 'Header-Name: Value'",
469
- multiple=True,
470
- type=str,
471
- callback=callbacks.validate_headers,
472
- )
473
- @grouped_option(
474
- "--auth",
475
- "-a",
476
- help="Provide the server authentication details in the 'USER:PASSWORD' format",
477
- type=str,
478
- callback=callbacks.validate_auth,
479
- )
480
- @grouped_option(
481
- "--auth-type",
482
- "-A",
483
- type=click.Choice(["basic", "digest"], case_sensitive=False),
484
- default="basic",
485
- help="Specify the authentication method. For custom authentication methods, see our Authentication documentation: https://schemathesis.readthedocs.io/en/stable/auth.html#custom-auth",
486
- show_default=True,
487
- metavar="",
488
- )
489
- @group("Filtering options")
490
- @with_filters
491
- @grouped_option(
492
- "--include-by",
493
- "include_by",
494
- type=str,
495
- help="Include API operations by expression",
496
- )
497
- @grouped_option(
498
- "--exclude-by",
499
- "exclude_by",
500
- type=str,
501
- help="Exclude API operations by expression",
502
- )
503
- @grouped_option(
504
- "--exclude-deprecated",
505
- help="Exclude deprecated API operations from testing",
506
- is_flag=True,
507
- is_eager=True,
508
- default=False,
509
- show_default=True,
510
- )
511
- @grouped_option(
512
- "--endpoint",
513
- "-E",
514
- "endpoints",
515
- type=str,
516
- multiple=True,
517
- help=r"[DEPRECATED] API operation path pattern (e.g., users/\d+)",
518
- callback=callbacks.validate_regex,
519
- hidden=True,
520
- )
521
- @grouped_option(
522
- "--method",
523
- "-M",
524
- "methods",
525
- type=str,
526
- multiple=True,
527
- help="[DEPRECATED] HTTP method (e.g., GET, POST)",
528
- callback=callbacks.validate_regex,
529
- hidden=True,
530
- )
531
- @grouped_option(
532
- "--tag",
533
- "-T",
534
- "tags",
535
- type=str,
536
- multiple=True,
537
- help="[DEPRECATED] Schema tag pattern",
538
- callback=callbacks.validate_regex,
539
- hidden=True,
540
- )
541
- @grouped_option(
542
- "--operation-id",
543
- "-O",
544
- "operation_ids",
545
- type=str,
546
- multiple=True,
547
- help="[DEPRECATED] OpenAPI operationId pattern",
548
- callback=callbacks.validate_regex,
549
- hidden=True,
550
- )
551
- @grouped_option(
552
- "--skip-deprecated-operations",
553
- help="[DEPRECATED] Exclude deprecated API operations from testing",
554
- is_flag=True,
555
- is_eager=True,
556
- default=False,
557
- show_default=True,
558
- hidden=True,
559
- )
560
- @group("Output options")
561
- @grouped_option(
562
- "--junit-xml",
563
- help="Output a JUnit-XML style report at the specified file path",
564
- type=click.File("w", encoding="utf-8"),
565
- )
566
- @grouped_option(
567
- "--cassette-path",
568
- help="Save the test outcomes in a VCR-compatible format",
569
- type=click.File("w", encoding="utf-8"),
570
- is_eager=True,
571
- )
572
- @grouped_option(
573
- "--cassette-format",
574
- help="Format of the saved cassettes",
575
- type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
576
- default=cassettes.CassetteFormat.VCR.name.lower(),
577
- callback=callbacks.convert_cassette_format,
578
- metavar="",
579
- )
580
- @grouped_option(
581
- "--cassette-preserve-exact-body-bytes",
582
- help="Retain exact byte sequence of payloads in cassettes, encoded as base64",
583
- is_flag=True,
584
- callback=callbacks.validate_preserve_exact_body_bytes,
585
- )
586
- @grouped_option(
587
- "--code-sample-style",
588
- help="Code sample style for reproducing failures",
589
- type=click.Choice([item.name for item in CodeSampleStyle]),
590
- default=CodeSampleStyle.default().name,
591
- callback=callbacks.convert_code_sample_style,
592
- metavar="",
593
- )
594
- @grouped_option(
595
- "--sanitize-output",
596
- type=bool,
597
- default=True,
598
- show_default=True,
599
- help="Enable or disable automatic output sanitization to obscure sensitive data",
600
- )
601
- @grouped_option(
602
- "--output-truncate",
603
- help="Truncate schemas and responses in error messages",
604
- type=str,
605
- default="true",
606
- show_default=True,
607
- callback=callbacks.convert_boolean_string,
608
- )
609
- @grouped_option(
610
- "--show-trace",
611
- help="Display complete traceback information for internal errors",
612
- is_flag=True,
613
- is_eager=True,
614
- default=False,
615
- show_default=True,
616
- )
617
- @grouped_option(
618
- "--debug-output-file",
619
- help="Save debugging information in a JSONL format at the specified file path",
620
- type=click.File("w", encoding="utf-8"),
621
- )
622
- @grouped_option(
623
- "--store-network-log",
624
- help="[DEPRECATED] Save the test outcomes in a VCR-compatible format",
625
- type=click.File("w", encoding="utf-8"),
626
- hidden=True,
627
- )
628
- @grouped_option(
629
- "--show-errors-tracebacks",
630
- help="[DEPRECATED] Display complete traceback information for internal errors",
631
- is_flag=True,
632
- is_eager=True,
633
- default=False,
634
- hidden=True,
635
- show_default=True,
636
- )
637
- @group("Data generation options")
638
- @grouped_option(
639
- "--data-generation-method",
640
- "-D",
641
- "data_generation_methods",
642
- help="Specify the approach Schemathesis uses to generate test data. "
643
- "Use 'positive' for valid data, 'negative' for invalid data, or 'all' for both",
644
- type=DATA_GENERATION_METHOD_TYPE,
645
- default=DataGenerationMethod.default().name,
646
- callback=callbacks.convert_data_generation_method,
647
- show_default=True,
648
- metavar="",
649
- )
650
- @grouped_option(
651
- "--stateful",
652
- help="Enable or disable stateful testing",
653
- type=click.Choice([item.name for item in Stateful]),
654
- default=Stateful.links.name,
655
- callback=callbacks.convert_stateful,
656
- metavar="",
657
- )
658
- @grouped_option(
659
- "--stateful-recursion-limit",
660
- help="Recursion depth limit for stateful testing",
661
- default=DEFAULT_STATEFUL_RECURSION_LIMIT,
662
- show_default=True,
663
- type=click.IntRange(1, 100),
664
- hidden=True,
665
- )
666
- @grouped_option(
667
- "--generation-allow-x00",
668
- help="Whether to allow the generation of `\x00` bytes within strings",
669
- type=str,
670
- default="true",
671
- show_default=True,
672
- callback=callbacks.convert_boolean_string,
673
- )
674
- @grouped_option(
675
- "--generation-codec",
676
- help="The codec used for generating strings",
677
- type=str,
678
- default="utf-8",
679
- callback=callbacks.validate_generation_codec,
680
- )
681
- @grouped_option(
682
- "--generation-with-security-parameters",
683
- help="Whether to generate security parameters",
684
- type=str,
685
- default="true",
686
- show_default=True,
687
- callback=callbacks.convert_boolean_string,
688
- )
689
- @grouped_option(
690
- "--generation-graphql-allow-null",
691
- help="Whether to use `null` values for optional arguments in GraphQL queries",
692
- type=str,
693
- default="true",
694
- show_default=True,
695
- callback=callbacks.convert_boolean_string,
696
- )
697
- @grouped_option(
698
- "--contrib-unique-data",
699
- "contrib_unique_data",
700
- help="Force the generation of unique test cases",
701
- is_flag=True,
702
- default=False,
703
- show_default=True,
704
- )
705
- @grouped_option(
706
- "--contrib-openapi-formats-uuid",
707
- "contrib_openapi_formats_uuid",
708
- help="Enable support for the 'uuid' string format in OpenAPI",
709
- is_flag=True,
710
- default=False,
711
- show_default=True,
712
- )
713
- @grouped_option(
714
- "--contrib-openapi-fill-missing-examples",
715
- "contrib_openapi_fill_missing_examples",
716
- help="Enable generation of random examples for API operations that do not have explicit examples",
717
- is_flag=True,
718
- default=False,
719
- show_default=True,
720
- )
721
- @grouped_option(
722
- "--target",
723
- "-t",
724
- "targets",
725
- multiple=True,
726
- help="Guide input generation to values more likely to expose bugs via targeted property-based testing",
727
- type=TARGETS_TYPE,
728
- default=DEFAULT_TARGETS_NAMES,
729
- show_default=True,
730
- metavar="",
731
- )
732
- @group("Open API options")
733
- @grouped_option(
734
- "--force-schema-version",
735
- help="Force the schema to be interpreted as a particular OpenAPI version",
736
- type=click.Choice(["20", "30"]),
737
- metavar="",
738
- )
739
- @grouped_option(
740
- "--set-query",
741
- "set_query",
742
- help=r"OpenAPI: Override a specific query parameter by specifying 'parameter=value'",
743
- multiple=True,
744
- type=str,
745
- callback=callbacks.validate_set_query,
746
- )
747
- @grouped_option(
748
- "--set-header",
749
- "set_header",
750
- help=r"OpenAPI: Override a specific header parameter by specifying 'parameter=value'",
751
- multiple=True,
752
- type=str,
753
- callback=callbacks.validate_set_header,
754
- )
755
- @grouped_option(
756
- "--set-cookie",
757
- "set_cookie",
758
- help=r"OpenAPI: Override a specific cookie parameter by specifying 'parameter=value'",
759
- multiple=True,
760
- type=str,
761
- callback=callbacks.validate_set_cookie,
762
- )
763
- @grouped_option(
764
- "--set-path",
765
- "set_path",
766
- help=r"OpenAPI: Override a specific path parameter by specifying 'parameter=value'",
767
- multiple=True,
768
- type=str,
769
- callback=callbacks.validate_set_path,
770
- )
771
- @group("Hypothesis engine options")
772
- @grouped_option(
773
- "--hypothesis-database",
774
- help="Storage for examples discovered by Hypothesis. "
775
- f"Use 'none' to disable, '{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}' for temporary storage, "
776
- f"or specify a file path for persistent storage",
777
- type=str,
778
- callback=callbacks.validate_hypothesis_database,
779
- )
780
- @grouped_option(
781
- "--hypothesis-deadline",
782
- help="Time limit for each test case generated by Hypothesis, in milliseconds. "
783
- "Exceeding this limit will cause the test to fail",
784
- type=OptionalInt(1, 5 * 60 * 1000),
785
- )
786
- @grouped_option(
787
- "--hypothesis-derandomize",
788
- help="Enables deterministic mode in Hypothesis, which eliminates random variation between tests",
789
- is_flag=True,
790
- is_eager=True,
791
- default=None,
792
- show_default=True,
793
- )
794
- @grouped_option(
795
- "--hypothesis-max-examples",
796
- help="The cap on the number of examples generated by Hypothesis for each API operation",
797
- type=click.IntRange(1),
798
- )
799
- @grouped_option(
800
- "--hypothesis-phases",
801
- help="Testing phases to execute",
802
- type=CsvEnumChoice(Phase),
803
- metavar="",
804
- )
805
- @grouped_option(
806
- "--hypothesis-no-phases",
807
- help="Testing phases to exclude from execution",
808
- type=CsvEnumChoice(Phase),
809
- metavar="",
810
- )
811
- @grouped_option(
812
- "--hypothesis-report-multiple-bugs",
813
- help="Report only the most easily reproducible error when multiple issues are found",
814
- type=bool,
815
- )
816
- @grouped_option(
817
- "--hypothesis-seed",
818
- help="Seed value for Hypothesis, ensuring reproducibility across test runs",
819
- type=int,
820
- )
821
- @grouped_option(
822
- "--hypothesis-suppress-health-check",
823
- help="A comma-separated list of Hypothesis health checks to disable",
824
- type=CsvEnumChoice(HealthCheck),
825
- metavar="",
826
- )
827
- @grouped_option(
828
- "--hypothesis-verbosity",
829
- help="Verbosity level of Hypothesis output",
830
- type=click.Choice([item.name for item in Verbosity]),
831
- callback=callbacks.convert_verbosity,
832
- metavar="",
833
- )
834
- @group("Schemathesis.io options")
835
- @grouped_option(
836
- "--report",
837
- "report_value",
838
- help="""Specify how the generated report should be handled.
839
- If used without an argument, the report data will automatically be uploaded to Schemathesis.io.
840
- If a file name is provided, the report will be stored in that file.
841
- The report data, consisting of a tar gz file with multiple JSON files, is subject to change""",
842
- is_flag=False,
843
- flag_value="",
844
- envvar=service.REPORT_ENV_VAR,
845
- callback=callbacks.convert_report, # type: ignore
846
- )
847
- @grouped_option(
848
- "--schemathesis-io-token",
849
- help="Schemathesis.io authentication token",
850
- type=str,
851
- envvar=service.TOKEN_ENV_VAR,
852
- )
853
- @grouped_option(
854
- "--schemathesis-io-url",
855
- help="Schemathesis.io base URL",
856
- default=service.DEFAULT_URL,
857
- type=str,
858
- envvar=service.URL_ENV_VAR,
859
- )
860
- @grouped_option(
861
- "--schemathesis-io-telemetry",
862
- help="Whether to send anonymized usage data to Schemathesis.io along with your report",
863
- type=str,
864
- default="true",
865
- show_default=True,
866
- callback=callbacks.convert_boolean_string,
867
- envvar=service.TELEMETRY_ENV_VAR,
868
- )
869
- @with_hosts_file
870
- @group("Global options")
871
- @grouped_option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
872
- @grouped_option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
873
- @grouped_option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
874
- @click.pass_context
875
- def run(
876
- ctx: click.Context,
877
- schema: str,
878
- api_name: str | None,
879
- auth: tuple[str, str] | None,
880
- auth_type: str,
881
- headers: dict[str, str],
882
- set_query: dict[str, str],
883
- set_header: dict[str, str],
884
- set_cookie: dict[str, str],
885
- set_path: dict[str, str],
886
- experiments: list,
887
- no_failfast: bool,
888
- coverage_unexpected_methods: set[str] | None,
889
- missing_required_header_allowed_statuses: list[str],
890
- positive_data_acceptance_allowed_statuses: list[str],
891
- negative_data_rejection_allowed_statuses: list[str],
892
- checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
893
- exclude_checks: Iterable[str] = (),
894
- data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
895
- max_response_time: int | None = None,
896
- targets: Iterable[str] = DEFAULT_TARGETS_NAMES,
897
- exit_first: bool = False,
898
- max_failures: int | None = None,
899
- dry_run: bool = False,
900
- include_path: Sequence[str] = (),
901
- include_path_regex: str | None = None,
902
- include_method: Sequence[str] = (),
903
- include_method_regex: str | None = None,
904
- include_name: Sequence[str] = (),
905
- include_name_regex: str | None = None,
906
- include_tag: Sequence[str] = (),
907
- include_tag_regex: str | None = None,
908
- include_operation_id: Sequence[str] = (),
909
- include_operation_id_regex: str | None = None,
910
- exclude_path: Sequence[str] = (),
911
- exclude_path_regex: str | None = None,
912
- exclude_method: Sequence[str] = (),
913
- exclude_method_regex: str | None = None,
914
- exclude_name: Sequence[str] = (),
915
- exclude_name_regex: str | None = None,
916
- exclude_tag: Sequence[str] = (),
917
- exclude_tag_regex: str | None = None,
918
- exclude_operation_id: Sequence[str] = (),
919
- exclude_operation_id_regex: str | None = None,
920
- include_by: str | None = None,
921
- exclude_by: str | None = None,
922
- exclude_deprecated: bool = False,
923
- endpoints: tuple[str, ...] = (),
924
- methods: tuple[str, ...] = (),
925
- tags: tuple[str, ...] = (),
926
- operation_ids: tuple[str, ...] = (),
927
- workers_num: int = DEFAULT_WORKERS,
928
- base_url: str | None = None,
929
- app: str | None = None,
930
- request_timeout: int | None = None,
931
- request_tls_verify: bool = True,
932
- request_cert: str | None = None,
933
- request_cert_key: str | None = None,
934
- request_proxy: str | None = None,
935
- validate_schema: bool = True,
936
- skip_deprecated_operations: bool = False,
937
- junit_xml: click.utils.LazyFile | None = None,
938
- debug_output_file: click.utils.LazyFile | None = None,
939
- show_errors_tracebacks: bool = False,
940
- show_trace: bool = False,
941
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
942
- cassette_path: click.utils.LazyFile | None = None,
943
- cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
944
- cassette_preserve_exact_body_bytes: bool = False,
945
- store_network_log: click.utils.LazyFile | None = None,
946
- wait_for_schema: float | None = None,
947
- fixups: tuple[str] = (), # type: ignore
948
- rate_limit: str | None = None,
949
- stateful: Stateful | None = None,
950
- stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
951
- force_schema_version: str | None = None,
952
- sanitize_output: bool = True,
953
- output_truncate: bool = True,
954
- contrib_unique_data: bool = False,
955
- contrib_openapi_formats_uuid: bool = False,
956
- contrib_openapi_fill_missing_examples: bool = False,
957
- hypothesis_database: str | None = None,
958
- hypothesis_deadline: int | NotSet | None = None,
959
- hypothesis_derandomize: bool | None = None,
960
- hypothesis_max_examples: int | None = None,
961
- hypothesis_phases: list[Phase] | None = None,
962
- hypothesis_no_phases: list[Phase] | None = None,
963
- hypothesis_report_multiple_bugs: bool | None = None,
964
- hypothesis_suppress_health_check: list[HealthCheck] | None = None,
965
- hypothesis_seed: int | None = None,
966
- hypothesis_verbosity: hypothesis.Verbosity | None = None,
967
- verbosity: int = 0,
968
- no_color: bool = False,
969
- report_value: str | None = None,
970
- generation_allow_x00: bool = True,
971
- generation_graphql_allow_null: bool = True,
972
- generation_with_security_parameters: bool = True,
973
- generation_codec: str = "utf-8",
974
- schemathesis_io_token: str | None = None,
975
- schemathesis_io_url: str = service.DEFAULT_URL,
976
- schemathesis_io_telemetry: bool = True,
977
- hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
978
- force_color: bool = False,
979
- **__kwargs,
980
- ) -> None:
981
- """Run tests against an API using a specified SCHEMA.
982
-
983
- [Required] SCHEMA: Path to an OpenAPI (`.json`, `.yml`) or GraphQL SDL file, or a URL pointing to such specifications
984
-
985
- [Optional] API_NAME: Identifier for uploading test data to Schemathesis.io
986
- """
987
- _hypothesis_phases: list[hypothesis.Phase] | None = None
988
- if hypothesis_phases is not None:
989
- _hypothesis_phases = [phase.as_hypothesis() for phase in hypothesis_phases]
990
- if hypothesis_no_phases is not None:
991
- raise click.UsageError(PHASES_INVALID_USAGE_MESSAGE)
992
- if hypothesis_no_phases is not None:
993
- _hypothesis_phases = Phase.filter_from_all(hypothesis_no_phases)
994
- _hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
995
- if hypothesis_suppress_health_check is not None:
996
- _hypothesis_suppress_health_check = [
997
- entry for health_check in hypothesis_suppress_health_check for entry in health_check.as_hypothesis()
998
- ]
999
-
1000
- if show_errors_tracebacks:
1001
- click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
1002
- show_trace = show_errors_tracebacks
1003
-
1004
- # Enable selected experiments
1005
- for experiment in experiments:
1006
- experiment.enable()
1007
-
1008
- override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
1009
-
1010
- generation_config = generation.GenerationConfig(
1011
- allow_x00=generation_allow_x00,
1012
- graphql_allow_null=generation_graphql_allow_null,
1013
- codec=generation_codec,
1014
- with_security_parameters=generation_with_security_parameters,
1015
- unexpected_methods=coverage_unexpected_methods,
1016
- )
1017
-
1018
- report: ReportToService | click.utils.LazyFile | None
1019
- if report_value is None:
1020
- report = None
1021
- elif report_value:
1022
- report = click.utils.LazyFile(report_value, mode="wb")
1023
- else:
1024
- report = REPORT_TO_SERVICE
1025
- started_at = current_datetime()
1026
-
1027
- if no_color and force_color:
1028
- raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
1029
- decide_color_output(ctx, no_color, force_color)
1030
-
1031
- check_auth(auth, headers, override)
1032
- selected_targets = tuple(target for target in targets_module.ALL_TARGETS if target.__name__ in targets)
1033
-
1034
- if store_network_log and cassette_path:
1035
- raise click.UsageError(CASSETTES_PATH_INVALID_USAGE_MESSAGE)
1036
- if store_network_log is not None:
1037
- click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
1038
- cassette_path = store_network_log
1039
-
1040
- output_config = OutputConfig(truncate=output_truncate)
1041
-
1042
- deprecated_filters = {
1043
- "--method": "--include-method",
1044
- "--endpoint": "--include-path",
1045
- "--tag": "--include-tag",
1046
- "--operation-id": "--include-operation-id",
1047
- }
1048
- for values, arg_name in (
1049
- (include_path, "--include-path"),
1050
- (include_method, "--include-method"),
1051
- (include_name, "--include-name"),
1052
- (include_tag, "--include-tag"),
1053
- (include_operation_id, "--include-operation-id"),
1054
- (exclude_path, "--exclude-path"),
1055
- (exclude_method, "--exclude-method"),
1056
- (exclude_name, "--exclude-name"),
1057
- (exclude_tag, "--exclude-tag"),
1058
- (exclude_operation_id, "--exclude-operation-id"),
1059
- (methods, "--method"),
1060
- (endpoints, "--endpoint"),
1061
- (tags, "--tag"),
1062
- (operation_ids, "--operation-id"),
1063
- ):
1064
- if values and arg_name in deprecated_filters:
1065
- replacement = deprecated_filters[arg_name]
1066
- click.secho(
1067
- f"Warning: Option `{arg_name}` is deprecated and will be removed in Schemathesis 4.0. "
1068
- f"Use `{replacement}` instead",
1069
- fg="yellow",
1070
- )
1071
- _ensure_unique_filter(values, arg_name)
1072
- include_by_function = _filter_by_expression_to_func(include_by, "--include-by")
1073
- exclude_by_function = _filter_by_expression_to_func(exclude_by, "--exclude-by")
1074
-
1075
- filter_set = FilterSet()
1076
- if include_by_function:
1077
- filter_set.include(include_by_function)
1078
- for name_ in include_name:
1079
- filter_set.include(name=name_)
1080
- for method in include_method:
1081
- filter_set.include(method=method)
1082
- if methods:
1083
- for method in methods:
1084
- filter_set.include(method_regex=method)
1085
- for path in include_path:
1086
- filter_set.include(path=path)
1087
- if endpoints:
1088
- for endpoint in endpoints:
1089
- filter_set.include(path_regex=endpoint)
1090
- for tag in include_tag:
1091
- filter_set.include(tag=tag)
1092
- if tags:
1093
- for tag in tags:
1094
- filter_set.include(tag_regex=tag)
1095
- for operation_id in include_operation_id:
1096
- filter_set.include(operation_id=operation_id)
1097
- if operation_ids:
1098
- for operation_id in operation_ids:
1099
- filter_set.include(operation_id_regex=operation_id)
1100
- if (
1101
- include_name_regex
1102
- or include_method_regex
1103
- or include_path_regex
1104
- or include_tag_regex
1105
- or include_operation_id_regex
1106
- ):
1107
- filter_set.include(
1108
- name_regex=include_name_regex,
1109
- method_regex=include_method_regex,
1110
- path_regex=include_path_regex,
1111
- tag_regex=include_tag_regex,
1112
- operation_id_regex=include_operation_id_regex,
1113
- )
1114
- if exclude_by_function:
1115
- filter_set.exclude(exclude_by_function)
1116
- for name_ in exclude_name:
1117
- filter_set.exclude(name=name_)
1118
- for method in exclude_method:
1119
- filter_set.exclude(method=method)
1120
- for path in exclude_path:
1121
- filter_set.exclude(path=path)
1122
- for tag in exclude_tag:
1123
- filter_set.exclude(tag=tag)
1124
- for operation_id in exclude_operation_id:
1125
- filter_set.exclude(operation_id=operation_id)
1126
- if (
1127
- exclude_name_regex
1128
- or exclude_method_regex
1129
- or exclude_path_regex
1130
- or exclude_tag_regex
1131
- or exclude_operation_id_regex
1132
- ):
1133
- filter_set.exclude(
1134
- name_regex=exclude_name_regex,
1135
- method_regex=exclude_method_regex,
1136
- path_regex=exclude_path_regex,
1137
- tag_regex=exclude_tag_regex,
1138
- operation_id_regex=exclude_operation_id_regex,
1139
- )
1140
- if exclude_deprecated or skip_deprecated_operations:
1141
- filter_set.exclude(is_deprecated)
1142
-
1143
- schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
1144
- token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
1145
- schema_kind = callbacks.parse_schema_kind(schema, app)
1146
- callbacks.validate_schema(schema, schema_kind, base_url=base_url, dry_run=dry_run, app=app, api_name=api_name)
1147
- client = None
1148
- schema_or_location: str | dict[str, Any] = schema
1149
- if schema_kind == callbacks.SchemaInputKind.NAME:
1150
- api_name = schema
1151
- if (
1152
- not isinstance(report, click.utils.LazyFile)
1153
- and api_name is not None
1154
- and schema_kind == callbacks.SchemaInputKind.NAME
1155
- ):
1156
- from ..service.client import ServiceClient
1157
-
1158
- client = ServiceClient(base_url=schemathesis_io_url, token=token)
1159
- # It is assigned above
1160
- if token is not None or schema_kind == callbacks.SchemaInputKind.NAME:
1161
- if token is None:
1162
- hostname = (
1163
- "Schemathesis.io"
1164
- if schemathesis_io_hostname == service.DEFAULT_HOSTNAME
1165
- else schemathesis_io_hostname
1166
- )
1167
- click.secho(f"Missing authentication for {hostname} upload", bold=True, fg="red")
1168
- click.echo(
1169
- f"\nYou've specified an API name, suggesting you want to upload data to {bold(hostname)}. "
1170
- "However, your CLI is not currently authenticated."
1171
- )
1172
- output.default.display_service_unauthorized(hostname)
1173
- raise click.exceptions.Exit(1) from None
1174
- name: str = cast(str, api_name)
1175
- import requests
1176
-
1177
- try:
1178
- details = client.get_api_details(name)
1179
- # Replace config values with ones loaded from the service
1180
- schema_or_location = details.specification.schema
1181
- default_environment = details.default_environment
1182
- base_url = base_url or (default_environment.url if default_environment else None)
1183
- except requests.HTTPError as exc:
1184
- handle_service_error(exc, name)
1185
- if report is REPORT_TO_SERVICE and not client:
1186
- from ..service.client import ServiceClient
1187
-
1188
- # Upload without connecting data to a certain API
1189
- client = ServiceClient(base_url=schemathesis_io_url, token=token)
1190
- if experimental.SCHEMA_ANALYSIS.is_enabled and not client:
1191
- from ..service.client import ServiceClient
1192
-
1193
- client = ServiceClient(base_url=schemathesis_io_url, token=token)
1194
- host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
1195
-
1196
- if "all" in checks:
1197
- selected_checks = checks_module.ALL_CHECKS
1198
- else:
1199
- selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
1200
-
1201
- checks_config = CheckConfig()
1202
- if experimental.POSITIVE_DATA_ACCEPTANCE.is_enabled:
1203
- from ..specs.openapi.checks import positive_data_acceptance
1204
-
1205
- selected_checks += (positive_data_acceptance,)
1206
- if positive_data_acceptance_allowed_statuses:
1207
- checks_config.positive_data_acceptance.allowed_statuses = positive_data_acceptance_allowed_statuses
1208
- if missing_required_header_allowed_statuses:
1209
- from ..specs.openapi.checks import missing_required_header
1210
-
1211
- selected_checks += (missing_required_header,)
1212
- checks_config.missing_required_header.allowed_statuses = missing_required_header_allowed_statuses
1213
- if negative_data_rejection_allowed_statuses:
1214
- checks_config.negative_data_rejection.allowed_statuses = negative_data_rejection_allowed_statuses
1215
- if experimental.COVERAGE_PHASE.is_enabled:
1216
- from ..specs.openapi.checks import unsupported_method
1217
-
1218
- selected_checks += (unsupported_method,)
1219
-
1220
- selected_checks = tuple(check for check in selected_checks if check.__name__ not in exclude_checks)
1221
-
1222
- if fixups:
1223
- if "all" in fixups:
1224
- _fixups.install()
1225
- else:
1226
- _fixups.install(fixups)
1227
-
1228
- if contrib_openapi_formats_uuid:
1229
- contrib.openapi.formats.uuid.install()
1230
- if contrib_openapi_fill_missing_examples:
1231
- contrib.openapi.fill_missing_examples.install()
1232
-
1233
- hypothesis_settings = prepare_hypothesis_settings(
1234
- database=hypothesis_database,
1235
- deadline=hypothesis_deadline,
1236
- derandomize=hypothesis_derandomize,
1237
- max_examples=hypothesis_max_examples,
1238
- phases=_hypothesis_phases,
1239
- report_multiple_bugs=hypothesis_report_multiple_bugs,
1240
- suppress_health_check=_hypothesis_suppress_health_check,
1241
- verbosity=hypothesis_verbosity,
1242
- )
1243
- event_stream = into_event_stream(
1244
- schema_or_location,
1245
- app=app,
1246
- base_url=base_url,
1247
- started_at=started_at,
1248
- validate_schema=validate_schema,
1249
- data_generation_methods=data_generation_methods,
1250
- force_schema_version=force_schema_version,
1251
- request_tls_verify=request_tls_verify,
1252
- request_proxy=request_proxy,
1253
- request_cert=prepare_request_cert(request_cert, request_cert_key),
1254
- wait_for_schema=wait_for_schema,
1255
- auth=auth,
1256
- auth_type=auth_type,
1257
- override=override,
1258
- headers=headers,
1259
- request_timeout=request_timeout,
1260
- seed=hypothesis_seed,
1261
- exit_first=exit_first,
1262
- no_failfast=no_failfast,
1263
- max_failures=max_failures,
1264
- unique_data=contrib_unique_data,
1265
- dry_run=dry_run,
1266
- store_interactions=cassette_path is not None,
1267
- checks=selected_checks,
1268
- max_response_time=max_response_time,
1269
- targets=selected_targets,
1270
- workers_num=workers_num,
1271
- rate_limit=rate_limit,
1272
- stateful=stateful,
1273
- stateful_recursion_limit=stateful_recursion_limit,
1274
- hypothesis_settings=hypothesis_settings,
1275
- generation_config=generation_config,
1276
- checks_config=checks_config,
1277
- output_config=output_config,
1278
- service_client=client,
1279
- filter_set=filter_set,
1280
- )
1281
- execute(
1282
- event_stream,
1283
- ctx=ctx,
1284
- hypothesis_settings=hypothesis_settings,
1285
- workers_num=workers_num,
1286
- rate_limit=rate_limit,
1287
- show_trace=show_trace,
1288
- wait_for_schema=wait_for_schema,
1289
- validate_schema=validate_schema,
1290
- cassette_path=cassette_path,
1291
- cassette_format=cassette_format,
1292
- cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
1293
- junit_xml=junit_xml,
1294
- verbosity=verbosity,
1295
- code_sample_style=code_sample_style,
1296
- data_generation_methods=data_generation_methods,
1297
- debug_output_file=debug_output_file,
1298
- sanitize_output=sanitize_output,
1299
- host_data=host_data,
1300
- client=client,
1301
- report=report,
1302
- telemetry=schemathesis_io_telemetry,
1303
- api_name=api_name,
1304
- location=schema,
1305
- base_url=base_url,
1306
- started_at=started_at,
1307
- output_config=output_config,
1308
- )
1309
-
1310
-
1311
- def _ensure_unique_filter(values: Sequence[str], arg_name: str) -> None:
1312
- if len(values) != len(set(values)):
1313
- duplicates = ",".join(sorted({value for value in values if values.count(value) > 1}))
1314
- raise click.UsageError(f"Duplicate values are not allowed for `{arg_name}`: {duplicates}")
1315
-
1316
-
1317
- def _filter_by_expression_to_func(value: str | None, arg_name: str) -> Callable | None:
1318
- if value:
1319
- try:
1320
- return expression_to_filter_function(value)
1321
- except ValueError:
1322
- raise click.UsageError(f"Invalid expression for {arg_name}: {value}") from None
1323
- return None
1324
-
1325
-
1326
- def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
1327
- if cert is not None and key is not None:
1328
- return cert, key
1329
- return cert
1330
-
1331
-
1332
- @dataclass
1333
- class LoaderConfig:
1334
- """Container for API loader parameters.
1335
-
1336
- The main goal is to avoid too many parameters in function signatures.
1337
- """
1338
-
1339
- schema_or_location: str | dict[str, Any]
1340
- app: Any
1341
- base_url: str | None
1342
- validate_schema: bool
1343
- data_generation_methods: tuple[DataGenerationMethod, ...]
1344
- force_schema_version: str | None
1345
- request_tls_verify: bool | str
1346
- request_proxy: str | None
1347
- request_cert: RequestCert | None
1348
- wait_for_schema: float | None
1349
- rate_limit: str | None
1350
- output_config: OutputConfig
1351
- generation_config: generation.GenerationConfig
1352
- # Network request parameters
1353
- auth: tuple[str, str] | None
1354
- auth_type: str | None
1355
- headers: dict[str, str] | None
1356
-
1357
-
1358
- def into_event_stream(
1359
- schema_or_location: str | dict[str, Any],
1360
- *,
1361
- app: Any,
1362
- base_url: str | None,
1363
- started_at: str,
1364
- validate_schema: bool,
1365
- data_generation_methods: tuple[DataGenerationMethod, ...],
1366
- force_schema_version: str | None,
1367
- request_tls_verify: bool | str,
1368
- request_proxy: str | None,
1369
- request_cert: RequestCert | None,
1370
- # Network request parameters
1371
- auth: tuple[str, str] | None,
1372
- auth_type: str | None,
1373
- override: CaseOverride,
1374
- headers: dict[str, str] | None,
1375
- request_timeout: int | None,
1376
- wait_for_schema: float | None,
1377
- filter_set: FilterSet,
1378
- # Runtime behavior
1379
- checks: Iterable[CheckFunction],
1380
- checks_config: CheckConfig,
1381
- max_response_time: int | None,
1382
- targets: Iterable[Target],
1383
- workers_num: int,
1384
- hypothesis_settings: hypothesis.settings | None,
1385
- generation_config: generation.GenerationConfig,
1386
- output_config: OutputConfig,
1387
- seed: int | None,
1388
- exit_first: bool,
1389
- no_failfast: bool,
1390
- max_failures: int | None,
1391
- rate_limit: str | None,
1392
- unique_data: bool,
1393
- dry_run: bool,
1394
- store_interactions: bool,
1395
- stateful: Stateful | None,
1396
- stateful_recursion_limit: int,
1397
- service_client: ServiceClient | None,
1398
- ) -> Generator[events.ExecutionEvent, None, None]:
1399
- try:
1400
- if app is not None:
1401
- app = load_app(app)
1402
- config = LoaderConfig(
1403
- schema_or_location=schema_or_location,
1404
- app=app,
1405
- base_url=base_url,
1406
- validate_schema=validate_schema,
1407
- data_generation_methods=data_generation_methods,
1408
- force_schema_version=force_schema_version,
1409
- request_proxy=request_proxy,
1410
- request_tls_verify=request_tls_verify,
1411
- request_cert=request_cert,
1412
- wait_for_schema=wait_for_schema,
1413
- rate_limit=rate_limit,
1414
- auth=auth,
1415
- auth_type=auth_type,
1416
- headers=headers,
1417
- output_config=output_config,
1418
- generation_config=generation_config,
1419
- )
1420
- schema = load_schema(config)
1421
- schema.filter_set = filter_set
1422
- yield from runner.from_schema(
1423
- schema,
1424
- auth=auth,
1425
- auth_type=auth_type,
1426
- override=override,
1427
- headers=headers,
1428
- request_timeout=request_timeout,
1429
- request_tls_verify=request_tls_verify,
1430
- request_proxy=request_proxy,
1431
- request_cert=request_cert,
1432
- seed=seed,
1433
- exit_first=exit_first,
1434
- no_failfast=no_failfast,
1435
- max_failures=max_failures,
1436
- started_at=started_at,
1437
- unique_data=unique_data,
1438
- dry_run=dry_run,
1439
- store_interactions=store_interactions,
1440
- checks=checks,
1441
- checks_config=checks_config,
1442
- max_response_time=max_response_time,
1443
- targets=targets,
1444
- workers_num=workers_num,
1445
- stateful=stateful,
1446
- stateful_recursion_limit=stateful_recursion_limit,
1447
- hypothesis_settings=hypothesis_settings,
1448
- generation_config=generation_config,
1449
- probe_config=probes.ProbeConfig(
1450
- base_url=config.base_url,
1451
- request=RequestConfig(
1452
- timeout=request_timeout,
1453
- tls_verify=config.request_tls_verify,
1454
- proxy=config.request_proxy,
1455
- cert=config.request_cert,
1456
- ),
1457
- auth=config.auth,
1458
- auth_type=config.auth_type,
1459
- headers=config.headers,
1460
- ),
1461
- service_client=service_client,
1462
- ).execute()
1463
- except SchemaError as error:
1464
- yield events.InternalError.from_schema_error(error)
1465
- except Exception as exc:
1466
- yield events.InternalError.from_exc(exc)
1467
-
1468
-
1469
- def load_schema(config: LoaderConfig) -> BaseSchema:
1470
- """Automatically load API schema."""
1471
- first: Callable[[LoaderConfig], BaseSchema]
1472
- second: Callable[[LoaderConfig], BaseSchema]
1473
- if is_probably_graphql(config.schema_or_location):
1474
- # Try GraphQL first, then fallback to Open API
1475
- first, second = (_load_graphql_schema, _load_openapi_schema)
1476
- else:
1477
- # Try Open API first, then fallback to GraphQL
1478
- first, second = (_load_openapi_schema, _load_graphql_schema)
1479
- return _try_load_schema(config, first, second)
1480
-
1481
-
1482
- def should_try_more(exc: SchemaError) -> bool:
1483
- import requests
1484
- from yaml.reader import ReaderError
1485
-
1486
- if isinstance(exc.__cause__, ReaderError) and "characters are not allowed" in str(exc.__cause__):
1487
- return False
1488
-
1489
- # We should not try other loaders for cases when we can't even establish connection
1490
- return not isinstance(exc.__cause__, requests.exceptions.ConnectionError) and exc.type not in (
1491
- SchemaErrorType.OPEN_API_INVALID_SCHEMA,
1492
- SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
1493
- SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
1494
- SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION,
1495
- )
1496
-
1497
-
1498
- Loader = Callable[[LoaderConfig], "BaseSchema"]
1499
-
1500
-
1501
- def _try_load_schema(config: LoaderConfig, first: Loader, second: Loader) -> BaseSchema:
1502
- from urllib3.exceptions import InsecureRequestWarning
1503
-
1504
- with warnings.catch_warnings():
1505
- warnings.simplefilter("ignore", InsecureRequestWarning)
1506
- try:
1507
- return first(config)
1508
- except SchemaError as exc:
1509
- if config.force_schema_version is None and should_try_more(exc):
1510
- try:
1511
- return second(config)
1512
- except Exception as second_exc:
1513
- if is_specific_exception(second, second_exc):
1514
- raise second_exc
1515
- # Re-raise the original error
1516
- raise exc
1517
-
1518
-
1519
- def is_specific_exception(loader: Loader, exc: Exception) -> bool:
1520
- return (
1521
- loader is _load_graphql_schema
1522
- and isinstance(exc, SchemaError)
1523
- and exc.type == SchemaErrorType.GRAPHQL_INVALID_SCHEMA
1524
- # In some cases it is not clear that the schema is even supposed to be GraphQL, e.g. an empty input
1525
- and "Syntax Error: Unexpected <EOF>." not in exc.extras
1526
- )
1527
-
1528
-
1529
- def _load_graphql_schema(config: LoaderConfig) -> GraphQLSchema:
1530
- loader = detect_loader(config.schema_or_location, config.app, is_openapi=False)
1531
- kwargs = get_graphql_loader_kwargs(loader, config)
1532
- return loader(config.schema_or_location, **kwargs)
1533
-
1534
-
1535
- def _load_openapi_schema(config: LoaderConfig) -> BaseSchema:
1536
- loader = detect_loader(config.schema_or_location, config.app, is_openapi=True)
1537
- kwargs = get_loader_kwargs(loader, config)
1538
- return loader(config.schema_or_location, **kwargs)
1539
-
1540
-
1541
- def detect_loader(schema_or_location: str | dict[str, Any], app: Any, is_openapi: bool) -> Callable:
1542
- """Detect API schema loader."""
1543
- if isinstance(schema_or_location, str):
1544
- if file_exists(schema_or_location):
1545
- # If there is an existing file with the given name,
1546
- # then it is likely that the user wants to load API schema from there
1547
- return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
1548
- if app is not None and not urlparse(schema_or_location).netloc:
1549
- # App is passed & location is relative
1550
- return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
1551
- # Default behavior
1552
- return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
1553
- return oas_loaders.from_dict if is_openapi else gql_loaders.from_dict # type: ignore
1554
-
1555
-
1556
- def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
1557
- """Detect the proper set of parameters for a loader."""
1558
- # These kwargs are shared by all loaders
1559
- kwargs = {
1560
- "app": config.app,
1561
- "base_url": config.base_url,
1562
- "validate_schema": config.validate_schema,
1563
- "force_schema_version": config.force_schema_version,
1564
- "data_generation_methods": config.data_generation_methods,
1565
- "rate_limit": config.rate_limit,
1566
- "output_config": config.output_config,
1567
- "generation_config": config.generation_config,
1568
- }
1569
- if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
1570
- kwargs["headers"] = config.headers
1571
- if loader in (oas_loaders.from_uri, oas_loaders.from_aiohttp):
1572
- _add_requests_kwargs(kwargs, config)
1573
- return kwargs
1574
-
1575
-
1576
- def get_graphql_loader_kwargs(
1577
- loader: Callable,
1578
- config: LoaderConfig,
1579
- ) -> dict[str, Any]:
1580
- """Detect the proper set of parameters for a loader."""
1581
- # These kwargs are shared by all loaders
1582
- kwargs = {
1583
- "app": config.app,
1584
- "base_url": config.base_url,
1585
- "data_generation_methods": config.data_generation_methods,
1586
- "rate_limit": config.rate_limit,
1587
- }
1588
- if loader not in (gql_loaders.from_path, gql_loaders.from_dict):
1589
- kwargs["headers"] = config.headers
1590
- if loader is gql_loaders.from_url:
1591
- _add_requests_kwargs(kwargs, config)
1592
- return kwargs
1593
-
1594
-
1595
- def _add_requests_kwargs(kwargs: dict[str, Any], config: LoaderConfig) -> None:
1596
- kwargs["verify"] = config.request_tls_verify
1597
- if config.request_cert is not None:
1598
- kwargs["cert"] = config.request_cert
1599
- if config.auth is not None:
1600
- kwargs["auth"] = get_requests_auth(config.auth, config.auth_type)
1601
- if config.wait_for_schema is not None:
1602
- kwargs["wait_for_schema"] = config.wait_for_schema
1603
-
1604
-
1605
- def is_probably_graphql(schema_or_location: str | dict[str, Any]) -> bool:
1606
- """Detect whether it is likely that the given location is a GraphQL endpoint."""
1607
- if isinstance(schema_or_location, str):
1608
- return schema_or_location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
1609
- return "__schema" in schema_or_location or (
1610
- "data" in schema_or_location and "__schema" in schema_or_location["data"]
1611
- )
1612
-
1613
-
1614
- def check_auth(auth: tuple[str, str] | None, headers: dict[str, str], override: CaseOverride) -> None:
1615
- auth_is_set = auth is not None
1616
- header_is_set = "authorization" in {header.lower() for header in headers}
1617
- override_is_set = "authorization" in {header.lower() for header in override.headers}
1618
- if len([is_set for is_set in (auth_is_set, header_is_set, override_is_set) if is_set]) > 1:
1619
- message = "The "
1620
- used = []
1621
- if auth_is_set:
1622
- used.append("`--auth`")
1623
- if header_is_set:
1624
- used.append("`--header`")
1625
- if override_is_set:
1626
- used.append("`--set-header`")
1627
- message += " and ".join(used)
1628
- message += " options were both used to set the 'Authorization' header, which is not permitted."
1629
- raise click.BadParameter(message)
1630
-
1631
-
1632
- def get_output_handler(workers_num: int) -> EventHandler:
1633
- if workers_num > 1:
1634
- output_style = OutputStyle.short
1635
- else:
1636
- output_style = OutputStyle.default
1637
- return output_style.value()
1638
-
1639
-
1640
- def load_hook(module_name: str) -> None:
1641
- """Load the given hook by importing it."""
1642
- try:
1643
- sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
1644
- __import__(module_name)
1645
- except Exception as exc:
1646
- click.secho("Unable to load Schemathesis extension hooks", fg="red", bold=True)
1647
- formatted_module_name = bold(f"'{module_name}'")
1648
- if isinstance(exc, ModuleNotFoundError) and exc.name == module_name:
1649
- click.echo(
1650
- f"\nAn attempt to import the module {formatted_module_name} failed because it could not be found."
1651
- )
1652
- click.echo("\nEnsure the module name is correctly spelled and reachable from the current directory.")
1653
- else:
1654
- click.echo(f"\nAn error occurred while importing the module {formatted_module_name}. Traceback:")
1655
- trace = extract_nth_traceback(exc.__traceback__, 1)
1656
- lines = traceback.format_exception(type(exc), exc, trace)
1657
- message = "".join(lines).strip()
1658
- click.secho(f"\n{message}", fg="red")
1659
- click.echo(f"\nFor more information on how to work with hooks, visit {EXTENSIONS_DOCUMENTATION_URL}")
1660
- raise click.exceptions.Exit(1) from None
1661
-
1662
-
1663
- class OutputStyle(Enum):
1664
- """Provide different output styles."""
1665
-
1666
- default = output.default.DefaultOutputStyleHandler
1667
- short = output.short.ShortOutputStyleHandler
1668
-
1669
-
1670
- def execute(
1671
- event_stream: Generator[events.ExecutionEvent, None, None],
1672
- *,
1673
- ctx: click.Context,
1674
- hypothesis_settings: hypothesis.settings,
1675
- workers_num: int,
1676
- rate_limit: str | None,
1677
- show_trace: bool,
1678
- wait_for_schema: float | None,
1679
- validate_schema: bool,
1680
- cassette_path: click.utils.LazyFile | None,
1681
- cassette_format: cassettes.CassetteFormat,
1682
- cassette_preserve_exact_body_bytes: bool,
1683
- junit_xml: click.utils.LazyFile | None,
1684
- verbosity: int,
1685
- code_sample_style: CodeSampleStyle,
1686
- data_generation_methods: tuple[DataGenerationMethod, ...],
1687
- debug_output_file: click.utils.LazyFile | None,
1688
- sanitize_output: bool,
1689
- host_data: service.hosts.HostData,
1690
- client: ServiceClient | None,
1691
- report: ReportToService | click.utils.LazyFile | None,
1692
- telemetry: bool,
1693
- api_name: str | None,
1694
- location: str,
1695
- base_url: str | None,
1696
- started_at: str,
1697
- output_config: OutputConfig,
1698
- ) -> None:
1699
- """Execute a prepared runner by drawing events from it and passing to a proper handler."""
1700
- handlers: list[EventHandler] = []
1701
- report_context: ServiceReportContext | FileReportContext | None = None
1702
- report_queue: Queue
1703
- if client:
1704
- # If API name is specified, validate it
1705
- report_queue = Queue()
1706
- report_context = ServiceReportContext(queue=report_queue, service_base_url=client.base_url)
1707
- handlers.append(
1708
- service.ServiceReportHandler(
1709
- client=client,
1710
- host_data=host_data,
1711
- api_name=api_name,
1712
- location=location,
1713
- base_url=base_url,
1714
- started_at=started_at,
1715
- out_queue=report_queue,
1716
- telemetry=telemetry,
1717
- )
1718
- )
1719
- elif isinstance(report, click.utils.LazyFile):
1720
- _open_file(report)
1721
- report_queue = Queue()
1722
- report_context = FileReportContext(queue=report_queue, filename=report.name)
1723
- handlers.append(
1724
- service.FileReportHandler(
1725
- file_handle=report,
1726
- api_name=api_name,
1727
- location=location,
1728
- base_url=base_url,
1729
- started_at=started_at,
1730
- out_queue=report_queue,
1731
- telemetry=telemetry,
1732
- )
1733
- )
1734
- if junit_xml is not None:
1735
- _open_file(junit_xml)
1736
- handlers.append(JunitXMLHandler(junit_xml))
1737
- if debug_output_file is not None:
1738
- _open_file(debug_output_file)
1739
- handlers.append(DebugOutputHandler(debug_output_file))
1740
- if cassette_path is not None:
1741
- # This handler should be first to have logs writing completed when the output handler will display statistic
1742
- _open_file(cassette_path)
1743
- handlers.append(
1744
- cassettes.CassetteWriter(
1745
- cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
1746
- )
1747
- )
1748
- for custom_handler in CUSTOM_HANDLERS:
1749
- handlers.append(custom_handler(*ctx.args, **ctx.params))
1750
- handlers.append(get_output_handler(workers_num))
1751
- if sanitize_output:
1752
- handlers.insert(0, SanitizationHandler())
1753
- execution_context = ExecutionContext(
1754
- hypothesis_settings=hypothesis_settings,
1755
- workers_num=workers_num,
1756
- rate_limit=rate_limit,
1757
- show_trace=show_trace,
1758
- wait_for_schema=wait_for_schema,
1759
- validate_schema=validate_schema,
1760
- cassette_path=cassette_path.name if cassette_path is not None else None,
1761
- junit_xml_file=junit_xml.name if junit_xml is not None else None,
1762
- verbosity=verbosity,
1763
- code_sample_style=code_sample_style,
1764
- report=report_context,
1765
- output_config=output_config,
1766
- )
1767
-
1768
- def shutdown() -> None:
1769
- for _handler in handlers:
1770
- _handler.shutdown()
1771
-
1772
- GLOBAL_HOOK_DISPATCHER.dispatch("after_init_cli_run_handlers", HookContext(), handlers, execution_context)
1773
- event = None
1774
- try:
1775
- for event in event_stream:
1776
- for handler in handlers:
1777
- try:
1778
- handler.handle_event(execution_context, event)
1779
- except Exception as exc:
1780
- # `Abort` is used for handled errors
1781
- if not isinstance(exc, click.Abort):
1782
- display_handler_error(handler, exc)
1783
- raise
1784
- except Exception as exc:
1785
- if isinstance(exc, click.Abort):
1786
- # To avoid showing "Aborted!" message, which is the default behavior in Click
1787
- sys.exit(1)
1788
- raise
1789
- finally:
1790
- shutdown()
1791
- if event is not None and event.is_terminal:
1792
- exit_code = get_exit_code(event)
1793
- sys.exit(exit_code)
1794
- # Event stream did not finish with a terminal event. Only possible if the handler is broken
1795
- click.secho("Unexpected error", fg="red")
1796
- sys.exit(1)
1797
-
1798
-
1799
- def _open_file(file: click.utils.LazyFile) -> None:
1800
- from ..utils import _ensure_parent
1801
-
1802
- try:
1803
- _ensure_parent(file.name, fail_silently=False)
1804
- except OSError as exc:
1805
- raise click.BadParameter(f"'{file.name}': {exc.strerror}") from exc
1806
- try:
1807
- file.open()
1808
- except click.FileError as exc:
1809
- raise click.BadParameter(exc.format_message()) from exc
1810
-
1811
-
1812
- def is_built_in_handler(handler: EventHandler) -> bool:
1813
- # Look for exact instances, not subclasses
1814
- return any(
1815
- type(handler) is class_
1816
- for class_ in (
1817
- output.default.DefaultOutputStyleHandler,
1818
- output.short.ShortOutputStyleHandler,
1819
- service.FileReportHandler,
1820
- service.ServiceReportHandler,
1821
- DebugOutputHandler,
1822
- cassettes.CassetteWriter,
1823
- JunitXMLHandler,
1824
- SanitizationHandler,
1825
- )
1826
- )
1827
-
1828
-
1829
- def display_handler_error(handler: EventHandler, exc: Exception) -> None:
1830
- """Display error that happened within."""
1831
- is_built_in = is_built_in_handler(handler)
1832
- if is_built_in:
1833
- click.secho("Internal Error", fg="red", bold=True)
1834
- click.secho("\nSchemathesis encountered an unexpected issue.")
1835
- trace = exc.__traceback__
1836
- else:
1837
- click.secho("CLI Handler Error", fg="red", bold=True)
1838
- click.echo(f"\nAn error occurred within your custom CLI handler `{bold(handler.__class__.__name__)}`.")
1839
- trace = extract_nth_traceback(exc.__traceback__, 1)
1840
- lines = traceback.format_exception(type(exc), exc, trace)
1841
- message = "".join(lines).strip()
1842
- click.secho(f"\n{message}", fg="red")
1843
- if is_built_in:
1844
- click.echo(
1845
- f"\nWe apologize for the inconvenience. This appears to be an internal issue.\n"
1846
- f"Please consider reporting this error to our issue tracker:\n\n {ISSUE_TRACKER_URL}."
1847
- )
1848
- else:
1849
- click.echo(
1850
- f"\nFor more information on implementing extensions for Schemathesis CLI, visit {EXTENSIONS_DOCUMENTATION_URL}"
1851
- )
1852
-
1853
-
1854
- def handle_service_error(exc: requests.HTTPError, api_name: str) -> NoReturn:
1855
- import requests
1856
-
1857
- response = cast(requests.Response, exc.response)
1858
- if response.status_code == 403:
1859
- error_message(response.json()["detail"])
1860
- elif response.status_code == 404:
1861
- error_message(f"API with name `{api_name}` not found!")
1862
- else:
1863
- output.default.display_service_error(service.Error(exc), message_prefix="❌ ")
1864
- sys.exit(1)
1865
-
1866
-
1867
- def get_exit_code(event: events.ExecutionEvent) -> int:
1868
- if isinstance(event, events.Finished):
1869
- if event.has_failures or event.has_errors:
1870
- return 1
1871
- return 0
1872
- # Practically not possible. May occur only if the output handler is broken - in this case we still will have the
1873
- # right exit code.
1874
- return 1
1875
-
1876
-
1877
- @schemathesis.command(short_help="Replay requests from a saved cassette.")
1878
- @click.argument("cassette_path", type=click.Path(exists=True))
1879
- @click.option("--id", "id_", help="ID of interaction to replay", type=str)
1880
- @click.option("--status", help="Status of interactions to replay", type=str)
1881
- @click.option("--uri", help="A regexp that filters interactions by their request URI", type=str)
1882
- @click.option("--method", help="A regexp that filters interactions by their request method", type=str)
1883
- @click.option("--no-color", help="Disable ANSI color escape codes", type=bool, is_flag=True)
1884
- @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes", type=bool, is_flag=True)
1885
- @click.option("--verbosity", "-v", help="Increase verbosity of the output", count=True)
1886
- @with_request_tls_verify
1887
- @with_request_proxy
1888
- @with_request_cert
1889
- @with_request_cert_key
1890
- @click.pass_context
1891
- def replay(
1892
- ctx: click.Context,
1893
- cassette_path: str,
1894
- id_: str | None,
1895
- status: str | None = None,
1896
- uri: str | None = None,
1897
- method: str | None = None,
1898
- no_color: bool = False,
1899
- verbosity: int = 0,
1900
- request_tls_verify: bool = True,
1901
- request_cert: str | None = None,
1902
- request_cert_key: str | None = None,
1903
- request_proxy: str | None = None,
1904
- force_color: bool = False,
1905
- ) -> None:
1906
- """Replay a cassette.
1907
-
1908
- Cassettes in VCR-compatible format can be replayed.
1909
- For example, ones that are recorded with the ``--cassette-path`` option of the `st run` command.
1910
- """
1911
- if no_color and force_color:
1912
- raise click.UsageError(COLOR_OPTIONS_INVALID_USAGE_MESSAGE)
1913
- decide_color_output(ctx, no_color, force_color)
1914
-
1915
- click.secho(f"{bold('Replaying cassette')}: {cassette_path}")
1916
- with open(cassette_path, "rb") as fd:
1917
- cassette = load_yaml(fd)
1918
- click.secho(f"{bold('Total interactions')}: {len(cassette['http_interactions'])}\n")
1919
- for replayed in cassettes.replay(
1920
- cassette,
1921
- id_=id_,
1922
- status=status,
1923
- uri=uri,
1924
- method=method,
1925
- request_tls_verify=request_tls_verify,
1926
- request_cert=prepare_request_cert(request_cert, request_cert_key),
1927
- request_proxy=request_proxy,
1928
- ):
1929
- click.secho(f" {bold('ID')} : {replayed.interaction['id']}")
1930
- click.secho(f" {bold('URI')} : {replayed.interaction['request']['uri']}")
1931
- click.secho(f" {bold('Old status code')} : {replayed.interaction['response']['status']['code']}")
1932
- click.secho(f" {bold('New status code')} : {replayed.response.status_code}")
1933
- if verbosity > 0:
1934
- data = replayed.interaction["response"]
1935
- old_body = ""
1936
- # Body may be missing for 204 responses
1937
- if "body" in data:
1938
- if "base64_string" in data["body"]:
1939
- content = data["body"]["base64_string"]
1940
- if content:
1941
- old_body = base64.b64decode(content).decode(errors="replace")
1942
- else:
1943
- old_body = data["body"]["string"]
1944
- click.secho(f" {bold('Old payload')} : {old_body}")
1945
- click.secho(f" {bold('New payload')} : {replayed.response.text}")
1946
- click.echo()
1947
-
1948
-
1949
- @schemathesis.command(short_help="Upload report to Schemathesis.io.")
1950
- @click.argument("report", type=click.File(mode="rb"))
1951
- @click.option(
1952
- "--schemathesis-io-token",
1953
- help="Schemathesis.io authentication token",
1954
- type=str,
1955
- envvar=service.TOKEN_ENV_VAR,
1956
- )
1957
- @click.option(
1958
- "--schemathesis-io-url",
1959
- help="Schemathesis.io base URL",
1960
- default=service.DEFAULT_URL,
1961
- type=str,
1962
- envvar=service.URL_ENV_VAR,
1963
- )
1964
- @with_request_tls_verify
1965
- @with_hosts_file
1966
- def upload(
1967
- report: io.BufferedReader,
1968
- hosts_file: str,
1969
- request_tls_verify: bool = True,
1970
- schemathesis_io_url: str = service.DEFAULT_URL,
1971
- schemathesis_io_token: str | None = None,
1972
- ) -> None:
1973
- """Upload report to Schemathesis.io."""
1974
- from ..service.client import ServiceClient
1975
- from ..service.models import UploadResponse, UploadSource
1976
-
1977
- schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
1978
- host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
1979
- token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
1980
- client = ServiceClient(base_url=schemathesis_io_url, token=token, verify=request_tls_verify)
1981
- ci_environment = service.ci.environment()
1982
- provider = ci_environment.provider if ci_environment is not None else None
1983
- response = client.upload_report(
1984
- report=report.read(),
1985
- correlation_id=host_data.correlation_id,
1986
- ci_provider=provider,
1987
- source=UploadSource.UPLOAD_COMMAND,
1988
- )
1989
- if isinstance(response, UploadResponse):
1990
- host_data.store_correlation_id(response.correlation_id)
1991
- click.echo(f"{response.message}\n{response.next_url}")
1992
- else:
1993
- error_message(f"Failed to upload report to {schemathesis_io_hostname}: " + bold(response.detail))
1994
- sys.exit(1)
1995
-
1996
-
1997
- @schemathesis.group(short_help="Authenticate with Schemathesis.io.")
1998
- def auth() -> None:
1999
- pass
2000
-
2001
-
2002
- @auth.command(short_help="Authenticate with a Schemathesis.io host.")
2003
- @click.argument("token", type=str, envvar=service.TOKEN_ENV_VAR)
2004
- @click.option(
2005
- "--hostname",
2006
- help="The hostname of the Schemathesis.io instance to authenticate with",
2007
- type=str,
2008
- default=service.DEFAULT_HOSTNAME,
2009
- envvar=service.HOSTNAME_ENV_VAR,
2010
- )
2011
- @click.option(
2012
- "--protocol",
2013
- type=click.Choice(["https", "http"]),
2014
- default=service.DEFAULT_PROTOCOL,
2015
- envvar=service.PROTOCOL_ENV_VAR,
2016
- )
2017
- @with_request_tls_verify
2018
- @with_hosts_file
2019
- def login(token: str, hostname: str, hosts_file: str, protocol: str, request_tls_verify: bool = True) -> None:
2020
- """Authenticate with a Schemathesis.io host."""
2021
- import requests
2022
-
2023
- try:
2024
- username = service.auth.login(token, hostname, protocol, request_tls_verify)
2025
- service.hosts.store(token, hostname, hosts_file)
2026
- success_message(f"Logged in into {hostname} as " + bold(username))
2027
- except requests.HTTPError as exc:
2028
- response = cast(requests.Response, exc.response)
2029
- detail = response.json()["detail"]
2030
- error_message(f"Failed to login into {hostname}: " + bold(detail))
2031
- sys.exit(1)
2032
-
2033
-
2034
- @auth.command(short_help="Remove authentication for a Schemathesis.io host.")
2035
- @click.option(
2036
- "--hostname",
2037
- help="The hostname of the Schemathesis.io instance to authenticate with",
2038
- type=str,
2039
- default=service.DEFAULT_HOSTNAME,
2040
- envvar=service.HOSTNAME_ENV_VAR,
2041
- )
2042
- @with_hosts_file
2043
- def logout(hostname: str, hosts_file: str) -> None:
2044
- """Remove authentication for a Schemathesis.io host."""
2045
- result = service.hosts.remove(hostname, hosts_file)
2046
- if result == service.hosts.RemoveAuth.success:
2047
- success_message(f"Logged out of {hostname} account")
2048
- else:
2049
- if result == service.hosts.RemoveAuth.no_match:
2050
- warning_message(f"Not logged in to {hostname}")
2051
- if result == service.hosts.RemoveAuth.no_hosts:
2052
- warning_message("Not logged in to any hosts")
2053
- if result == service.hosts.RemoveAuth.error:
2054
- error_message(f"Failed to read the hosts file. Try to remove {hosts_file}")
2055
- sys.exit(1)
2056
-
2057
-
2058
- def success_message(message: str) -> None:
2059
- click.secho(click.style("✔️", fg="green") + f" {message}")
2060
-
2061
-
2062
- def warning_message(message: str) -> None:
2063
- click.secho(click.style("🟡️", fg="yellow") + f" {message}")
2064
-
2065
-
2066
- def error_message(message: str) -> None:
2067
- click.secho(f"❌ {message}")
2068
-
2069
-
2070
- def bold(message: str) -> str:
2071
- return click.style(message, bold=True)
2072
-
2073
-
2074
- def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -> None:
2075
- if force_color:
2076
- ctx.color = True
2077
- elif no_color or "NO_COLOR" in os.environ:
2078
- ctx.color = False
2079
-
2080
-
2081
- def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
2082
- """Add a new CLI option to `st run`."""
2083
- run.params.append(cls(args, **kwargs))
2084
-
2085
-
2086
- @dataclass
2087
- class Group:
2088
- name: str
2089
-
2090
- def add_option(self, *args: Any, **kwargs: Any) -> None:
2091
- kwargs["cls"] = GroupedOption
2092
- kwargs["group"] = self.name
2093
- add_option(*args, **kwargs)
2094
-
2095
-
2096
22
  def add_group(name: str, *, index: int | None = None) -> Group:
2097
23
  """Add a custom options group to `st run`."""
2098
24
  if index is not None:
2099
- GROUPS.insert(index, name)
25
+ GROUPS[name] = OptionGroup(name=name, order=index)
2100
26
  else:
2101
- GROUPS.append(name)
27
+ GROUPS[name] = OptionGroup(name=name)
2102
28
  return Group(name)
2103
-
2104
-
2105
- def handler() -> Callable[[type], None]:
2106
- """Register a new CLI event handler."""
2107
-
2108
- def _wrapper(cls: type) -> None:
2109
- CUSTOM_HANDLERS.append(cls)
2110
-
2111
- return _wrapper
2112
-
2113
-
2114
- @HookDispatcher.register_spec([HookScope.GLOBAL])
2115
- def after_init_cli_run_handlers(
2116
- context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
2117
- ) -> None:
2118
- """Called after CLI hooks are initialized.
2119
-
2120
- Might be used to add extra event handlers.
2121
- """
2122
-
2123
-
2124
- @HookDispatcher.register_spec([HookScope.GLOBAL])
2125
- def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
2126
- """Called before every network call in CLI tests.
2127
-
2128
- Aims to modify the argument passed to `case.call`.
2129
- Note that you need to modify `kwargs` in-place.
2130
- """