schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,21 @@
1
1
  import os
2
2
  from dataclasses import dataclass, field
3
3
 
4
- from ..constants import TRUE_VALUES
4
+ from schemathesis.core import string_to_boolean
5
5
 
6
6
 
7
7
  @dataclass(eq=False)
8
8
  class Experiment:
9
9
  name: str
10
- verbose_name: str
11
10
  env_var: str
12
11
  description: str
13
12
  discussion_url: str
14
13
  _storage: "ExperimentSet" = field(repr=False)
15
14
 
15
+ @property
16
+ def label(self) -> str:
17
+ return self.name.lower().replace(" ", "-")
18
+
16
19
  def enable(self) -> None:
17
20
  self._storage.enable(self)
18
21
 
@@ -25,7 +28,7 @@ class Experiment:
25
28
 
26
29
  @property
27
30
  def is_env_var_set(self) -> bool:
28
- return os.getenv(self.env_var, "").lower() in TRUE_VALUES
31
+ return string_to_boolean(os.getenv(self.env_var, "")) is True
29
32
 
30
33
 
31
34
  @dataclass
@@ -33,12 +36,9 @@ class ExperimentSet:
33
36
  available: set = field(default_factory=set)
34
37
  enabled: set = field(default_factory=set)
35
38
 
36
- def create_experiment(
37
- self, name: str, verbose_name: str, env_var: str, description: str, discussion_url: str
38
- ) -> Experiment:
39
+ def create_experiment(self, name: str, env_var: str, description: str, discussion_url: str) -> Experiment:
39
40
  instance = Experiment(
40
41
  name=name,
41
- verbose_name=verbose_name,
42
42
  env_var=f"{ENV_PREFIX}_{env_var}",
43
43
  description=description,
44
44
  discussion_url=discussion_url,
@@ -64,11 +64,15 @@ class ExperimentSet:
64
64
 
65
65
  ENV_PREFIX = "SCHEMATHESIS_EXPERIMENTAL"
66
66
  GLOBAL_EXPERIMENTS = ExperimentSet()
67
-
68
- OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
69
- name="openapi-3.1",
70
- verbose_name="OpenAPI 3.1",
71
- env_var="OPENAPI_3_1",
72
- description="Support for response validation",
73
- discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
67
+ COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
68
+ name="Coverage phase",
69
+ env_var="COVERAGE_PHASE",
70
+ description="Generate covering test cases",
71
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
72
+ )
73
+ POSITIVE_DATA_ACCEPTANCE = GLOBAL_EXPERIMENTS.create_experiment(
74
+ name="Positive Data Acceptance",
75
+ env_var="POSITIVE_DATA_ACCEPTANCE",
76
+ description="Verifying schema-conformant data is accepted",
77
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2499",
74
78
  )
schemathesis/filters.py CHANGED
@@ -1,15 +1,19 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
+
2
3
  from __future__ import annotations
4
+
5
+ import json
3
6
  import re
4
7
  from dataclasses import dataclass, field
5
8
  from functools import partial
6
9
  from types import SimpleNamespace
7
- from typing import TYPE_CHECKING, Callable, List, Union, Protocol
10
+ from typing import TYPE_CHECKING, Any, Callable, List, Protocol, Union
8
11
 
9
- from .exceptions import UsageError
12
+ from schemathesis.core.errors import IncorrectUsage
13
+ from schemathesis.core.transforms import resolve_pointer
10
14
 
11
15
  if TYPE_CHECKING:
12
- from .models import APIOperation
16
+ from schemathesis.schemas import APIOperation
13
17
 
14
18
 
15
19
  class HasAPIOperation(Protocol):
@@ -49,16 +53,21 @@ class Matcher:
49
53
  func = partial(by_value_list, attribute=attribute, expected=expected)
50
54
  else:
51
55
  func = partial(by_value, attribute=attribute, expected=expected)
