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.
Files changed (123) hide show
  1. schemathesis/__init__.py +3 -3
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +1 -3
  4. schemathesis/_hypothesis.py +6 -0
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +1 -0
  7. schemathesis/_rate_limiter.py +2 -1
  8. schemathesis/_xml.py +1 -0
  9. schemathesis/auths.py +4 -2
  10. schemathesis/checks.py +8 -5
  11. schemathesis/cli/__init__.py +8 -1
  12. schemathesis/cli/callbacks.py +3 -4
  13. schemathesis/cli/cassettes.py +6 -4
  14. schemathesis/cli/constants.py +2 -0
  15. schemathesis/cli/context.py +3 -0
  16. schemathesis/cli/debug.py +2 -1
  17. schemathesis/cli/handlers.py +1 -1
  18. schemathesis/cli/options.py +1 -0
  19. schemathesis/cli/output/default.py +50 -22
  20. schemathesis/cli/output/short.py +21 -10
  21. schemathesis/cli/sanitization.py +1 -0
  22. schemathesis/code_samples.py +1 -0
  23. schemathesis/constants.py +1 -0
  24. schemathesis/contrib/openapi/__init__.py +1 -1
  25. schemathesis/contrib/openapi/fill_missing_examples.py +2 -0
  26. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  27. schemathesis/contrib/unique_data.py +2 -1
  28. schemathesis/exceptions.py +40 -26
  29. schemathesis/experimental/__init__.py +14 -0
  30. schemathesis/extra/_aiohttp.py +1 -0
  31. schemathesis/extra/_server.py +1 -0
  32. schemathesis/extra/pytest_plugin.py +13 -24
  33. schemathesis/failures.py +32 -3
  34. schemathesis/filters.py +2 -1
  35. schemathesis/fixups/__init__.py +1 -0
  36. schemathesis/fixups/fast_api.py +2 -2
  37. schemathesis/fixups/utf8_bom.py +1 -2
  38. schemathesis/generation/__init__.py +2 -1
  39. schemathesis/hooks.py +3 -1
  40. schemathesis/internal/copy.py +19 -3
  41. schemathesis/internal/deprecation.py +1 -1
  42. schemathesis/internal/jsonschema.py +2 -1
  43. schemathesis/internal/result.py +1 -1
  44. schemathesis/internal/transformation.py +1 -0
  45. schemathesis/lazy.py +3 -2
  46. schemathesis/loaders.py +4 -2
  47. schemathesis/models.py +20 -5
  48. schemathesis/parameters.py +1 -0
  49. schemathesis/runner/__init__.py +1 -1
  50. schemathesis/runner/events.py +21 -4
  51. schemathesis/runner/impl/core.py +61 -33
  52. schemathesis/runner/impl/solo.py +2 -1
  53. schemathesis/runner/impl/threadpool.py +4 -0
  54. schemathesis/runner/probes.py +1 -1
  55. schemathesis/runner/serialization.py +1 -1
  56. schemathesis/sanitization.py +2 -0
  57. schemathesis/schemas.py +1 -4
  58. schemathesis/service/ci.py +1 -0
  59. schemathesis/service/client.py +7 -7
  60. schemathesis/service/events.py +2 -1
  61. schemathesis/service/extensions.py +5 -5
  62. schemathesis/service/hosts.py +1 -0
  63. schemathesis/service/metadata.py +2 -1
  64. schemathesis/service/models.py +2 -1
  65. schemathesis/service/report.py +3 -3
  66. schemathesis/service/serialization.py +54 -23
  67. schemathesis/service/usage.py +1 -0
  68. schemathesis/specs/graphql/_cache.py +1 -1
  69. schemathesis/specs/graphql/loaders.py +1 -1
  70. schemathesis/specs/graphql/nodes.py +1 -0
  71. schemathesis/specs/graphql/scalars.py +2 -2
  72. schemathesis/specs/graphql/schemas.py +7 -7
  73. schemathesis/specs/graphql/validation.py +1 -2
  74. schemathesis/specs/openapi/_hypothesis.py +17 -11
  75. schemathesis/specs/openapi/checks.py +102 -9
  76. schemathesis/specs/openapi/converter.py +2 -1
  77. schemathesis/specs/openapi/definitions.py +2 -1
  78. schemathesis/specs/openapi/examples.py +7 -9
  79. schemathesis/specs/openapi/expressions/__init__.py +29 -2
  80. schemathesis/specs/openapi/expressions/context.py +1 -1
  81. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  82. schemathesis/specs/openapi/expressions/lexer.py +19 -18
  83. schemathesis/specs/openapi/expressions/nodes.py +24 -4
  84. schemathesis/specs/openapi/expressions/parser.py +26 -5
  85. schemathesis/specs/openapi/filters.py +1 -0
  86. schemathesis/specs/openapi/links.py +35 -7
  87. schemathesis/specs/openapi/loaders.py +13 -11
  88. schemathesis/specs/openapi/negative/__init__.py +2 -1
  89. schemathesis/specs/openapi/negative/mutations.py +1 -0
  90. schemathesis/specs/openapi/parameters.py +1 -0
  91. schemathesis/specs/openapi/schemas.py +27 -38
  92. schemathesis/specs/openapi/security.py +1 -0
  93. schemathesis/specs/openapi/serialization.py +1 -0
  94. schemathesis/specs/openapi/stateful/__init__.py +159 -70
  95. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  96. schemathesis/specs/openapi/stateful/types.py +13 -0
  97. schemathesis/specs/openapi/utils.py +1 -0
  98. schemathesis/specs/openapi/validation.py +1 -0
  99. schemathesis/stateful/__init__.py +4 -2
  100. schemathesis/stateful/config.py +66 -0
  101. schemathesis/stateful/context.py +93 -0
  102. schemathesis/stateful/events.py +209 -0
  103. schemathesis/stateful/runner.py +233 -0
  104. schemathesis/stateful/sink.py +68 -0
  105. schemathesis/stateful/state_machine.py +39 -22
  106. schemathesis/stateful/statistic.py +20 -0
  107. schemathesis/stateful/validation.py +66 -0
  108. schemathesis/targets.py +1 -0
  109. schemathesis/throttling.py +23 -3
  110. schemathesis/transports/__init__.py +28 -10
  111. schemathesis/transports/auth.py +1 -0
  112. schemathesis/transports/content_types.py +1 -1
  113. schemathesis/transports/headers.py +2 -1
  114. schemathesis/transports/responses.py +6 -4
  115. schemathesis/types.py +1 -0
  116. schemathesis/utils.py +1 -0
  117. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/METADATA +1 -1
  118. schemathesis-3.30.0.dist-info/RECORD +150 -0
  119. schemathesis/specs/openapi/stateful/links.py +0 -94
  120. schemathesis-3.29.1.dist-info/RECORD +0 -141
  121. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/WHEEL +0 -0
  122. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/entry_points.txt +0 -0
  123. {schemathesis-3.29.1.dist-info → schemathesis-3.30.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 schemathesis.transports import RequestsTransport
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
- yield from self._execute(results, stop_event)
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
- feedback = Feedback(self.stateful, operation)
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
- yield from self._run_tests(
272
- feedback.get_stateful_tests,
273
- template,
274
- settings=settings,
275
- generation_config=generation_config,
276
- seed=seed,
277
- recursion_level=recursion_level + 1,
278
- results=results,
279
- headers=headers,
280
- **kwargs,
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.add_test_case(case, response)
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.add_test_case(case, response)
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.add_test_case(case, response)
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
@@ -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"}
@@ -22,8 +22,8 @@ from ..transports.auth import get_requests_auth
22
22
  if TYPE_CHECKING:
23
23
  import requests
24
24
 
25
- from ..types import RequestCert
26
25
  from ..schemas import BaseSchema
26
+ from ..types import RequestCert
27
27
 
28
28
 
29
29
  HEADER_NAME = "X-Schemathesis-Probe"
@@ -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 serialize_payload, deserialize_payload
31
+ from ..transports import deserialize_payload, serialize_payload
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  import hypothesis.errors
@@ -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]]:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import os
4
5
  from dataclasses import asdict, dataclass
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
- import json
2
+
3
3
  import hashlib
