schemathesis 3.39.15__py3-none-any.whl → 4.0.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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,8 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- from functools import lru_cache, reduce
5
- from operator import or_
4
+ from functools import lru_cache
6
5
  from typing import TYPE_CHECKING, TypeVar
7
6
 
8
7
  if TYPE_CHECKING:
@@ -29,7 +28,7 @@ def default_settings() -> settings:
29
28
  T = TypeVar("T")
30
29
 
31
30
 
32
- def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
31
+ def generate_one(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
33
32
  examples: list[T] = []
34
33
  add_single_example(strategy, examples)
35
34
  return examples[0]
@@ -49,11 +48,3 @@ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> Non
49
48
  example_generating_inner_function = seed(SCHEMATHESIS_BENCHMARK_SEED)(example_generating_inner_function)
50
49
 
51
50
  example_generating_inner_function()
52
-
53
-
54
- def combine_strategies(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
55
- """Combine a list of strategies into a single one.
56
-
57
- If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
58
- """
59
- return reduce(or_, strategies[1:], strategies[0])
@@ -0,0 +1,66 @@
1
+ """Integrating `hypothesis.given` into Schemathesis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from inspect import getfullargspec
6
+ from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union
7
+
8
+ from schemathesis.core.errors import IncorrectUsage
9
+ from schemathesis.core.marks import Mark
10
+
11
+ if TYPE_CHECKING:
12
+ from hypothesis.strategies import SearchStrategy
13
+
14
+
15
+ __all__ = ["is_given_applied", "given_proxy", "merge_given_args", "GivenInput", "GivenArgsMark", "GivenKwargsMark"]
16
+
17
+ EllipsisType = type(...)
18
+ GivenInput = Union["SearchStrategy", EllipsisType] # type: ignore[valid-type]
19
+
20
+ GivenArgsMark = Mark[tuple](attr_name="given_args", default=())
21
+ GivenKwargsMark = Mark[dict[str, Any]](attr_name="given_kwargs", default=dict)
22
+
23
+
24
+ def is_given_applied(func: Callable) -> bool:
25
+ return GivenArgsMark.is_set(func) or GivenKwargsMark.is_set(func)
26
+
27
+
28
+ def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[Callable], Callable]:
29
+ """Proxy Hypothesis strategies to ``hypothesis.given``."""
30
+
31
+ def wrapper(func: Callable) -> Callable:
32
+ if is_given_applied(func):
33
+
34
+ def wrapped_test(*_: Any, **__: Any) -> NoReturn:
35
+ raise IncorrectUsage(
36
+ f"You have applied `given` to the `{func.__name__}` test more than once, which "
37
+ "overrides the previous decorator. You need to pass all arguments to the same `given` call."
38
+ )
39
+
40
+ return wrapped_test
41
+
42
+ GivenArgsMark.set(func, args)
43
+ GivenKwargsMark.set(func, kwargs)
44
+ return func
45
+
46
+ return wrapper
47
+
48
+
49
+ def merge_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
50
+ """Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
51
+
52
+ Kwargs are modified inplace.
53
+ """
54
+ if args:
55
+ argspec = getfullargspec(func)
56
+ for name, strategy in zip(reversed([arg for arg in argspec.args if arg != "case"]), reversed(args)):
57
+ kwargs[name] = strategy
58
+ return kwargs
59
+
60
+
61
+ def validate_given_args(func: Callable, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
62
+ from hypothesis.core import is_invalid_test
63
+ from hypothesis.internal.reflection import get_signature
64
+
65
+ signature = get_signature(func)
66
+ return is_invalid_test(func, signature, args, kwargs) # type: ignore
@@ -0,0 +1,14 @@
1
+ from contextlib import contextmanager
2
+ from typing import Generator
3
+
4
+ from hypothesis.reporting import with_reporter
5
+
6
+
7
+ def ignore(_: str) -> None:
8
+ pass
9
+
10
+
11
+ @contextmanager
12
+ def ignore_hypothesis_output() -> Generator:
13
+ with with_reporter(ignore): # type: ignore
14
+ yield
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import reduce
4
+ from operator import or_
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis import strategies as st
9
+
10
+
11
+ def combine(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
12
+ """Combine a list of strategies into a single one.
13
+
14
+ If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
15
+ """
16
+ return reduce(or_, strategies[1:], strategies[0])
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from schemathesis.generation import GenerationMode
7
+
8
+
9
+ class TestPhase(str, Enum):
10
+ __test__ = False
11
+
12
+ EXAMPLES = "examples"
13
+ COVERAGE = "coverage"
14
+ FUZZING = "fuzzing"
15
+
16
+
17
+ class ComponentKind(str, Enum):
18
+ """Components that can be generated."""
19
+
20
+ QUERY = "query"
21
+ PATH_PARAMETERS = "path_parameters"
22
+ HEADERS = "headers"
23
+ COOKIES = "cookies"
24
+ BODY = "body"
25
+
26
+
27
+ @dataclass
28
+ class ComponentInfo:
29
+ """Information about how a specific component was generated."""
30
+
31
+ mode: GenerationMode
32
+
33
+ __slots__ = ("mode",)
34
+
35
+
36
+ @dataclass
37
+ class GeneratePhaseData:
38
+ """Metadata specific to generate phase."""
39
+
40
+
41
+ @dataclass
42
+ class ExplicitPhaseData:
43
+ """Metadata specific to explicit phase."""
44
+
45
+
46
+ @dataclass
47
+ class CoveragePhaseData:
48
+ """Metadata specific to coverage phase."""
49
+
50
+ description: str
51
+ location: str | None
52
+ parameter: str | None
53
+ parameter_location: str | None
54
+
55
+ __slots__ = ("description", "location", "parameter", "parameter_location")
56
+
57
+
58
+ @dataclass
59
+ class PhaseInfo:
60
+ """Phase-specific information."""
61
+
62
+ name: TestPhase
63
+ data: CoveragePhaseData | ExplicitPhaseData | GeneratePhaseData
64
+
65
+ __slots__ = ("name", "data")
66
+
67
+ @classmethod
68
+ def coverage(
69
+ cls,
70
+ description: str,
71
+ location: str | None = None,
72
+ parameter: str | None = None,
73
+ parameter_location: str | None = None,
74
+ ) -> PhaseInfo:
75
+ return cls(
76
+ name=TestPhase.COVERAGE,
77
+ data=CoveragePhaseData(
78
+ description=description, location=location, parameter=parameter, parameter_location=parameter_location
79
+ ),
80
+ )
81
+
82
+ @classmethod
83
+ def generate(cls) -> PhaseInfo:
84
+ return cls(name=TestPhase.FUZZING, data=GeneratePhaseData())
85
+
86
+
87
+ @dataclass
88
+ class GenerationInfo:
89
+ """Information about test case generation."""
90
+
91
+ time: float
92
+ mode: GenerationMode
93
+
94
+ __slots__ = ("time", "mode")
95
+
96
+
97
+ @dataclass
98
+ class CaseMetadata:
99
+ """Complete metadata for generated cases."""
100
+
101
+ generation: GenerationInfo
102
+ components: dict[ComponentKind, ComponentInfo]
103
+ phase: PhaseInfo
104
+
105
+ __slots__ = ("generation", "components", "phase")
106
+
107
+ def __init__(
108
+ self,
109
+ generation: GenerationInfo,
110
+ components: dict[ComponentKind, ComponentInfo],
111
+ phase: PhaseInfo,
112
+ ) -> None:
113
+ self.generation = generation
114
+ self.components = components
115
+ self.phase = phase
@@ -0,0 +1,93 @@
1
+ """Support for Targeted Property-Based Testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Sequence
7
+
8
+ from schemathesis.core.registries import Registry
9
+ from schemathesis.core.transport import Response
10
+ from schemathesis.generation.case import Case
11
+
12
+
13
+ @dataclass
14
+ class MetricContext:
15
+ """Context for evaluating a metric on a single test execution.
16
+
17
+ This object bundles together the test `case` that was sent and
18
+ the corresponding HTTP `response`. Metric functions receive an
19
+ instance of `MetricContext` to compute a numeric score.
20
+ """
21
+
22
+ case: Case
23
+ """Generated test case."""
24
+ response: Response
25
+ """The HTTP response returned by the server for this test case."""
26
+
27
+ __slots__ = ("case", "response")
28
+
29
+
30
+ MetricFunction = Callable[[MetricContext], float]
31
+
32
+ METRICS = Registry[MetricFunction]()
33
+
34
+
35
+ def metric(func: MetricFunction) -> MetricFunction:
36
+ """Decorator to register a custom metric for targeted property-based testing.
37
+
38
+ Example:
39
+ ```python
40
+ import schemathesis
41
+
42
+ @schemathesis.metric
43
+ def response_size(ctx: schemathesis.MetricContext) -> float:
44
+ return float(len(ctx.response.content))
45
+ ```
46
+
47
+ """
48
+ return METRICS.register(func)
49
+
50
+
51
+ @metric
52
+ def response_time(ctx: MetricContext) -> float:
53
+ """Response time as a metric to maximize."""
54
+ return ctx.response.elapsed
55
+
56
+
57
+ class MetricCollector:
58
+ """Collect multiple observations for metrics."""
59
+
60
+ __slots__ = ("metrics", "observations")
61
+
62
+ def __init__(self, metrics: list[MetricFunction] | None = None) -> None:
63
+ self.metrics = metrics or []
64
+ self.observations: dict[str, list[float]] = {metric.__name__: [] for metric in self.metrics}
65
+
66
+ def reset(self) -> None:
67
+ """Reset all collected observations."""
68
+ for metric in self.metrics:
69
+ self.observations[metric.__name__].clear()
70
+
71
+ def store(self, case: Case, response: Response) -> None:
72
+ """Calculate metrics & store them."""
73
+ ctx = MetricContext(case=case, response=response)
74
+ for metric in self.metrics:
75
+ self.observations[metric.__name__].append(metric(ctx))
76
+
77
+ def maximize(self) -> None:
78
+ """Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
79
+ import hypothesis
80
+
81
+ for metric in self.metrics:
82
+ # Currently aggregation is just a sum
83
+ value = sum(self.observations[metric.__name__])
84
+ hypothesis.target(value, label=metric.__name__)
85
+
86
+
87
+ def maximize(metrics: Sequence[MetricFunction], case: Case, response: Response) -> None:
88
+ import hypothesis
89
+
90
+ ctx = MetricContext(case=case, response=response)
91
+ for metric in metrics:
92
+ value = metric(ctx)
93
+ hypothesis.target(value, label=metric.__name__)
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class GenerationMode(str, Enum):
7
+ """Defines what data Schemathesis generates for tests."""
8
+
9
+ # Generate data, that fits the API schema
10
+ POSITIVE = "positive"
11
+ # Doesn't fit the API schema
12
+ NEGATIVE = "negative"
13
+
14
+ @property
15
+ def is_positive(self) -> bool:
16
+ return self == GenerationMode.POSITIVE
17
+
18
+ @property
19
+ def is_negative(self) -> bool:
20
+ return self == GenerationMode.NEGATIVE
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Iterator
6
+
7
+ from schemathesis.config import ProjectConfig
8
+ from schemathesis.core.transforms import diff
9
+ from schemathesis.generation.meta import ComponentKind
10
+
11
+ if TYPE_CHECKING:
12
+ from schemathesis.generation.case import Case
13
+ from schemathesis.schemas import APIOperation, Parameter
14
+
15
+
16
+ @dataclass
17
+ class Override:
18
+ """Overrides for various parts of a test case."""
19
+
20
+ query: dict[str, str]
21
+ headers: dict[str, str]
22
+ cookies: dict[str, str]
23
+ path_parameters: dict[str, str]
24
+
25
+ def items(self) -> Iterator[tuple[str, dict[str, str]]]:
26
+ for key, value in (
27
+ ("query", self.query),
28
+ ("headers", self.headers),
29
+ ("cookies", self.cookies),
30
+ ("path_parameters", self.path_parameters),
31
+ ):
32
+ if value:
33
+ yield key, value
34
+
35
+ @classmethod
36
+ def from_components(cls, components: dict[ComponentKind, StoredValue], case: Case) -> Override:
37
+ return Override(
38
+ **{
39
+ kind.value: get_component_diff(stored=stored, current=getattr(case, kind.value))
40
+ for kind, stored in components.items()
41
+ }
42
+ )
43
+
44
+
45
+ def for_operation(config: ProjectConfig, *, operation: APIOperation) -> Override:
46
+ operation_config = config.operations.get_for_operation(operation)
47
+
48
+ output = Override(query={}, headers={}, cookies={}, path_parameters={})
49
+ groups = [
50
+ (output.query, operation.query),
51
+ (output.headers, operation.headers),
52
+ (output.cookies, operation.cookies),
53
+ (output.path_parameters, operation.path_parameters),
54
+ ]
55
+ for container, params in groups:
56
+ for param in params:
57
+ # Attempt to get the override from the operation-specific configuration.
58
+ value = None
59
+ if operation_config:
60
+ value = _get_override_value(param, operation_config.parameters)
61
+ # Fallback to the global project configuration.
62
+ if value is None:
63
+ value = _get_override_value(param, config.parameters)
64
+ if value is not None:
65
+ container[param.name] = value
66
+
67
+ return output
68
+
69
+
70
+ def _get_override_value(param: Parameter, parameters: dict[str, Any]) -> Any:
71
+ key = param.name
72
+ full_key = f"{param.location}.{param.name}"
73
+ if key in parameters:
74
+ return parameters[key]
75
+ elif full_key in parameters:
76
+ return parameters[full_key]
77
+ return None
78
+
79
+
80
+ @dataclass
81
+ class StoredValue:
82
+ value: dict[str, Any] | None
83
+ is_generated: bool
84
+
85
+ __slots__ = ("value", "is_generated")
86
+
87
+
88
+ def store_original_state(value: dict[str, Any] | None) -> dict[str, Any] | None:
89
+ if isinstance(value, Mapping):
90
+ return value.copy()
91
+ return value
92
+
93
+
94
+ def get_component_diff(stored: StoredValue, current: dict[str, Any] | None) -> dict[str, Any]:
95
+ """Calculate difference between stored and current components."""
96
+ if not (current and stored.value):
97
+ return {}
98
+ if stored.is_generated:
99
+ return diff(stored.value, current)
100
+ return current
101
+
102
+
103
+ def store_components(case: Case) -> dict[ComponentKind, StoredValue]:
104
+ """Store original component states for a test case."""
105
+ return {
106
+ kind: StoredValue(
107
+ value=store_original_state(getattr(case, kind.value)),
108
+ is_generated=bool(case.meta and kind in case.meta.components),
109
+ )
110
+ for kind in [
111
+ ComponentKind.QUERY,
112
+ ComponentKind.HEADERS,
113
+ ComponentKind.COOKIES,
114
+ ComponentKind.PATH_PARAMETERS,
115
+ ]
116
+ }
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ import hypothesis
7
+
8
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
9
+
10
+ __all__ = [
11
+ "APIStateMachine",
12
+ ]
13
+
14
+
15
+ def __getattr__(name: str) -> type[APIStateMachine]:
16
+ if name == "APIStateMachine":
17
+ from schemathesis.generation.stateful.state_machine import APIStateMachine
18
+
19
+ return APIStateMachine
20
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
21
+
22
+
23
+ STATEFUL_TESTS_LABEL = "Stateful tests"
24
+
25
+
26
+ def run_state_machine_as_test(
27
+ state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
28
+ ) -> None:
29
+ """Run a state machine as a test.
30
+
31
+ It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
32
+ """
33
+ from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
34
+
35
+ __tracebackhide__ = True
36
+
37
+ return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)