schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,9 @@
2
2
 
3
3
  These are basic entities that describe what data could be sent to the API.
4
4
  """
5
+
5
6
  from __future__ import annotations
7
+
6
8
  from dataclasses import dataclass, field
7
9
  from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar
8
10
 
@@ -53,6 +55,8 @@ class ParameterSet(Generic[P]):
53
55
 
54
56
  items: list[P] = field(default_factory=list)
55
57
 
58
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
59
+
56
60
  def add(self, parameter: P) -> None:
57
61
  """Add a new parameter."""
58
62
  self.items.append(parameter)
@@ -1,34 +1,39 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from random import Random
4
- from typing import Any, Callable, Generator, Iterable, TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable
5
5
  from urllib.parse import urlparse
6
6
 
7
- from .._override import CaseOverride
8
- from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
9
7
  from ..constants import (
10
8
  DEFAULT_DEADLINE,
11
9
  DEFAULT_STATEFUL_RECURSION_LIMIT,
12
10
  HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
13
11
  )
14
- from ..internal.deprecation import deprecated_function
12
+ from ..exceptions import SchemaError
13
+ from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod, GenerationConfig
14
+ from ..internal.checks import CheckConfig
15
15
  from ..internal.datetime import current_datetime
16
+ from ..internal.deprecation import deprecated_function
16
17
  from ..internal.validation import file_exists
17
- from ..transports.auth import get_requests_auth
18
- from ..exceptions import SchemaError
19
18
  from ..loaders import load_app
20
19
  from ..specs.graphql import loaders as gql_loaders
21
20
  from ..specs.openapi import loaders as oas_loaders
22
21
  from ..targets import DEFAULT_TARGETS, Target
22
+ from ..transports import RequestConfig
23
+ from ..transports.auth import get_requests_auth
23
24
  from ..types import Filter, NotSet, RawAuth, RequestCert
25
+ from . import events
26
+ from .probes import ProbeConfig
24
27
 
25
28
  if TYPE_CHECKING:
26
- from . import events
29
+ import hypothesis
30
+
31
+ from .._override import CaseOverride
27
32
  from ..models import CheckFunction
28
33
  from ..schemas import BaseSchema
29
- from .impl import BaseRunner
34
+ from ..service.client import ServiceClient
30
35
  from ..stateful import Stateful
31
- import hypothesis
36
+ from .impl import BaseRunner
32
37
 
33
38
 
34
39
  @deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
@@ -75,6 +80,8 @@ def prepare(
75
80
  hypothesis_report_multiple_bugs: bool | None = None,
76
81
  hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
77
82
  hypothesis_verbosity: hypothesis.Verbosity | None = None,
83
+ probe_config: ProbeConfig | None = None,
84
+ service_client: ServiceClient | None = None,
78
85
  ) -> Generator[events.ExecutionEvent, None, None]:
79
86
  """Prepare a generator that will run test cases against the given API definition."""
80
87
  from ..checks import DEFAULT_CHECKS
@@ -128,6 +135,8 @@ def prepare(
128
135
  stateful_recursion_limit=stateful_recursion_limit,
129
136
  count_operations=count_operations,
130
137
  count_links=count_links,
138
+ probe_config=probe_config,
139
+ service_client=service_client,
131
140
  )
132
141
 
133
142
 
@@ -188,6 +197,8 @@ def execute_from_schema(
188
197
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
189
198
  count_operations: bool = True,
190
199
  count_links: bool = True,
200
+ probe_config: ProbeConfig | None = None,
201
+ service_client: ServiceClient | None,
191
202
  ) -> Generator[events.ExecutionEvent, None, None]:
192
203
  """Execute tests for the given schema.
193
204
 
@@ -237,6 +248,8 @@ def execute_from_schema(
237
248
  stateful_recursion_limit=stateful_recursion_limit,
238
249
  count_operations=count_operations,
239
250
  count_links=count_links,
251
+ probe_config=probe_config,
252
+ service_client=service_client,
240
253
  ).execute()
241
254
  except SchemaError as error:
242
255
  yield events.InternalError.from_schema_error(error)
@@ -267,7 +280,7 @@ def load_schema(
267
280
  operation_id: Filter | None = None,
268
281
  ) -> BaseSchema:
