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/serializers.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import binascii
|
|
2
|
-
import os
|
|
3
|
-
from io import BytesIO
|
|
4
|
-
from typing import TYPE_CHECKING, Any, Callable, Collection, Dict, Generator, Optional, Type
|
|
5
|
-
|
|
6
|
-
import attr
|
|
7
|
-
import yaml
|
|
8
|
-
from typing_extensions import Protocol, runtime_checkable
|
|
9
|
-
|
|
10
|
-
from .utils import is_json_media_type, is_plain_text_media_type, parse_content_type
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from .models import Case
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from yaml import CSafeDumper as SafeDumper
|
|
18
|
-
except ImportError:
|
|
19
|
-
# pylint: disable=unused-import
|
|
20
|
-
from yaml import SafeDumper # type: ignore
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
SERIALIZERS: Dict[str, Type["Serializer"]] = {}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
27
|
-
class SerializerContext:
|
|
28
|
-
"""The context for serialization process.
|
|
29
|
-
|
|
30
|
-
:ivar Case case: Generated example that is being processed.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
case: "Case" = attr.ib() # pragma: no mutate
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@runtime_checkable
|
|
37
|
-
class Serializer(Protocol):
|
|
38
|
-
"""Transform generated data to a form supported by the transport layer.
|
|
39
|
-
|
|
40
|
-
For example, to handle multipart payloads, we need to serialize them differently for
|
|
41
|
-
`requests` and `werkzeug` transports.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def as_requests(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
|
|
45
|
-
raise NotImplementedError
|
|
46
|
-
|
|
47
|
-
def as_werkzeug(self, context: SerializerContext, payload: Any) -> Dict[str, Any]:
|
|
48
|
-
raise NotImplementedError
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def register(media_type: str, *, aliases: Collection[str] = ()) -> Callable[[Type[Serializer]], Type[Serializer]]:
|
|
52
|
-
"""Register a serializer for the given media type.
|
|
53
|
-
|
|
54
|
-
Schemathesis uses ``requests`` for regular network calls and ``werkzeug`` for WSGI applications. Your serializer
|
|
55
|
-
should have two methods, ``as_requests`` and ``as_werkzeug``, providing keyword arguments that Schemathesis will
|
|
56
|
-
pass to ``requests.request`` and ``werkzeug.Client.open`` respectively.
|
|
57
|
-
|
|
58
|
-
.. code-block:: python
|
|
59
|
-
|
|
60
|
-
@register("text/csv")
|
|
61
|
-
class CSVSerializer:
|
|
62
|
-
def as_requests(self, context, value):
|
|
63
|
-
return {"data": to_csv(value)}
|
|
64
|
-
|
|
65
|
-
def as_werkzeug(self, context, value):
|
|
66
|
-
return {"data": to_csv(value)}
|
|
67
|
-
|
|
68
|
-
The primary purpose of serializers is to transform data from its Python representation to the format suitable
|
|
69
|
-
for making an API call. The generated data structure depends on your schema, but its type matches
|
|
70
|
-
Python equivalents to the JSON Schema types.
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
def wrapper(serializer: Type[Serializer]) -> Type[Serializer]:
|
|
75
|
-
if not issubclass(serializer, Serializer):
|
|
76
|
-
raise TypeError(
|
|
77
|
-
f"`{serializer.__name__}` is not a valid serializer. "
|
|
78
|
-
f"Check `schemathesis.serializers.Serializer` documentation for examples."
|
|
79
|
-
)
|
|
80
|
-
SERIALIZERS[media_type] = serializer
|
|
81
|
-
for alias in aliases:
|
|
82
|
-
SERIALIZERS[alias] = serializer
|
|
83
|
-
return serializer
|
|
84
|
-
|
|
85
|
-
return wrapper
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def unregister(media_type: str) -> None:
|
|
89
|
-
"""Remove registered serializer for the given media type."""
|
|
90
|
-
del SERIALIZERS[media_type]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _to_json(value: Any) -> Dict[str, Any]:
|
|
94
|
-
if isinstance(value, bytes):
|
|
95
|
-
# Possible to get via explicit examples, e.g. `externalValue`
|
|
96
|
-
return {"data": value}
|
|
97
|
-
if value is None:
|
|
98
|
-
# If the body is `None`, then the app expects `null`, but `None` is also the default value for the `json`
|
|
99
|
-
# argument in `requests.request` and `werkzeug.Client.open` which makes these cases indistinguishable.
|
|
100
|
-
# Therefore we explicitly create such payload
|
|
101
|
-
return {"data": b"null"}
|
|
102
|
-
return {"json": value}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@register("application/json")
|
|
106
|
-
class JSONSerializer:
|
|
107
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
108
|
-
return _to_json(value)
|
|
109
|
-
|
|
110
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
111
|
-
return _to_json(value)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _to_yaml(value: Any) -> Dict[str, Any]:
|
|
115
|
-
if isinstance(value, bytes):
|
|
116
|
-
return {"data": value}
|
|
117
|
-
return {"data": yaml.dump(value, Dumper=SafeDumper)}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@register("text/yaml", aliases=("text/x-yaml", "application/x-yaml", "text/vnd.yaml"))
|
|
121
|
-
class YAMLSerializer:
|
|
122
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
123
|
-
return _to_yaml(value)
|
|
124
|
-
|
|
125
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
126
|
-
return _to_yaml(value)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _should_coerce_to_bytes(item: Any) -> bool:
|
|
130
|
-
"""Whether the item should be converted to bytes."""
|
|
131
|
-
# These types are OK in forms, others should be coerced to bytes
|
|
132
|
-
return not isinstance(item, (bytes, str, int))
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _prepare_form_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
|
-
"""Make the generated data suitable for sending as multipart.
|
|
137
|
-
|
|
138
|
-
If the schema is loose, Schemathesis can generate data that can't be sent as multipart. In these cases,
|
|
139
|
-
we convert it to bytes and send it as-is, ignoring any conversion errors.
|
|
140
|
-
|
|
141
|
-
NOTE. This behavior might change in the future.
|
|
142
|
-
"""
|
|
143
|
-
for name, value in data.items():
|
|
144
|
-
if isinstance(value, list):
|
|
145
|
-
data[name] = [_to_bytes(item) if _should_coerce_to_bytes(item) else item for item in value]
|
|
146
|
-
elif _should_coerce_to_bytes(value):
|
|
147
|
-
data[name] = _to_bytes(value)
|
|
148
|
-
return data
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _to_bytes(value: Any) -> bytes:
|
|
152
|
-
"""Convert the input value to bytes and ignore any conversion errors."""
|
|
153
|
-
if isinstance(value, bytes):
|
|
154
|
-
return value
|
|
155
|
-
return str(value).encode(errors="ignore")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def choose_boundary() -> str:
|
|
159
|
-
"""Random boundary name."""
|
|
160
|
-
return binascii.hexlify(os.urandom(16)).decode("ascii")
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def _encode_multipart(value: Any, boundary: str) -> bytes:
|
|
164
|
-
"""Encode any value as multipart.
|
|
165
|
-
|
|
166
|
-
NOTE. It doesn't aim to be 100% correct multipart payload, but rather a way to send data which is not intended to
|
|
167
|
-
be used as multipart, in cases when the API schema dictates so.
|
|
168
|
-
"""
|
|
169
|
-
# For such cases we stringify the value and wrap it to a randomly-generated boundary
|
|
170
|
-
body = BytesIO()
|
|
171
|
-
body.write(f"--{boundary}\r\n".encode())
|
|
172
|
-
body.write(str(value).encode())
|
|
173
|
-
body.write(f"--{boundary}--\r\n".encode("latin-1"))
|
|
174
|
-
return body.getvalue()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
@register("multipart/form-data")
|
|
178
|
-
class MultipartSerializer:
|
|
179
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
180
|
-
if isinstance(value, bytes):
|
|
181
|
-
return {"data": value}
|
|
182
|
-
if isinstance(value, dict):
|
|
183
|
-
multipart = _prepare_form_data(value)
|
|
184
|
-
files, data = context.case.operation.prepare_multipart(multipart)
|
|
185
|
-
return {"files": files, "data": data}
|
|
186
|
-
# Uncommon schema. For example - `{"type": "string"}`
|
|
187
|
-
boundary = choose_boundary()
|
|
188
|
-
raw_data = _encode_multipart(value, boundary)
|
|
189
|
-
content_type = f"multipart/form-data; boundary={boundary}"
|
|
190
|
-
return {"data": raw_data, "headers": {"Content-Type": content_type}}
|
|
191
|
-
|
|
192
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
193
|
-
return {"data": value}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@register("application/x-www-form-urlencoded")
|
|
197
|
-
class URLEncodedFormSerializer:
|
|
198
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
199
|
-
return {"data": value}
|
|
200
|
-
|
|
201
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
202
|
-
return {"data": value}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@register("text/plain")
|
|
206
|
-
class TextSerializer:
|
|
207
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
208
|
-
if isinstance(value, bytes):
|
|
209
|
-
return {"data": value}
|
|
210
|
-
return {"data": str(value).encode("utf8")}
|
|
211
|
-
|
|
212
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
213
|
-
if isinstance(value, bytes):
|
|
214
|
-
return {"data": value}
|
|
215
|
-
return {"data": str(value)}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
@register("application/octet-stream")
|
|
219
|
-
class OctetStreamSerializer:
|
|
220
|
-
def as_requests(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
221
|
-
return {"data": _to_bytes(value)}
|
|
222
|
-
|
|
223
|
-
def as_werkzeug(self, context: SerializerContext, value: Any) -> Dict[str, Any]:
|
|
224
|
-
return {"data": _to_bytes(value)}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def get_matching_media_types(media_type: str) -> Generator[str, None, None]:
|
|
228
|
-
"""Get all registered media types matching the given media type."""
|
|
229
|
-
if media_type == "*/*":
|
|
230
|
-
# Shortcut to avoid comparing all values
|
|
231
|
-
yield from iter(SERIALIZERS)
|
|
232
|
-
else:
|
|
233
|
-
main, sub = parse_content_type(media_type)
|
|
234
|
-
if main == "application" and (sub == "json" or sub.endswith("+json")):
|
|
235
|
-
yield media_type
|
|
236
|
-
else:
|
|
237
|
-
for registered_media_type in SERIALIZERS:
|
|
238
|
-
target_main, target_sub = parse_content_type(registered_media_type)
|
|
239
|
-
if main in ("*", target_main) and sub in ("*", target_sub):
|
|
240
|
-
yield registered_media_type
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def get_first_matching_media_type(media_type: str) -> Optional[str]:
|
|
244
|
-
return next(get_matching_media_types(media_type), None)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def get(media_type: str) -> Optional[Type[Serializer]]:
|
|
248
|
-
"""Get an appropriate serializer for the given media type."""
|
|
249
|
-
if is_json_media_type(media_type):
|
|
250
|
-
media_type = "application/json"
|
|
251
|
-
if is_plain_text_media_type(media_type):
|
|
252
|
-
media_type = "text/plain"
|
|
253
|
-
return SERIALIZERS.get(media_type)
|
schemathesis/service/__init__.py
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from . import auth, hosts
|
|
2
|
-
from .client import ServiceClient
|
|
3
|
-
from .constants import (
|
|
4
|
-
DEFAULT_HOSTNAME,
|
|
5
|
-
DEFAULT_HOSTS_PATH,
|
|
6
|
-
DEFAULT_PROTOCOL,
|
|
7
|
-
DEFAULT_URL,
|
|
8
|
-
HOSTNAME_ENV_VAR,
|
|
9
|
-
HOSTS_PATH_ENV_VAR,
|
|
10
|
-
PROTOCOL_ENV_VAR,
|
|
11
|
-
TOKEN_ENV_VAR,
|
|
12
|
-
URL_ENV_VAR,
|
|
13
|
-
WORKER_CHECK_PERIOD,
|
|
14
|
-
WORKER_FINISH_TIMEOUT,
|
|
15
|
-
)
|
|
16
|
-
from .events import Completed, Error, Event, Timeout
|
|
17
|
-
from .handler import ServiceReporter
|
|
18
|
-
from .models import TestRun
|
schemathesis/service/auth.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from . import metadata
|
|
2
|
-
from .client import ServiceClient
|
|
3
|
-
from .constants import DEFAULT_HOSTNAME, DEFAULT_PROTOCOL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def login(token: str, hostname: str = DEFAULT_HOSTNAME, protocol: str = DEFAULT_PROTOCOL, verify: bool = True) -> str:
|
|
7
|
-
"""Make a login request to SaaS."""
|
|
8
|
-
client = ServiceClient(f"{protocol}://{hostname}", token, verify=verify)
|
|
9
|
-
response = client.cli_login(metadata=metadata.Metadata())
|
|
10
|
-
return response.username
|
schemathesis/service/client.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from typing import Any, Dict
|
|
2
|
-
from urllib.parse import urljoin
|
|
3
|
-
|
|
4
|
-
import attr
|
|
5
|
-
import requests
|
|
6
|
-
from requests.adapters import HTTPAdapter, Retry
|
|
7
|
-
|
|
8
|
-
from ..constants import USER_AGENT
|
|
9
|
-
from .constants import REQUEST_TIMEOUT
|
|
10
|
-
from .metadata import Metadata
|
|
11
|
-
from .models import ApiConfig, AuthResponse, TestRun
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class ServiceClient(requests.Session):
|
|
15
|
-
"""A more convenient session to send requests to Schemathesis.io."""
|
|
16
|
-
|
|
17
|
-
def __init__(self, base_url: str, token: str, *, timeout: int = REQUEST_TIMEOUT, verify: bool = True):
|
|
18
|
-
super().__init__()
|
|
19
|
-
self.timeout = timeout
|
|
20
|
-
self.verify = verify
|
|
21
|
-
self.base_url = base_url
|
|
22
|
-
self.headers.update({"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT})
|
|
23
|
-
# Automatically check responses for 4XX and 5XX
|
|
24
|
-
self.hooks["response"] = [lambda response, *args, **kwargs: response.raise_for_status()] # type: ignore
|
|
25
|
-
adapter = HTTPAdapter(max_retries=Retry(5))
|
|
26
|
-
self.mount("https://", adapter)
|
|
27
|
-
self.mount("http://", adapter)
|
|
28
|
-
|
|
29
|
-
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: # type: ignore
|
|
30
|
-
kwargs.setdefault("timeout", self.timeout)
|
|
31
|
-
kwargs.setdefault("verify", self.verify)
|
|
32
|
-
# All requests will be done against the base url
|
|
33
|
-
url = urljoin(self.base_url, url)
|
|
34
|
-
return super().request(method, url, *args, **kwargs)
|
|
35
|
-
|
|
36
|
-
def create_test_run(self, api_slug: str) -> TestRun:
|
|
37
|
-
"""Create a new test run on the Schemathesis.io side."""
|
|
38
|
-
response = self.post("/runs/", json={"api_slug": api_slug})
|
|
39
|
-
data = response.json()
|
|
40
|
-
config = data["config"]
|
|
41
|
-
return TestRun(
|
|
42
|
-
run_id=data["run_id"],
|
|
43
|
-
short_url=data["short_url"],
|
|
44
|
-
config=ApiConfig(location=config["location"], base_url=config["base_url"]),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
def finish_test_run(self, run_id: str) -> None:
|
|
48
|
-
"""Finish a test run on the Schemathesis.io side.
|
|
49
|
-
|
|
50
|
-
Only needed in corner cases when Schemathesis CLI fails with an internal error in itself, not in the runner.
|
|
51
|
-
"""
|
|
52
|
-
self.post(f"/runs/{run_id}/finish/")
|
|
53
|
-
|
|
54
|
-
def send_event(self, run_id: str, data: Dict[str, Any]) -> None:
|
|
55
|
-
"""Send a single event to Schemathesis.io."""
|
|
56
|
-
self.post(f"/runs/{run_id}/events/", json=data)
|
|
57
|
-
|
|
58
|
-
def cli_login(self, metadata: Metadata) -> AuthResponse:
|
|
59
|
-
"""Send a login request."""
|
|
60
|
-
response = self.post("/auth/cli/login/", json={"metadata": attr.asdict(metadata)})
|
|
61
|
-
data = response.json()
|
|
62
|
-
return AuthResponse(username=data["username"])
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
|
|
3
|
-
DEFAULT_HOSTNAME = "api.schemathesis.io"
|
|
4
|
-
# The main Schemathesis.io API address
|
|
5
|
-
DEFAULT_URL = f"https://{DEFAULT_HOSTNAME}/"
|
|
6
|
-
DEFAULT_PROTOCOL = "https"
|
|
7
|
-
# A sentinel to signal the worker thread to stop
|
|
8
|
-
STOP_MARKER = object()
|
|
9
|
-
# Timeout for each API call
|
|
10
|
-
REQUEST_TIMEOUT = 1
|
|
11
|
-
# The time the main thread will wait for the worker thread to finish its job before exiting
|
|
12
|
-
WORKER_FINISH_TIMEOUT = 10.0
|
|
13
|
-
# A period between checking the worker thread for events
|
|
14
|
-
WORKER_CHECK_PERIOD = 0.005
|
|
15
|
-
# Wait until the worker terminates
|
|
16
|
-
WORKER_JOIN_TIMEOUT = 10
|
|
17
|
-
# Version of the hosts file format
|
|
18
|
-
HOSTS_FORMAT_VERSION = "0.1"
|
|
19
|
-
# Default path to the hosts file
|
|
20
|
-
DEFAULT_HOSTS_PATH = pathlib.Path.home() / ".config/schemathesis/hosts.toml"
|
|
21
|
-
TOKEN_ENV_VAR = "SCHEMATHESIS_TOKEN"
|
|
22
|
-
HOSTNAME_ENV_VAR = "SCHEMATHESIS_HOSTNAME"
|
|
23
|
-
PROTOCOL_ENV_VAR = "SCHEMATHESIS_PROTOCOL"
|
|
24
|
-
HOSTS_PATH_ENV_VAR = "SCHEMATHESIS_HOSTS_PATH"
|
|
25
|
-
URL_ENV_VAR = "SCHEMATHESIS_URL"
|
schemathesis/service/events.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import attr
|
|
2
|
-
|
|
3
|
-
from ..utils import format_exception
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Event:
|
|
7
|
-
"""Signalling events coming from the Schemathesis.io worker.
|
|
8
|
-
|
|
9
|
-
The purpose is to communicate with the thread that writes to stdout.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
@property
|
|
13
|
-
def name(self) -> str:
|
|
14
|
-
return self.__class__.__name__.upper()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@attr.s(slots=True)
|
|
18
|
-
class Completed(Event):
|
|
19
|
-
"""The handler finished successfully."""
|
|
20
|
-
|
|
21
|
-
short_url: str = attr.ib()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@attr.s(slots=True)
|
|
25
|
-
class Error(Event):
|
|
26
|
-
"""Internal error inside the Schemathesis.io handler."""
|
|
27
|
-
|
|
28
|
-
exception: Exception = attr.ib()
|
|
29
|
-
|
|
30
|
-
def get_message(self, include_traceback: bool = False) -> str:
|
|
31
|
-
return format_exception(self.exception, include_traceback=include_traceback)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@attr.s(slots=True)
|
|
35
|
-
class Timeout(Event):
|
|
36
|
-
"""The handler did not finish its work in time.
|
|
37
|
-
|
|
38
|
-
This event is not created in the handler itself, but rather in the main thread code to uniform the processing.
|
|
39
|
-
"""
|
schemathesis/service/handler.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from queue import Queue
|
|
3
|
-
|
|
4
|
-
import attr
|
|
5
|
-
|
|
6
|
-
from ..cli.context import ExecutionContext
|
|
7
|
-
from ..cli.handlers import EventHandler
|
|
8
|
-
from ..runner import events
|
|
9
|
-
from . import worker
|
|
10
|
-
from .client import ServiceClient
|
|
11
|
-
from .constants import STOP_MARKER, WORKER_JOIN_TIMEOUT
|
|
12
|
-
from .models import TestRun
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@attr.s(slots=True) # pragma: no mutate
|
|
16
|
-
class ServiceReporter(EventHandler):
|
|
17
|
-
"""Send events to the worker that communicates with Schemathesis.io."""
|
|
18
|
-
|
|
19
|
-
client: ServiceClient = attr.ib() # pragma: no mutate
|
|
20
|
-
test_run: TestRun = attr.ib() # pragma: no mutate
|
|
21
|
-
out_queue: Queue = attr.ib() # pragma: no mutate
|
|
22
|
-
in_queue: Queue = attr.ib(factory=Queue) # pragma: no mutate
|
|
23
|
-
worker: threading.Thread = attr.ib(init=False) # pragma: no mutate
|
|
24
|
-
|
|
25
|
-
def __attrs_post_init__(self) -> None:
|
|
26
|
-
# A worker thread, that does all the work concurrently
|
|
27
|
-
self.worker = threading.Thread(
|
|
28
|
-
target=worker.start,
|
|
29
|
-
kwargs={
|
|
30
|
-
"client": self.client,
|
|
31
|
-
"test_run": self.test_run,
|
|
32
|
-
"in_queue": self.in_queue,
|
|
33
|
-
"out_queue": self.out_queue,
|
|
34
|
-
},
|
|
35
|
-
)
|
|
36
|
-
self.worker.start()
|
|
37
|
-
|
|
38
|
-
def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
|
|
39
|
-
self.in_queue.put(event)
|
|
40
|
-
|
|
41
|
-
def shutdown(self) -> None:
|
|
42
|
-
self._stop_worker()
|
|
43
|
-
|
|
44
|
-
def _stop_worker(self) -> None:
|
|
45
|
-
self.in_queue.put(STOP_MARKER)
|
|
46
|
-
self.worker.join(WORKER_JOIN_TIMEOUT)
|
schemathesis/service/hosts.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""Work with stored auth data."""
|
|
2
|
-
import enum
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
5
|
-
|
|
6
|
-
import tomli
|
|
7
|
-
import tomli_w
|
|
8
|
-
|
|
9
|
-
from ..types import PathLike
|
|
10
|
-
from .constants import DEFAULT_HOSTNAME, DEFAULT_HOSTS_PATH, HOSTS_FORMAT_VERSION
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def store(token: str, hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> None:
|
|
14
|
-
"""Store a new token for a host."""
|
|
15
|
-
# Don't use any file-based locking for simplicity
|
|
16
|
-
hosts = load(hosts_file)
|
|
17
|
-
hosts[hostname] = {"version": HOSTS_FORMAT_VERSION, "token": token}
|
|
18
|
-
_dump_hosts(hosts_file, hosts)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def load(path: PathLike) -> Dict[str, Any]:
|
|
22
|
-
"""Load the given hosts file.
|
|
23
|
-
|
|
24
|
-
Return an empty dict if it doesn't exist.
|
|
25
|
-
"""
|
|
26
|
-
try:
|
|
27
|
-
with open(path, "rb") as fd:
|
|
28
|
-
return tomli.load(fd)
|
|
29
|
-
except FileNotFoundError:
|
|
30
|
-
# Try to create the parent dir - it could be the first run, when the config dir doesn't exist yet
|
|
31
|
-
try:
|
|
32
|
-
Path(path).parent.mkdir(mode=0o755, parents=True, exist_ok=True)
|
|
33
|
-
except OSError:
|
|
34
|
-
# Ignore permission errors, etc
|
|
35
|
-
pass
|
|
36
|
-
return {}
|
|
37
|
-
except tomli.TOMLDecodeError:
|
|
38
|
-
return {}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@enum.unique
|
|
42
|
-
class RemoveAuth(enum.Enum):
|
|
43
|
-
success = 1
|
|
44
|
-
no_match = 2
|
|
45
|
-
no_hosts = 3
|
|
46
|
-
error = 4
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def remove(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> RemoveAuth:
|
|
50
|
-
"""Remove authentication for a Schemathesis.io host."""
|
|
51
|
-
try:
|
|
52
|
-
with open(hosts_file, "rb") as fd:
|
|
53
|
-
hosts = tomli.load(fd)
|
|
54
|
-
try:
|
|
55
|
-
hosts.pop(hostname)
|
|
56
|
-
_dump_hosts(hosts_file, hosts)
|
|
57
|
-
return RemoveAuth.success
|
|
58
|
-
except KeyError:
|
|
59
|
-
return RemoveAuth.no_match
|
|
60
|
-
except FileNotFoundError:
|
|
61
|
-
return RemoveAuth.no_hosts
|
|
62
|
-
except tomli.TOMLDecodeError:
|
|
63
|
-
return RemoveAuth.error
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def get_token(hostname: str = DEFAULT_HOSTNAME, hosts_file: PathLike = DEFAULT_HOSTS_PATH) -> Optional[str]:
|
|
67
|
-
"""Load a token for a host."""
|
|
68
|
-
return load(hosts_file).get(hostname, {}).get("token")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _dump_hosts(path: PathLike, hosts: Dict[str, Any]) -> None:
|
|
72
|
-
"""Write hosts data to a file."""
|
|
73
|
-
with open(path, "wb") as fd:
|
|
74
|
-
tomli_w.dump(hosts, fd)
|
schemathesis/service/metadata.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"""Useful info to collect from CLI usage."""
|
|
2
|
-
import platform
|
|
3
|
-
|
|
4
|
-
import attr
|
|
5
|
-
|
|
6
|
-
from ..constants import __version__
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@attr.s(slots=True)
|
|
10
|
-
class PlatformMetadata:
|
|
11
|
-
# System / OS name, e.g. "Linux" or "Windows".
|
|
12
|
-
system: str = attr.ib(factory=platform.system)
|
|
13
|
-
# System release, e.g. "5.14" or "NT".
|
|
14
|
-
release: str = attr.ib(factory=platform.release)
|
|
15
|
-
# Machine type, e.g. "i386".
|
|
16
|
-
machine: str = attr.ib(factory=platform.machine)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@attr.s(slots=True)
|
|
20
|
-
class InterpreterMetadata:
|
|
21
|
-
# The Python version as "major.minor.patch".
|
|
22
|
-
version: str = attr.ib(factory=platform.python_version)
|
|
23
|
-
# Python implementation, e.g. "CPython" or "PyPy".
|
|
24
|
-
implementation: str = attr.ib(factory=platform.python_implementation)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@attr.s(slots=True)
|
|
28
|
-
class CliMetadata:
|
|
29
|
-
# Schemathesis package version.
|
|
30
|
-
version: str = attr.ib(default=__version__)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@attr.s(slots=True)
|
|
34
|
-
class Metadata:
|
|
35
|
-
"""CLI environment metadata."""
|
|
36
|
-
|
|
37
|
-
# Information about the host platform.
|
|
38
|
-
platform: PlatformMetadata = attr.ib(factory=PlatformMetadata)
|
|
39
|
-
# Python interpreter info.
|
|
40
|
-
interpreter: InterpreterMetadata = attr.ib(factory=InterpreterMetadata)
|
|
41
|
-
# CLI info itself.
|
|
42
|
-
cli: CliMetadata = attr.ib(factory=CliMetadata)
|
schemathesis/service/models.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
import attr
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@attr.s(slots=True)
|
|
7
|
-
class ApiConfig:
|
|
8
|
-
location: str = attr.ib()
|
|
9
|
-
base_url: Optional[str] = attr.ib()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@attr.s(slots=True)
|
|
13
|
-
class TestRun:
|
|
14
|
-
run_id: str = attr.ib()
|
|
15
|
-
short_url: str = attr.ib()
|
|
16
|
-
config: ApiConfig = attr.ib()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@attr.s(slots=True)
|
|
20
|
-
class AuthResponse:
|
|
21
|
-
username: str = attr.ib()
|