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
schemathesis/schemas.py CHANGED
@@ -1,17 +1,11 @@
1
- """Schema objects provide a convenient interface to raw schemas.
2
-
3
- Their responsibilities:
4
- - Provide a unified way to work with different types of schemas
5
- - Give all paths / methods combinations that are available directly from the schema;
6
-
7
- They give only static definitions of paths.
8
- """
9
1
  from __future__ import annotations
10
- from collections.abc import Mapping, MutableMapping
2
+
3
+ from collections.abc import Mapping
11
4
  from contextlib import nullcontext
12
5
  from dataclasses import dataclass, field
13
6
  from functools import lru_cache
14
7
  from typing import (
8
+ TYPE_CHECKING,
15
9
  Any,
16
10
  Callable,
17
11
  ContextManager,
@@ -21,45 +15,58 @@ from typing import (
21
15
  NoReturn,
22
16
  Sequence,
23
17
  TypeVar,
24
- TYPE_CHECKING,
25
18
  )
26
19
  from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
27
20
 
28
- import hypothesis
29
- from hypothesis.strategies import SearchStrategy
30
- from pyrate_limiter import Limiter
31
-
32
- from .constants import NOT_SET
21
+ from ._dependency_versions import IS_PYRATE_LIMITER_ABOVE_3
33
22
  from ._hypothesis import create_test
34
23
  from .auths import AuthStorage
35
24
  from .code_samples import CodeSampleStyle
25
+ from .constants import NOT_SET
26
+ from .exceptions import OperationSchemaError, UsageError
27
+ from .filters import (
28
+ FilterSet,
29
+ FilterValue,
30
+ MatcherFunc,
31
+ RegexValue,
32
+ filter_set_from_components,
33
+ is_deprecated,
34
+ )
36
35
  from .generation import (
37
36
  DEFAULT_DATA_GENERATION_METHODS,
38
37
  DataGenerationMethod,
39
38
  DataGenerationMethodInput,
40
39
  GenerationConfig,
40
+ combine_strategies,
41
41
  )
42
- from .exceptions import OperationSchemaError, UsageError
43
- from .hooks import HookContext, HookDispatcher, HookScope, dispatch
44
- from .internal.result import Result, Ok
42
+ from .hooks import HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
43
+ from .internal.deprecation import warn_filtration_arguments
44
+ from .internal.output import OutputConfig
45
+ from .internal.result import Ok, Result
45
46
  from .models import APIOperation, Case
46
- from .stateful.state_machine import APIStateMachine
47
- from .stateful import Stateful, StatefulTest
48
- from .types import (
49
- Body,
50
- Cookies,
51
- Filter,
52
- FormData,
53
- GenericTest,
54
- Headers,
55
- NotSet,
56
- PathParameters,
57
- Query,
58
- )
59
- from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy, combine_strategies
47
+ from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
60
48
 
61
49
  if TYPE_CHECKING:
50
+ import hypothesis
51
+ from hypothesis.strategies import SearchStrategy
52
+ from pyrate_limiter import Limiter
53
+
54
+ from .stateful import Stateful, StatefulTest
55
+ from .stateful.state_machine import APIStateMachine
56
+ from .transports import Transport
62
57
  from .transports.responses import GenericResponse
58
+ from .types import (
59
+ Body,
60
+ Cookies,
61
+ Filter,
62
+ FormData,
63
+ GenericTest,
64
+ Headers,
65
+ NotSet,
66
+ PathParameters,
67
+ Query,
68
+ Specification,
69
+ )
63
70
 
64
71
 
65
72
  C = TypeVar("C", bound=Case)
@@ -73,41 +80,118 @@ def get_full_path(base_path: str, path: str) -> str:
73
80
  @dataclass(eq=False)
74
81
  class BaseSchema(Mapping):
75
82
  raw_schema: dict[str, Any]
83
+ transport: Transport
84
+ specification: Specification
76
85
  location: str | None = None
77
86
  base_url: str | None = None
78
- method: Filter | None = None
79
- endpoint: Filter | None = None
80
- tag: Filter | None = None
81
- operation_id: Filter | None = None
87
+ filter_set: FilterSet = field(default_factory=FilterSet)
82
88
  app: Any = None
83
89
  hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
84
90
  auth: AuthStorage = field(default_factory=AuthStorage)
85
91
  test_function: GenericTest | None = None
86
92
  validate_schema: bool = True
87
- skip_deprecated_operations: bool = False
88
93
  data_generation_methods: list[DataGenerationMethod] = field(
89
94
  default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
90
95
  )
91
96
  generation_config: GenerationConfig = field(default_factory=GenerationConfig)
97
+ output_config: OutputConfig = field(default_factory=OutputConfig)
92
98
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
93
99
  rate_limiter: Limiter | None = None
94
100
  sanitize_output: bool = True
95
101
 
102
+ def __post_init__(self) -> None:
103
+ self.hook = to_filterable_hook(self.hooks) # type: ignore[method-assign]
104
+
105
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
106
+
107
+ def include(
108
+ self,
109
+ func: MatcherFunc | None = None,
110
+ *,
111
+ name: FilterValue | None = None,
112
+ name_regex: str | None = None,
113
+ method: FilterValue | None = None,
114
+ method_regex: str | None = None,
115
+ path: FilterValue | None = None,
116
+ path_regex: str | None = None,
117
+ tag: FilterValue | None = None,
118
+ tag_regex: RegexValue | None = None,
119
+ operation_id: FilterValue | None = None,
120
+ operation_id_regex: RegexValue | None = None,
121
+ ) -> BaseSchema:
122
+ """Include only operations that match the given filters."""
123
+ filter_set = self.filter_set.clone()
124
+ filter_set.include(
125
+ func,
126
+ name=name,
127
+ name_regex=name_regex,
128
+ method=method,
129
+ method_regex=method_regex,
130
+ path=path,
131
+ path_regex=path_regex,
132
+ tag=tag,
133
+ tag_regex=tag_regex,
134
+ operation_id=operation_id,
135
+ operation_id_regex=operation_id_regex,
136
+ )
137
+ return self.clone(filter_set=filter_set)
138
+
139
+ def exclude(
140
+ self,
141
+ func: MatcherFunc | None = None,
142
+ *,
143
+ name: FilterValue | None = None,
144
+ name_regex: str | None = None,
145
+ method: FilterValue | None = None,
146
+ method_regex: str | None = None,
147
+ path: FilterValue | None = None,
148
+ path_regex: str | None = None,
149
+ tag: FilterValue | None = None,
150
+ tag_regex: RegexValue | None = None,
151
+ operation_id: FilterValue | None = None,
152
+ operation_id_regex: RegexValue | None = None,
153
+ deprecated: bool = False,
154
+ ) -> BaseSchema:
155
+ """Include only operations that match the given filters."""
156
+ filter_set = self.filter_set.clone()
157
+ if deprecated:
158
+ if func is None:
159
+ func = is_deprecated
160
+ else:
161
+ filter_set.exclude(is_deprecated)
162
+ filter_set.exclude(
163
+ func,
164
+ name=name,
165
+ name_regex=name_regex,
166
+ method=method,
167
+ method_regex=method_regex,
168
+ path=path,
169
+ path_regex=path_regex,
170
+ tag=tag,
171
+ tag_regex=tag_regex,
172
+ operation_id=operation_id,
173
+ operation_id_regex=operation_id_regex,
174
+ )
175
+ return self.clone(filter_set=filter_set)
176
+
96
177
  def __iter__(self) -> Iterator[str]:
97
- return iter(self.operations)
178
+ raise NotImplementedError
98
179
 
99
180
  def __getitem__(self, item: str) -> APIOperationMap:
100
181
  __tracebackhide__ = True
101
182
  try:
102
- return self.operations[item]
183
+ return self._get_operation_map(item)
103
184
  except KeyError as exc:
104
185
  self.on_missing_operation(item, exc)
105
186
 
187
+ def _get_operation_map(self, key: str) -> APIOperationMap:
188
+ raise NotImplementedError
189
+
106
190
  def on_missing_operation(self, item: str, exc: KeyError) -> NoReturn:
107
191
  raise NotImplementedError
108
192
 
109
193
  def __len__(self) -> int:
110
- return len(self.operations)
194
+ return self.operations_count
111
195
 
112
196
  def hook(self, hook: str | Callable) -> Callable:
113
197
  return self.hooks.register(hook)
@@ -150,18 +234,6 @@ class BaseSchema(Mapping):
150
234
  def validate(self) -> None:
151
235
  raise NotImplementedError
152
236
 
153
- @property
154
- def operations(self) -> dict[str, APIOperationMap]:
155
- if not hasattr(self, "_operations"):
156
- operations = self.get_all_operations()
157
- self._operations = self._store_operations(operations)
158
- return self._operations
159
-
160
- def _store_operations(
161
- self, operations: Generator[Result[APIOperation, OperationSchemaError], None, None]
162
- ) -> dict[str, APIOperationMap]:
163
- raise NotImplementedError
164
-
165
237
  @property
166
238
  def operations_count(self) -> int:
167
239
  raise NotImplementedError
@@ -171,11 +243,13 @@ class BaseSchema(Mapping):
171
243
  raise NotImplementedError
172
244
 
173
245
  def get_all_operations(
174
- self, hooks: HookDispatcher | None = None
246
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
175
247
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
176
248
  raise NotImplementedError
177
249
 
178
- def get_strategies_from_examples(self, operation: APIOperation) -> list[SearchStrategy[Case]]:
250
+ def get_strategies_from_examples(
251
+ self, operation: APIOperation, as_strategy_kwargs: dict[str, Any] | None = None
252
+ ) -> list[SearchStrategy[Case]]:
179
253
  """Get examples from the API operation."""
180
254
  raise NotImplementedError
181
255
 
@@ -204,7 +278,7 @@ class BaseSchema(Mapping):
204
278
  _given_kwargs: dict[str, GivenInput] | None = None,
205
279
  ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
206
280
  """Generate all operations and Hypothesis tests for them."""
207
- for result in self.get_all_operations(hooks=hooks):
281
+ for result in self.get_all_operations(hooks=hooks, generation_config=generation_config):
208
282
  if isinstance(result, Ok):
209
283
  operation = result.ok()
210
284
  _as_strategy_kwargs: dict[str, Any] | None
@@ -242,6 +316,21 @@ class BaseSchema(Mapping):
242
316
  CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
243
317
  )
244
318
 
319
+ for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
320
+ value = locals()[name]
321
+ if value is not NOT_SET:
322
+ warn_filtration_arguments(name)
323
+
324
+ filter_set = filter_set_from_components(
325
+ include=True,
326
+ method=method,
327
+ endpoint=endpoint,
328
+ tag=tag,
329
+ operation_id=operation_id,
330
+ skip_deprecated_operations=skip_deprecated_operations,
331
+ parent=self.filter_set,
332
+ )
333
+
245
334
  def wrapper(func: GenericTest) -> GenericTest:
246
335
  if hasattr(func, PARAMETRIZE_MARKER):
247
336
 
@@ -256,13 +345,9 @@ class BaseSchema(Mapping):
256
345
  HookDispatcher.add_dispatcher(func)
257
346
  cloned = self.clone(
258
347
  test_function=func,
259
- method=method,
260
- endpoint=endpoint,
261
- tag=tag,
262
- operation_id=operation_id,
263
348
  validate_schema=validate_schema,
264
- skip_deprecated_operations=skip_deprecated_operations,
265
349
  data_generation_methods=data_generation_methods,
350
+ filter_set=filter_set,
266
351
  code_sample_style=_code_sample_style, # type: ignore
267
352
  )
268
353
  setattr(func, PARAMETRIZE_MARKER, cloned)
@@ -279,37 +364,26 @@ class BaseSchema(Mapping):
279
364
  *,
280
365
  base_url: str | None | NotSet = NOT_SET,
281
366
  test_function: GenericTest | None = None,
282
- method: Filter | None = NOT_SET,
283
- endpoint: Filter | None = NOT_SET,
284
- tag: Filter | None = NOT_SET,
285
- operation_id: Filter | None = NOT_SET,
286
367
  app: Any = NOT_SET,
287
368
  hooks: HookDispatcher | NotSet = NOT_SET,
288
369
  auth: AuthStorage | NotSet = NOT_SET,
289
370
  validate_schema: bool | NotSet = NOT_SET,
290
- skip_deprecated_operations: bool | NotSet = NOT_SET,
291
371
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
292
372
  generation_config: GenerationConfig | NotSet = NOT_SET,
373
+ output_config: OutputConfig | NotSet = NOT_SET,
293
374
  code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
294
375
  rate_limiter: Limiter | None = NOT_SET,
295
376
  sanitize_output: bool | NotSet | None = NOT_SET,
377
+ filter_set: FilterSet | None = None,
296
378
  ) -> BaseSchema:
297
379
  if base_url is NOT_SET:
298
380
  base_url = self.base_url
299
- if method is NOT_SET:
300
- method = self.method
301
- if endpoint is NOT_SET:
302
- endpoint = self.endpoint
303
- if tag is NOT_SET:
304
- tag = self.tag
305
- if operation_id is NOT_SET:
306
- operation_id = self.operation_id
307
381
  if app is NOT_SET:
308
382
  app = self.app
309
383
  if validate_schema is NOT_SET:
310
384
  validate_schema = self.validate_schema
311
- if skip_deprecated_operations is NOT_SET:
312
- skip_deprecated_operations = self.skip_deprecated_operations
385
+ if filter_set is None:
386
+ filter_set = self.filter_set
313
387
  if hooks is NOT_SET:
314
388
  hooks = self.hooks
315
389
  if auth is NOT_SET:
@@ -318,6 +392,8 @@ class BaseSchema(Mapping):
318
392
  data_generation_methods = self.data_generation_methods
319
393
  if generation_config is NOT_SET:
320
394
  generation_config = self.generation_config
395
+ if output_config is NOT_SET:
396
+ output_config = self.output_config
321
397
  if code_sample_style is NOT_SET:
322
398
  code_sample_style = self.code_sample_style
323
399
  if rate_limiter is NOT_SET:
