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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- 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 +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- 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 +748 -82
- 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 +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- 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.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- 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 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
schemathesis/cli/cassettes.py
DELETED
|
@@ -1,351 +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 IO, 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
|
-
from yaml.emitter import Emitter
|
|
15
|
-
|
|
16
|
-
from .. import constants
|
|
17
|
-
from ..models import Request, Response
|
|
18
|
-
from ..runner import events
|
|
19
|
-
from ..runner.serialization import SerializedCheck, SerializedInteraction
|
|
20
|
-
from ..types import RequestCert
|
|
21
|
-
from .context import ExecutionContext
|
|
22
|
-
from .handlers import EventHandler
|
|
23
|
-
|
|
24
|
-
# Wait until the worker terminates
|
|
25
|
-
WRITER_WORKER_JOIN_TIMEOUT = 1
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
29
|
-
class CassetteWriter(EventHandler):
|
|
30
|
-
"""Write interactions in a YAML cassette.
|
|
31
|
-
|
|
32
|
-
A low-level interface is used to write data to YAML file during the test run and reduce the delay at
|
|
33
|
-
the end of the test run.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
file_handle: click.utils.LazyFile = attr.ib() # pragma: no mutate
|
|
37
|
-
preserve_exact_body_bytes: bool = attr.ib() # pragma: no mutate
|
|
38
|
-
queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
|
|
39
|
-
worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
|
|
40
|
-
|
|
41
|
-
def __attrs_post_init__(self) -> None:
|
|
42
|
-
self.worker = threading.Thread(
|
|
43
|
-
target=worker,
|
|
44
|
-
kwargs={
|
|
45
|
-
"file_handle": self.file_handle,
|
|
46
|
-
"preserve_exact_body_bytes": self.preserve_exact_body_bytes,
|
|
47
|
-
"queue": self.queue,
|
|
48
|
-
},
|
|
49
|
-
)
|
|
50
|
-
self.worker.start()
|
|
51
|
-
|
|
52
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
53
|
-
if isinstance(event, events.Initialized):
|
|
54
|
-
# In the beginning we write metadata and start `http_interactions` list
|
|
55
|
-
self.queue.put(Initialize())
|
|
56
|
-
if isinstance(event, events.AfterExecution):
|
|
57
|
-
# Seed is always present at this point, the original Optional[int] type is there because `TestResult`
|
|
58
|
-
# instance is created before `seed` is generated on the hypothesis side
|
|
59
|
-
seed = cast(int, event.result.seed)
|
|
60
|
-
self.queue.put(
|
|
61
|
-
Process(
|
|
62
|
-
seed=seed,
|
|
63
|
-
interactions=event.result.interactions,
|
|
64
|
-
)
|
|
65
|
-
)
|
|
66
|
-
if isinstance(event, events.Finished):
|
|
67
|
-
self.shutdown()
|
|
68
|
-
|
|
69
|
-
def shutdown(self) -> None:
|
|
70
|
-
self.queue.put(Finalize())
|
|
71
|
-
self._stop_worker()
|
|
72
|
-
|
|
73
|
-
def _stop_worker(self) -> None:
|
|
74
|
-
self.worker.join(WRITER_WORKER_JOIN_TIMEOUT)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
78
|
-
class Initialize:
|
|
79
|
-
"""Start up, the first message to make preparations before proceeding the input data."""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
83
|
-
class Process:
|
|
84
|
-
"""A new chunk of data should be processed."""
|
|
85
|
-
|
|
86
|
-
seed: int = attr.ib() # pragma: no mutate
|
|
87
|
-
interactions: List[SerializedInteraction] = attr.ib() # pragma: no mutate
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
91
|
-
class Finalize:
|
|
92
|
-
"""The work is done and there will be no more messages to process."""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def get_command_representation() -> str:
|
|
96
|
-
"""Get how Schemathesis was run."""
|
|
97
|
-
# It is supposed to be executed from Schemathesis CLI, not via Click's `command.invoke`
|
|
98
|
-
if not sys.argv[0].endswith(("schemathesis", "st")):
|
|
99
|
-
return "<unknown entrypoint>"
|
|
100
|
-
args = " ".join(sys.argv[1:])
|
|
101
|
-
return f"st {args}"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def worker(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
|
|
105
|
-
"""Write YAML to a file in an incremental manner.
|
|
106
|
-
|
|
107
|
-
This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
|
|
108
|
-
- It is much faster. The string-based approach gives only ~2.5% time overhead when `yaml.CDumper` has ~11.2%;
|
|
109
|
-
- Implementation complexity. We have a quite simple format where all values are strings, and it is much simpler to
|
|
110
|
-
implement it with string composition rather than with adjusting `yaml.Serializer` to emit explicit types.
|
|
111
|
-
Another point is that with `pyyaml` we need to emit events and handle some low-level details like providing
|
|
112
|
-
tags, anchors to have incremental writing, with strings it is much simpler.
|
|
113
|
-
"""
|
|
114
|
-
current_id = 1
|
|
115
|
-
stream = file_handle.open()
|
|
116
|
-
|
|
117
|
-
def format_header_values(values: List[str]) -> str:
|
|
118
|
-
return "\n".join(f" - {json.dumps(v)}" for v in values)
|
|
119
|
-
|
|
120
|
-
def format_headers(headers: Dict[str, List[str]]) -> str:
|
|
121
|
-
return "\n".join(f" {name}:\n{format_header_values(values)}" for name, values in headers.items())
|
|
122
|
-
|
|
123
|
-
def format_check_message(message: Optional[str]) -> str:
|
|
124
|
-
return "~" if message is None else f"{repr(message)}"
|
|
125
|
-
|
|
126
|
-
def format_checks(checks: List[SerializedCheck]) -> str:
|
|
127
|
-
return "\n".join(
|
|
128
|
-
f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
|
|
129
|
-
for check in checks
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
if preserve_exact_body_bytes:
|
|
133
|
-
|
|
134
|
-
def format_request_body(output: IO, request: Request) -> None:
|
|
135
|
-
if request.body is not None:
|
|
136
|
-
output.write(
|
|
137
|
-
f""" body:
|
|
138
|
-
encoding: 'utf-8'
|
|
139
|
-
base64_string: '{request.body}'"""
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
def format_response_body(output: IO, response: Response) -> None:
|
|
143
|
-
if response.body is not None:
|
|
144
|
-
output.write(
|
|
145
|
-
f""" body:
|
|
146
|
-
encoding: '{response.encoding}'
|
|
147
|
-
base64_string: '{response.body}'"""
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
else:
|
|
151
|
-
|
|
152
|
-
def format_request_body(output: IO, request: Request) -> None:
|
|
153
|
-
if request.body is not None:
|
|
154
|
-
string = _safe_decode(request.body, "utf8")
|
|
155
|
-
output.write(
|
|
156
|
-
""" body:
|
|
157
|
-
encoding: 'utf-8'
|
|
158
|
-
string: """
|
|
159
|
-
)
|
|
160
|
-
write_double_quoted(output, string)
|
|
161
|
-
|
|
162
|
-
def format_response_body(output: IO, response: Response) -> None:
|
|
163
|
-
if response.body is not None:
|
|
164
|
-
encoding = response.encoding or "utf8"
|
|
165
|
-
string = _safe_decode(response.body, encoding)
|
|
166
|
-
output.write(
|
|
167
|
-
f""" body:
|
|
168
|
-
encoding: '{encoding}'
|
|
169
|
-
string: """
|
|
170
|
-
)
|
|
171
|
-
write_double_quoted(output, string)
|
|
172
|
-
|
|
173
|
-
while True:
|
|
174
|
-
item = queue.get()
|
|
175
|
-
if isinstance(item, Initialize):
|
|
176
|
-
stream.write(
|
|
177
|
-
f"""command: '{get_command_representation()}'
|
|
178
|
-
recorded_with: 'Schemathesis {constants.__version__}'
|
|
179
|
-
http_interactions:"""
|
|
180
|
-
)
|
|
181
|
-
elif isinstance(item, Process):
|
|
182
|
-
for interaction in item.interactions:
|
|
183
|
-
status = interaction.status.name.upper()
|
|
184
|
-
# Body payloads are handled via separate `stream.write` calls to avoid some allocations
|
|
185
|
-
stream.write(
|
|
186
|
-
f"""\n- id: '{current_id}'
|
|
187
|
-
status: '{status}'
|
|
188
|
-
seed: '{item.seed}'
|
|
189
|
-
elapsed: '{interaction.response.elapsed}'
|
|
190
|
-
recorded_at: '{interaction.recorded_at}'
|
|
191
|
-
checks:
|
|
192
|
-
{format_checks(interaction.checks)}
|
|
193
|
-
request:
|
|
194
|
-
uri: '{interaction.request.uri}'
|
|
195
|
-
method: '{interaction.request.method}'
|
|
196
|
-
headers:
|
|
197
|
-
{format_headers(interaction.request.headers)}
|
|
198
|
-
"""
|
|
199
|
-
)
|
|
200
|
-
format_request_body(stream, interaction.request)
|
|
201
|
-
stream.write(
|
|
202
|
-
f"""
|
|
203
|
-
response:
|
|
204
|
-
status:
|
|
205
|
-
code: '{interaction.response.status_code}'
|
|
206
|
-
message: {json.dumps(interaction.response.message)}
|
|
207
|
-
headers:
|
|
208
|
-
{format_headers(interaction.response.headers)}
|
|
209
|
-
"""
|
|
210
|
-
)
|
|
211
|
-
format_response_body(stream, interaction.response)
|
|
212
|
-
stream.write(
|
|
213
|
-
f"""
|
|
214
|
-
http_version: '{interaction.response.http_version}'"""
|
|
215
|
-
)
|
|
216
|
-
current_id += 1
|
|
217
|
-
else:
|
|
218
|
-
break
|
|
219
|
-
file_handle.close()
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _safe_decode(value: str, encoding: str) -> str:
|
|
223
|
-
"""Decode base64-encoded body bytes as a string."""
|
|
224
|
-
return base64.b64decode(value).decode(encoding, "replace")
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def write_double_quoted(stream: IO, text: str) -> None:
|
|
228
|
-
"""Writes a valid YAML string enclosed in double quotes."""
|
|
229
|
-
# Adapted from `yaml.Emitter.write_double_quoted`:
|
|
230
|
-
# - Doesn't split the string, therefore doesn't track the current column
|
|
231
|
-
# - Doesn't encode the input
|
|
232
|
-
# - Allows Unicode unconditionally
|
|
233
|
-
stream.write('"')
|
|
234
|
-
start = end = 0
|
|
235
|
-
length = len(text)
|
|
236
|
-
while end <= length:
|
|
237
|
-
ch = None
|
|
238
|
-
if end < length:
|
|
239
|
-
ch = text[end]
|
|
240
|
-
if (
|
|
241
|
-
ch is None
|
|
242
|
-
or ch in '"\\\x85\u2028\u2029\uFEFF'
|
|
243
|
-
or not ("\x20" <= ch <= "\x7E" or ("\xA0" <= ch <= "\uD7FF" or "\uE000" <= ch <= "\uFFFD"))
|
|
244
|
-
):
|
|
245
|
-
if start < end:
|
|
246
|
-
stream.write(text[start:end])
|
|
247
|
-
start = end
|
|
248
|
-
if ch is not None:
|
|
249
|
-
# Escape character
|
|
250
|
-
if ch in Emitter.ESCAPE_REPLACEMENTS:
|
|
251
|
-
data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
|
|
252
|
-
elif ch <= "\xFF":
|
|
253
|
-
data = "\\x%02X" % ord(ch)
|
|
254
|
-
elif ch <= "\uFFFF":
|
|
255
|
-
data = "\\u%04X" % ord(ch)
|
|
256
|
-
else:
|
|
257
|
-
data = "\\U%08X" % ord(ch)
|
|
258
|
-
stream.write(data)
|
|
259
|
-
start = end + 1
|
|
260
|
-
end += 1
|
|
261
|
-
stream.write('"')
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
265
|
-
class Replayed:
|
|
266
|
-
interaction: Dict[str, Any] = attr.ib() # pragma: no mutate
|
|
267
|
-
response: requests.Response = attr.ib() # pragma: no mutate
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def replay(
|
|
271
|
-
cassette: Dict[str, Any],
|
|
272
|
-
id_: Optional[str] = None,
|
|
273
|
-
status: Optional[str] = None,
|
|
274
|
-
uri: Optional[str] = None,
|
|
275
|
-
method: Optional[str] = None,
|
|
276
|
-
request_tls_verify: bool = True,
|
|
277
|
-
request_cert: Optional[RequestCert] = None,
|
|
278
|
-
) -> Generator[Replayed, None, None]:
|
|
279
|
-
"""Replay saved interactions."""
|
|
280
|
-
session = requests.Session()
|
|
281
|
-
session.verify = request_tls_verify
|
|
282
|
-
session.cert = request_cert
|
|
283
|
-
for interaction in filter_cassette(cassette["http_interactions"], id_, status, uri, method):
|
|
284
|
-
request = get_prepared_request(interaction["request"])
|
|
285
|
-
response = session.send(request) # type: ignore
|
|
286
|
-
yield Replayed(interaction, response)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def filter_cassette(
|
|
290
|
-
interactions: List[Dict[str, Any]],
|
|
291
|
-
id_: Optional[str] = None,
|
|
292
|
-
status: Optional[str] = None,
|
|
293
|
-
uri: Optional[str] = None,
|
|
294
|
-
method: Optional[str] = None,
|
|
295
|
-
) -> Iterator[Dict[str, Any]]:
|
|
296
|
-
|
|
297
|
-
filters = []
|
|
298
|
-
|
|
299
|
-
def id_filter(item: Dict[str, Any]) -> bool:
|
|
300
|
-
return item["id"] == id_
|
|
301
|
-
|
|
302
|
-
def status_filter(item: Dict[str, Any]) -> bool:
|
|
303
|
-
status_ = cast(str, status)
|
|
304
|
-
return item["status"].upper() == status_.upper()
|
|
305
|
-
|
|
306
|
-
def uri_filter(item: Dict[str, Any]) -> bool:
|
|
307
|
-
uri_ = cast(str, uri)
|
|
308
|
-
return bool(re.search(uri_, item["request"]["uri"]))
|
|
309
|
-
|
|
310
|
-
def method_filter(item: Dict[str, Any]) -> bool:
|
|
311
|
-
method_ = cast(str, method)
|
|
312
|
-
return bool(re.search(method_, item["request"]["method"]))
|
|
313
|
-
|
|
314
|
-
if id_ is not None:
|
|
315
|
-
filters.append(id_filter)
|
|
316
|
-
|
|
317
|
-
if status is not None:
|
|
318
|
-
filters.append(status_filter)
|
|
319
|
-
|
|
320
|
-
if uri is not None:
|
|
321
|
-
filters.append(uri_filter)
|
|
322
|
-
|
|
323
|
-
if method is not None:
|
|
324
|
-
filters.append(method_filter)
|
|
325
|
-
|
|
326
|
-
def is_match(interaction: Dict[str, Any]) -> bool:
|
|
327
|
-
return all(filter_(interaction) for filter_ in filters)
|
|
328
|
-
|
|
329
|
-
return filter(is_match, interactions)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def get_prepared_request(data: Dict[str, Any]) -> requests.PreparedRequest:
|
|
333
|
-
"""Create a `requests.PreparedRequest` from a serialized one."""
|
|
334
|
-
prepared = requests.PreparedRequest()
|
|
335
|
-
prepared.method = data["method"]
|
|
336
|
-
prepared.url = data["uri"]
|
|
337
|
-
prepared._cookies = RequestsCookieJar() # type: ignore
|
|
338
|
-
if "body" in data:
|
|
339
|
-
body = data["body"]
|
|
340
|
-
if "base64_string" in body:
|
|
341
|
-
content = body["base64_string"]
|
|
342
|
-
if content:
|
|
343
|
-
prepared.body = base64.b64decode(content)
|
|
344
|
-
else:
|
|
345
|
-
content = body["string"]
|
|
346
|
-
if content:
|
|
347
|
-
prepared.body = content.encode("utf8")
|
|
348
|
-
# There is always 1 value in a request
|
|
349
|
-
headers = [(key, value[0]) for key, value in data["headers"].items()]
|
|
350
|
-
prepared.headers = CaseInsensitiveDict(headers)
|
|
351
|
-
return prepared
|
schemathesis/cli/context.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
from queue import Queue
|
|
4
|
-
from typing import List, Optional
|
|
5
|
-
|
|
6
|
-
import attr
|
|
7
|
-
import hypothesis
|
|
8
|
-
|
|
9
|
-
from ..constants import CodeSampleStyle
|
|
10
|
-
from ..runner.serialization import SerializedTestResult
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
14
|
-
class ServiceContext:
|
|
15
|
-
url: str = attr.ib() # pragma: no mutate
|
|
16
|
-
queue: Queue = attr.ib() # pragma: no mutate
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
20
|
-
class ExecutionContext:
|
|
21
|
-
"""Storage for the current context of the execution."""
|
|
22
|
-
|
|
23
|
-
hypothesis_settings: hypothesis.settings = attr.ib() # pragma: no mutate
|
|
24
|
-
hypothesis_output: List[str] = attr.ib(factory=list) # pragma: no mutate
|
|
25
|
-
workers_num: int = attr.ib(default=1) # pragma: no mutate
|
|
26
|
-
show_errors_tracebacks: bool = attr.ib(default=False) # pragma: no mutate
|
|
27
|
-
validate_schema: bool = attr.ib(default=True) # pragma: no mutate
|
|
28
|
-
operations_processed: int = attr.ib(default=0) # pragma: no mutate
|
|
29
|
-
# It is set in runtime, from a `Initialized` event
|
|
30
|
-
operations_count: Optional[int] = attr.ib(default=None) # pragma: no mutate
|
|
31
|
-
current_line_length: int = attr.ib(default=0) # pragma: no mutate
|
|
32
|
-
terminal_size: os.terminal_size = attr.ib(factory=shutil.get_terminal_size) # pragma: no mutate
|
|
33
|
-
results: List[SerializedTestResult] = attr.ib(factory=list) # pragma: no mutate
|
|
34
|
-
cassette_path: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
35
|
-
junit_xml_file: Optional[str] = attr.ib(default=None) # pragma: no mutate
|
|
36
|
-
verbosity: int = attr.ib(default=0) # pragma: no mutate
|
|
37
|
-
code_sample_style: CodeSampleStyle = attr.ib(default=CodeSampleStyle.default()) # pragma: no mutate
|
|
38
|
-
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,70 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
from typing import Any, List, NoReturn, Optional, Set, Tuple, 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 BaseCsvChoice(click.Choice):
|
|
21
|
-
def parse_value(self, value: str) -> Tuple[List[str], Set[str]]:
|
|
22
|
-
selected = [item for item in value.split(",") if item]
|
|
23
|
-
invalid_options = set(selected) - set(self.choices)
|
|
24
|
-
return selected, invalid_options
|
|
25
|
-
|
|
26
|
-
def fail_on_invalid_options(self, invalid_options: Set[str], selected: List[str]) -> NoReturn:
|
|
27
|
-
# Sort to keep the error output consistent with the passed values
|
|
28
|
-
sorted_options = ", ".join(sorted(invalid_options, key=selected.index))
|
|
29
|
-
available_options = ", ".join(self.choices)
|
|
30
|
-
self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class CsvEnumChoice(BaseCsvChoice):
|
|
34
|
-
def __init__(self, choices: Type[Enum]):
|
|
35
|
-
self.enum = choices
|
|
36
|
-
super().__init__(tuple(choices.__members__))
|
|
37
|
-
|
|
38
|
-
def convert( # type: ignore[return]
|
|
39
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
40
|
-
) -> List[Enum]:
|
|
41
|
-
selected, invalid_options = self.parse_value(value)
|
|
42
|
-
if not invalid_options and selected:
|
|
43
|
-
return [self.enum[item] for item in selected]
|
|
44
|
-
self.fail_on_invalid_options(invalid_options, selected)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class CsvChoice(BaseCsvChoice):
|
|
48
|
-
def convert(
|
|
49
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
50
|
-
) -> List[str]:
|
|
51
|
-
selected, invalid_options = self.parse_value(value)
|
|
52
|
-
if not invalid_options and selected:
|
|
53
|
-
return selected
|
|
54
|
-
self.fail_on_invalid_options(invalid_options, selected)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
not_set = NotSet()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class OptionalInt(click.types.IntRange):
|
|
61
|
-
def convert( # type: ignore
|
|
62
|
-
self, value: str, param: Optional[click.core.Parameter], ctx: Optional[click.core.Context]
|
|
63
|
-
) -> Union[int, NotSet]:
|
|
64
|
-
if value.lower() == "none":
|
|
65
|
-
return not_set
|
|
66
|
-
try:
|
|
67
|
-
int(value)
|
|
68
|
-
return super().convert(value, param, ctx)
|
|
69
|
-
except ValueError:
|
|
70
|
-
self.fail("%s is not a valid integer or None" % value, param, ctx)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from . import default, short
|