52
- label = f"{attribute}={repr(expected)}"
56
+ label = f"{attribute}={expected!r}"
53
57
  return cls(func, label=label, _hash=hash(label))
54
58
 
55
59
  @classmethod
56
60
  def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
57
61
  """Matcher that checks whether the specified attribute has the provided regex."""
58
62
  if isinstance(regex, str):
59
- regex = re.compile(regex)
63
+ flags: re.RegexFlag | int
64
+ if attribute == "method":
65
+ flags = re.IGNORECASE
66
+ else:
67
+ flags = 0
68
+ regex = re.compile(regex, flags=flags)
60
69
  func = partial(by_regex, attribute=attribute, regex=regex)
61
- label = f"{attribute}_regex={repr(regex)}"
70
+ label = f"{attribute}_regex={regex!r}"
62
71
  return cls(func, label=label, _hash=hash(label))
63
72
 
64
73
  def match(self, ctx: HasAPIOperation) -> bool:
@@ -69,6 +78,8 @@ class Matcher:
69
78
  def get_operation_attribute(operation: APIOperation, attribute: str) -> str | list[str] | None:
70
79
  if attribute == "tag":
71
80
  return operation.tags
81
+ if attribute == "operation_id":
82
+ return operation.definition.raw.get("operationId")
72
83
  # Just uppercase `method`
73
84
  value = getattr(operation, attribute)
74
85
  if attribute == "method":
@@ -99,8 +110,8 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
99
110
  if value is None:
100
111
  return False
101
112
  if isinstance(value, list):
102
- return any(bool(regex.match(entry)) for entry in value)
103
- return bool(regex.match(value))
113
+ return any(bool(regex.search(entry)) for entry in value)
114
+ return bool(regex.search(value))
104
115
 
105
116
 
106
117
  @dataclass(repr=False, frozen=True)
@@ -109,6 +120,8 @@ class Filter:
109
120
 
110
121
  matchers: tuple[Matcher, ...]
111
122
 
123
+ __slots__ = ("matchers",)
124
+
112
125
  def __repr__(self) -> str:
113
126
  inner = " && ".join(matcher.label for matcher in self.matchers)
114
127
  return f"<{self.__class__.__name__}: [{inner}]>"
@@ -125,8 +138,17 @@ class Filter:
125
138
  class FilterSet:
126
139
  """Combines multiple filters to apply inclusion and exclusion rules on API operations."""
127
140
 
128
- _includes: set[Filter] = field(default_factory=set)
129
- _excludes: set[Filter] = field(default_factory=set)
141
+ _includes: set[Filter]
142
+ _excludes: set[Filter]
143
+
144
+ __slots__ = ("_includes", "_excludes")
145
+
146
+ def __init__(self, _includes: set[Filter] | None = None, _excludes: set[Filter] | None = None) -> None:
147
+ self._includes = _includes or set()
148
+ self._excludes = _excludes or set()
149
+
150
+ def clone(self) -> FilterSet:
151
+ return FilterSet(_includes=self._includes.copy(), _excludes=self._excludes.copy())
130
152
 
131
153
  def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
132
154
  """Get a filtered list of the given operations that match the filters."""
@@ -166,6 +188,8 @@ class FilterSet:
166
188
  path_regex: RegexValue | None = None,
167
189
  tag: FilterValue | None = None,
168
190
  tag_regex: RegexValue | None = None,
191
+ operation_id: FilterValue | None = None,
192
+ operation_id_regex: RegexValue | None = None,
169
193
  ) -> None:
170
194
  """Add a new INCLUDE filter."""
171
195
  self._add_filter(
@@ -179,6 +203,8 @@ class FilterSet:
179
203
  path_regex=path_regex,
180
204
  tag=tag,
181
205
  tag_regex=tag_regex,
206
+ operation_id=operation_id,
207
+ operation_id_regex=operation_id_regex,
182
208
  )
183
209
 
