schemathesis 3.13.0__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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +753 -74
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/cli/callbacks.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
from contextlib import contextmanager
|
|
4
|
-
from typing import Dict, Generator, List, Optional, Tuple, Union
|
|
5
|
-
from urllib.parse import urlparse
|
|
6
|
-
|
|
7
|
-
import click
|
|
8
|
-
import hypothesis
|
|
9
|
-
from requests import PreparedRequest, RequestException
|
|
10
|
-
|
|
11
|
-
from .. import utils
|
|
12
|
-
from ..constants import CodeSampleStyle, DataGenerationMethod
|
|
13
|
-
from ..stateful import Stateful
|
|
14
|
-
from .constants import DEFAULT_WORKERS
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def validate_schema(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
|
|
18
|
-
if "app" not in ctx.params:
|
|
19
|
-
try:
|
|
20
|
-
netloc = urlparse(raw_value).netloc
|
|
21
|
-
except ValueError as exc:
|
|
22
|
-
raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
|
|
23
|
-
if not netloc:
|
|
24
|
-
if "\x00" in raw_value or not utils.file_exists(raw_value):
|
|
25
|
-
raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.")
|
|
26
|
-
if "base_url" not in ctx.params and not ctx.params.get("dry_run", False):
|
|
27
|
-
raise click.UsageError('Missing argument, "--base-url" is required for SCHEMA specified by file.')
|
|
28
|
-
else:
|
|
29
|
-
_validate_url(raw_value)
|
|
30
|
-
return raw_value
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _validate_url(value: str) -> None:
|
|
34
|
-
try:
|
|
35
|
-
PreparedRequest().prepare_url(value, {}) # type: ignore
|
|
36
|
-
except RequestException as exc:
|
|
37
|
-
raise click.UsageError("Invalid SCHEMA, must be a valid URL or file path.") from exc
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_value: str) -> str:
|
|
41
|
-
try:
|
|
42
|
-
netloc = urlparse(raw_value).netloc
|
|
43
|
-
except ValueError as exc:
|
|
44
|
-
raise click.UsageError("Invalid base URL") from exc
|
|
45
|
-
if raw_value and not netloc:
|
|
46
|
-
raise click.UsageError("Invalid base URL")
|
|
47
|
-
return raw_value
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
APPLICATION_FORMAT_MESSAGE = (
|
|
51
|
-
"Can not import application from the given module!\n"
|
|
52
|
-
"The `--app` option value should be in format:\n\n path:variable\n\n"
|
|
53
|
-
"where `path` is an importable path to a Python module,\n"
|
|
54
|
-
"and `variable` is a variable name inside that module."
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]) -> Optional[str]:
|
|
59
|
-
if raw_value is None:
|
|
60
|
-
return raw_value
|
|
61
|
-
try:
|
|
62
|
-
utils.import_app(raw_value)
|
|
63
|
-
# String is returned instead of an app because it might be passed to a subprocess
|
|
64
|
-
# Since most app instances are not-transferable to another process, they are passed as strings and
|
|
65
|
-
# imported in a subprocess
|
|
66
|
-
return raw_value
|
|
67
|
-
except Exception as exc:
|
|
68
|
-
show_errors_tracebacks = ctx.params["show_errors_tracebacks"]
|
|
69
|
-
message = utils.format_exception(exc, show_errors_tracebacks).strip()
|
|
70
|
-
click.secho(f"{APPLICATION_FORMAT_MESSAGE}\n\nException:\n\n{message}", fg="red")
|
|
71
|
-
if not show_errors_tracebacks:
|
|
72
|
-
click.secho(
|
|
73
|
-
"\nAdd this option to your command line parameters to see full tracebacks: --show-errors-tracebacks",
|
|
74
|
-
fg="red",
|
|
75
|
-
)
|
|
76
|
-
raise click.exceptions.Exit(1)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def validate_auth(
|
|
80
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
|
|
81
|
-
) -> Optional[Tuple[str, str]]:
|
|
82
|
-
if raw_value is not None:
|
|
83
|
-
with reraise_format_error(raw_value):
|
|
84
|
-
user, password = tuple(raw_value.split(":"))
|
|
85
|
-
if not user:
|
|
86
|
-
raise click.BadParameter("Username should not be empty")
|
|
87
|
-
if not utils.is_latin_1_encodable(user):
|
|
88
|
-
raise click.BadParameter("Username should be latin-1 encodable")
|
|
89
|
-
if not utils.is_latin_1_encodable(password):
|
|
90
|
-
raise click.BadParameter("Password should be latin-1 encodable")
|
|
91
|
-
return user, password
|
|
92
|
-
return None
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def validate_headers(
|
|
96
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]
|
|
97
|
-
) -> Dict[str, str]:
|
|
98
|
-
headers = {}
|
|
99
|
-
for header in raw_value:
|
|
100
|
-
with reraise_format_error(header):
|
|
101
|
-
key, value = header.split(":", maxsplit=1)
|
|
102
|
-
value = value.lstrip()
|
|
103
|
-
key = key.strip()
|
|
104
|
-
if not key:
|
|
105
|
-
raise click.BadParameter("Header name should not be empty")
|
|
106
|
-
if not utils.is_latin_1_encodable(key):
|
|
107
|
-
raise click.BadParameter("Header name should be latin-1 encodable")
|
|
108
|
-
if not utils.is_latin_1_encodable(value):
|
|
109
|
-
raise click.BadParameter("Header value should be latin-1 encodable")
|
|
110
|
-
if utils.has_invalid_characters(key, value):
|
|
111
|
-
raise click.BadParameter("Invalid return character or leading space in header")
|
|
112
|
-
headers[key] = value
|
|
113
|
-
return headers
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def validate_regex(ctx: click.core.Context, param: click.core.Parameter, raw_value: Tuple[str, ...]) -> Tuple[str, ...]:
|
|
117
|
-
for value in raw_value:
|
|
118
|
-
try:
|
|
119
|
-
re.compile(value)
|
|
120
|
-
except (re.error, OverflowError, RuntimeError) as exc:
|
|
121
|
-
raise click.BadParameter(f"Invalid regex: {exc.args[0]}")
|
|
122
|
-
return raw_value
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def validate_request_cert_key(
|
|
126
|
-
ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
|
|
127
|
-
) -> Optional[str]:
|
|
128
|
-
if raw_value is not None and "request_cert" not in ctx.params:
|
|
129
|
-
raise click.UsageError('Missing argument, "--request-cert" should be specified as well.')
|
|
130
|
-
return raw_value
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def convert_verbosity(
|
|
134
|
-
ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]
|
|
135
|
-
) -> Optional[hypothesis.Verbosity]:
|
|
136
|
-
if value is None:
|
|
137
|
-
return value
|
|
138
|
-
return hypothesis.Verbosity[value]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value: Optional[str]) -> Optional[Stateful]:
|
|
142
|
-
if value is None:
|
|
143
|
-
return value
|
|
144
|
-
return Stateful[value]
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
|
|
148
|
-
return CodeSampleStyle.from_str(value)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def convert_data_generation_method(
|
|
152
|
-
ctx: click.core.Context, param: click.core.Parameter, value: str
|
|
153
|
-
) -> List[DataGenerationMethod]:
|
|
154
|
-
return [DataGenerationMethod[value]]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def convert_request_tls_verify(ctx: click.core.Context, param: click.core.Parameter, value: str) -> Union[str, bool]:
|
|
158
|
-
if value.lower() in ("y", "yes", "t", "true", "on", "1"):
|
|
159
|
-
return True
|
|
160
|
-
if value.lower() in ("n", "no", "f", "false", "off", "0"):
|
|
161
|
-
return False
|
|
162
|
-
return value
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@contextmanager
|
|
166
|
-
def reraise_format_error(raw_value: str) -> Generator[None, None, None]:
|
|
167
|
-
try:
|
|
168
|
-
yield
|
|
169
|
-
except ValueError as exc:
|
|
170
|
-
raise click.BadParameter(f"Should be in KEY:VALUE format. Got: {raw_value}") from exc
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def get_workers_count() -> int:
|
|
174
|
-
"""Detect the number of available CPUs for the current process, if possible.
|
|
175
|
-
|
|
176
|
-
Use ``DEFAULT_WORKERS`` if not possible to detect.
|
|
177
|
-
"""
|
|
178
|
-
if hasattr(os, "sched_getaffinity"):
|
|
179
|
-
# In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
|
|
180
|
-
return len(os.sched_getaffinity(0))
|
|
181
|
-
# Number of CPUs in the system, or 1 if undetermined
|
|
182
|
-
return os.cpu_count() or DEFAULT_WORKERS
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def convert_workers(ctx: click.core.Context, param: click.core.Parameter, value: str) -> int:
|
|
186
|
-
if value == "auto":
|
|
187
|
-
return get_workers_count()
|
|
188
|
-
return int(value)
|
schemathesis/cli/cassettes.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
import sys
|
|
5
|
-
import threading
|
|
6
|
-
from queue import Queue
|
|
7
|
-
from typing import Any, Dict, Generator, Iterator, List, Optional, cast
|
|
8
|
-
|
|
9
|
-
import attr
|
|
10
|
-
import click
|
|
11
|
-
import requests
|
|
12
|
-
from requests.cookies import RequestsCookieJar
|
|
13
|
-
from requests.structures import CaseInsensitiveDict
|
|
14
|
-
|
|
15
|
-
from .. import constants
|
|
16
|
-
from ..models import Request, Response
|
|
17
|
-
from ..runner import events
|
|
18
|
-
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
|
19
|
-
from .context import ExecutionContext
|
|
20
|
-
from .handlers import EventHandler
|
|
21
|
-
|
|
22
|
-
# Wait until the worker terminates
|
|
23
|
-
WRITER_WORKER_JOIN_TIMEOUT = 1
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
27
|
-
class CassetteWriter(EventHandler):
|
|
28
|
-
"""Write interactions in a YAML cassette.
|
|
29
|
-
|
|
30
|
-
A low-level interface is used to write data to YAML file during the test run and reduce the delay at
|
|
31
|
-
the end of the test run.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
|
-
file_handle: click.utils.LazyFile = attr.ib() # pragma: no mutate
|
|
35
|
-
queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
|
|
36
|
-
worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
|
|
37
|
-
|
|
38
|
-
def __attrs_post_init__(self) -> None:
|
|
39
|
-
self.worker = threading.Thread(target=worker, kwargs={"file_handle": self.file_handle, "queue": self.queue})
|
|
40
|
-
self.worker.start()
|
|
41
|
-
|
|
42
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
43
|
-
if isinstance(event, events.Initialized):
|
|
44
|
-
# In the beginning we write metadata and start `http_interactions` list
|
|
45
|
-
self.queue.put(Initialize())
|
|
46
|
-
if isinstance(event, events.AfterExecution):
|
|
47
|
-
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
48
|
-
# instance is created before `seed` is generated on the hypothesis side
|
|
49
|
-
seed = cast(int, event.result.seed)
|
|
50
|
-
self.queue.put(
|
|
51
|
-
Process(
|
|
52
|
-
seed=seed,
|
|
53
|
-
interactions=event.result.interactions,
|
|
54
|
-
)
|
|
55
|
-
)
|
|
56
|
-
if isinstance(event, events.Finished):
|
|
57
|
-
self.shutdown()
|
|
58
|
-
|
|
59
|
-
def shutdown(self) -> None:
|
|
60
|
-
self.queue.put(Finalize())
|
|
61
|
-
self._stop_worker()
|
|
62
|
-
|
|
63
|
-
def _stop_worker(self) -> None:
|
|
64
|
-
self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
68
|
-
class Initialize:
|
|
69
|
-
"""Start up, the first message to make preparations before proceeding the input data."""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
73
|
-
class Process:
|
|
74
|
-
"""A new chunk of data should be processed."""
|
|
75
|
-
|
|
76
|
-
seed: int = attr.ib() # pragma: no mutate
|
|
77
|
-
interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
81
|
-
class Finalize:
|
|
82
|
-
"""The work is done and there will be no more messages to process."""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def get_command_representation() -> str:
|
|
86
|
-
"""Get how Schemathesis was run."""
|
|
87
|
-
# It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
|
|
88
|
-
if not sys.argv[0].endswith("schemathesis"):
|
|
89
|
-
return "<unknown entrypoint>"
|
|
90
|
-
args = " ".join(sys.argv[1:])
|
|
91
|
-
return f"schemathesis {args}"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def worker(file_handle: click.utils.LazyFile, queue: Queue) -> None:
|
|
95
|
-
"""Write YAML to a file in an incremental manner.
|
|
96
|
-
|
|
97
|
-
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
|
98
|
-
- It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
|
|
99
|
-
- Implementation complexity. We have a quite simple format where all values are strings, and it is much simpler to
|
|
100
|
-
implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit types.
|
|
101
|
-
Another point is that with `pyyaml` we need to emit events and handle some low-level details like providing
|
|
102
|
-
tags, anchors to have incremental writing, with strings it is much simpler.
|
|
103
|
-
"""
|
|
104
|
-
current_id = 1
|
|
105
|
-
stream = file_handle.open()
|
|
106
|
-
|
|
107
|
-
def format_header_values(values: List[str]) -> str:
|
|
108
|
-
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
|
109
|
-
|
|
110
|
-
def format_headers(headers: Dict[str, List[str]]) -> str:
|
|
111
|
-
return "\n".join(f" {name}:\n{format_header_values(values)}" for name, values in headers.items())
|
|
112
|
-
|
|
113
|
-
def format_check_message(message: Optional[str]) -> str:
|
|
114
|
-
return "~" if message is None else f"{repr(message)}"
|
|
115
|
-
|
|
116
|
-
def format_checks(checks: List[SerializedCheck]) -> str:
|
|
117
|
-
return "\n".join(
|
|
118
|
-
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
|
119
|
-
for check in checks
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
def format_request_body(request: Request) -> str:
|
|
123
|
-
if request.body is not None:
|
|
124
|
-
return f""" body:
|
|
125
|
-
encoding: 'utf-8'
|
|
126
|
-
base64_string: '{request.body}'"""
|
|
127
|
-
return ""
|
|
128
|
-
|
|
129
|
-
def format_response_body(response: Response) -> str:
|
|
130
|
-
if response.body is not None:
|
|
131
|
-
return f""" body:
|
|
132
|
-
encoding: '{response.encoding}'
|
|
133
|
-
base64_string: '{response.body}'"""
|
|
134
|
-
return ""
|
|
135
|
-
|
|
136
|
-
while True:
|
|
137
|
-
item = queue.get()
|
|
138
|
-
if isinstance(item, Initialize):
|
|
139
|
-
stream.write(
|
|
140
|
-
f"""command: '{get_command_representation()}'
|
|
141
|
-
recorded_with: 'Schemathesis {constants.__version__}'
|
|
142
|
-
http_interactions:"""
|
|
143
|
-
)
|
|
144
|
-
elif isinstance(item, Process):
|
|
145
|
-
for interaction in item.interactions:
|
|
146
|
-
status = interaction.status.name.upper()
|
|
147
|
-
stream.write(
|
|
148
|
-
f"""\n- id: '{current_id}'
|
|
149
|
-
status: '{status}'
|
|
150
|
-
seed: '{item.seed}'
|
|
151
|
-
elapsed: '{interaction.response.elapsed}'
|
|
152
|
-
recorded_at: '{interaction.recorded_at}'
|
|
153
|
-
checks:
|
|
154
|
-
{format_checks(interaction.checks)}
|
|
155
|
-
request:
|
|
156
|
-
uri: '{interaction.request.uri}'
|
|
157
|
-
method: '{interaction.request.method}'
|
|
158
|
-
headers:
|
|
159
|
-
{format_headers(interaction.request.headers)}
|
|
160
|
-
{format_request_body(interaction.request)}
|
|
161
|
-
response:
|
|
162
|
-
status:
|
|
163
|
-
code: '{interaction.response.status_code}'
|
|
164
|
-
message: {json.dumps(interaction.response.message)}
|
|
165
|
-
headers:
|
|
166
|
-
{format_headers(interaction.response.headers)}
|
|
167
|
-
{format_response_body(interaction.response)}
|
|
168
|
-
http_version: '{interaction.response.http_version}'"""
|
|
169
|
-
)
|
|
170
|
-
current_id += 1
|
|
171
|
-
else:
|
|
172
|
-
break
|
|
173
|
-
file_handle.close()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
177
|
-
class Replayed:
|
|
178
|
-
interaction: Dict[str, Any] = attr.ib() # pragma: no mutate
|
|
179
|
-
response: requests.Response = attr.ib() # pragma: no mutate
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def replay(
|
|
183
|
-
cassette: Dict[str, Any],
|
|
184
|
-
id_: Optional[str] = None,
|
|
185
|
-
status: Optional[str] = None,
|
|
186
|
-
uri: Optional[str] = None,
|
|
187
|
-
method: Optional[str] = None,
|
|
188
|
-
) -> Generator[Replayed, None, None]:
|
|
189
|
-
"""Replay saved interactions."""
|
|
190
|
-
session = requests.Session()
|
|
191
|
-
for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
|
|
192
|
-
request = get_prepared_request(interaction["request"])
|
|
193
|
-
response = session.send(request) # type: ignore
|
|
194
|
-
yield Replayed(interaction, response)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def filter_cassette(
|
|
198
|
-
interactions: List[Dict[str, Any]],
|
|
199
|
-
id_: Optional[str] = None,
|
|
200
|
-
status: Optional[str] = None,
|
|
201
|
-
uri: Optional[str] = None,
|
|
202
|
-
method: Optional[str] = None,
|
|
203
|
-
) -> Iterator[Dict[str, Any]]:
|
|
204
|
-
|
|
205
|
-
filters = []
|
|
206
|
-
|
|
207
|
-
def id_filter(item: Dict[str, Any]) -> bool:
|
|
208
|
-
return item["id"] == id_
|
|
209
|
-
|
|
210
|
-
def status_filter(item: Dict[str, Any]) -> bool:
|
|
211
|
-
status_ = cast(str, status)
|
|
212
|
-
return item["status"].upper() == status_.upper()
|
|
213
|
-
|
|
214
|
-
def uri_filter(item: Dict[str, Any]) -> bool:
|
|
215
|
-
uri_ = cast(str, uri)
|
|
216
|
-
return bool(re.search(uri_, item["request"]["uri"]))
|
|
217
|
-
|
|
218
|
-
def method_filter(item: Dict[str, Any]) -> bool:
|
|
219
|
-
method_ = cast(str, method)
|
|
220
|
-
return bool(re.search(method_, item["request"]["method"]))
|
|
221
|
-
|
|
222
|
-
if id_ is not None:
|
|
223
|
-
filters.append(id_filter)
|
|
224
|
-
|
|
225
|
-
if status is not None:
|
|
226
|
-
filters.append(status_filter)
|
|
227
|
-
|
|
228
|
-
if uri is not None:
|
|
229
|
-
filters.append(uri_filter)
|
|
230
|
-
|
|
231
|
-
if method is not None:
|
|
232
|
-
filters.append(method_filter)
|
|
233
|
-
|
|
234
|
-
def is_match(interaction: Dict[str, Any]) -> bool:
|
|
235
|
-
return all(filter_(interaction) for filter_ in filters)
|
|
236
|
-
|
|
237
|
-
return filter(is_match, interactions)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def get_prepared_request(data: Dict[str, Any]) -> requests.PreparedRequest:
|
|
241
|
-
"""Create a `requests.PreparedRequest` from a serialized one."""
|
|
242
|
-
prepared = requests.PreparedRequest()
|
|
243
|
-
prepared.method = data["method"]
|
|
244
|
-
prepared.url = data["uri"]
|
|
245
|
-
prepared._cookies = RequestsCookieJar() # type: ignore
|
|
246
|
-
if "body" in data:
|
|
247
|
-
encoded = data["body"]["base64_string"]
|
|
248
|
-
if encoded:
|
|
249
|
-
prepared.body = base64.b64decode(encoded)
|
|
250
|
-
# There is always 1 value in a request
|
|
251
|
-
headers = [(key, value[0]) for key, value in data["headers"].items()]
|
|
252
|
-
prepared.headers = CaseInsensitiveDict(headers)
|
|
253
|
-
return prepared
|
schemathesis/cli/context.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
from queue import Queue
|
|
4
|
-
from typing import List, Optional
|
|
5
|
-
|
|
6
|
-
import attr
|
|
7
|
-
|
|
8
|
-
from ..constants import CodeSampleStyle
|
|
9
|
-
from ..runner.serialization import SerializedTestResult
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
13
|
-
class ServiceContext:
|
|
14
|
-
url: str = attr.ib() # pragma: no mutate
|
|
15
|
-
queue: Queue = attr.ib() # pragma: no mutate
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
19
|
-
class ExecutionContext:
|
|
20
|
-
"""Storage for the current context of the execution."""
|
|
21
|
-
|
|
22
|
-
hypothesis_output: List[str] = attr.ib(factory=list) # pragma: no mutate
|
|
23
|
-
workers_num: int = attr.ib(default=1) # pragma: no mutate
|
|
24
|
-
show_errors_tracebacks: bool = attr.ib(default=False) # pragma: no mutate
|
|
25
|
-
validate_schema: bool = attr.ib(default=True) # pragma: no mutate
|
|
26
|
-
operations_processed: int = attr.ib(default=0) # pragma: no mutate
|
|
27
|
-
# It is set in runtime, from a `Initialized` event
|
|
28
|
-
operations_count: Optional[int] = attr.ib(default=None) # pragma: no mutate
|
|
29
|
-
current_line_length: int = attr.ib(default=0) # pragma: no mutate
|
|
30
|
-
terminal_size: os.terminal_size = attr.ib(factory=shutil.get_terminal_size) # pragma: no mutate
|
|
31
|
-
results: List[SerializedTestResult] = attr.ib(factory=list) # pragma: no mutate
|
|
32
|
-
cassette_file_name: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
33
|
-
junit_xml_file: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
34
|
-
verbosity: int = attr.ib(default=0) # pragma: no mutate
|
|
35
|
-
code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
|
|
36
|
-
service: Optional[ServiceContext] = attr.ib(default=None) # pragma: no mutate
|
schemathesis/cli/debug.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import attr
|
|
4
|
-
from click.utils import LazyFile
|
|
5
|
-
|
|
6
|
-
from ..runner import events
|
|
7
|
-
from .handlers import EventHandler, ExecutionContext
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
11
|
-
class DebugOutputHandler(EventHandler):
|
|
12
|
-
file_handle: LazyFile = attr.ib() # pragma: no mutate
|
|
13
|
-
|
|
14
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
15
|
-
stream = self.file_handle.open()
|
|
16
|
-
data = event.asdict()
|
|
17
|
-
stream.write(json.dumps(data))
|
|
18
|
-
stream.write("\n")
|
|
19
|
-
|
|
20
|
-
def shutdown(self) -> None:
|
|
21
|
-
self.file_handle.close()
|
schemathesis/cli/handlers.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
from ..runner import events
|
|
2
|
-
from .context import ExecutionContext
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class EventHandler:
|
|
6
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
7
|
-
raise NotImplementedError
|
|
8
|
-
|
|
9
|
-
def shutdown(self) -> None:
|
|
10
|
-
# Do nothing by default
|
|
11
|
-
pass
|
schemathesis/cli/junitxml.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import platform
|
|
2
|
-
from typing import List, Optional
|
|
3
|
-
|
|
4
|
-
import attr
|
|
5
|
-
from click.utils import LazyFile
|
|
6
|
-
from junit_xml import TestCase, TestSuite, to_xml_report_file
|
|
7
|
-
|
|
8
|
-
from ..models import Status
|
|
9
|
-
from ..runner import events
|
|
10
|
-
from ..runner.serialization import deduplicate_failures
|
|
11
|
-
from .handlers import EventHandler, ExecutionContext
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
15
|
-
class JunitXMLHandler(EventHandler):
|
|
16
|
-
file_handle: LazyFile = attr.ib() # pragma: no mutate
|
|
17
|
-
test_cases: List = attr.ib(factory=list) # pragma: no mutate
|
|
18
|
-
start_time: Optional[float] = attr.ib(default=None) # pragma: no mutate
|
|
19
|
-
|
|
20
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
21
|
-
if isinstance(event, events.Initialized):
|
|
22
|
-
self.start_time = event.start_time
|
|
23
|
-
if isinstance(event, events.AfterExecution):
|
|
24
|
-
test_case = TestCase(
|
|
25
|
-
f"{event.result.method} {event.result.path}",
|
|
26
|
-
elapsed_sec=event.elapsed_time,
|
|
27
|
-
allow_multiple_subelements=True,
|
|
28
|
-
)
|
|
29
|
-
if event.status == Status.failure:
|
|
30
|
-
checks = deduplicate_failures(event.result.checks)
|
|
31
|
-
for idx, check in enumerate(checks, 1):
|
|
32
|
-
# `check.message` is always not empty for events with `failure` status
|
|
33
|
-
test_case.add_failure_info(message=f"{idx}. {check.message}")
|
|
34
|
-
if event.status == Status.error:
|
|
35
|
-
test_case.add_error_info(
|
|
36
|
-
message=event.result.errors[-1].exception, output=event.result.errors[-1].exception_with_traceback
|
|
37
|
-
)
|
|
38
|
-
self.test_cases.append(test_case)
|
|
39
|
-
if isinstance(event, events.Finished):
|
|
40
|
-
test_suites = [TestSuite("schemathesis", test_cases=self.test_cases, hostname=platform.node())]
|
|
41
|
-
to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
|
schemathesis/cli/options.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any, List, Optional, Type, Union
|
|
3
|
-
|
|
4
|
-
import click
|
|
5
|
-
|
|
6
|
-
from ..types import NotSet
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CustomHelpMessageChoice(click.Choice):
|
|
10
|
-
"""Allows you to customize how choices are displayed in the help message."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, *args: Any, choices_repr: str, **kwargs: Any):
|
|
13
|
-
super().__init__(*args, **kwargs)
|
|
14
|
-
self.choices_repr = choices_repr
|
|
15
|
-
|
|
16
|
-
def get_metavar(self, param: click.Parameter) -> str:
|
|
17
|
-
return self.choices_repr
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class CSVOption(click.Choice):
|
|
21
|
-
def __init__(self, choices: Type[Enum]):
|
|
22
|
-
self.enum = choices
|
|
23
|
-
super().__init__(tuple(choices.__members__))
|
|
24
|
-
|
|
25
|
-
def convert( # type: ignore[return]
|
|
26
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
27
|
-
) -> List[Enum]:
|
|
28
|
-
items = [item for item in value.split(",") if item]
|
|
29
|
-
invalid_options = set(items) - set(self.choices)
|
|
30
|
-
if not invalid_options and items:
|
|
31
|
-
return [self.enum[item] for item in items]
|
|
32
|
-
# Sort to keep the error output consistent with the passed values
|
|
33
|
-
sorted_options = ", ".join(sorted(invalid_options, key=items.index))
|
|
34
|
-
available_options = ", ".join(self.choices)
|
|
35
|
-
self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
not_set = NotSet()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class OptionalInt(click.types.IntRange):
|
|
42
|
-
def convert( # type: ignore
|
|
43
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
44
|
-
) -> Union[int, NotSet]:
|
|
45
|
-
if value == "None":
|
|
46
|
-
return not_set
|
|
47
|
-
try:
|
|
48
|
-
int(value)
|
|
49
|
-
return super().convert(value, param, ctx)
|
|
50
|
-
except ValueError:
|
|
51
|
-
self.fail("%s is not a valid integer or None" % value, param, ctx)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from . import default, short
|