4
4
  import http
5
+ import json
5
6
  from dataclasses import asdict
6
- from typing import Any, TYPE_CHECKING
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
- ProjectDetails,
20
+ AnalysisSuccess,
21
21
  AuthResponse,
22
22
  FailedUploadResponse,
23
- UploadResponse,
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
 
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
 
4
- from . import ci
5
5
  from ..exceptions import format_exception
6
+ from . import ci
6
7
 
7
8
 
8
9
  class Event:
@@ -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, Any
6
+ from typing import TYPE_CHECKING, Any, Callable, Optional
7
7
 
8
8
  from ..graphql import nodes
9
- from ..internal.result import Result, Ok, Err
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
 
@@ -1,6 +1,7 @@
1
1
  """Work with stored auth data."""
2
2
 
3
3
  from __future__ import annotations
4
+
4
5
  import enum
5
6
  import tempfile
6
7
  from dataclasses import dataclass
@@ -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
@@ -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, Literal
5
+ from typing import Any, Iterable, Literal, TypedDict, Union
5
6
 
6
7
 
7
8
  class UploadSource(str, Enum):
@@ -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 Any, TYPE_CHECKING
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:
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import sys
3
4
  from typing import Any
4
5
 
@@ -4,8 +4,8 @@ from dataclasses import dataclass, field
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
- from ...schemas import APIOperationMap
8
7
  from ...models import APIOperation
8
+ from ...schemas import APIOperationMap
9
9
 
10
10
 
11
11
  @dataclass
@@ -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()
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
4
5
  if TYPE_CHECKING:
@@ -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 OperationSchemaError, OperationNotFound
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 BaseSchema, APIOperationMap
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
 
@@ -1,5 +1,4 @@
1
- from typing import Any, cast, List
2
-
1
+ from typing import Any, List, cast
3
2
 
4
3
  from ... import failures
5
4
  from ...exceptions import get_grouped_graphql_error, get_unexpected_graphql_response_error