184
210
  def exclude(
@@ -193,6 +219,8 @@ class FilterSet:
193
219
  path_regex: RegexValue | None = None,
194
220
  tag: FilterValue | None = None,
195
221
  tag_regex: RegexValue | None = None,
222
+ operation_id: FilterValue | None = None,
223
+ operation_id_regex: RegexValue | None = None,
196
224
  ) -> None:
197
225
  """Add a new EXCLUDE filter."""
198
226
  self._add_filter(
@@ -206,6 +234,8 @@ class FilterSet:
206
234
  path_regex=path_regex,
207
235
  tag=tag,
208
236
  tag_regex=tag_regex,
237
+ operation_id=operation_id,
238
+ operation_id_regex=operation_id_regex,
209
239
  )
210
240
 
211
241
  def _add_filter(
@@ -221,29 +251,32 @@ class FilterSet:
221
251
  path_regex: RegexValue | None = None,
222
252
  tag: FilterValue | None = None,
223
253
  tag_regex: RegexValue | None = None,
254
+ operation_id: FilterValue | None = None,
255
+ operation_id_regex: RegexValue | None = None,
224
256
  ) -> None:
225
257
  matchers = []
226
258
  if func is not None:
227
259
  matchers.append(Matcher.for_function(func))
228
260
  for attribute, expected, regex in (
229
- ("verbose_name", name, name_regex),
261
+ ("label", name, name_regex),
230
262
  ("method", method, method_regex),
231
263
  ("path", path, path_regex),
232
264
  ("tag", tag, tag_regex),
265
+ ("operation_id", operation_id, operation_id_regex),
233
266
  ):
234
267
  if expected is not None and regex is not None:
235
268
  # To match anything the regex should match the expected value, hence passing them together is useless
236
- raise UsageError(ERROR_EXPECTED_AND_REGEX)
269
+ raise IncorrectUsage(ERROR_EXPECTED_AND_REGEX)
237
270
  if expected is not None:
238
271
  matchers.append(Matcher.for_value(attribute, expected))
239
272
  if regex is not None:
240
273
  matchers.append(Matcher.for_regex(attribute, regex))
241
274
 
242
275
  if not matchers:
243
- raise UsageError(ERROR_EMPTY_FILTER)
276
+ raise IncorrectUsage(ERROR_EMPTY_FILTER)
244
277
  filter_ = Filter(matchers=tuple(matchers))
245
278
  if filter_ in self._includes or filter_ in self._excludes:
246
- raise UsageError(ERROR_FILTER_EXISTS)
279
+ raise IncorrectUsage(ERROR_FILTER_EXISTS)
247
280
  if include:
248
281
  self._includes.add(filter_)
249
282
  else:
@@ -274,8 +307,12 @@ def attach_filter_chain(
274
307
  name_regex: str | None = None,
275
308
  method: FilterValue | None = None,
276
309
  method_regex: str | None = None,
310
+ tag: FilterValue | None = None,
311
+ tag_regex: RegexValue | None = None,
277
312
  path: FilterValue | None = None,
278
313
  path_regex: str | None = None,
314
+ operation_id: FilterValue | None = None,
315
+ operation_id_regex: RegexValue | None = None,
279
316
  ) -> Callable:
280
317
  __tracebackhide__ = True
