schemathesis 3.39.16__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/cli/cassettes.py
DELETED
@@ -1,561 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import base64
|
4
|
-
import enum
|
5
|
-
import json
|
6
|
-
import re
|
7
|
-
import sys
|
8
|
-
import threading
|
9
|
-
from dataclasses import dataclass, field
|
10
|
-
from http.cookies import SimpleCookie
|
11
|
-
from queue import Queue
|
12
|
-
from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
|
13
|
-
from urllib.parse import parse_qsl, urlparse
|
14
|
-
|
15
|
-
import harfile
|
16
|
-
|
17
|
-
from ..constants import SCHEMATHESIS_VERSION
|
18
|
-
from ..runner import events
|
19
|
-
from .handlers import EventHandler
|
20
|
-
|
21
|
-
if TYPE_CHECKING:
|
22
|
-
import click
|
23
|
-
import requests
|
24
|
-
|
25
|
-
from ..models import Request, Response
|
26
|
-
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
27
|
-
from ..types import RequestCert
|
28
|
-
from .context import ExecutionContext
|
29
|
-
|
30
|
-
# Wait until the worker terminates
|
31
|
-
WRITER_WORKER_JOIN_TIMEOUT = 1
|
32
|
-
|
33
|
-
|
34
|
-
class CassetteFormat(str, enum.Enum):
|
35
|
-
"""Type of the cassette."""
|
36
|
-
|
37
|
-
VCR = "vcr"
|
38
|
-
HAR = "har"
|
39
|
-
|
40
|
-
@classmethod
|
41
|
-
def from_str(cls, value: str) -> CassetteFormat:
|
42
|
-
try:
|
43
|
-
return cls[value.upper()]
|
44
|
-
except KeyError:
|
45
|
-
available_formats = ", ".join(cls)
|
46
|
-
raise ValueError(
|
47
|
-
f"Invalid value for cassette format: {value}. Available formats: {available_formats}"
|
48
|
-
) from None
|
49
|
-
|
50
|
-
|
51
|
-
@dataclass
|
52
|
-
class CassetteWriter(EventHandler):
|
53
|
-
"""Write interactions in a YAML cassette.
|
54
|
-
|
55
|
-
A low-level interface is used to write data to YAML file during the test run and reduce the delay at
|
56
|
-
the end of the test run.
|
57
|
-
"""
|
58
|
-
|
59
|
-
file_handle: click.utils.LazyFile
|
60
|
-
format: CassetteFormat
|
61
|
-
preserve_exact_body_bytes: bool
|
62
|
-
queue: Queue = field(default_factory=Queue)
|
63
|
-
worker: threading.Thread = field(init=False)
|
64
|
-
|
65
|
-
def __post_init__(self) -> None:
|
66
|
-
kwargs = {
|
67
|
-
"file_handle": self.file_handle,
|
68
|
-
"queue": self.queue,
|
69
|
-
"preserve_exact_body_bytes": self.preserve_exact_body_bytes,
|
70
|
-
}
|
71
|
-
writer: Callable
|
72
|
-
if self.format == CassetteFormat.HAR:
|
73
|
-
writer = har_writer
|
74
|
-
else:
|
75
|
-
writer = vcr_writer
|
76
|
-
self.worker = threading.Thread(name="SchemathesisCassetteWriter", target=writer, kwargs=kwargs)
|
77
|
-
self.worker.start()
|
78
|
-
|
79
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
80
|
-
if isinstance(event, events.Initialized):
|
81
|
-
# In the beginning we write metadata and start `http_interactions` list
|
82
|
-
self.queue.put(Initialize(seed=event.seed))
|
83
|
-
elif isinstance(event, events.AfterExecution):
|
84
|
-
self.queue.put(
|
85
|
-
Process(
|
86
|
-
correlation_id=event.correlation_id,
|
87
|
-
thread_id=event.thread_id,
|
88
|
-
interactions=event.result.interactions,
|
89
|
-
)
|
90
|
-
)
|
91
|
-
elif isinstance(event, events.AfterStatefulExecution):
|
92
|
-
self.queue.put(
|
93
|
-
Process(
|
94
|
-
# Correlation ID is not used in stateful testing
|
95
|
-
correlation_id="",
|
96
|
-
thread_id=event.thread_id,
|
97
|
-
interactions=event.result.interactions,
|
98
|
-
)
|
99
|
-
)
|
100
|
-
elif isinstance(event, events.Finished):
|
101
|
-
self.shutdown()
|
102
|
-
|
103
|
-
def shutdown(self) -> None:
|
104
|
-
self.queue.put(Finalize())
|
105
|
-
self._stop_worker()
|
106
|
-
|
107
|
-
def _stop_worker(self) -> None:
|
108
|
-
self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
|
109
|
-
|
110
|
-
|
111
|
-
@dataclass
|
112
|
-
class Initialize:
|
113
|
-
"""Start up, the first message to make preparations before proceeding the input data."""
|
114
|
-
|
115
|
-
seed: int | None
|
116
|
-
|
117
|
-
|
118
|
-
@dataclass
|
119
|
-
class Process:
|
120
|
-
"""A new chunk of data should be processed."""
|
121
|
-
|
122
|
-
correlation_id: str
|
123
|
-
thread_id: int
|
124
|
-
interactions: list[SerializedInteraction]
|
125
|
-
|
126
|
-
|
127
|
-
@dataclass
|
128
|
-
class Finalize:
|
129
|
-
"""The work is done and there will be no more messages to process."""
|
130
|
-
|
131
|
-
|
132
|
-
def get_command_representation() -> str:
|
133
|
-
"""Get how Schemathesis was run."""
|
134
|
-
# It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
|
135
|
-
if not sys.argv[0].endswith(("schemathesis", "st")):
|
136
|
-
return "<unknown entrypoint>"
|
137
|
-
args = " ".join(sys.argv[1:])
|
138
|
-
return f"st {args}"
|
139
|
-
|
140
|
-
|
141
|
-
def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
142
|
-
"""Write YAML to a file in an incremental manner.
|
143
|
-
|
144
|
-
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
145
|
-
- It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
|
146
|
-
- Implementation complexity. We have a quite simple format where almost all values are strings, and it is much
|
147
|
-
simpler to implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit
|
148
|
-
types. Another point is that with `pyyaml` we need to emit events and handle some low-level details like
|
149
|
-
providing tags, anchors to have incremental writing, with primitive types it is much simpler.
|
150
|
-
"""
|
151
|
-
current_id = 1
|
152
|
-
stream = file_handle.open()
|
153
|
-
|
154
|
-
def format_header_values(values: list[str]) -> str:
|
155
|
-
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
156
|
-
|
157
|
-
def format_headers(headers: dict[str, list[str]]) -> str:
|
158
|
-
return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
|
159
|
-
|
160
|
-
def format_check_message(message: str | None) -> str:
|
161
|
-
return "~" if message is None else f"{message!r}"
|
162
|
-
|
163
|
-
def format_checks(checks: list[SerializedCheck]) -> str:
|
164
|
-
if not checks:
|
165
|
-
return " checks: []"
|
166
|
-
items = "\n".join(
|
167
|
-
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
168
|
-
for check in checks
|
169
|
-
)
|
170
|
-
return f"""
|
171
|
-
checks:
|
172
|
-
{items}"""
|
173
|
-
|
174
|
-
if preserve_exact_body_bytes:
|
175
|
-
|
176
|
-
def format_request_body(output: IO, request: Request) -> None:
|
177
|
-
if request.body is not None:
|
178
|
-
output.write(
|
179
|
-
f"""
|
180
|
-
body:
|
181
|
-
encoding: 'utf-8'
|
182
|
-
base64_string: '{request.body}'"""
|
183
|
-
)
|
184
|
-
|
185
|
-
def format_response_body(output: IO, response: Response) -> None:
|
186
|
-
if response.body is not None:
|
187
|
-
output.write(
|
188
|
-
f""" body:
|
189
|
-
encoding: '{response.encoding}'
|
190
|
-
base64_string: '{response.body}'"""
|
191
|
-
)
|
192
|
-
|
193
|
-
else:
|
194
|
-
|
195
|
-
def format_request_body(output: IO, request: Request) -> None:
|
196
|
-
if request.body is not None:
|
197
|
-
string = _safe_decode(request.body, "utf8")
|
198
|
-
output.write(
|
199
|
-
"""
|
200
|
-
body:
|
201
|
-
encoding: 'utf-8'
|
202
|
-
string: """
|
203
|
-
)
|
204
|
-
write_double_quoted(output, string)
|
205
|
-
|
206
|
-
def format_response_body(output: IO, response: Response) -> None:
|
207
|
-
if response.body is not None:
|
208
|
-
encoding = response.encoding or "utf8"
|
209
|
-
string = _safe_decode(response.body, encoding)
|
210
|
-
output.write(
|
211
|
-
f""" body:
|
212
|
-
encoding: '{encoding}'
|
213
|
-
string: """
|
214
|
-
)
|
215
|
-
write_double_quoted(output, string)
|
216
|
-
|
217
|
-
seed = "null"
|
218
|
-
while True:
|
219
|
-
item = queue.get()
|
220
|
-
if isinstance(item, Initialize):
|
221
|
-
seed = f"'{item.seed}'"
|
222
|
-
stream.write(
|
223
|
-
f"""command: '{get_command_representation()}'
|
224
|
-
recorded_with: 'Schemathesis {SCHEMATHESIS_VERSION}'
|
225
|
-
http_interactions:"""
|
226
|
-
)
|
227
|
-
elif isinstance(item, Process):
|
228
|
-
for interaction in item.interactions:
|
229
|
-
status = interaction.status.name.upper()
|
230
|
-
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
231
|
-
phase = f"'{interaction.phase.value}'" if interaction.phase is not None else "null"
|
232
|
-
stream.write(
|
233
|
-
f"""\n- id: '{current_id}'
|
234
|
-
status: '{status}'
|
235
|
-
seed: {seed}
|
236
|
-
thread_id: {item.thread_id}
|
237
|
-
correlation_id: '{item.correlation_id}'
|
238
|
-
data_generation_method: '{interaction.data_generation_method.value}'
|
239
|
-
meta:
|
240
|
-
description: """
|
241
|
-
)
|
242
|
-
|
243
|
-
if interaction.description is not None:
|
244
|
-
write_double_quoted(stream, interaction.description)
|
245
|
-
else:
|
246
|
-
stream.write("null")
|
247
|
-
|
248
|
-
stream.write("\n location: ")
|
249
|
-
if interaction.location is not None:
|
250
|
-
write_double_quoted(stream, interaction.location)
|
251
|
-
else:
|
252
|
-
stream.write("null")
|
253
|
-
|
254
|
-
stream.write("\n parameter: ")
|
255
|
-
if interaction.parameter is not None:
|
256
|
-
write_double_quoted(stream, interaction.parameter)
|
257
|
-
else:
|
258
|
-
stream.write("null")
|
259
|
-
|
260
|
-
stream.write("\n parameter_location: ")
|
261
|
-
if interaction.parameter_location is not None:
|
262
|
-
write_double_quoted(stream, interaction.parameter_location)
|
263
|
-
else:
|
264
|
-
stream.write("null")
|
265
|
-
stream.write(
|
266
|
-
f"""
|
267
|
-
phase: {phase}
|
268
|
-
elapsed: '{interaction.response.elapsed if interaction.response else 0}'
|
269
|
-
recorded_at: '{interaction.recorded_at}'
|
270
|
-
{format_checks(interaction.checks)}
|
271
|
-
request:
|
272
|
-
uri: '{interaction.request.uri}'
|
273
|
-
method: '{interaction.request.method}'
|
274
|
-
headers:
|
275
|
-
{format_headers(interaction.request.headers)}"""
|
276
|
-
)
|
277
|
-
format_request_body(stream, interaction.request)
|
278
|
-
if interaction.response is not None:
|
279
|
-
stream.write(
|
280
|
-
f"""
|
281
|
-
response:
|
282
|
-
status:
|
283
|
-
code: '{interaction.response.status_code}'
|
284
|
-
message: {json.dumps(interaction.response.message)}
|
285
|
-
headers:
|
286
|
-
{format_headers(interaction.response.headers)}
|
287
|
-
"""
|
288
|
-
)
|
289
|
-
format_response_body(stream, interaction.response)
|
290
|
-
stream.write(
|
291
|
-
f"""
|
292
|
-
http_version: '{interaction.response.http_version}'"""
|
293
|
-
)
|
294
|
-
else:
|
295
|
-
stream.write("""
|
296
|
-
response: null
|
297
|
-
""")
|
298
|
-
current_id += 1
|
299
|
-
else:
|
300
|
-
break
|
301
|
-
file_handle.close()
|
302
|
-
|
303
|
-
|
304
|
-
def _safe_decode(value: str, encoding: str) -> str:
|
305
|
-
"""Decode base64-encoded body bytes as a string."""
|
306
|
-
return base64.b64decode(value).decode(encoding, "replace")
|
307
|
-
|
308
|
-
|
309
|
-
def write_double_quoted(stream: IO, text: str) -> None:
|
310
|
-
"""Writes a valid YAML string enclosed in double quotes."""
|
311
|
-
from yaml.emitter import Emitter
|
312
|
-
|
313
|
-
# Adapted from `yaml.Emitter.write_double_quoted`:
|
314
|
-
# - Doesn't split the string, therefore doesn't track the current column
|
315
|
-
# - Doesn't encode the input
|
316
|
-
# - Allows Unicode unconditionally
|
317
|
-
stream.write('"')
|
318
|
-
start = end = 0
|
319
|
-
length = len(text)
|
320
|
-
while end <= length:
|
321
|
-
ch = None
|
322
|
-
if end < length:
|
323
|
-
ch = text[end]
|
324
|
-
if (
|
325
|
-
ch is None
|
326
|
-
or ch in '"\\\x85\u2028\u2029\ufeff'
|
327
|
-
or not ("\x20" <= ch <= "\x7e" or ("\xa0" <= ch <= "\ud7ff" or "\ue000" <= ch <= "\ufffd"))
|
328
|
-
):
|
329
|
-
if start < end:
|
330
|
-
stream.write(text[start:end])
|
331
|
-
start = end
|
332
|
-
if ch is not None:
|
333
|
-
# Escape character
|
334
|
-
if ch in Emitter.ESCAPE_REPLACEMENTS:
|
335
|
-
data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
|
336
|
-
elif ch <= "\xff":
|
337
|
-
data = f"\\x{ord(ch):02X}"
|
338
|
-
elif ch <= "\uffff":
|
339
|
-
data = f"\\u{ord(ch):04X}"
|
340
|
-
else:
|
341
|
-
data = f"\\U{ord(ch):08X}"
|
342
|
-
stream.write(data)
|
343
|
-
start = end + 1
|
344
|
-
end += 1
|
345
|
-
stream.write('"')
|
346
|
-
|
347
|
-
|
348
|
-
def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
349
|
-
if preserve_exact_body_bytes:
|
350
|
-
|
351
|
-
def get_body(body: str) -> str:
|
352
|
-
return body
|
353
|
-
else:
|
354
|
-
|
355
|
-
def get_body(body: str) -> str:
|
356
|
-
return base64.b64decode(body).decode("utf-8", errors="replace")
|
357
|
-
|
358
|
-
with harfile.open(file_handle) as har:
|
359
|
-
while True:
|
360
|
-
item = queue.get()
|
361
|
-
if isinstance(item, Process):
|
362
|
-
for interaction in item.interactions:
|
363
|
-
query_params = urlparse(interaction.request.uri).query
|
364
|
-
if interaction.request.body is not None:
|
365
|
-
post_data = harfile.PostData(
|
366
|
-
mimeType=interaction.request.headers.get("Content-Type", [""])[0],
|
367
|
-
text=get_body(interaction.request.body),
|
368
|
-
)
|
369
|
-
else:
|
370
|
-
post_data = None
|
371
|
-
if interaction.response is not None:
|
372
|
-
content_type = interaction.response.headers.get("Content-Type", [""])[0]
|
373
|
-
content = harfile.Content(
|
374
|
-
size=interaction.response.body_size or 0,
|
375
|
-
mimeType=content_type,
|
376
|
-
text=get_body(interaction.response.body) if interaction.response.body is not None else None,
|
377
|
-
encoding="base64"
|
378
|
-
if interaction.response.body is not None and preserve_exact_body_bytes
|
379
|
-
else None,
|
380
|
-
)
|
381
|
-
http_version = f"HTTP/{interaction.response.http_version}"
|
382
|
-
response = harfile.Response(
|
383
|
-
status=interaction.response.status_code,
|
384
|
-
httpVersion=http_version,
|
385
|
-
statusText=interaction.response.message,
|
386
|
-
headers=[
|
387
|
-
harfile.Record(name=name, value=values[0])
|
388
|
-
for name, values in interaction.response.headers.items()
|
389
|
-
],
|
390
|
-
cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
|
391
|
-
content=content,
|
392
|
-
headersSize=_headers_size(interaction.response.headers),
|
393
|
-
bodySize=interaction.response.body_size or 0,
|
394
|
-
redirectURL=interaction.response.headers.get("Location", [""])[0],
|
395
|
-
)
|
396
|
-
time = round(interaction.response.elapsed * 1000, 2)
|
397
|
-
else:
|
398
|
-
response = HARFILE_NO_RESPONSE
|
399
|
-
time = 0
|
400
|
-
http_version = ""
|
401
|
-
|
402
|
-
har.add_entry(
|
403
|
-
startedDateTime=interaction.recorded_at,
|
404
|
-
time=time,
|
405
|
-
request=harfile.Request(
|
406
|
-
method=interaction.request.method.upper(),
|
407
|
-
url=interaction.request.uri,
|
408
|
-
httpVersion=http_version,
|
409
|
-
headers=[
|
410
|
-
harfile.Record(name=name, value=values[0])
|
411
|
-
for name, values in interaction.request.headers.items()
|
412
|
-
],
|
413
|
-
queryString=[
|
414
|
-
harfile.Record(name=name, value=value)
|
415
|
-
for name, value in parse_qsl(query_params, keep_blank_values=True)
|
416
|
-
],
|
417
|
-
cookies=_extract_cookies(interaction.request.headers.get("Cookie", [])),
|
418
|
-
headersSize=_headers_size(interaction.request.headers),
|
419
|
-
bodySize=interaction.request.body_size or 0,
|
420
|
-
postData=post_data,
|
421
|
-
),
|
422
|
-
response=response,
|
423
|
-
timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
|
424
|
-
)
|
425
|
-
elif isinstance(item, Finalize):
|
426
|
-
break
|
427
|
-
|
428
|
-
|
429
|
-
HARFILE_NO_RESPONSE = harfile.Response(
|
430
|
-
status=0,
|
431
|
-
httpVersion="",
|
432
|
-
statusText="",
|
433
|
-
headers=[],
|
434
|
-
cookies=[],
|
435
|
-
content=harfile.Content(),
|
436
|
-
)
|
437
|
-
|
438
|
-
|
439
|
-
def _headers_size(headers: dict[str, list[str]]) -> int:
|
440
|
-
size = 0
|
441
|
-
for name, values in headers.items():
|
442
|
-
# 4 is for ": " and "\r\n"
|
443
|
-
size += len(name) + 4 + len(values[0])
|
444
|
-
return size
|
445
|
-
|
446
|
-
|
447
|
-
def _extract_cookies(headers: list[str]) -> list[harfile.Cookie]:
|
448
|
-
return [cookie for items in headers for item in items for cookie in _cookie_to_har(item)]
|
449
|
-
|
450
|
-
|
451
|
-
def _cookie_to_har(cookie: str) -> Iterator[harfile.Cookie]:
|
452
|
-
parsed = SimpleCookie(cookie)
|
453
|
-
for name, data in parsed.items():
|
454
|
-
yield harfile.Cookie(
|
455
|
-
name=name,
|
456
|
-
value=data.value,
|
457
|
-
path=data["path"] or None,
|
458
|
-
domain=data["domain"] or None,
|
459
|
-
expires=data["expires"] or None,
|
460
|
-
httpOnly=data["httponly"] or None,
|
461
|
-
secure=data["secure"] or None,
|
462
|
-
)
|
463
|
-
|
464
|
-
|
465
|
-
@dataclass
|
466
|
-
class Replayed:
|
467
|
-
interaction: dict[str, Any]
|
468
|
-
response: requests.Response
|
469
|
-
|
470
|
-
|
471
|
-
def replay(
|
472
|
-
cassette: dict[str, Any],
|
473
|
-
id_: str | None = None,
|
474
|
-
status: str | None = None,
|
475
|
-
uri: str | None = None,
|
476
|
-
method: str | None = None,
|
477
|
-
request_tls_verify: bool = True,
|
478
|
-
request_cert: RequestCert | None = None,
|
479
|
-
request_proxy: str | None = None,
|
480
|
-
) -> Generator[Replayed, None, None]:
|
481
|
-
"""Replay saved interactions."""
|
482
|
-
import requests
|
483
|
-
|
484
|
-
session = requests.Session()
|
485
|
-
session.verify = request_tls_verify
|
486
|
-
session.cert = request_cert
|
487
|
-
kwargs = {}
|
488
|
-
if request_proxy is not None:
|
489
|
-
kwargs["proxies"] = {"all": request_proxy}
|
490
|
-
for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
|
491
|
-
request = get_prepared_request(interaction["request"])
|
492
|
-
response = session.send(request, **kwargs) # type: ignore
|
493
|
-
yield Replayed(interaction, response)
|
494
|
-
|
495
|
-
|
496
|
-
def filter_cassette(
|
497
|
-
interactions: list[dict[str, Any]],
|
498
|
-
id_: str | None = None,
|
499
|
-
status: str | None = None,
|
500
|
-
uri: str | None = None,
|
501
|
-
method: str | None = None,
|
502
|
-
) -> Iterator[dict[str, Any]]:
|
503
|
-
filters = []
|
504
|
-
|
505
|
-
def id_filter(item: dict[str, Any]) -> bool:
|
506
|
-
return item["id"] == id_
|
507
|
-
|
508
|
-
def status_filter(item: dict[str, Any]) -> bool:
|
509
|
-
status_ = cast(str, status)
|
510
|
-
return item["status"].upper() == status_.upper()
|
511
|
-
|
512
|
-
def uri_filter(item: dict[str, Any]) -> bool:
|
513
|
-
uri_ = cast(str, uri)
|
514
|
-
return bool(re.search(uri_, item["request"]["uri"]))
|
515
|
-
|
516
|
-
def method_filter(item: dict[str, Any]) -> bool:
|
517
|
-
method_ = cast(str, method)
|
518
|
-
return bool(re.search(method_, item["request"]["method"]))
|
519
|
-
|
520
|
-
if id_ is not None:
|
521
|
-
filters.append(id_filter)
|
522
|
-
|
523
|
-
if status is not None:
|
524
|
-
filters.append(status_filter)
|
525
|
-
|
526
|
-
if uri is not None:
|
527
|
-
filters.append(uri_filter)
|
528
|
-
|
529
|
-
if method is not None:
|
530
|
-
filters.append(method_filter)
|
531
|
-
|
532
|
-
def is_match(interaction: dict[str, Any]) -> bool:
|
533
|
-
return all(filter_(interaction) for filter_ in filters)
|
534
|
-
|
535
|
-
return filter(is_match, interactions)
|
536
|
-
|
537
|
-
|
538
|
-
def get_prepared_request(data: dict[str, Any]) -> requests.PreparedRequest:
|
539
|
-
"""Create a `requests.PreparedRequest` from a serialized one."""
|
540
|
-
import requests
|
541
|
-
from requests.cookies import RequestsCookieJar
|
542
|
-
from requests.structures import CaseInsensitiveDict
|
543
|
-
|
544
|
-
prepared = requests.PreparedRequest()
|
545
|
-
prepared.method = data["method"]
|
546
|
-
prepared.url = data["uri"]
|
547
|
-
prepared._cookies = RequestsCookieJar() # type: ignore
|
548
|
-
if "body" in data:
|
549
|
-
body = data["body"]
|
550
|
-
if "base64_string" in body:
|
551
|
-
content = body["base64_string"]
|
552
|
-
if content:
|
553
|
-
prepared.body = base64.b64decode(content)
|
554
|
-
else:
|
555
|
-
content = body["string"]
|
556
|
-
if content:
|
557
|
-
prepared.body = content.encode("utf8")
|
558
|
-
# There is always 1 value in a request
|
559
|
-
headers = [(key, value[0]) for key, value in data["headers"].items()]
|
560
|
-
prepared.headers = CaseInsensitiveDict(headers)
|
561
|
-
return prepared
|
schemathesis/cli/context.py
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import shutil
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
from typing import TYPE_CHECKING, Generator
|
6
|
-
|
7
|
-
from ..code_samples import CodeSampleStyle
|
8
|
-
from ..internal.deprecation import deprecated_property
|
9
|
-
from ..internal.output import OutputConfig
|
10
|
-
|
11
|
-
if TYPE_CHECKING:
|
12
|
-
import os
|
13
|
-
from queue import Queue
|
14
|
-
|
15
|
-
import hypothesis
|
16
|
-
|
17
|
-
from ..internal.result import Result
|
18
|
-
from ..runner.probes import ProbeRun
|
19
|
-
from ..runner.serialization import SerializedTestResult
|
20
|
-
from ..service.models import AnalysisResult
|
21
|
-
from ..stateful.sink import StateMachineSink
|
22
|
-
|
23
|
-
|
24
|
-
@dataclass
|
25
|
-
class ServiceReportContext:
|
26
|
-
queue: Queue
|
27
|
-
service_base_url: str
|
28
|
-
|
29
|
-
|
30
|
-
@dataclass
|
31
|
-
class FileReportContext:
|
32
|
-
queue: Queue
|
33
|
-
filename: str | None = None
|
34
|
-
|
35
|
-
|
36
|
-
@dataclass
|
37
|
-
class ExecutionContext:
|
38
|
-
"""Storage for the current context of the execution."""
|
39
|
-
|
40
|
-
hypothesis_settings: hypothesis.settings
|
41
|
-
hypothesis_output: list[str] = field(default_factory=list)
|
42
|
-
workers_num: int = 1
|
43
|
-
rate_limit: str | None = None
|
44
|
-
show_trace: bool = False
|
45
|
-
wait_for_schema: float | None = None
|
46
|
-
validate_schema: bool = True
|
47
|
-
operations_processed: int = 0
|
48
|
-
# It is set in runtime, from the `Initialized` event
|
49
|
-
operations_count: int | None = None
|
50
|
-
seed: int | None = None
|
51
|
-
current_line_length: int = 0
|
52
|
-
terminal_size: os.terminal_size = field(default_factory=shutil.get_terminal_size)
|
53
|
-
results: list[SerializedTestResult] = field(default_factory=list)
|
54
|
-
cassette_path: str | None = None
|
55
|
-
junit_xml_file: str | None = None
|
56
|
-
is_interrupted: bool = False
|
57
|
-
verbosity: int = 0
|
58
|
-
code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
|
59
|
-
report: ServiceReportContext | FileReportContext | None = None
|
60
|
-
probes: list[ProbeRun] | None = None
|
61
|
-
analysis: Result[AnalysisResult, Exception] | None = None
|
62
|
-
output_config: OutputConfig = field(default_factory=OutputConfig)
|
63
|
-
state_machine_sink: StateMachineSink | None = None
|
64
|
-
initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
65
|
-
summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
|
66
|
-
|
67
|
-
@deprecated_property(removed_in="4.0", replacement="show_trace")
|
68
|
-
def show_errors_tracebacks(self) -> bool:
|
69
|
-
return self.show_trace
|
70
|
-
|
71
|
-
def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
|
72
|
-
self.initialization_lines.append(line)
|
73
|
-
|
74
|
-
def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
|
75
|
-
self.summary_lines.append(line)
|
schemathesis/cli/debug.py
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import json
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from typing import TYPE_CHECKING
|
6
|
-
|
7
|
-
from .handlers import EventHandler
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from click.utils import LazyFile
|
11
|
-
|
12
|
-
from ..runner import events
|
13
|
-
from .context import ExecutionContext
|
14
|
-
|
15
|
-
|
16
|
-
@dataclass
|
17
|
-
class DebugOutputHandler(EventHandler):
|
18
|
-
file_handle: LazyFile
|
19
|
-
|
20
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
21
|
-
stream = self.file_handle.open()
|
22
|
-
data = event.asdict()
|
23
|
-
stream.write(json.dumps(data))
|
24
|
-
stream.write("\n")
|
25
|
-
|
26
|
-
def shutdown(self) -> None:
|
27
|
-
self.file_handle.close()
|
schemathesis/cli/handlers.py
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING, Any
|
4
|
-
|
5
|
-
if TYPE_CHECKING:
|
6
|
-
from ..runner import events
|
7
|
-
from .context import ExecutionContext
|
8
|
-
|
9
|
-
|
10
|
-
class EventHandler:
|
11
|
-
def __init__(self, *args: Any, **params: Any) -> None:
|
12
|
-
pass
|
13
|
-
|
14
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
15
|
-
raise NotImplementedError
|
16
|
-
|
17
|
-
def shutdown(self) -> None:
|
18
|
-
# Do nothing by default
|
19
|
-
pass
|