269
282
  """Load schema via specified loader and parameters."""
270
- loader_options = {
283
+ loader_options: dict[str, Any] = {
271
284
  key: value
272
285
  for key, value in (
273
286
  ("base_url", base_url),
@@ -333,18 +346,24 @@ def from_schema(
333
346
  request_cert: RequestCert | None = None,
334
347
  seed: int | None = None,
335
348
  exit_first: bool = False,
349
+ no_failfast: bool = False,
336
350
  max_failures: int | None = None,
337
351
  started_at: str | None = None,
352
+ unique_data: bool = False,
338
353
  dry_run: bool = False,
339
354
  store_interactions: bool = False,
340
355
  stateful: Stateful | None = None,
341
356
  stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
342
357
  count_operations: bool = True,
343
358
  count_links: bool = True,
359
+ probe_config: ProbeConfig | None = None,
360
+ checks_config: CheckConfig | None = None,
361
+ service_client: ServiceClient | None = None,
344
362
  ) -> BaseRunner:
345
- from starlette.applications import Starlette
346
363
  import hypothesis
364
+
347
365
  from ..checks import DEFAULT_CHECKS
366
+ from ..transports.asgi import is_asgi_app
348
367
  from .impl import (
349
368
  SingleThreadASGIRunner,
350
369
  SingleThreadRunner,
@@ -355,9 +374,16 @@ def from_schema(
355
374
  )
356
375
 
357
376
  checks = checks or DEFAULT_CHECKS
377
+ checks_config = checks_config or CheckConfig()
378
+ probe_config = probe_config or ProbeConfig()
358
379
 
359
380
  hypothesis_settings = hypothesis_settings or hypothesis.settings(deadline=DEFAULT_DEADLINE)
360
- generation_config = generation_config or GenerationConfig()
381
+ request_config = RequestConfig(
382
+ timeout=request_timeout,
383
+ tls_verify=request_tls_verify,
384
+ proxy=request_proxy,
385
+ cert=request_cert,
386
+ )
361
387
 
362
388
  # Use the same seed for all tests unless `derandomize=True` is used
363
389
  if seed is None and not hypothesis_settings.derandomize:
@@ -379,21 +405,23 @@ def from_schema(
379
405
  headers=headers,
380
406
  seed=seed,
381
407
  workers_num=workers_num,
382
- request_timeout=request_timeout,
383
- request_tls_verify=request_tls_verify,
384
- request_proxy=request_proxy,
385
- request_cert=request_cert,
408
+ request_config=request_config,
386
409
  exit_first=exit_first,
410
+ no_failfast=no_failfast,
387
411
  max_failures=max_failures,
388
412
  started_at=started_at,
413
+ unique_data=unique_data,
389
414
  dry_run=dry_run,
390
415
  store_interactions=store_interactions,
391
416
  stateful=stateful,
392
417
  stateful_recursion_limit=stateful_recursion_limit,
393
418
  count_operations=count_operations,
394
419
  count_links=count_links,
420
+ probe_config=probe_config,
421
+ checks_config=checks_config,
422
+ service_client=service_client,
395
423
  )
396
- if isinstance(schema.app, Starlette):
424
+ if is_asgi_app(schema.app):
397
425
  return ThreadPoolASGIRunner(
398
426
  schema=schema,
399
427
  checks=checks,
@@ -407,14 +435,19 @@ def from_schema(
407
435
  headers=headers,
408
436
  seed=seed,
409
437
  exit_first=exit_first,
438
+ no_failfast=no_failfast,
410
439
  max_failures=max_failures,
411
440
  started_at=started_at,
441
+ unique_data=unique_data,
412
442
  dry_run=dry_run,
413
443
  store_interactions=store_interactions,
414
444
  stateful=stateful,
415
445
  stateful_recursion_limit=stateful_recursion_limit,
416
446
  count_operations=count_operations,
417
447
  count_links=count_links,
448
+ probe_config=probe_config,
449
+ checks_config=checks_config,
450
+ service_client=service_client,
418
451
  )
419
452
  return ThreadPoolWSGIRunner(
420
453
  schema=schema,
@@ -430,14 +463,19 @@ def from_schema(
430
463
  seed=seed,
431
464
  workers_num=workers_num,
432
465
  exit_first=exit_first,
466
+ no_failfast=no_failfast,
433
467
  max_failures=max_failures,
434
468
  started_at=started_at,
469
+ unique_data=unique_data,
435
470
  dry_run=dry_run,
436
471
  store_interactions=store_interactions,
437
472
  stateful=stateful,
438
473
  stateful_recursion_limit=stateful_recursion_limit,
439
474
  count_operations=count_operations,
440
475
  count_links=count_links,
476
+ probe_config=probe_config,
477
+ checks_config=checks_config,
478
+ service_client=service_client,
441
479
  )
442
480
  if not schema.app:
443
481
  return SingleThreadRunner(
@@ -452,21 +490,23 @@ def from_schema(
452
490
  override=override,
453
491
  headers=headers,
454
492
  seed=seed,
455
- request_timeout=request_timeout,
456
- request_tls_verify=request_tls_verify,
457
- request_proxy=request_proxy,
458
- request_cert=request_cert,
493
+ request_config=request_config,
459
494
  exit_first=exit_first,
495
+ no_failfast=no_failfast,
460
496
  max_failures=max_failures,
461
497
  started_at=started_at,
498
+ unique_data=unique_data,
462
499
  dry_run=dry_run,
463
500
  store_interactions=store_interactions,
464
501
  stateful=stateful,
465
502
  stateful_recursion_limit=stateful_recursion_limit,
466
503
  count_operations=count_operations,
467
504
  count_links=count_links,
505
+ probe_config=probe_config,
506
+ checks_config=checks_config,
507
+ service_client=service_client,
468
508
  )
469
- if isinstance(schema.app, Starlette):
509
+ if is_asgi_app(schema.app):
470
510
  return SingleThreadASGIRunner(
471
511
  schema=schema,
472
512
  checks=checks,
@@ -480,14 +520,19 @@ def from_schema(
480
520
  headers=headers,
481
521
  seed=seed,
482
522
  exit_first=exit_first,
523
+ no_failfast=no_failfast,
483
524
  max_failures=max_failures,
484
525
  started_at=started_at,
526
+ unique_data=unique_data,
485
527
  dry_run=dry_run,
486
528
  store_interactions=store_interactions,
487
529
  stateful=stateful,
488
530
  stateful_recursion_limit=stateful_recursion_limit,
489
531
  count_operations=count_operations,
490
532
  count_links=count_links,
533
+ probe_config=probe_config,
534
+ checks_config=checks_config,
535
+ service_client=service_client,
491
536
  )
492
537
  return SingleThreadWSGIRunner(
493
538
  schema=schema,
@@ -502,14 +547,19 @@ def from_schema(
502
547
  headers=headers,
503
548
  seed=seed,
504
549
  exit_first=exit_first,
550
+ no_failfast=no_failfast,
505
551
  max_failures=max_failures,
506
552
  started_at=started_at,
553
+ unique_data=unique_data,
507
554
  dry_run=dry_run,
508
555
  store_interactions=store_interactions,
509
556
  stateful=stateful,
510
557
  stateful_recursion_limit=stateful_recursion_limit,
511
558
  count_operations=count_operations,
512
559
  count_links=count_links,
560
+ probe_config=probe_config,
561
+ checks_config=checks_config,
562
+ service_client=service_client,
513
563
  )
514
564
 
515
565
 
@@ -1,19 +1,23 @@
1
1
  from __future__ import annotations
2
+
2
3
  import enum
3
4
  import threading
4
5
  import time
5
6
  from dataclasses import asdict, dataclass, field
6
- from typing import Any, TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Any
7
8
 
9
+ from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
8
10
  from ..internal.datetime import current_datetime
9
- from ..generation import DataGenerationMethod
10
- from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
11
+ from ..internal.result import Err, Ok, Result
11
12
  from .serialization import SerializedError, SerializedTestResult
12
13
 
13
-
14
14
  if TYPE_CHECKING:
15
+ from ..generation import DataGenerationMethod
15
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
16
- from ..schemas import BaseSchema
17
+ from ..schemas import BaseSchema, Specification
18
+ from ..service.models import AnalysisResult
19
+ from ..stateful import events
20
+ from . import probes
17
21
 
18
22
 
19
23
  @dataclass
@@ -35,6 +39,7 @@ class Initialized(ExecutionEvent):
35
39
  """Runner is initialized, settings are prepared, requests session is ready."""
36
40
 
37
41
  schema: dict[str, Any]
42
+ specification: Specification
38
43
  # Total number of operations in the schema
39
44
  operations_count: int | None
40
45
  # Total number of links in the schema
@@ -44,6 +49,8 @@ class Initialized(ExecutionEvent):
44
49
  seed: int | None
45
50
  # The base URL against which the tests are running
46
51
  base_url: str
52
+ # The base path part of every operation
53
+ base_path: str
47
54
  # API schema specification name
48
55
  specification_name: str
49
56
  # Monotonic clock value when the test run started. Used to properly calculate run duration, since this clock
@@ -60,22 +67,69 @@ class Initialized(ExecutionEvent):
60
67
  schema: BaseSchema,
61
68
  count_operations: bool = True,
62
69
  count_links: bool = True,
70
+ start_time: float | None = None,
63
71
  started_at: str | None = None,
64
72
  seed: int | None,
65
73
  ) -> Initialized:
66
74
  """Computes all needed data from a schema instance."""
67
75
  return cls(
68
76
  schema=schema.raw_schema,
77
+ specification=schema.specification,
69
78
  operations_count=schema.operations_count if count_operations else None,
70
79
  links_count=schema.links_count if count_links else None,
71
80
  location=schema.location,
72
81
  base_url=schema.get_base_url(),
82
+ base_path=schema.base_path,
83
+ start_time=start_time or time.monotonic(),
73
84
  started_at=started_at or current_datetime(),
74
85
  specification_name=schema.verbose_name,
75
86
  seed=seed,
76
87
  )
77
88
 
78
89
 
90
+ @dataclass
91
+ class BeforeProbing(ExecutionEvent):
92
+ pass
93
+
94
+
95
+ @dataclass
96
+ class AfterProbing(ExecutionEvent):
97
+ probes: list[probes.ProbeRun] | None
98
+
99
+ def asdict(self, **kwargs: Any) -> dict[str, Any]:
100
+ probes = self.probes or []
101
+ return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
102
+
103
+
104
+ @dataclass
105
+ class BeforeAnalysis(ExecutionEvent):
106
+ pass
107
+
108
+
109
+ @dataclass
110
+ class AfterAnalysis(ExecutionEvent):
111
+ analysis: Result[AnalysisResult, Exception] | None
112
+
113
+ def _serialize(self) -> dict[str, Any]:
114
+ from ..service.models import AnalysisSuccess
115
+
116
+ data = {}
117
+ if isinstance(self.analysis, Ok):
118
+ result = self.analysis.ok()
119
+ if isinstance(result, AnalysisSuccess):
120
+ data["analysis_id"] = result.id
121
+ else:
122
+ data["error"] = result.message
123
+ elif isinstance(self.analysis, Err):
124
+ data["error"] = format_exception(self.analysis.err())
125
+ return data
126
+
127
+ def asdict(self, **kwargs: Any) -> dict[str, Any]:
128
+ data = self._serialize()
129
+ data["event_type"] = self.__class__.__name__
130
+ return data
131
+
132
+
79
133
  class CurrentOperationMixin:
80
134
  method: str
81
135
  path: str
@@ -188,6 +242,9 @@ class InternalErrorType(str, enum.Enum):
188
242
  OTHER = "other"
189
243
 
190
244
 
245
+ DEFAULT_INTERNAL_ERROR_MESSAGE = "An internal error occurred during the test run"
246
+
247
+
191
248
  @dataclass
192
249
  class InternalError(ExecutionEvent):
193
250
  """An error that happened inside the runner."""
@@ -226,7 +283,7 @@ class InternalError(ExecutionEvent):
226
283
  type_=InternalErrorType.OTHER,
227
284
  subtype=None,
228
285
  title="Test Execution Error",
229
- message="An internal error occurred during the test run",
286
+ message=DEFAULT_INTERNAL_ERROR_MESSAGE,
230
287
  extras=[],
231
288
  )
232
289
 
@@ -255,6 +312,29 @@ class InternalError(ExecutionEvent):
255
312
  )
256
313
 
257
314
 
315
+ @dataclass
316
+ class StatefulEvent(ExecutionEvent):
317
+ """Represents an event originating from the state machine runner."""
318
+
319
+ data: events.StatefulEvent
320
+
321
+ __slots__ = ("data",)
322
+
323
+ def asdict(self, **kwargs: Any) -> dict[str, Any]:
324
+ return {"data": self.data.asdict(**kwargs), "event_type": self.__class__.__name__}
325
+
326
+
327
+ @dataclass
328
+ class AfterStatefulExecution(ExecutionEvent):
329
+ """Happens after the stateful test run."""
330
+
331
+ status: Status
332
+ result: SerializedTestResult
333
+ elapsed_time: float
334
+ data_generation_method: list[DataGenerationMethod]
335
+ thread_id: int = field(default_factory=threading.get_ident)
336
+
337
+
258
338
  @dataclass
259
339
  class Finished(ExecutionEvent):
260
340
  """The final event of the run.
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from ...constants import NOT_SET
7
+ from ...internal.checks import CheckConfig
8
+ from ...models import TestResult, TestResultSet
9
+
10
+ if TYPE_CHECKING:
11
+ import threading
12
+
13
+ from ..._override import CaseOverride
14
+ from ...exceptions import OperationSchemaError
15
+ from ...models import Case
16
+ from ...types import NotSet, RawAuth
17
+
18
+
19
+ @dataclass
20
+ class RunnerContext:
21
+ """Holds context shared for a test run."""
22
+
23
+ data: TestResultSet
24
+ auth: RawAuth | None
25
+ seed: int | None
26
+ stop_event: threading.Event
27
+ unique_data: bool
28
+ outcome_cache: dict[int, BaseException | None]
29
+ checks_config: CheckConfig
30
+ override: CaseOverride | None
31
+ no_failfast: bool
32
+
33
+ __slots__ = (
34
+ "data",
35
+ "auth",
36
+ "seed",
37
+ "stop_event",
38
+ "unique_data",
39
+ "outcome_cache",
40
+ "checks_config",
41
+ "override",
42
+ "no_failfast",
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ seed: int | None,
49
+ auth: RawAuth | None,
50
+ stop_event: threading.Event,
51
+ unique_data: bool,
52
+ checks_config: CheckConfig,
53
+ override: CaseOverride | None,
54
+ no_failfast: bool,
55
+ ) -> None:
56
+ self.data = TestResultSet(seed=seed)
57
+ self.auth = auth
58
+ self.seed = seed
59
+ self.stop_event = stop_event
60
+ self.outcome_cache = {}
61
+ self.unique_data = unique_data
62
+ self.checks_config = checks_config
63
+ self.override = override
64
+ self.no_failfast = no_failfast
65
+
66
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
67
+
68
+ @property
69
+ def is_stopped(self) -> bool:
70
+ return self.stop_event.is_set()
71
+
72
+ @property
73
+ def has_all_not_found(self) -> bool:
74
+ """Check if all responses are 404."""
75
+ has_not_found = False
76
+ for entry in self.data.results:
77
+ for check in entry.checks:
78
+ if check.response is not None:
79
+ if check.response.status_code == 404:
80
+ has_not_found = True
81
+ else:
82
+ # There are non-404 responses, no reason to check any other response
83
+ return False
84
+ # Only happens if all responses are 404, or there are no responses at all.
85
+ # In the first case, it returns True, for the latter - False
86
+ return has_not_found
87
+
88
+ def add_result(self, result: TestResult) -> None:
89
+ self.data.append(result)
90
+
91
+ def add_generic_error(self, error: OperationSchemaError) -> None:
92
+ self.data.generic_errors.append(error)
93
+
94
+ def add_warning(self, message: str) -> None:
95
+ self.data.add_warning(message)
96
+
97
+ def cache_outcome(self, case: Case, outcome: BaseException | None) -> None:
98
+ self.outcome_cache[hash(case)] = outcome
99
+
100
+ def get_cached_outcome(self, case: Case) -> BaseException | None | NotSet:
101
+ return self.outcome_cache.get(hash(case), NOT_SET)
102
+
103
+
104
+ ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"