281
318
  filter_func(
@@ -284,8 +321,12 @@ def attach_filter_chain(
284
321
  name_regex=name_regex,
285
322
  method=method,
286
323
  method_regex=method_regex,
324
+ tag=tag,
325
+ tag_regex=tag_regex,
287
326
  path=path,
288
327
  path_regex=path_regex,
328
+ operation_id=operation_id,
329
+ operation_id_regex=operation_id_regex,
289
330
  )
290
331
  return target
291
332
 
@@ -293,3 +334,51 @@ def attach_filter_chain(
293
334
  proxy.__name__ = attribute
294
335
 
295
336
  setattr(target, attribute, proxy)
337
+
338
+
339
+ def is_deprecated(ctx: HasAPIOperation) -> bool:
340
+ return ctx.operation.definition.raw.get("deprecated") is True
341
+
342
+
343
+ def parse_expression(expression: str) -> tuple[str, str, Any]:
344
+ expression = expression.strip()
345
+
346
+ # Find the operator
347
+ for op in ("==", "!="):
348
+ try:
349
+ pointer, value = expression.split(op, 1)
350
+ break
351
+ except ValueError:
352
+ continue
353
+ else:
354
+ raise ValueError(f"Invalid expression: {expression}")
355
+
356
+ pointer = pointer.strip()
357
+ value = value.strip()
358
+ if not pointer or not value:
359
+ raise ValueError(f"Invalid expression: {expression}")
360
+ # Parse the JSON value
361
+ try:
362
+ return pointer, op, json.loads(value)
363
+ except json.JSONDecodeError:
364
+ # If it's not valid JSON, treat it as a string
365
+ return pointer, op, value
366
+
367
+
368
+ def expression_to_filter_function(expression: str) -> Callable[[HasAPIOperation], bool]:
369
+ pointer, op, value = parse_expression(expression)
370
+
371
+ if op == "==":
372
+
373
+ def filter_function(ctx: HasAPIOperation) -> bool:
374
+ definition = ctx.operation.definition.resolved
375
+ resolved = resolve_pointer(definition, pointer)
376
+ return resolved == value
377
+ else:
378
+
379
+ def filter_function(ctx: HasAPIOperation) -> bool:
380
+ definition = ctx.operation.definition.resolved
381
+ resolved = resolve_pointer(definition, pointer)
382
+ return resolved != value
383
+
384
+ return filter_function
@@ -1,46 +1,16 @@
1
1
  from __future__ import annotations
2
- import random
3
- from dataclasses import dataclass
4
- from enum import Enum
5
- from typing import Union, Iterable
6
-
7
-
8
- class DataGenerationMethod(str, Enum):
9
- """Defines what data Schemathesis generates for tests."""
10
-
11
- # Generate data, that fits the API schema
12
- positive = "positive"
13
- # Doesn't fit the API schema
14
- negative = "negative"
15
-
16
- @classmethod
17
- def default(cls) -> DataGenerationMethod:
18
- return cls.positive
19
-
20
- @classmethod
21
- def all(cls) -> list[DataGenerationMethod]:
22
- return list(DataGenerationMethod)
23
2
 
24
- def as_short_name(self) -> str:
25
- return {
26
- DataGenerationMethod.positive: "P",
27
- DataGenerationMethod.negative: "N",
28
- }[self]
29
-
30
- @property
31
- def is_negative(self) -> bool:
32
- return self == DataGenerationMethod.negative
3
+ import random
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING
33
6
 
34
- @classmethod
35
- def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
36
- if isinstance(value, DataGenerationMethod):
37
- return [value]
38
- return list(value)
7
+ from schemathesis.generation.modes import GenerationMode as GenerationMode
39
8
 
9
+ if TYPE_CHECKING:
10
+ from hypothesis.strategies import SearchStrategy
40
11
 
41
- DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
42
12
 
43
- DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
13
+ DEFAULT_GENERATOR_MODES = (GenerationMode.default(),)
44
14
 
45
15
 
46
16
  CASE_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@@ -58,11 +28,25 @@ def generate_random_case_id(length: int = 6) -> str:
58
28
  return output
59
29
 
60
30
 
31
+ @dataclass
32
+ class HeaderConfig:
33
+ """Configuration for generating headers."""
34
+
35
+ strategy: SearchStrategy[str] | None = None
36
+
37
+
61
38
  @dataclass
62
39
  class GenerationConfig:
63
40
  """Holds various configuration options relevant for data generation."""
64
41
 
42
+ modes: list[GenerationMode] = field(default_factory=lambda: [GenerationMode.default()])
65
43
  # Allow generating `\x00` bytes in strings
66
44
  allow_x00: bool = True
45
+ # Allowing using `null` for optional arguments in GraphQL queries
46
+ graphql_allow_null: bool = True
67
47
  # Generate strings using the given codec
68
48
  codec: str | None = "utf-8"
49
+ # Whether to generate security parameters
50
+ with_security_parameters: bool = True
51
+ # Header generation configuration
52
+ headers: HeaderConfig = field(default_factory=HeaderConfig)
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Mapping
5
+
6
+ from schemathesis.checks import CHECKS, CheckContext, CheckFunction, run_checks
7
+ from schemathesis.core import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER, NotSet, curl
8
+ from schemathesis.core.failures import FailureGroup, failure_report_title, format_failures
9
+ from schemathesis.core.transport import Response
10
+ from schemathesis.generation import generate_random_case_id
11
+ from schemathesis.generation.meta import CaseMetadata
12
+ from schemathesis.generation.overrides import Override, store_components
13
+ from schemathesis.hooks import HookContext, dispatch
14
+ from schemathesis.transport.prepare import prepare_request
15
+
16
+ if TYPE_CHECKING:
17
+ import requests.auth
18
+ from requests.structures import CaseInsensitiveDict
19
+
20
+ from schemathesis.schemas import APIOperation
21
+
22
+
23
+ @dataclass
24
+ class Case:
25
+ """A single test case parameters."""
26
+
27
+ operation: APIOperation
28
+ method: str
29
+ path: str
30
+ # Unique test case identifier
31
+ id: str = field(default_factory=generate_random_case_id, compare=False)
32
+ path_parameters: dict[str, Any] | None = None
33
+ headers: CaseInsensitiveDict | None = None
34
+ cookies: dict[str, Any] | None = None
35
+ query: dict[str, Any] | None = None
36
+ # By default, there is no body, but we can't use `None` as the default value because it clashes with `null`
37
+ # which is a valid payload.
38
+ body: list | dict[str, Any] | str | int | float | bool | bytes | NotSet = NOT_SET
39
+ # The media type for cases with a payload. For example, "application/json"
40
+ media_type: str | None = None
41
+
42
+ meta: CaseMetadata | None = field(compare=False, default=None)
43
+
44
+ _auth: requests.auth.AuthBase | None = None
45
+ _has_explicit_auth: bool = False
46
+
47
+ def __post_init__(self) -> None:
48
+ self._components = store_components(self)
49
+
50
+ @property
51
+ def _override(self) -> Override:
52
+ return Override.from_components(self._components, self)
53
+
54
+ def __repr__(self) -> str:
55
+ output = f"{self.__class__.__name__}("
56
+ first = True
57
+ for name in ("path_parameters", "headers", "cookies", "query", "body"):
58
+ value = getattr(self, name)
59
+ if value is not None and not isinstance(value, NotSet):
60
+ if first:
61
+ first = False
62
+ else:
63
+ output += ", "
64
+ output += f"{name}={value!r}"
65
+ return f"{output})"
66
+
67
+ def __hash__(self) -> int:
68
+ return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
69
+
70
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
71
+
72
+ def as_curl_command(self, headers: Mapping[str, Any] | None = None, verify: bool = True) -> str:
73
+ """Construct a curl command for a given case."""
74
+ request_data = prepare_request(self, headers, self.operation.schema.output_config.sanitize)
75
+ return curl.generate(
76
+ method=str(request_data.method),
77
+ url=str(request_data.url),
78
+ body=request_data.body,
79
+ verify=verify,
80
+ headers=dict(request_data.headers),
81
+ known_generated_headers=dict(self.headers or {}),
82
+ )
83
+
84
+ def as_transport_kwargs(self, base_url: str | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]:
85
+ """Convert the test case into a dictionary acceptable by the underlying transport call."""
86
+ return self.operation.schema.transport.serialize_case(self, base_url=base_url, headers=headers)
87
+
88
+ def call(
89
+ self,
90
+ base_url: str | None = None,
91
+ session: requests.Session | None = None,
92
+ headers: dict[str, Any] | None = None,
93
+ params: dict[str, Any] | None = None,
94
+ cookies: dict[str, Any] | None = None,
95
+ **kwargs: Any,
96
+ ) -> Response:
97
+ hook_context = HookContext(operation=self.operation)
98
+ dispatch("before_call", hook_context, self, **kwargs)
99
+ if self.operation.app is not None:
100
+ kwargs["app"] = self.operation.app
101
+ response = self.operation.schema.transport.send(
102
+ self,
103
+ session=session,
104
+ base_url=base_url,
105
+ headers=headers,
106
+ params=params,
107
+ cookies=cookies,
108
+ **kwargs,
109
+ )
110
+ dispatch("after_call", hook_context, self, response)
111
+ return response
112
+
113
+ def validate_response(
114
+ self,
115
+ response: Response,
116
+ checks: list[CheckFunction] | None = None,
117
+ additional_checks: list[CheckFunction] | None = None,
118
+ excluded_checks: list[CheckFunction] | None = None,
119
+ headers: dict[str, Any] | None = None,
120
+ transport_kwargs: dict[str, Any] | None = None,
121
+ ) -> None:
122
+ """Validate application response.
123
+
124
+ By default, all available checks will be applied.
125
+
126
+ :param response: Application response.
127
+ :param checks: A tuple of check functions that accept ``response`` and ``case``.
128
+ :param additional_checks: A tuple of additional checks that will be executed after ones from the ``checks``
129
+ argument.
130
+ :param excluded_checks: Checks excluded from the default ones.
131
+ """
132
+ __tracebackhide__ = True
133
+ from requests.structures import CaseInsensitiveDict
134
+
135
+ checks = [
136
+ check
137
+ for check in list(checks or CHECKS.get_all()) + list(additional_checks or [])
138
+ if check not in set(excluded_checks or [])
139
+ ]
140
+
141
+ ctx = CheckContext(
142
+ override=self._override,
143
+ auth=None,
144
+ headers=CaseInsensitiveDict(headers) if headers else None,
145
+ config={},
146
+ transport_kwargs=transport_kwargs,
147
+ recorder=None,
148
+ )
149
+ failures = run_checks(
150
+ case=self,
151
+ response=response,
152
+ ctx=ctx,
153
+ checks=checks,
154
+ on_failure=lambda _, collected, failure: collected.add(failure),
155
+ )
156
+ if failures:
157
+ _failures = list(failures)
158
+ message = failure_report_title(_failures) + "\n"
159
+ verify = getattr(response, "verify", True)
160
+ curl = self.as_curl_command(headers=dict(response.request.headers), verify=verify)
161
+ message += format_failures(
162
+ case_id=None,
163
+ response=response,
164
+ failures=_failures,
165
+ curl=curl,
166
+ config=self.operation.schema.output_config,
167
+ )
168
+ raise FailureGroup(_failures, message) from None
169
+
170
+ def call_and_validate(
171
+ self,
172
+ base_url: str | None = None,
173
+ session: requests.Session | None = None,
174
+ headers: dict[str, Any] | None = None,
175
+ checks: list[CheckFunction] | None = None,
176
+ additional_checks: list[CheckFunction] | None = None,
177
+ excluded_checks: list[CheckFunction] | None = None,
178
+ **kwargs: Any,
179
+ ) -> Response:
180
+ __tracebackhide__ = True
181
+ response = self.call(base_url, session, headers, **kwargs)
182
+ self.validate_response(
183
+ response,
184
+ checks,
185
+ headers=headers,
186
+ additional_checks=additional_checks,
187
+ excluded_checks=excluded_checks,
188
+ transport_kwargs=kwargs,
189
+ )
190
+ return response