@@ -327,23 +403,22 @@ class BaseSchema(Mapping):
327
403
 
328
404
  return self.__class__(
329
405
  self.raw_schema,
406
+ specification=self.specification,
330
407
  location=self.location,
331
408
  base_url=base_url, # type: ignore
332
- method=method,
333
- endpoint=endpoint,
334
- tag=tag,
335
- operation_id=operation_id,
336
409
  app=app,
337
410
  hooks=hooks, # type: ignore
338
411
  auth=auth, # type: ignore
339
412
  test_function=test_function,
340
413
  validate_schema=validate_schema, # type: ignore
341
- skip_deprecated_operations=skip_deprecated_operations, # type: ignore
342
414
  data_generation_methods=data_generation_methods, # type: ignore
343
415
  generation_config=generation_config, # type: ignore
416
+ output_config=output_config, # type: ignore
344
417
  code_sample_style=code_sample_style, # type: ignore
345
418
  rate_limiter=rate_limiter, # type: ignore
346
419
  sanitize_output=sanitize_output, # type: ignore
420
+ filter_set=filter_set, # type: ignore
421
+ transport=self.transport,
347
422
  )
348
423
 
349
424
  def get_local_hook_dispatcher(self) -> HookDispatcher | None:
@@ -400,10 +475,7 @@ class BaseSchema(Mapping):
400
475
  raise NotImplementedError
