schemathesis 3.29.1__py3-none-any.whl → 3.30.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 +3 -3
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +1 -3
- schemathesis/_hypothesis.py +6 -0
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +1 -0
- schemathesis/_rate_limiter.py +2 -1
- schemathesis/_xml.py +1 -0
- schemathesis/auths.py +4 -2
- schemathesis/checks.py +8 -5
- schemathesis/cli/__init__.py +8 -1
- schemathesis/cli/callbacks.py +3 -4
- schemathesis/cli/cassettes.py +6 -4
- schemathesis/cli/constants.py +2 -0
- schemathesis/cli/context.py +3 -0
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +1 -1
- schemathesis/cli/options.py +1 -0
- schemathesis/cli/output/default.py +50 -22
- schemathesis/cli/output/short.py +21 -10
- schemathesis/cli/sanitization.py +1 -0
- schemathesis/code_samples.py +1 -0
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +2 -1
- schemathesis/exceptions.py +40 -26
- schemathesis/experimental/__init__.py +14 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +13 -24
- schemathesis/failures.py +32 -3
- schemathesis/filters.py +2 -1
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +2 -1
- schemathesis/hooks.py +3 -1
- schemathesis/internal/copy.py +19 -3
- schemathesis/internal/deprecation.py +1 -1
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +1 -0
- schemathesis/lazy.py +3 -2
- schemathesis/loaders.py +4 -2
- schemathesis/models.py +20 -5
- schemathesis/parameters.py +1 -0
- schemathesis/runner/__init__.py +1 -1
- schemathesis/runner/events.py +21 -4
- schemathesis/runner/impl/core.py +61 -33
- schemathesis/runner/impl/solo.py +2 -1
- schemathesis/runner/impl/threadpool.py +4 -0
- schemathesis/runner/probes.py +1 -1
- schemathesis/runner/serialization.py +1 -1
- schemathesis/sanitization.py +2 -0
- schemathesis/schemas.py +1 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +7 -7
- schemathesis/service/events.py +2 -1
- schemathesis/service/extensions.py +5 -5
- schemathesis/service/hosts.py +1 -0
- schemathesis/service/metadata.py +2 -1
- schemathesis/service/models.py +2 -1
- schemathesis/service/report.py +3 -3
- schemathesis/service/serialization.py +54 -23
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +1 -1
- schemathesis/specs/graphql/loaders.py +1 -1
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +7 -7
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +17 -11
- schemathesis/specs/openapi/checks.py +102 -9
- schemathesis/specs/openapi/converter.py +2 -1
- schemathesis/specs/openapi/definitions.py +2 -1
- schemathesis/specs/openapi/examples.py +7 -9
- schemathesis/specs/openapi/expressions/__init__.py +29 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +19 -18
- schemathesis/specs/openapi/expressions/nodes.py +24 -4
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/filters.py +1 -0
- schemathesis/specs/openapi/links.py +35 -7
- schemathesis/specs/openapi/loaders.py +13 -11
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -0
- schemathesis/specs/openapi/parameters.py +1 -0
- schemathesis/specs/openapi/schemas.py +27 -38
- schemathesis/specs/openapi/security.py +1 -0
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +159 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +13 -0
- schemathesis/specs/openapi/utils.py +1 -0
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +4 -2
- schemathesis/stateful/config.py +66 -0
- schemathesis/stateful/context.py +93 -0
- schemathesis/stateful/events.py +209 -0
- schemathesis/stateful/runner.py +233 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +39 -22
- schemathesis/stateful/statistic.py +20 -0
- schemathesis/stateful/validation.py +66 -0
- schemathesis/targets.py +1 -0
- schemathesis/throttling.py +23 -3
- schemathesis/transports/__init__.py +28 -10
- schemathesis/transports/auth.py +1 -0
- schemathesis/transports/content_types.py +1 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +6 -4
- schemathesis/types.py +1 -0
- schemathesis/utils.py +1 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/METADATA +1 -1
- schemathesis-3.30.0.dist-info/RECORD +150 -0
- schemathesis/specs/openapi/stateful/links.py +0 -94
- schemathesis-3.29.1.dist-info/RECORD +0 -141
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/WHEEL +0 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/licenses/LICENSE +0 -0
schemathesis/runner/impl/core.py
CHANGED
|
@@ -21,9 +21,7 @@ from jsonschema.exceptions import SchemaError as JsonSchemaError
|
|
|
21
21
|
from jsonschema.exceptions import ValidationError
|
|
22
22
|
from requests.auth import HTTPDigestAuth, _basic_auth_str
|
|
23
23
|
|
|
24
|
-
from
|
|
25
|
-
|
|
26
|
-
from ... import failures, hooks
|
|
24
|
+
from ... import experimental, failures, hooks
|
|
27
25
|
from ..._compat import MultipleFailures
|
|
28
26
|
from ..._hypothesis import (
|
|
29
27
|
get_invalid_example_headers_mark,
|
|
@@ -65,7 +63,10 @@ from ...service import extensions
|
|
|
65
63
|
from ...service.models import AnalysisResult, AnalysisSuccess
|
|
66
64
|
from ...specs.openapi import formats
|
|
67
65
|
from ...stateful import Feedback, Stateful
|
|
66
|
+
from ...stateful import events as stateful_events
|
|
67
|
+
from ...stateful import runner as stateful_runner
|
|
68
68
|
from ...targets import Target, TargetContext
|
|
69
|
+
from ...transports import RequestsTransport, prepare_timeout
|
|
69
70
|
from ...types import RawAuth, RequestCert
|
|
70
71
|
from ...utils import capture_hypothesis_output
|
|
71
72
|
from .. import probes
|
|
@@ -191,7 +192,9 @@ class BaseRunner:
|
|
|
191
192
|
return
|
|
192
193
|
|
|
193
194
|
try:
|
|
194
|
-
|
|
195
|
+
if not experimental.STATEFUL_ONLY.is_enabled:
|
|
196
|
+
yield from self._execute(results, stop_event)
|
|
197
|
+
yield from self._run_stateful_tests(results)
|
|
195
198
|
except KeyboardInterrupt:
|
|
196
199
|
yield events.Interrupted()
|
|
197
200
|
|
|
@@ -211,6 +214,32 @@ class BaseRunner:
|
|
|
211
214
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
212
215
|
raise NotImplementedError
|
|
213
216
|
|
|
217
|
+
def _run_stateful_tests(self, results: TestResultSet) -> Generator[events.ExecutionEvent, None, None]:
|
|
218
|
+
# Run new-style stateful tests
|
|
219
|
+
if self.stateful is not None and experimental.STATEFUL_TEST_RUNNER.is_enabled:
|
|
220
|
+
result = TestResult(
|
|
221
|
+
method="",
|
|
222
|
+
path="",
|
|
223
|
+
verbose_name="Stateful tests",
|
|
224
|
+
data_generation_method=self.schema.data_generation_methods,
|
|
225
|
+
)
|
|
226
|
+
config = stateful_runner.StatefulTestRunnerConfig(
|
|
227
|
+
checks=tuple(self.checks),
|
|
228
|
+
headers=self.headers or {},
|
|
229
|
+
hypothesis_settings=self.hypothesis_settings,
|
|
230
|
+
exit_first=self.exit_first,
|
|
231
|
+
request_timeout=self.request_timeout,
|
|
232
|
+
)
|
|
233
|
+
state_machine = self.schema.as_state_machine()
|
|
234
|
+
runner = state_machine.runner(config=config)
|
|
235
|
+
for stateful_event in runner.execute():
|
|
236
|
+
if isinstance(stateful_event, stateful_events.SuiteFinished):
|
|
237
|
+
for failure in stateful_event.failures:
|
|
238
|
+
result.checks.append(failure)
|
|
239
|
+
yield events.StatefulEvent(data=stateful_event)
|
|
240
|
+
results.append(result)
|
|
241
|
+
yield events.AfterStatefulExecution(result=SerializedTestResult.from_test_result(result))
|
|
242
|
+
|
|
214
243
|
def _run_tests(
|
|
215
244
|
self,
|
|
216
245
|
maker: Callable,
|
|
@@ -246,7 +275,10 @@ class BaseRunner:
|
|
|
246
275
|
):
|
|
247
276
|
if isinstance(result, Ok):
|
|
248
277
|
operation, test = result.ok()
|
|
249
|
-
|
|
278
|
+
if self.stateful is not None and not experimental.STATEFUL_TEST_RUNNER.is_enabled:
|
|
279
|
+
feedback = Feedback(self.stateful, operation)
|
|
280
|
+
else:
|
|
281
|
+
feedback = None
|
|
250
282
|
# Track whether `BeforeExecution` was already emitted.
|
|
251
283
|
# Schema error may happen before / after `BeforeExecution`, but it should be emitted only once
|
|
252
284
|
# and the `AfterExecution` event should have the same correlation id as previous `BeforeExecution`
|
|
@@ -268,17 +300,18 @@ class BaseRunner:
|
|
|
268
300
|
if isinstance(event, events.Interrupted):
|
|
269
301
|
return
|
|
270
302
|
# Additional tests, generated via the `feedback` instance
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
303
|
+
if feedback is not None:
|
|
304
|
+
yield from self._run_tests(
|
|
305
|
+
feedback.get_stateful_tests,
|
|
306
|
+
template,
|
|
307
|
+
settings=settings,
|
|
308
|
+
generation_config=generation_config,
|
|
309
|
+
seed=seed,
|
|
310
|
+
recursion_level=recursion_level + 1,
|
|
311
|
+
results=results,
|
|
312
|
+
headers=headers,
|
|
313
|
+
**kwargs,
|
|
314
|
+
)
|
|
282
315
|
except OperationSchemaError as exc:
|
|
283
316
|
yield from handle_schema_error(
|
|
284
317
|
exc,
|
|
@@ -788,7 +821,7 @@ def network_test(
|
|
|
788
821
|
request_cert: RequestCert | None,
|
|
789
822
|
store_interactions: bool,
|
|
790
823
|
headers: dict[str, Any] | None,
|
|
791
|
-
feedback: Feedback,
|
|
824
|
+
feedback: Feedback | None,
|
|
792
825
|
max_response_time: int | None,
|
|
793
826
|
data_generation_methods: list[DataGenerationMethod],
|
|
794
827
|
dry_run: bool,
|
|
@@ -830,7 +863,7 @@ def _network_test(
|
|
|
830
863
|
timeout: float | None,
|
|
831
864
|
store_interactions: bool,
|
|
832
865
|
headers: dict[str, Any] | None,
|
|
833
|
-
feedback: Feedback,
|
|
866
|
+
feedback: Feedback | None,
|
|
834
867
|
request_tls_verify: bool,
|
|
835
868
|
request_proxy: str | None,
|
|
836
869
|
request_cert: RequestCert | None,
|
|
@@ -877,7 +910,8 @@ def _network_test(
|
|
|
877
910
|
status = Status.failure
|
|
878
911
|
raise
|
|
879
912
|
finally:
|
|
880
|
-
feedback
|
|
913
|
+
if feedback is not None:
|
|
914
|
+
feedback.add_test_case(case, response)
|
|
881
915
|
if store_interactions:
|
|
882
916
|
result.store_requests_response(case, response, status, check_results)
|
|
883
917
|
return response
|
|
@@ -891,14 +925,6 @@ def get_session(auth: HTTPDigestAuth | RawAuth | None = None) -> Generator[reque
|
|
|
891
925
|
yield session
|
|
892
926
|
|
|
893
927
|
|
|
894
|
-
def prepare_timeout(timeout: int | None) -> float | None:
|
|
895
|
-
"""Request timeout is in milliseconds, but `requests` uses seconds."""
|
|
896
|
-
output: int | float | None = timeout
|
|
897
|
-
if timeout is not None:
|
|
898
|
-
output = timeout / 1000
|
|
899
|
-
return output
|
|
900
|
-
|
|
901
|
-
|
|
902
928
|
def wsgi_test(
|
|
903
929
|
case: Case,
|
|
904
930
|
checks: Iterable[CheckFunction],
|
|
@@ -908,7 +934,7 @@ def wsgi_test(
|
|
|
908
934
|
auth_type: str | None,
|
|
909
935
|
headers: dict[str, Any] | None,
|
|
910
936
|
store_interactions: bool,
|
|
911
|
-
feedback: Feedback,
|
|
937
|
+
feedback: Feedback | None,
|
|
912
938
|
max_response_time: int | None,
|
|
913
939
|
data_generation_methods: list[DataGenerationMethod],
|
|
914
940
|
dry_run: bool,
|
|
@@ -939,7 +965,7 @@ def _wsgi_test(
|
|
|
939
965
|
result: TestResult,
|
|
940
966
|
headers: dict[str, Any],
|
|
941
967
|
store_interactions: bool,
|
|
942
|
-
feedback: Feedback,
|
|
968
|
+
feedback: Feedback | None,
|
|
943
969
|
max_response_time: int | None,
|
|
944
970
|
) -> WSGIResponse:
|
|
945
971
|
from ...transports.responses import WSGIResponse
|
|
@@ -968,7 +994,8 @@ def _wsgi_test(
|
|
|
968
994
|
status = Status.failure
|
|
969
995
|
raise
|
|
970
996
|
finally:
|
|
971
|
-
feedback
|
|
997
|
+
if feedback is not None:
|
|
998
|
+
feedback.add_test_case(case, response)
|
|
972
999
|
if store_interactions:
|
|
973
1000
|
result.store_wsgi_response(case, response, headers, response.elapsed.total_seconds(), status, check_results)
|
|
974
1001
|
return response
|
|
@@ -1001,7 +1028,7 @@ def asgi_test(
|
|
|
1001
1028
|
result: TestResult,
|
|
1002
1029
|
store_interactions: bool,
|
|
1003
1030
|
headers: dict[str, Any] | None,
|
|
1004
|
-
feedback: Feedback,
|
|
1031
|
+
feedback: Feedback | None,
|
|
1005
1032
|
max_response_time: int | None,
|
|
1006
1033
|
data_generation_methods: list[DataGenerationMethod],
|
|
1007
1034
|
dry_run: bool,
|
|
@@ -1034,7 +1061,7 @@ def _asgi_test(
|
|
|
1034
1061
|
result: TestResult,
|
|
1035
1062
|
store_interactions: bool,
|
|
1036
1063
|
headers: dict[str, Any] | None,
|
|
1037
|
-
feedback: Feedback,
|
|
1064
|
+
feedback: Feedback | None,
|
|
1038
1065
|
max_response_time: int | None,
|
|
1039
1066
|
) -> requests.Response:
|
|
1040
1067
|
hook_context = HookContext(operation=case.operation)
|
|
@@ -1059,7 +1086,8 @@ def _asgi_test(
|
|
|
1059
1086
|
status = Status.failure
|
|
1060
1087
|
raise
|
|
1061
1088
|
finally:
|
|
1062
|
-
feedback
|
|
1089
|
+
if feedback is not None:
|
|
1090
|
+
feedback.add_test_case(case, response)
|
|
1063
1091
|
if store_interactions:
|
|
1064
1092
|
result.store_requests_response(case, response, status, check_results)
|
|
1065
1093
|
return response
|
schemathesis/runner/impl/solo.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import threading
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from typing import Generator
|
|
5
6
|
|
|
6
7
|
from ...models import TestResultSet
|
|
7
|
-
from ...types import RequestCert
|
|
8
8
|
from ...transports.auth import get_requests_auth
|
|
9
|
+
from ...types import RequestCert
|
|
9
10
|
from .. import events
|
|
10
11
|
from .core import BaseRunner, asgi_test, get_session, network_test, wsgi_test
|
|
11
12
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import ctypes
|
|
3
4
|
import queue
|
|
4
5
|
import threading
|
|
5
6
|
import time
|
|
7
|
+
import warnings
|
|
6
8
|
from dataclasses import dataclass
|
|
7
9
|
from queue import Queue
|
|
8
10
|
from typing import Any, Callable, Generator, Iterable, cast
|
|
9
11
|
|
|
10
12
|
import hypothesis
|
|
13
|
+
from hypothesis.errors import HypothesisWarning
|
|
11
14
|
|
|
12
15
|
from ..._hypothesis import create_test
|
|
13
16
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
@@ -39,6 +42,7 @@ def _run_task(
|
|
|
39
42
|
headers: dict[str, Any] | None = None,
|
|
40
43
|
**kwargs: Any,
|
|
41
44
|
) -> None:
|
|
45
|
+
warnings.filterwarnings("ignore", message="The recursion limit will not be reset", category=HypothesisWarning)
|
|
42
46
|
as_strategy_kwargs = {}
|
|
43
47
|
if headers is not None:
|
|
44
48
|
as_strategy_kwargs["headers"] = {key: value for key, value in headers.items() if key.lower() != "user-agent"}
|
schemathesis/runner/probes.py
CHANGED
|
@@ -28,7 +28,7 @@ from ..exceptions import (
|
|
|
28
28
|
make_unique_by_key,
|
|
29
29
|
)
|
|
30
30
|
from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
|
|
31
|
-
from ..transports import
|
|
31
|
+
from ..transports import deserialize_payload, serialize_payload
|
|
32
32
|
|
|
33
33
|
if TYPE_CHECKING:
|
|
34
34
|
import hypothesis.errors
|
schemathesis/sanitization.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import threading
|
|
3
4
|
from collections.abc import MutableMapping, MutableSequence
|
|
4
5
|
from dataclasses import dataclass, replace
|
|
@@ -9,6 +10,7 @@ from .constants import NOT_SET
|
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from requests import PreparedRequest
|
|
13
|
+
|
|
12
14
|
from .models import Case, CaseSource, Request
|
|
13
15
|
from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
|
|
14
16
|
from .transports.responses import GenericResponse
|
schemathesis/schemas.py
CHANGED
|
@@ -397,10 +397,7 @@ class BaseSchema(Mapping):
|
|
|
397
397
|
raise NotImplementedError
|
|
398
398
|
|
|
399
399
|
def as_state_machine(self) -> type[APIStateMachine]:
|
|
400
|
-
"""Create a state machine class.
|
|
401
|
-
|
|
402
|
-
Use it for stateful testing.
|
|
403
|
-
"""
|
|
400
|
+
"""Create a state machine class."""
|
|
404
401
|
raise NotImplementedError
|
|
405
402
|
|
|
406
403
|
def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
|
schemathesis/service/ci.py
CHANGED
schemathesis/service/client.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import hashlib
|
|
4
4
|
import http
|
|
5
|
+
import json
|
|
5
6
|
from dataclasses import asdict
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
7
8
|
from urllib.parse import urljoin
|
|
8
9
|
|
|
9
10
|
import requests
|
|
@@ -14,19 +15,18 @@ from .ci import CIProvider
|
|
|
14
15
|
from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
|
|
15
16
|
from .metadata import Metadata, collect_dependency_versions
|
|
16
17
|
from .models import (
|
|
17
|
-
AnalysisSuccess,
|
|
18
18
|
AnalysisError,
|
|
19
19
|
AnalysisResult,
|
|
20
|
-
|
|
20
|
+
AnalysisSuccess,
|
|
21
21
|
AuthResponse,
|
|
22
22
|
FailedUploadResponse,
|
|
23
|
-
|
|
24
|
-
UploadSource,
|
|
23
|
+
ProjectDetails,
|
|
25
24
|
ProjectEnvironment,
|
|
26
25
|
Specification,
|
|
26
|
+
UploadResponse,
|
|
27
|
+
UploadSource,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
31
|
from ..runner import probes
|
|
32
32
|
|
schemathesis/service/events.py
CHANGED
|
@@ -3,17 +3,17 @@ from __future__ import annotations
|
|
|
3
3
|
import base64
|
|
4
4
|
import re
|
|
5
5
|
from ipaddress import IPv4Network, IPv6Network
|
|
6
|
-
from typing import TYPE_CHECKING, Callable, Optional
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
7
7
|
|
|
8
8
|
from ..graphql import nodes
|
|
9
|
-
from ..internal.result import
|
|
9
|
+
from ..internal.result import Err, Ok, Result
|
|
10
10
|
from .models import (
|
|
11
11
|
Extension,
|
|
12
|
-
SchemaPatchesExtension,
|
|
13
|
-
StrategyDefinition,
|
|
14
|
-
OpenApiStringFormatsExtension,
|
|
15
12
|
GraphQLScalarsExtension,
|
|
16
13
|
MediaTypesExtension,
|
|
14
|
+
OpenApiStringFormatsExtension,
|
|
15
|
+
SchemaPatchesExtension,
|
|
16
|
+
StrategyDefinition,
|
|
17
17
|
TransformFunctionDefinition,
|
|
18
18
|
)
|
|
19
19
|
|
schemathesis/service/hosts.py
CHANGED
schemathesis/service/metadata.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Useful info to collect from CLI usage."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import os
|
|
5
6
|
import platform
|
|
6
|
-
from importlib import metadata
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from importlib import metadata
|
|
8
9
|
|
|
9
10
|
from ..constants import SCHEMATHESIS_VERSION
|
|
10
11
|
from .constants import DOCKER_IMAGE_ENV_VAR
|
schemathesis/service/models.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from dataclasses import dataclass, field
|
|
3
4
|
from enum import Enum
|
|
4
|
-
from typing import Any, Iterable, TypedDict, Union
|
|
5
|
+
from typing import Any, Iterable, Literal, TypedDict, Union
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class UploadSource(str, Enum):
|
schemathesis/service/report.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import enum
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
@@ -9,7 +10,7 @@ from contextlib import suppress
|
|
|
9
10
|
from dataclasses import asdict, dataclass, field
|
|
10
11
|
from io import BytesIO
|
|
11
12
|
from queue import Queue
|
|
12
|
-
from typing import
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
13
14
|
|
|
14
15
|
import click
|
|
15
16
|
|
|
@@ -22,11 +23,10 @@ from .metadata import Metadata
|
|
|
22
23
|
from .models import UploadResponse
|
|
23
24
|
from .serialization import serialize_event
|
|
24
25
|
|
|
25
|
-
|
|
26
26
|
if TYPE_CHECKING:
|
|
27
|
-
from .client import ServiceClient
|
|
28
27
|
from ..cli.context import ExecutionContext
|
|
29
28
|
from ..runner.events import ExecutionEvent
|
|
29
|
+
from .client import ServiceClient
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
@dataclass
|
|
@@ -7,9 +7,10 @@ from ..exceptions import format_exception
|
|
|
7
7
|
from ..internal.result import Err, Ok
|
|
8
8
|
from ..internal.transformation import merge_recursively
|
|
9
9
|
from ..models import Response
|
|
10
|
-
from .models import AnalysisSuccess
|
|
11
10
|
from ..runner import events
|
|
12
|
-
from ..runner.serialization import SerializedCase
|
|
11
|
+
from ..runner.serialization import SerializedCase, SerializedCheck
|
|
12
|
+
from ..stateful import events as stateful_events
|
|
13
|
+
from .models import AnalysisSuccess
|
|
13
14
|
|
|
14
15
|
S = TypeVar("S", bound=events.ExecutionEvent)
|
|
15
16
|
SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
|
|
@@ -81,6 +82,27 @@ def _serialize_response(response: Response) -> dict[str, Any]:
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
|
|
86
|
+
return {
|
|
87
|
+
"name": check.name,
|
|
88
|
+
"value": check.value,
|
|
89
|
+
"request": {
|
|
90
|
+
"method": check.request.method,
|
|
91
|
+
"uri": check.request.uri,
|
|
92
|
+
"body": check.request.body,
|
|
93
|
+
"headers": check.request.headers,
|
|
94
|
+
},
|
|
95
|
+
"response": _serialize_response(check.response) if check.response is not None else None,
|
|
96
|
+
"example": _serialize_case(check.example),
|
|
97
|
+
"message": check.message,
|
|
98
|
+
"context": asdict(check.context) if check.context is not None else None, # type: ignore
|
|
99
|
+
"history": [
|
|
100
|
+
{"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
|
|
101
|
+
for entry in check.history
|
|
102
|
+
],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
84
106
|
def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
|
|
85
107
|
return {
|
|
86
108
|
"correlation_id": event.correlation_id,
|
|
@@ -89,27 +111,7 @@ def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] |
|
|
|
89
111
|
"elapsed_time": event.elapsed_time,
|
|
90
112
|
"data_generation_method": event.data_generation_method,
|
|
91
113
|
"result": {
|
|
92
|
-
"checks": [
|
|
93
|
-
{
|
|
94
|
-
"name": check.name,
|
|
95
|
-
"value": check.value,
|
|
96
|
-
"request": {
|
|
97
|
-
"method": check.request.method,
|
|
98
|
-
"uri": check.request.uri,
|
|
99
|
-
"body": check.request.body,
|
|
100
|
-
"headers": check.request.headers,
|
|
101
|
-
},
|
|
102
|
-
"response": _serialize_response(check.response) if check.response is not None else None,
|
|
103
|
-
"example": _serialize_case(check.example),
|
|
104
|
-
"message": check.message,
|
|
105
|
-
"context": asdict(check.context) if check.context is not None else None, # type: ignore
|
|
106
|
-
"history": [
|
|
107
|
-
{"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
|
|
108
|
-
for entry in check.history
|
|
109
|
-
],
|
|
110
|
-
}
|
|
111
|
-
for check in event.result.checks
|
|
112
|
-
],
|
|
114
|
+
"checks": [_serialize_check(check) for check in event.result.checks],
|
|
113
115
|
"errors": [asdict(error) for error in event.result.errors],
|
|
114
116
|
"skip_reason": event.result.skip_reason,
|
|
115
117
|
},
|
|
@@ -147,6 +149,29 @@ def serialize_finished(event: events.Finished) -> dict[str, Any] | None:
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
|
|
152
|
+
def serialize_stateful_event(event: events.StatefulEvent) -> dict[str, Any] | None:
|
|
153
|
+
if isinstance(event.data, stateful_events.RunStarted):
|
|
154
|
+
return {
|
|
155
|
+
"data": {
|
|
156
|
+
"timestamp": event.data.timestamp,
|
|
157
|
+
"started_at": event.data.started_at,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
elif isinstance(event.data, stateful_events.SuiteFinished):
|
|
161
|
+
return {
|
|
162
|
+
"data": {
|
|
163
|
+
"timestamp": event.data.timestamp,
|
|
164
|
+
"status": event.data.status,
|
|
165
|
+
"failures": [_serialize_check(SerializedCheck.from_check(failure)) for failure in event.data.failures],
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {"data": asdict(event.data)}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
|
|
172
|
+
return {"result": asdict(event.result)}
|
|
173
|
+
|
|
174
|
+
|
|
150
175
|
SERIALIZER_MAP = {
|
|
151
176
|
events.Initialized: serialize_initialized,
|
|
152
177
|
events.BeforeProbing: serialize_before_probing,
|
|
@@ -157,6 +182,8 @@ SERIALIZER_MAP = {
|
|
|
157
182
|
events.AfterExecution: serialize_after_execution,
|
|
158
183
|
events.Interrupted: serialize_interrupted,
|
|
159
184
|
events.InternalError: serialize_internal_error,
|
|
185
|
+
events.StatefulEvent: serialize_stateful_event,
|
|
186
|
+
events.AfterStatefulExecution: serialize_after_stateful_execution,
|
|
160
187
|
events.Finished: serialize_finished,
|
|
161
188
|
}
|
|
162
189
|
|
|
@@ -173,6 +200,8 @@ def serialize_event(
|
|
|
173
200
|
on_after_execution: SerializeFunc | None = None,
|
|
174
201
|
on_interrupted: SerializeFunc | None = None,
|
|
175
202
|
on_internal_error: SerializeFunc | None = None,
|
|
203
|
+
on_stateful_event: SerializeFunc | None = None,
|
|
204
|
+
on_after_stateful_execution: SerializeFunc | None = None,
|
|
176
205
|
on_finished: SerializeFunc | None = None,
|
|
177
206
|
extra: dict[str, Any] | None = None,
|
|
178
207
|
) -> dict[str, dict[str, Any] | None]:
|
|
@@ -188,6 +217,8 @@ def serialize_event(
|
|
|
188
217
|
events.AfterExecution: on_after_execution,
|
|
189
218
|
events.Interrupted: on_interrupted,
|
|
190
219
|
events.InternalError: on_internal_error,
|
|
220
|
+
events.StatefulEvent: on_stateful_event,
|
|
221
|
+
events.AfterStatefulExecution: on_after_stateful_execution,
|
|
191
222
|
events.Finished: on_finished,
|
|
192
223
|
}.get(event.__class__)
|
|
193
224
|
if serializer is None:
|
schemathesis/service/usage.py
CHANGED
|
@@ -233,8 +233,8 @@ def from_dict(
|
|
|
233
233
|
:param app: A WSGI app instance.
|
|
234
234
|
:return: GraphQLSchema
|
|
235
235
|
"""
|
|
236
|
-
from .schemas import GraphQLSchema
|
|
237
236
|
from ... import transports
|
|
237
|
+
from .schemas import GraphQLSchema
|
|
238
238
|
|
|
239
239
|
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
|
240
240
|
hook_context = HookContext()
|
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
from functools import lru_cache
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from ...exceptions import UsageError
|
|
8
7
|
|
|
9
8
|
if TYPE_CHECKING:
|
|
@@ -31,9 +30,10 @@ def scalar(name: str, strategy: st.SearchStrategy[graphql.ValueNode]) -> None:
|
|
|
31
30
|
@lru_cache
|
|
32
31
|
def get_extra_scalar_strategies() -> dict[str, st.SearchStrategy]:
|
|
33
32
|
"""Get all extra GraphQL strategies."""
|
|
34
|
-
from . import nodes
|
|
35
33
|
from hypothesis import strategies as st
|
|
36
34
|
|
|
35
|
+
from . import nodes
|
|
36
|
+
|
|
37
37
|
dates = st.dates().map(str)
|
|
38
38
|
times = st.times().map("%sZ".__mod__)
|
|
39
39
|
|
|
@@ -6,16 +6,16 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from difflib import get_close_matches
|
|
7
7
|
from enum import unique
|
|
8
8
|
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
9
10
|
Any,
|
|
10
11
|
Callable,
|
|
11
12
|
Generator,
|
|
13
|
+
Iterator,
|
|
14
|
+
Mapping,
|
|
15
|
+
NoReturn,
|
|
12
16
|
Sequence,
|
|
13
17
|
TypeVar,
|
|
14
18
|
cast,
|
|
15
|
-
TYPE_CHECKING,
|
|
16
|
-
NoReturn,
|
|
17
|
-
Mapping,
|
|
18
|
-
Iterator,
|
|
19
19
|
)
|
|
20
20
|
from urllib.parse import urlsplit, urlunsplit
|
|
21
21
|
|
|
@@ -25,12 +25,11 @@ from hypothesis.strategies import SearchStrategy
|
|
|
25
25
|
from hypothesis_graphql import strategies as gql_st
|
|
26
26
|
from requests.structures import CaseInsensitiveDict
|
|
27
27
|
|
|
28
|
-
from ..openapi.constants import LOCATION_TO_CONTAINER
|
|
29
28
|
from ... import auths
|
|
30
29
|
from ...auths import AuthStorage
|
|
31
30
|
from ...checks import not_a_server_error
|
|
32
31
|
from ...constants import NOT_SET
|
|
33
|
-
from ...exceptions import
|
|
32
|
+
from ...exceptions import OperationNotFound, OperationSchemaError
|
|
34
33
|
from ...generation import DataGenerationMethod, GenerationConfig
|
|
35
34
|
from ...hooks import (
|
|
36
35
|
GLOBAL_HOOK_DISPATCHER,
|
|
@@ -41,9 +40,10 @@ from ...hooks import (
|
|
|
41
40
|
)
|
|
42
41
|
from ...internal.result import Ok, Result
|
|
43
42
|
from ...models import APIOperation, Case, CheckFunction, OperationDefinition
|
|
44
|
-
from ...schemas import
|
|
43
|
+
from ...schemas import APIOperationMap, BaseSchema
|
|
45
44
|
from ...stateful import Stateful, StatefulTest
|
|
46
45
|
from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
|
|
46
|
+
from ..openapi.constants import LOCATION_TO_CONTAINER
|
|
47
47
|
from ._cache import OperationCache
|
|
48
48
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
|
49
49
|
|