schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,1220 +1,28 @@
1
- # pylint: disable=too-many-lines,redefined-outer-name
2
- import enum
3
- import os
4
- import sys
5
- import traceback
6
- from collections import defaultdict
7
- from enum import Enum
8
- from queue import Queue
9
- from typing import Any, Callable, Dict, Generator, Iterable, List, NoReturn, Optional, Tuple, Union
10
- from urllib.parse import urlparse
11
-
12
- import attr
13
- import click
14
- import hypothesis
15
- import requests
16
- import yaml
17
-
18
- from .. import checks as checks_module
19
- from .. import fixups as _fixups
20
- from .. import runner, service
21
- from .. import targets as targets_module
22
- from ..constants import (
23
- DEFAULT_DATA_GENERATION_METHODS,
24
- DEFAULT_RESPONSE_TIMEOUT,
25
- DEFAULT_STATEFUL_RECURSION_LIMIT,
26
- HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
27
- CodeSampleStyle,
28
- DataGenerationMethod,
29
- )
30
- from ..exceptions import HTTPError, SchemaLoadingError
31
- from ..fixups import ALL_FIXUPS
32
- from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
33
- from ..models import Case, CheckFunction
34
- from ..runner import events, prepare_hypothesis_settings
35
- from ..schemas import BaseSchema
36
- from ..specs.graphql import loaders as gql_loaders
37
- from ..specs.graphql.schemas import GraphQLSchema
38
- from ..specs.openapi import loaders as oas_loaders
39
- from ..stateful import Stateful
40
- from ..targets import Target
41
- from ..types import Filter, PathLike, RequestCert
42
- from ..utils import GenericResponse, file_exists, get_requests_auth, import_app
43
- from . import callbacks, cassettes, output
44
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
45
- from .context import ExecutionContext, ServiceContext
46
- from .debug import DebugOutputHandler
47
- from .handlers import EventHandler
48
- from .junitxml import JunitXMLHandler
49
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
50
-
51
- try:
52
- from yaml import CSafeLoader as SafeLoader
53
- except ImportError:
54
- # pylint: disable=unused-import
55
- from yaml import SafeLoader # type: ignore
56
-
57
-
58
- def _get_callable_names(items: Tuple[Callable, ...]) -> Tuple[str, ...]:
59
- return tuple(item.__name__ for item in items)
60
-
61
-
62
- CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
63
-
64
- DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
65
- ALL_CHECKS_NAMES = _get_callable_names(checks_module.ALL_CHECKS)
66
- CHECKS_TYPE = CsvChoice((*ALL_CHECKS_NAMES, "all"))
67
-
68
- DEFAULT_TARGETS_NAMES = _get_callable_names(targets_module.DEFAULT_TARGETS)
69
- ALL_TARGETS_NAMES = _get_callable_names(targets_module.ALL_TARGETS)
70
- TARGETS_TYPE = click.Choice((*ALL_TARGETS_NAMES, "all"))
71
-
72
- DATA_GENERATION_METHOD_TYPE = click.Choice([item.name for item in DataGenerationMethod])
73
-
74
- DEPRECATED_CASSETTE_PATH_OPTION_WARNING = (
75
- "Warning: Option `--store-network-log` is deprecated and will be removed in Schemathesis 4.0. "
76
- "Use `--cassette-path` instead."
77
- )
78
- CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
79
-
80
-
81
- def register_target(function: Target) -> Target:
82
- """Register a new testing target for schemathesis CLI.
83
-
84
- :param function: A function that will be called to calculate a metric passed to ``hypothesis.target``.
85
- """
86
- targets_module.ALL_TARGETS += (function,)
87
- TARGETS_TYPE.choices += (function.__name__,) # type: ignore
88
- return function
89
-
90
-
91
- def register_check(function: CheckFunction) -> CheckFunction:
92
- """Register a new check for schemathesis CLI.
93
-
94
- :param function: A function to validate API responses.
95
-
96
- .. code-block:: python
97
-
98
- @schemathesis.register_check
99
- def new_check(response, case):
100
- # some awesome assertions!
101
- ...
102
- """
103
- checks_module.ALL_CHECKS += (function,)
104
- CHECKS_TYPE.choices += (function.__name__,) # type: ignore
105
- return function
106
-
107
-
108
- def reset_checks() -> None:
109
- """Get checks list to their default state."""
110
- # Useful in tests
111
- checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
112
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
113
-
114
-
115
- def reset_targets() -> None:
116
- """Get targets list to their default state."""
117
- # Useful in tests
118
- targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
119
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
120
-
121
-
122
- class DeprecatedOption(click.Option):
123
- def __init__(self, *args: Any, removed_in: str, **kwargs: Any) -> None:
124
- super().__init__(*args, **kwargs)
125
- self.removed_in = removed_in
126
-
127
- def handle_parse_result(self, ctx: click.Context, opts: Dict[str, Any], args: List[str]) -> Tuple[Any, List[str]]:
128
- if self.name in opts:
129
- opt_names = "/".join(f"`{name}`" for name in self.opts)
130
- verb = "is" if len(self.opts) == 1 else "are"
131
- click.secho(
132
- f"\nWARNING: {opt_names} {verb} deprecated and will be removed in Schemathesis {self.removed_in}\n",
133
- fg="yellow",
134
- )
135
- return super().handle_parse_result(ctx, opts, args)
136
-
137
-
138
- @click.group(context_settings=CONTEXT_SETTINGS)
139
- @click.option("--pre-run", help="A module to execute before the running the tests.", type=str)
140
- @click.version_option()
141
- def schemathesis(pre_run: Optional[str] = None) -> None:
142
- """Command line tool for testing your web application built with Open API / GraphQL specifications."""
143
- if pre_run:
144
- load_hook(pre_run)
145
-
146
-
147
- class ParameterGroup(enum.Enum):
148
- filtering = "Filtering", "These options define what parts of the API will be tested."
149
- validation = "Validation", "Options, responsible for how responses & schemas will be checked."
150
- hypothesis = "Hypothesis", "Configuration of the underlying Hypothesis engine."
151
- generic = "Generic", None
152
-
153
-
154
- class CommandWithCustomHelp(click.Command):
155
- def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
156
- # Group options first
157
- groups = defaultdict(list)
158
- for param in self.get_params(ctx):
159
- rv = param.get_help_record(ctx)
160
- if rv is not None:
161
- if isinstance(param, GroupedOption):
162
- group = param.group
163
- else:
164
- group = ParameterGroup.generic
165
- groups[group].append(rv)
166
- # Then display groups separately with optional description
167
- for group in ParameterGroup:
168
- opts = groups[group]
169
- group_name, description = group.value
170
- with formatter.section(f"{group_name} options"):
171
- if description:
172
- formatter.write_paragraph()
173
- formatter.write_text(description)
174
- formatter.write_paragraph()
175
- formatter.write_dl(opts)
176
-
177
-
178
- class GroupedOption(click.Option):
179
- def __init__(self, *args: Any, group: ParameterGroup, **kwargs: Any):
180
- super().__init__(*args, **kwargs)
181
- self.group = group
182
-
183
-
184
- with_request_tls_verify = click.option(
185
- "--request-tls-verify",
186
- help="Controls whether Schemathesis verifies the server's TLS certificate. "
187
- "You can also pass the path to a CA_BUNDLE file for private certs.",
188
- type=str,
189
- default="true",
190
- show_default=True,
191
- callback=callbacks.convert_request_tls_verify,
192
- )
193
- with_request_cert = click.option(
194
- "--request-cert",
195
- help="File path of unencrypted client certificate for authentication. "
196
- "The certificate can be bundled with a private key (e.g. PEM) or the private "
197
- "key can be provided with the --request-cert-key argument.",
198
- type=click.Path(exists=True),
199
- default=None,
200
- show_default=False,
201
- )
202
- with_request_cert_key = click.option(
203
- "--request-cert-key",
204
- help="File path of the private key of the client certificate.",
205
- type=click.Path(exists=True),
206
- default=None,
207
- show_default=False,
208
- callback=callbacks.validate_request_cert_key,
209
- )
210
- with_hosts_file = click.option(
211
- "--hosts-file",
212
- help="Path to a file to store the Schemathesis.io auth configuration.",
213
- type=click.Path(dir_okay=False, writable=True),
214
- default=service.DEFAULT_HOSTS_PATH,
215
- envvar=service.HOSTS_PATH_ENV_VAR,
216
- )
217
-
218
-
219
- @schemathesis.command(short_help="Perform schemathesis test.", cls=CommandWithCustomHelp)
220
- @click.argument("schema", type=str)
221
- @click.argument("api_slug", type=str, required=False)
222
- @click.option(
223
- "--checks",
224
- "-c",
225
- multiple=True,
226
- help="List of checks to run.",
227
- type=CHECKS_TYPE,
228
- default=DEFAULT_CHECKS_NAMES,
229
- cls=GroupedOption,
230
- group=ParameterGroup.validation,
231
- callback=callbacks.convert_checks,
232
- show_default=True,
233
- )
234
- @click.option(
235
- "--data-generation-method",
236
- "-D",
237
- "data_generation_methods",
238
- help="Defines how Schemathesis generates data for tests.",
239
- type=DATA_GENERATION_METHOD_TYPE,
240
- default=DataGenerationMethod.default(),
241
- callback=callbacks.convert_data_generation_method,
242
- show_default=True,
243
- )
244
- @click.option(
245
- "--max-response-time",
246
- help="A custom check that will fail if the response time is greater than the specified one in milliseconds.",
247
- type=click.IntRange(min=1),
248
- cls=GroupedOption,
249
- group=ParameterGroup.validation,
250
- )
251
- @click.option(
252
- "--target",
253
- "-t",
254
- "targets",
255
- multiple=True,
256
- help="Targets for input generation.",
257
- type=TARGETS_TYPE,
258
- default=DEFAULT_TARGETS_NAMES,
259
- show_default=True,
260
- )
261
- @click.option(
262
- "-x",
263
- "--exitfirst",
264
- "exit_first",
265
- is_flag=True,
266
- default=False,
267
- help="Exit instantly on first error or failed test.",
268
- show_default=True,
269
- )
270
- @click.option(
271
- "--dry-run",
272
- "dry_run",
273
- is_flag=True,
274
- default=False,
275
- help="Disable sending data to the application and checking responses. "
276
- "Helpful to verify whether data is generated at all.",
277
- )
278
- @click.option(
279
- "--auth", "-a", help="Server user and password. Example: USER:PASSWORD", type=str, callback=callbacks.validate_auth
280
- )
281
- @click.option(
282
- "--auth-type",
283
- "-A",
284
- type=click.Choice(["basic", "digest"], case_sensitive=False),
285
- default="basic",
286
- help="The authentication mechanism to be used. Defaults to 'basic'.",
287
- show_default=True,
288
- )
289
- @click.option(
290
- "--header",
291
- "-H",
292
- "headers",
293
- help=r"Custom header that will be used in all requests to the server. Example: Authorization: Bearer\ 123",
294
- multiple=True,
295
- type=str,
296
- callback=callbacks.validate_headers,
297
- )
298
- @click.option(
299
- "--endpoint",
300
- "-E",
301
- "endpoints",
302
- type=str,
303
- multiple=True,
304
- help=r"Filter schemathesis tests by API operation path pattern. Example: users/\d+",
305
- callback=callbacks.validate_regex,
306
- cls=GroupedOption,
307
- group=ParameterGroup.filtering,
308
- )
309
- @click.option(
310
- "--method",
311
- "-M",
312
- "methods",
313
- type=str,
314
- multiple=True,
315
- help="Filter schemathesis tests by HTTP method.",
316
- callback=callbacks.validate_regex,
317
- cls=GroupedOption,
318
- group=ParameterGroup.filtering,
319
- )
320
- @click.option(
321
- "--tag",
322
- "-T",
323
- "tags",
324
- type=str,
325
- multiple=True,
326
- help="Filter schemathesis tests by schema tag pattern.",
327
- callback=callbacks.validate_regex,
328
- cls=GroupedOption,
329
- group=ParameterGroup.filtering,
330
- )
331
- @click.option(
332
- "--operation-id",
333
- "-O",
334
- "operation_ids",
335
- type=str,
336
- multiple=True,
337
- help="Filter schemathesis tests by operationId pattern.",
338
- callback=callbacks.validate_regex,
339
- cls=GroupedOption,
340
- group=ParameterGroup.filtering,
341
- )
342
- @click.option(
343
- "--workers",
344
- "-w",
345
- "workers_num",
346
- help="Number of workers to run tests.",
347
- type=CustomHelpMessageChoice(
348
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
349
- choices_repr=f"[auto|{MIN_WORKERS}-{MAX_WORKERS}]",
350
- ),
351
- default=str(DEFAULT_WORKERS),
352
- show_default=True,
353
- callback=callbacks.convert_workers,
354
- )
355
- @click.option(
356
- "--base-url",
357
- "-b",
358
- help="Base URL address of the API, required for SCHEMA if specified by file.",
359
- type=str,
360
- callback=callbacks.validate_base_url,
361
- )
362
- @click.option("--app", help="WSGI/ASGI application to test.", type=str, callback=callbacks.validate_app)
363
- @click.option(
364
- "--request-timeout",
365
- help="Timeout in milliseconds for network requests during the test run.",
366
- type=click.IntRange(1),
367
- default=DEFAULT_RESPONSE_TIMEOUT,
368
- )
369
- @with_request_tls_verify
370
- @with_request_cert
371
- @with_request_cert_key
372
- @click.option(
373
- "--validate-schema",
374
- help="Enable or disable validation of input schema.",
375
- type=bool,
376
- default=False,
377
- show_default=True,
378
- cls=GroupedOption,
379
- group=ParameterGroup.validation,
380
- )
381
- @click.option(
382
- "--skip-deprecated-operations",
383
- help="Skip testing of deprecated API operations.",
384
- is_flag=True,
385
- is_eager=True,
386
- default=False,
387
- show_default=True,
388
- cls=GroupedOption,
389
- group=ParameterGroup.filtering,
390
- )
391
- @click.option(
392
- "--junit-xml", help="Create junit-xml style report file at given path.", type=click.File("w", encoding="utf-8")
393
- )
394
- @click.option(
395
- "--debug-output-file",
396
- help="Save debug output as JSON lines in the given file.",
397
- type=click.File("w", encoding="utf-8"),
398
- )
399
- @click.option(
400
- "--show-errors-tracebacks",
401
- help="Show full tracebacks for internal errors.",
402
- is_flag=True,
403
- is_eager=True,
404
- default=False,
405
- show_default=True,
406
- )
407
- @click.option(
408
- "--code-sample-style",
409
- help="Controls the style of code samples for failure reproduction.",
410
- type=click.Choice([item.name for item in CodeSampleStyle]),
411
- default=CodeSampleStyle.default().name,
412
- callback=callbacks.convert_code_sample_style,
413
- )
414
- @click.option(
415
- "--cassette-path",
416
- help="Save test results as a VCR-compatible cassette.",
417
- type=click.File("w", encoding="utf-8"),
418
- is_eager=True,
419
- )
420
- @click.option(
421
- "--cassette-preserve-exact-body-bytes",
422
- help="Encode payloads in cassettes as base64.",
423
- is_flag=True,
424
- callback=callbacks.validate_preserve_exact_body_bytes,
425
- )
426
- @click.option(
427
- "--store-network-log",
428
- help="[DEPRECATED] Store requests and responses into a file.",
429
- type=click.File("w", encoding="utf-8"),
430
- )
431
- @click.option(
432
- "--fixups",
433
- help="Install specified compatibility fixups.",
434
- multiple=True,
435
- type=click.Choice(list(ALL_FIXUPS) + ["all"]),
436
- )
437
- @click.option(
438
- "--stateful",
439
- help="Utilize stateful testing capabilities.",
440
- type=click.Choice([item.name for item in Stateful]),
441
- default=Stateful.links.name,
442
- callback=callbacks.convert_stateful,
443
- )
444
- @click.option(
445
- "--stateful-recursion-limit",
446
- help="Limit recursion depth for stateful testing.",
447
- default=DEFAULT_STATEFUL_RECURSION_LIMIT,
448
- show_default=True,
449
- type=click.IntRange(1, 100),
450
- cls=DeprecatedOption,
451
- removed_in="4.0",
452
- )
453
- @click.option(
454
- "--force-schema-version",
455
- help="Force Schemathesis to parse the input schema with the specified spec version.",
456
- type=click.Choice(["20", "30"]),
457
- )
458
- @click.option(
459
- "--hypothesis-database",
460
- help="A way to store found examples in Hypothesis' database. "
461
- "You can either disable it completely with `none`, "
462
- f"do not persist bugs between test runs with `{HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER}` "
463
- "or use an arbitrary path to store examples as files.",
464
- type=str,
465
- cls=GroupedOption,
466
- group=ParameterGroup.hypothesis,
467
- )
468
- @click.option(
469
- "--hypothesis-deadline",
470
- help="Duration in milliseconds that each individual example with a test is not allowed to exceed.",
471
- # max value to avoid overflow. It is the maximum amount of days in milliseconds
472
- type=OptionalInt(1, 999999999 * 24 * 3600 * 1000),
473
- cls=GroupedOption,
474
- group=ParameterGroup.hypothesis,
475
- )
476
- @click.option(
477
- "--hypothesis-derandomize",
478
- help="Use Hypothesis's deterministic mode.",
479
- is_flag=True,
480
- default=None,
481
- show_default=True,
482
- cls=GroupedOption,
483
- group=ParameterGroup.hypothesis,
484
- )
485
- @click.option(
486
- "--hypothesis-max-examples",
487
- help="Maximum number of generated examples per each method/path combination.",
488
- type=click.IntRange(1),
489
- cls=GroupedOption,
490
- group=ParameterGroup.hypothesis,
491
- )
492
- @click.option(
493
- "--hypothesis-phases",
494
- help="Control which phases should be run.",
495
- type=CsvEnumChoice(hypothesis.Phase),
496
- cls=GroupedOption,
497
- group=ParameterGroup.hypothesis,
498
- )
499
- @click.option(
500
- "--hypothesis-report-multiple-bugs",
501
- help="Raise only the exception with the smallest minimal example.",
502
- type=bool,
503
- cls=GroupedOption,
504
- group=ParameterGroup.hypothesis,
505
- )
506
- @click.option(
507
- "--hypothesis-seed",
508
- help="Set a seed to use for all Hypothesis tests.",
509
- type=int,
510
- cls=GroupedOption,
511
- group=ParameterGroup.hypothesis,
512
- )
513
- @click.option(
514
- "--hypothesis-suppress-health-check",
515
- help="Comma-separated list of health checks to disable.",
516
- type=CsvEnumChoice(hypothesis.HealthCheck),
517
- cls=GroupedOption,
518
- group=ParameterGroup.hypothesis,
519
- )
520
- @click.option(
521
- "--hypothesis-verbosity",
522
- help="Verbosity level of Hypothesis messages.",
523
- type=click.Choice([item.name for item in hypothesis.Verbosity]),
524
- callback=callbacks.convert_verbosity,
525
- cls=GroupedOption,
526
- group=ParameterGroup.hypothesis,
527
- )
528
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
529
- @click.option(
530
- "--schemathesis-io-token",
531
- help="Schemathesis.io authentication token.",
532
- type=str,
533
- envvar=service.TOKEN_ENV_VAR,
534
- )
535
- @click.option(
536
- "--schemathesis-io-url",
537
- help="Schemathesis.io base URL.",
538
- default=service.DEFAULT_URL,
539
- type=str,
540
- envvar=service.URL_ENV_VAR,
541
- )
542
- @with_hosts_file
543
- @click.option("--verbosity", "-v", help="Reduce verbosity of error output.", count=True)
544
- @click.pass_context
545
- def run(
546
- ctx: click.Context,
547
- schema: str,
548
- api_slug: Optional[str],
549
- auth: Optional[Tuple[str, str]],
550
- auth_type: str,
551
- headers: Dict[str, str],
552
- checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
553
- data_generation_methods: Tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
554
- max_response_time: Optional[int] = None,
555
- targets: Iterable[str] = DEFAULT_TARGETS_NAMES,
556
- exit_first: bool = False,
557
- dry_run: bool = False,
558
- endpoints: Optional[Filter] = None,
559
- methods: Optional[Filter] = None,
560
- tags: Optional[Filter] = None,
561
- operation_ids: Optional[Filter] = None,
562
- workers_num: int = DEFAULT_WORKERS,
563
- base_url: Optional[str] = None,
564
- app: Optional[str] = None,
565
- request_timeout: Optional[int] = None,
566
- request_tls_verify: bool = True,
567
- request_cert: Optional[str] = None,
568
- request_cert_key: Optional[str] = None,
569
- validate_schema: bool = True,
570
- skip_deprecated_operations: bool = False,
571
- junit_xml: Optional[click.utils.LazyFile] = None,
572
- debug_output_file: Optional[click.utils.LazyFile] = None,
573
- show_errors_tracebacks: bool = False,
574
- code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
575
- cassette_path: Optional[click.utils.LazyFile] = None,
576
- cassette_preserve_exact_body_bytes: bool = False,
577
- store_network_log: Optional[click.utils.LazyFile] = None,
578
- fixups: Tuple[str] = (), # type: ignore
579
- stateful: Optional[Stateful] = None,
580
- stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
581
- force_schema_version: Optional[str] = None,
582
- hypothesis_database: Optional[str] = None,
583
- hypothesis_deadline: Optional[Union[int, NotSet]] = None,
584
- hypothesis_derandomize: Optional[bool] = None,
585
- hypothesis_max_examples: Optional[int] = None,
586
- hypothesis_phases: Optional[List[hypothesis.Phase]] = None,
587
- hypothesis_report_multiple_bugs: Optional[bool] = None,
588
- hypothesis_suppress_health_check: Optional[List[hypothesis.HealthCheck]] = None,
589
- hypothesis_seed: Optional[int] = None,
590
- hypothesis_verbosity: Optional[hypothesis.Verbosity] = None,
591
- verbosity: int = 0,
592
- no_color: bool = False,
593
- schemathesis_io_token: Optional[str] = None,
594
- schemathesis_io_url: str = service.DEFAULT_URL,
595
- hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
596
- ) -> None:
597
- """Perform schemathesis test against an API specified by SCHEMA.
598
-
599
- SCHEMA must be a valid URL or file path pointing to an Open API / GraphQL specification.
600
-
601
- API_SLUG is an API identifier to upload data to Schemathesis.io.
602
- """
603
- # pylint: disable=too-many-locals
604
- maybe_disable_color(ctx, no_color)
605
- check_auth(auth, headers)
606
- selected_targets = tuple(target for target in targets_module.ALL_TARGETS if target.__name__ in targets)
607
-
608
- if store_network_log and cassette_path:
609
- error_message(CASSETTES_PATH_INVALID_USAGE_MESSAGE)
610
- sys.exit(1)
611
- if store_network_log is not None:
612
- click.secho(DEPRECATED_CASSETTE_PATH_OPTION_WARNING, fg="yellow")
613
- cassette_path = store_network_log
614
-
615
- schemathesis_io_hostname = urlparse(schemathesis_io_url).netloc
616
- token = schemathesis_io_token or service.hosts.get_token(hostname=schemathesis_io_hostname, hosts_file=hosts_file)
617
- schema_kind = callbacks.parse_schema_kind(schema, app)
618
- callbacks.validate_schema(schema, schema_kind, base_url=base_url, dry_run=dry_run, app=app, api_slug=api_slug)
619
- client = None
620
- test_run = None
621
- if api_slug is not None or schema_kind == callbacks.SchemaInputKind.SLUG:
622
- if token is None:
623
- hostname = (
624
- "Schemathesis.io" if schemathesis_io_hostname == service.DEFAULT_HOSTNAME else schemathesis_io_hostname
625
- )
626
- raise click.UsageError(
627
- "\n\n"
628
- f"You are trying to upload data to {hostname}, but your CLI appears to be not authenticated.\n\n"
629
- "To authenticate, grab your token from `app.schemathesis.io` and run `st auth login <TOKEN>`\n"
630
- "Alternatively, you can pass the token explicitly via the `--schemathesis-io-token` option / "
631
- f"`{service.TOKEN_ENV_VAR}` environment variable\n\n"
632
- "See https://schemathesis.readthedocs.io/en/stable/service.html for more details"
633
- )
634
- client = service.ServiceClient(base_url=schemathesis_io_url, token=token)
635
- try:
636
- test_run = client.create_test_run(schema)
637
- if schema_kind == callbacks.SchemaInputKind.SLUG:
638
- # Replace config values with ones loaded from the service
639
- schema = test_run.config.location
640
- base_url = base_url or test_run.config.base_url
641
- except requests.HTTPError as exc:
642
- handle_service_error(exc)
643
-
644
- if "all" in checks:
645
- selected_checks = checks_module.ALL_CHECKS
1
+ from __future__ import annotations
2
+
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
9
+
10
+ __all__ = [
11
+ "schemathesis",
12
+ "run",
13
+ "EventHandler",
14
+ "ExecutionContext",
15
+ "LoadingStarted",
16
+ "LoadingFinished",
17
+ "add_group",
18
+ "handler",
19
+ ]
20
+
21
+
22
+ def add_group(name: str, *, index: int | None = None) -> Group:
23
+ """Add a custom options group to `st run`."""
24
+ if index is not None:
25
+ GROUPS[name] = OptionGroup(name=name, order=index)
646
26
  else:
647
- selected_checks = tuple(check for check in checks_module.ALL_CHECKS if check.__name__ in checks)
648
-
649
- if fixups:
650
- if "all" in fixups:
651
- _fixups.install()
652
- else:
653
- _fixups.install(fixups)
654
- hypothesis_settings = prepare_hypothesis_settings(
655
- database=hypothesis_database,
656
- deadline=hypothesis_deadline,
657
- derandomize=hypothesis_derandomize,
658
- max_examples=hypothesis_max_examples,
659
- phases=hypothesis_phases,
660
- report_multiple_bugs=hypothesis_report_multiple_bugs,
661
- suppress_health_check=hypothesis_suppress_health_check,
662
- verbosity=hypothesis_verbosity,
663
- )
664
- event_stream = into_event_stream(
665
- schema,
666
- app=app,
667
- base_url=base_url,
668
- validate_schema=validate_schema,
669
- skip_deprecated_operations=skip_deprecated_operations,
670
- data_generation_methods=data_generation_methods,
671
- force_schema_version=force_schema_version,
672
- request_tls_verify=request_tls_verify,
673
- request_cert=prepare_request_cert(request_cert, request_cert_key),
674
- auth=auth,
675
- auth_type=auth_type,
676
- headers=headers,
677
- endpoint=endpoints or None,
678
- method=methods or None,
679
- tag=tags or None,
680
- operation_id=operation_ids or None,
681
- request_timeout=request_timeout,
682
- seed=hypothesis_seed,
683
- exit_first=exit_first,
684
- dry_run=dry_run,
685
- store_interactions=cassette_path is not None,
686
- checks=selected_checks,
687
- max_response_time=max_response_time,
688
- targets=selected_targets,
689
- workers_num=workers_num,
690
- stateful=stateful,
691
- stateful_recursion_limit=stateful_recursion_limit,
692
- hypothesis_settings=hypothesis_settings,
693
- )
694
- execute(
695
- event_stream,
696
- hypothesis_settings,
697
- workers_num,
698
- show_errors_tracebacks,
699
- validate_schema,
700
- cassette_path,
701
- cassette_preserve_exact_body_bytes,
702
- junit_xml,
703
- verbosity,
704
- code_sample_style,
705
- debug_output_file,
706
- schemathesis_io_url,
707
- client,
708
- test_run,
709
- )
710
-
711
-
712
- def prepare_request_cert(cert: Optional[str], key: Optional[str]) -> Optional[RequestCert]:
713
- if cert is not None and key is not None:
714
- return cert, key
715
- return cert
716
-
717
-
718
- @attr.s(slots=True)
719
- class LoaderConfig:
720
- """Container for API loader parameters.
721
-
722
- The main goal is to avoid too many parameters in function signatures.
723
- """
724
-
725
- schema_location: str = attr.ib() # pragma: no mutate
726
- app: Any = attr.ib() # pragma: no mutate
727
- base_url: Optional[str] = attr.ib() # pragma: no mutate
728
- validate_schema: bool = attr.ib() # pragma: no mutate
729
- skip_deprecated_operations: bool = attr.ib() # pragma: no mutate
730
- data_generation_methods: Tuple[DataGenerationMethod, ...] = attr.ib() # pragma: no mutate
731
- force_schema_version: Optional[str] = attr.ib() # pragma: no mutate
732
- request_tls_verify: Union[bool, str] = attr.ib() # pragma: no mutate
733
- request_cert: Optional[RequestCert] = attr.ib() # pragma: no mutate
734
- # Network request parameters
735
- auth: Optional[Tuple[str, str]] = attr.ib() # pragma: no mutate
736
- auth_type: Optional[str] = attr.ib() # pragma: no mutate
737
- headers: Optional[Dict[str, str]] = attr.ib() # pragma: no mutate
738
- # Schema filters
739
- endpoint: Optional[Filter] = attr.ib() # pragma: no mutate
740
- method: Optional[Filter] = attr.ib() # pragma: no mutate
741
- tag: Optional[Filter] = attr.ib() # pragma: no mutate
742
- operation_id: Optional[Filter] = attr.ib() # pragma: no mutate
743
-
744
-
745
- def into_event_stream(
746
- schema_location: str,
747
- *,
748
- app: Any,
749
- base_url: Optional[str],
750
- validate_schema: bool,
751
- skip_deprecated_operations: bool,
752
- data_generation_methods: Tuple[DataGenerationMethod, ...],
753
- force_schema_version: Optional[str],
754
- request_tls_verify: Union[bool, str],
755
- request_cert: Optional[RequestCert],
756
- # Network request parameters
757
- auth: Optional[Tuple[str, str]],
758
- auth_type: Optional[str],
759
- headers: Optional[Dict[str, str]],
760
- request_timeout: Optional[int],
761
- # Schema filters
762
- endpoint: Optional[Filter],
763
- method: Optional[Filter],
764
- tag: Optional[Filter],
765
- operation_id: Optional[Filter],
766
- # Runtime behavior
767
- checks: Iterable[CheckFunction],
768
- max_response_time: Optional[int],
769
- targets: Iterable[Target],
770
- workers_num: int,
771
- hypothesis_settings: Optional[hypothesis.settings],
772
- seed: Optional[int],
773
- exit_first: bool,
774
- dry_run: bool,
775
- store_interactions: bool,
776
- stateful: Optional[Stateful],
777
- stateful_recursion_limit: int,
778
- ) -> Generator[events.ExecutionEvent, None, None]:
779
- try:
780
- if app is not None:
781
- app = import_app(app)
782
- config = LoaderConfig(
783
- schema_location=schema_location,
784
- app=app,
785
- base_url=base_url,
786
- validate_schema=validate_schema,
787
- skip_deprecated_operations=skip_deprecated_operations,
788
- data_generation_methods=data_generation_methods,
789
- force_schema_version=force_schema_version,
790
- request_tls_verify=request_tls_verify,
791
- request_cert=request_cert,
792
- auth=auth,
793
- auth_type=auth_type,
794
- headers=headers,
795
- endpoint=endpoint or None,
796
- method=method or None,
797
- tag=tag or None,
798
- operation_id=operation_id or None,
799
- )
800
- loaded_schema = load_schema(config)
801
- yield from runner.from_schema(
802
- loaded_schema,
803
- auth=auth,
804
- auth_type=auth_type,
805
- headers=headers,
806
- request_timeout=request_timeout,
807
- request_tls_verify=request_tls_verify,
808
- request_cert=request_cert,
809
- seed=seed,
810
- exit_first=exit_first,
811
- dry_run=dry_run,
812
- store_interactions=store_interactions,
813
- checks=checks,
814
- max_response_time=max_response_time,
815
- targets=targets,
816
- workers_num=workers_num,
817
- stateful=stateful,
818
- stateful_recursion_limit=stateful_recursion_limit,
819
- hypothesis_settings=hypothesis_settings,
820
- ).execute()
821
- except Exception as exc:
822
- yield events.InternalError.from_exc(exc)
823
-
824
-
825
- def load_schema(config: LoaderConfig) -> BaseSchema:
826
- """Automatically load API schema."""
827
- first: Callable[[LoaderConfig], BaseSchema]
828
- second: Callable[[LoaderConfig], BaseSchema]
829
- if is_probably_graphql(config.schema_location):
830
- # Try GraphQL first, then fallback to Open API
831
- first, second = (_load_graphql_schema, _load_openapi_schema)
832
- else:
833
- # Try Open API first, then fallback to GraphQL
834
- first, second = (_load_openapi_schema, _load_graphql_schema)
835
- return _try_load_schema(config, first, second)
836
-
837
-
838
- def _try_load_schema(
839
- config: LoaderConfig, first: Callable[[LoaderConfig], BaseSchema], second: Callable[[LoaderConfig], BaseSchema]
840
- ) -> BaseSchema:
841
- try:
842
- return first(config)
843
- except (HTTPError, SchemaLoadingError) as exc:
844
- try:
845
- return second(config)
846
- except (HTTPError, SchemaLoadingError):
847
- # Raise the first loader's error
848
- raise exc # pylint: disable=raise-missing-from
849
-
850
-
851
- def _load_graphql_schema(config: LoaderConfig) -> GraphQLSchema:
852
- loader = detect_loader(config.schema_location, config.app, is_openapi=False)
853
- kwargs = get_graphql_loader_kwargs(loader, config)
854
- return loader(config.schema_location, **kwargs)
855
-
856
-
857
- def _load_openapi_schema(config: LoaderConfig) -> BaseSchema:
858
- loader = detect_loader(config.schema_location, config.app, is_openapi=True)
859
- kwargs = get_loader_kwargs(loader, config)
860
- return loader(config.schema_location, **kwargs)
861
-
862
-
863
- def detect_loader(schema_location: str, app: Any, is_openapi: bool) -> Callable:
864
- """Detect API schema loader."""
865
- if file_exists(schema_location):
866
- # If there is an existing file with the given name,
867
- # then it is likely that the user wants to load API schema from there
868
- return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
869
- if app is not None and not urlparse(schema_location).netloc:
870
- # App is passed & location is relative
871
- return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
872
- # Default behavior
873
- return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
874
-
875
-
876
- def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> Dict[str, Any]:
877
- """Detect the proper set of parameters for a loader."""
878
- # These kwargs are shared by all loaders
879
- kwargs = {
880
- "app": config.app,
881
- "base_url": config.base_url,
882
- "method": config.method,
883
- "endpoint": config.endpoint,
884
- "tag": config.tag,
885
- "operation_id": config.operation_id,
886
- "skip_deprecated_operations": config.skip_deprecated_operations,
887
- "validate_schema": config.validate_schema,
888
- "force_schema_version": config.force_schema_version,
889
- "data_generation_methods": config.data_generation_methods,
890
- }
891
- if loader is not oas_loaders.from_path:
892
- kwargs["headers"] = config.headers
893
- if loader in (oas_loaders.from_uri, oas_loaders.from_aiohttp):
894
- _add_requests_kwargs(kwargs, config)
895
- return kwargs
896
-
897
-
898
- def get_graphql_loader_kwargs(
899
- loader: Callable,
900
- config: LoaderConfig,
901
- ) -> Dict[str, Any]:
902
- """Detect the proper set of parameters for a loader."""
903
- # These kwargs are shared by all loaders
904
- kwargs = {
905
- "app": config.app,
906
- "base_url": config.base_url,
907
- "data_generation_methods": config.data_generation_methods,
908
- }
909
- if loader is not gql_loaders.from_path:
910
- kwargs["headers"] = config.headers
911
- if loader is gql_loaders.from_url:
912
- _add_requests_kwargs(kwargs, config)
913
- return kwargs
914
-
915
-
916
- def _add_requests_kwargs(kwargs: Dict[str, Any], config: LoaderConfig) -> None:
917
- kwargs["verify"] = config.request_tls_verify
918
- if config.request_cert is not None:
919
- kwargs["cert"] = config.request_cert
920
- if config.auth is not None:
921
- kwargs["auth"] = get_requests_auth(config.auth, config.auth_type)
922
-
923
-
924
- def is_probably_graphql(location: str) -> bool:
925
- """Detect whether it is likely that the given location is a GraphQL endpoint."""
926
- return location.endswith(("/graphql", "/graphql/"))
927
-
928
-
929
- def check_auth(auth: Optional[Tuple[str, str]], headers: Dict[str, str]) -> None:
930
- if auth is not None and "authorization" in {header.lower() for header in headers}:
931
- raise click.BadParameter("Passing `--auth` together with `--header` that sets `Authorization` is not allowed.")
932
-
933
-
934
- def get_output_handler(workers_num: int) -> EventHandler:
935
- if workers_num > 1:
936
- output_style = OutputStyle.short
937
- else:
938
- output_style = OutputStyle.default
939
- return output_style.value()
940
-
941
-
942
- def load_hook(module_name: str) -> None:
943
- """Load the given hook by importing it."""
944
- try:
945
- sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
946
- __import__(module_name)
947
- except Exception as exc:
948
- click.secho("An exception happened during the hook loading:\n", fg="red")
949
- message = traceback.format_exc()
950
- click.secho(message, fg="red")
951
- raise click.Abort() from exc
952
-
953
-
954
- class OutputStyle(Enum):
955
- """Provide different output styles."""
956
-
957
- default = output.default.DefaultOutputStyleHandler
958
- short = output.short.ShortOutputStyleHandler
959
-
960
-
961
- def execute(
962
- event_stream: Generator[events.ExecutionEvent, None, None],
963
- hypothesis_settings: hypothesis.settings,
964
- workers_num: int,
965
- show_errors_tracebacks: bool,
966
- validate_schema: bool,
967
- cassette_path: Optional[click.utils.LazyFile],
968
- cassette_preserve_exact_body_bytes: bool,
969
- junit_xml: Optional[click.utils.LazyFile],
970
- verbosity: int,
971
- code_sample_style: CodeSampleStyle,
972
- debug_output_file: Optional[click.utils.LazyFile],
973
- schemathesis_io_url: str,
974
- client: Optional[service.ServiceClient],
975
- test_run: Optional[service.TestRun],
976
- ) -> None:
977
- """Execute a prepared runner by drawing events from it and passing to a proper handler."""
978
- handlers: List[EventHandler] = []
979
- service_context = None
980
- if client is not None and test_run is not None:
981
- service_queue: Queue = Queue()
982
- service_context = ServiceContext(url=schemathesis_io_url, queue=service_queue)
983
- reporter = service.ServiceReporter(client=client, test_run=test_run, out_queue=service_queue)
984
- handlers.append(reporter)
985
- if junit_xml is not None:
986
- handlers.append(JunitXMLHandler(junit_xml))
987
- if debug_output_file is not None:
988
- handlers.append(DebugOutputHandler(debug_output_file))
989
- if cassette_path is not None:
990
- # This handler should be first to have logs writing completed when the output handler will display statistic
991
- handlers.append(
992
- cassettes.CassetteWriter(cassette_path, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes)
993
- )
994
- handlers.append(get_output_handler(workers_num))
995
- execution_context = ExecutionContext(
996
- hypothesis_settings=hypothesis_settings,
997
- workers_num=workers_num,
998
- show_errors_tracebacks=show_errors_tracebacks,
999
- validate_schema=validate_schema,
1000
- cassette_path=cassette_path.name if cassette_path is not None else None,
1001
- junit_xml_file=junit_xml.name if junit_xml is not None else None,
1002
- verbosity=verbosity,
1003
- code_sample_style=code_sample_style,
1004
- service=service_context,
1005
- )
1006
-
1007
- def shutdown() -> None:
1008
- for _handler in handlers:
1009
- _handler.shutdown()
1010
-
1011
- GLOBAL_HOOK_DISPATCHER.dispatch("after_init_cli_run_handlers", HookContext(), handlers, execution_context)
1012
- event = None
1013
- try:
1014
- for event in event_stream:
1015
- for handler in handlers:
1016
- handler.handle_event(execution_context, event)
1017
- except Exception as exc:
1018
- if isinstance(exc, click.Abort):
1019
- # To avoid showing "Aborted!" message, which is the default behavior in Click
1020
- sys.exit(1)
1021
- raise
1022
- finally:
1023
- shutdown()
1024
- if event is not None and event.is_terminal:
1025
- exit_code = get_exit_code(event)
1026
- sys.exit(exit_code)
1027
- # Event stream did not finish with a terminal event. Only possible if the handler is broken
1028
- click.secho("Unexpected error", fg="red")
1029
- sys.exit(1)
1030
-
1031
-
1032
- def handle_service_error(exc: requests.HTTPError) -> NoReturn:
1033
- if exc.response.status_code == 404:
1034
- error_message("API_SLUG not found!")
1035
- else:
1036
- output.default.display_service_error(service.Error(exc))
1037
- sys.exit(1)
1038
-
1039
-
1040
- def get_exit_code(event: events.ExecutionEvent) -> int:
1041
- if isinstance(event, events.Finished):
1042
- if event.has_failures or event.has_errors:
1043
- return 1
1044
- return 0
1045
- # Practically not possible. May occur only if the output handler is broken - in this case we still will have the
1046
- # right exit code.
1047
- return 1
1048
-
1049
-
1050
- @schemathesis.command(short_help="Replay requests from a saved cassette.")
1051
- @click.argument("cassette_path", type=click.Path(exists=True))
1052
- @click.option("--id", "id_", help="ID of interaction to replay.", type=str)
1053
- @click.option("--status", help="Status of interactions to replay.", type=str)
1054
- @click.option("--uri", help="A regexp that filters interactions by their request URI.", type=str)
1055
- @click.option("--method", help="A regexp that filters interactions by their request method.", type=str)
1056
- @click.option("--no-color", help="Disable ANSI color escape codes.", type=bool, is_flag=True)
1057
- @with_request_tls_verify
1058
- @with_request_cert
1059
- @with_request_cert_key
1060
- @click.pass_context
1061
- def replay(
1062
- ctx: click.Context,
1063
- cassette_path: str,
1064
- id_: Optional[str],
1065
- status: Optional[str] = None,
1066
- uri: Optional[str] = None,
1067
- method: Optional[str] = None,
1068
- no_color: bool = False,
1069
- request_tls_verify: bool = True,
1070
- request_cert: Optional[str] = None,
1071
- request_cert_key: Optional[str] = None,
1072
- ) -> None:
1073
- """Replay a cassette.
1074
-
1075
- Cassettes in VCR-compatible format can be replayed.
1076
- For example, ones that are recorded with ``store-network-log`` option of `st run` command.
1077
- """
1078
- maybe_disable_color(ctx, no_color)
1079
- click.secho(f"{bold('Replaying cassette')}: {cassette_path}")
1080
- with open(cassette_path, "rb") as fd:
1081
- cassette = yaml.load(fd, Loader=SafeLoader)
1082
- click.secho(f"{bold('Total interactions')}: {len(cassette['http_interactions'])}\n")
1083
- for replayed in cassettes.replay(
1084
- cassette,
1085
- id_=id_,
1086
- status=status,
1087
- uri=uri,
1088
- method=method,
1089
- request_tls_verify=request_tls_verify,
1090
- request_cert=prepare_request_cert(request_cert, request_cert_key),
1091
- ):
1092
- click.secho(f" {bold('ID')} : {replayed.interaction['id']}")
1093
- click.secho(f" {bold('URI')} : {replayed.interaction['request']['uri']}")
1094
- click.secho(f" {bold('Old status code')} : {replayed.interaction['response']['status']['code']}")
1095
- click.secho(f" {bold('New status code')} : {replayed.response.status_code}\n")
1096
-
1097
-
1098
- @schemathesis.group(short_help="Authenticate Schemathesis.io.")
1099
- def auth() -> None:
1100
- pass
1101
-
1102
-
1103
- @auth.command(short_help="Authenticate with a Schemathesis.io host.")
1104
- @click.argument("token", type=str, envvar=service.TOKEN_ENV_VAR)
1105
- @click.option(
1106
- "--hostname",
1107
- help="The hostname of the Schemathesis.io instance to authenticate with",
1108
- type=str,
1109
- default=service.DEFAULT_HOSTNAME,
1110
- envvar=service.HOSTNAME_ENV_VAR,
1111
- )
1112
- @click.option(
1113
- "--protocol",
1114
- type=click.Choice(["https", "http"]),
1115
- default=service.DEFAULT_PROTOCOL,
1116
- envvar=service.PROTOCOL_ENV_VAR,
1117
- )
1118
- @with_request_tls_verify
1119
- @with_hosts_file
1120
- def login(token: str, hostname: str, hosts_file: str, protocol: str, request_tls_verify: bool = True) -> None:
1121
- """Authenticate with a Schemathesis.io host.
1122
-
1123
- Example:
1124
- st auth login MY_TOKEN
1125
-
1126
- """
1127
- try:
1128
- username = service.auth.login(token, hostname, protocol, request_tls_verify)
1129
- service.hosts.store(token, hostname, hosts_file)
1130
- success_message(f"Logged in into {hostname} as " + bold(username))
1131
- except requests.HTTPError as exc:
1132
- detail = exc.response.json()["detail"]
1133
- error_message(f"Failed to login into {hostname}: " + bold(detail))
1134
- sys.exit(1)
1135
-
1136
-
1137
- @auth.command(short_help="Remove authentication for a Schemathesis.io host.")
1138
- @click.option(
1139
- "--hostname",
1140
- help="The hostname of the Schemathesis.io instance to authenticate with",
1141
- type=str,
1142
- default=service.DEFAULT_HOSTNAME,
1143
- envvar=service.HOSTNAME_ENV_VAR,
1144
- )
1145
- @with_hosts_file
1146
- def logout(hostname: str, hosts_file: str) -> None:
1147
- """Remove authentication for a Schemathesis.io host."""
1148
- result = service.hosts.remove(hostname, hosts_file)
1149
- if result == service.hosts.RemoveAuth.success:
1150
- success_message(f"Logged out of {hostname} account")
1151
- else:
1152
- if result == service.hosts.RemoveAuth.no_match:
1153
- warning_message(f"Not logged in to {hostname}")
1154
- if result == service.hosts.RemoveAuth.no_hosts:
1155
- warning_message("Not logged in to any hosts")
1156
- if result == service.hosts.RemoveAuth.error:
1157
- error_message(f"Failed to read the hosts file. Try to remove {hosts_file}")
1158
- sys.exit(1)
1159
-
1160
-
1161
- def success_message(message: str) -> None:
1162
- click.secho(click.style("✔️", fg="green") + f" {message}")
1163
-
1164
-
1165
- def warning_message(message: str) -> None:
1166
- click.secho(click.style("🟡️", fg="yellow") + f" {message}")
1167
-
1168
-
1169
- def error_message(message: str) -> None:
1170
- click.secho(f"❌ {message}")
1171
-
1172
-
1173
- def bold(message: str) -> str:
1174
- return click.style(message, bold=True)
1175
-
1176
-
1177
- def maybe_disable_color(ctx: click.Context, no_color: bool) -> None:
1178
- if no_color or "NO_COLOR" in os.environ:
1179
- ctx.color = False
1180
-
1181
-
1182
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1183
- def after_init_cli_run_handlers(
1184
- context: HookContext, handlers: List[EventHandler], execution_context: ExecutionContext
1185
- ) -> None:
1186
- """Called after CLI hooks are initialized.
1187
-
1188
- Might be used to add extra event handlers.
1189
- """
1190
-
1191
-
1192
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1193
- def before_call(context: HookContext, case: Case) -> None:
1194
- """Called before every network call in CLI tests.
1195
-
1196
- Use cases:
1197
- - Modification of `case`. For example, adding some pre-determined value to its query string.
1198
- - Logging
1199
- """
1200
-
1201
-
1202
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1203
- def after_call(context: HookContext, case: Case, response: GenericResponse) -> None:
1204
- """Called after every network call in CLI tests.
1205
-
1206
- Note that you need to modify the response in-place.
1207
-
1208
- Use cases:
1209
- - Response post-processing, like modifying its payload.
1210
- - Logging
1211
- """
1212
-
1213
-
1214
- @HookDispatcher.register_spec([HookScope.GLOBAL])
1215
- def process_call_kwargs(context: HookContext, case: Case, kwargs: Dict[str, Any]) -> None:
1216
- """Called before every network call in CLI tests.
1217
-
1218
- Aims to modify the argument passed to `case.call` / `case.call_wsgi` / `case.call_asgi`.
1219
- Note that you need to modify `kwargs` in-place.
1220
- """
27
+ GROUPS[name] = OptionGroup(name=name)
28
+ return Group(name)