401
476
 
402
477
  def as_state_machine(self) -> type[APIStateMachine]:
403
- """Create a state machine class.
404
-
405
- Use it for stateful testing.
406
- """
478
+ """Create a state machine class."""
407
479
  raise NotImplementedError
408
480
 
409
481
  def get_links(self, operation: APIOperation) -> dict[str, dict[str, Any]]:
@@ -422,7 +494,10 @@ class BaseSchema(Mapping):
422
494
  """Limit the rate of sending generated requests."""
423
495
  label = urlparse(self.base_url).netloc
424
496
  if self.rate_limiter is not None:
425
- return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
497
+ if IS_PYRATE_LIMITER_ABOVE_3:
498
+ self.rate_limiter.try_acquire(label)
499
+ else:
500
+ return self.rate_limiter.ratelimit(label, delay=True, max_delay=0)
426
501
  return nullcontext()
427
502
 
428
503
  def _get_payload_schema(self, definition: dict[str, Any], media_type: str) -> dict[str, Any] | None:
@@ -437,39 +512,33 @@ class BaseSchema(Mapping):
437
512
  **kwargs: Any,
438
513
  ) -> SearchStrategy:
439
514
  """Build a strategy for generating test cases for all defined API operations."""
440
- assert len(self.operations) > 0, "No API operations found"
441
515
  strategies = [
442
- operation.as_strategy(
516
+ operation.ok().as_strategy(
443
517
  hooks=hooks,
444
518
  auth_storage=auth_storage,
445
519
  data_generation_method=data_generation_method,
446
520
  generation_config=generation_config,
447
521
  **kwargs,
448
522
  )
449
- for operations in self.operations.values()
450
- for operation in operations.values()
523
+ for operation in self.get_all_operations(hooks=hooks)
524
+ if isinstance(operation, Ok)
451
525
  ]
452
526
  return combine_strategies(strategies)
453
527
 
454
528
 
455
529
  @dataclass
456
- class APIOperationMap(MutableMapping):
457
- data: MutableMapping
458
-
459
- def __setitem__(self, key: str, value: APIOperation) -> None:
460
- self.data[key] = value
530
+ class APIOperationMap(Mapping):
531
+ _schema: BaseSchema
532
+ _data: Mapping
461
533
 
462
534
  def __getitem__(self, item: str) -> APIOperation:
463
- return self.data[item]
464
-
465
- def __delitem__(self, key: str) -> None:
466
- del self.data[key]
535
+ return self._data[item]
467
536
 
468
537
  def __len__(self) -> int:
469
- return len(self.data)
538
+ return len(self._data)
470
539
 
471
540
  def __iter__(self) -> Iterator[str]:
472
- return iter(self.data)
541
+ return iter(self._data)
473
542
 
474
543
  def as_strategy(
475
544
  self,
@@ -480,7 +549,6 @@ class APIOperationMap(MutableMapping):
480
549
  **kwargs: Any,
481
550
  ) -> SearchStrategy:
482
551
  """Build a strategy for generating test cases for all API operations defined in this subset."""
483
- assert len(self.data) > 0, "No API operations found"
484
552
  strategies = [
485
553
  operation.as_strategy(
486
554
  hooks=hooks,
@@ -489,6 +557,6 @@ class APIOperationMap(MutableMapping):
489
557
  generation_config=generation_config,
490
558
  **kwargs,
491
559
  )
492
- for operation in self.data.values()
560
+ for operation in self._data.values()
493
561
  ]
494
562
  return combine_strategies(strategies)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import binascii
3
4
  import os
4
5
  from dataclasses import dataclass
@@ -10,13 +11,14 @@ from typing import (
10
11
  Collection,
11
12
  Dict,
12
13
  Generator,
13
- cast,
14
14
  Protocol,
15
+ cast,
15
16
  runtime_checkable,
16
17
  )
17
18
 
18
- from .internal.copy import fast_deepcopy
19
19
  from ._xml import _to_xml
20
+ from .internal.copy import fast_deepcopy
21
+ from .internal.jsonschema import traverse_schema
20
22
  from .transports.content_types import (
21
23
  is_json_media_type,
22
24
  is_plain_text_media_type,
@@ -41,6 +43,11 @@ class Binary(str):
41
43
 
42
44
  data: bytes
43
45
 
46
+ __slots__ = ("data",)
47
+
48
+ def __hash__(self) -> int:
49
+ return hash(self.data)
50
+
44
51
 
45
52
  @dataclass
46
53
  class SerializerContext:
@@ -150,6 +157,10 @@ class JSONSerializer:
150
157
  return _to_json(value)
151
158
 
152
159
 
160
+ def _replace_binary(value: dict) -> dict:
161
+ return {key: value.data if isinstance(value, Binary) else value for key, value in value.items()}
162
+
163
+
153
164
  def _to_yaml(value: Any) -> dict[str, Any]:
154
165
  import yaml
155
166
 
@@ -162,10 +173,12 @@ def _to_yaml(value: Any) -> dict[str, Any]:
162
173
  return {"data": value}
163
174
  if isinstance(value, Binary):
164
175
  return {"data": value.data}
176
+ if isinstance(value, (list, dict)):
177
+ value = traverse_schema(value, _replace_binary)
165
178
  return {"data": yaml.dump(value, Dumper=SafeDumper)}
166
179
 
167
180
 
168
- @register("text/yaml", aliases=("text/x-yaml", "application/x-yaml", "text/vnd.yaml"))
181
+ @register("text/yaml", aliases=("text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"))
169
182
  class YAMLSerializer:
170
183
  def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
171
184
  return _to_yaml(value)
@@ -233,7 +246,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
233
246
  return body.getvalue()
234
247
 
235
248
 
236
- @register("multipart/form-data")
249
+ @register("multipart/form-data", aliases=("multipart/mixed",))
237
250
  class MultipartSerializer:
238
251
  def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
239
252
  if isinstance(value, bytes):
@@ -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,27 +1,35 @@
1
1
  from __future__ import annotations
2
+
2
3
  import hashlib
3
4
  import http
5
+ import json
4
6
  from dataclasses import asdict
5
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
6
8
  from urllib.parse import urljoin
7
9
 
8
10
  import requests
9
11
  from requests.adapters import HTTPAdapter, Retry
10
12
 
11
13
  from ..constants import USER_AGENT
12
- from .ci import CIProvider
13
14
  from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
14
- from .metadata import Metadata
15
+ from .metadata import Metadata, collect_dependency_versions
15
16
  from .models import (
16
- ProjectDetails,
17
+ AnalysisError,
18
+ AnalysisResult,
19
+ AnalysisSuccess,
17
20
  AuthResponse,
18
21
  FailedUploadResponse,
19
- UploadResponse,
20
- UploadSource,
22
+ ProjectDetails,
21
23
  ProjectEnvironment,
22
24
  Specification,
25
+ UploadResponse,
26
+ UploadSource,
23
27
  )
24
28
 
29
+ if TYPE_CHECKING:
30
+ from ..runner import probes
31
+ from .ci import CIProvider
32
+
25
33
 
26
34
  def response_hook(response: requests.Response, **_kwargs: Any) -> None:
27
35
  if response.status_code != http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
@@ -98,3 +106,28 @@ class ServiceClient(requests.Session):
98
106
  if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
99
107
  return FailedUploadResponse(detail=data["detail"])
100
108
  return UploadResponse(message=data["message"], next_url=data["next"], correlation_id=data["correlation_id"])
109
+
110
+ def analyze_schema(self, probes: list[probes.ProbeRun] | None, schema: dict[str, Any]) -> AnalysisResult:
111
+ """Analyze the API schema."""
112
+ # Manual serialization reduces the size of the payload a bit
113
+ dependencies = collect_dependency_versions()
114
+ if probes is not None:
115
+ _probes = [probe.serialize() for probe in probes]
116
+ else:
117
+ _probes = []
118
+ content = json.dumps(
119
+ {
120
+ "probes": _probes,
121
+ "schema": schema,
122
+ "dependencies": list(map(asdict, dependencies)),
123
+ },
124
+ separators=(",", ":"),
125
+ )
126
+ response = self.post("/cli/analysis/", data=content, headers={"Content-Type": "application/json"}, timeout=None)
127
+ if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
128
+ try:
129
+ message = response.json()["detail"]
130
+ except json.JSONDecodeError:
131
+ message = response.text
132
+ return AnalysisError(message=message)
133
+ return AnalysisSuccess.from_dict(response.json())
@@ -1,9 +1,13 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
3
5
 
4
- from . import ci
5
6
  from ..exceptions import format_exception
6
7
 
8
+ if TYPE_CHECKING:
9
+ from . import ci
10
+
7
11
 
8
12
  class Event:
9
13
  """Signalling events coming from the Schemathesis.io worker.