schemathesis 4.0.0a10__py3-none-any.whl → 4.0.0a12__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 (111) hide show
  1. schemathesis/__init__.py +29 -30
  2. schemathesis/auths.py +65 -24
  3. schemathesis/checks.py +73 -39
  4. schemathesis/cli/commands/__init__.py +51 -3
  5. schemathesis/cli/commands/data.py +10 -0
  6. schemathesis/cli/commands/run/__init__.py +163 -274
  7. schemathesis/cli/commands/run/context.py +8 -4
  8. schemathesis/cli/commands/run/events.py +11 -1
  9. schemathesis/cli/commands/run/executor.py +70 -78
  10. schemathesis/cli/commands/run/filters.py +15 -165
  11. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  12. schemathesis/cli/commands/run/handlers/junitxml.py +5 -4
  13. schemathesis/cli/commands/run/handlers/output.py +195 -121
  14. schemathesis/cli/commands/run/loaders.py +35 -50
  15. schemathesis/cli/commands/run/validation.py +52 -162
  16. schemathesis/cli/core.py +5 -3
  17. schemathesis/cli/ext/fs.py +7 -5
  18. schemathesis/cli/ext/options.py +0 -21
  19. schemathesis/config/__init__.py +189 -0
  20. schemathesis/config/_auth.py +51 -0
  21. schemathesis/config/_checks.py +268 -0
  22. schemathesis/config/_diff_base.py +99 -0
  23. schemathesis/config/_env.py +21 -0
  24. schemathesis/config/_error.py +156 -0
  25. schemathesis/config/_generation.py +149 -0
  26. schemathesis/config/_health_check.py +24 -0
  27. schemathesis/config/_operations.py +327 -0
  28. schemathesis/config/_output.py +171 -0
  29. schemathesis/config/_parameters.py +19 -0
  30. schemathesis/config/_phases.py +187 -0
  31. schemathesis/config/_projects.py +523 -0
  32. schemathesis/config/_rate_limit.py +17 -0
  33. schemathesis/config/_report.py +120 -0
  34. schemathesis/config/_validator.py +9 -0
  35. schemathesis/config/_warnings.py +25 -0
  36. schemathesis/config/schema.json +885 -0
  37. schemathesis/core/__init__.py +2 -0
  38. schemathesis/core/compat.py +16 -9
  39. schemathesis/core/errors.py +24 -4
  40. schemathesis/core/failures.py +6 -7
  41. schemathesis/core/hooks.py +20 -0
  42. schemathesis/core/output/__init__.py +14 -37
  43. schemathesis/core/output/sanitization.py +3 -146
  44. schemathesis/core/transport.py +36 -1
  45. schemathesis/core/validation.py +16 -0
  46. schemathesis/engine/__init__.py +2 -4
  47. schemathesis/engine/context.py +42 -43
  48. schemathesis/engine/core.py +7 -5
  49. schemathesis/engine/errors.py +60 -1
  50. schemathesis/engine/events.py +10 -2
  51. schemathesis/engine/phases/__init__.py +10 -0
  52. schemathesis/engine/phases/probes.py +11 -8
  53. schemathesis/engine/phases/stateful/__init__.py +2 -1
  54. schemathesis/engine/phases/stateful/_executor.py +104 -46
  55. schemathesis/engine/phases/stateful/context.py +2 -2
  56. schemathesis/engine/phases/unit/__init__.py +23 -15
  57. schemathesis/engine/phases/unit/_executor.py +110 -21
  58. schemathesis/engine/phases/unit/_pool.py +1 -1
  59. schemathesis/errors.py +2 -0
  60. schemathesis/filters.py +2 -3
  61. schemathesis/generation/__init__.py +5 -33
  62. schemathesis/generation/case.py +6 -3
  63. schemathesis/generation/coverage.py +154 -124
  64. schemathesis/generation/hypothesis/builder.py +70 -20
  65. schemathesis/generation/meta.py +3 -3
  66. schemathesis/generation/metrics.py +93 -0
  67. schemathesis/generation/modes.py +0 -8
  68. schemathesis/generation/overrides.py +37 -1
  69. schemathesis/generation/stateful/__init__.py +4 -0
  70. schemathesis/generation/stateful/state_machine.py +9 -1
  71. schemathesis/graphql/loaders.py +159 -16
  72. schemathesis/hooks.py +62 -35
  73. schemathesis/openapi/checks.py +12 -8
  74. schemathesis/openapi/generation/filters.py +10 -8
  75. schemathesis/openapi/loaders.py +142 -17
  76. schemathesis/pytest/lazy.py +2 -5
  77. schemathesis/pytest/loaders.py +24 -0
  78. schemathesis/pytest/plugin.py +33 -2
  79. schemathesis/schemas.py +21 -66
  80. schemathesis/specs/graphql/scalars.py +37 -3
  81. schemathesis/specs/graphql/schemas.py +23 -18
  82. schemathesis/specs/openapi/_hypothesis.py +26 -28
  83. schemathesis/specs/openapi/checks.py +37 -36
  84. schemathesis/specs/openapi/examples.py +4 -3
  85. schemathesis/specs/openapi/formats.py +32 -5
  86. schemathesis/specs/openapi/media_types.py +44 -1
  87. schemathesis/specs/openapi/negative/__init__.py +2 -2
  88. schemathesis/specs/openapi/patterns.py +46 -16
  89. schemathesis/specs/openapi/references.py +2 -3
  90. schemathesis/specs/openapi/schemas.py +19 -22
  91. schemathesis/specs/openapi/stateful/__init__.py +12 -6
  92. schemathesis/transport/__init__.py +54 -16
  93. schemathesis/transport/prepare.py +38 -13
  94. schemathesis/transport/requests.py +12 -9
  95. schemathesis/transport/wsgi.py +11 -12
  96. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/METADATA +50 -97
  97. schemathesis-4.0.0a12.dist-info/RECORD +164 -0
  98. schemathesis/cli/commands/run/checks.py +0 -79
  99. schemathesis/cli/commands/run/hypothesis.py +0 -78
  100. schemathesis/cli/commands/run/reports.py +0 -72
  101. schemathesis/cli/hooks.py +0 -36
  102. schemathesis/contrib/__init__.py +0 -9
  103. schemathesis/contrib/openapi/__init__.py +0 -9
  104. schemathesis/contrib/openapi/fill_missing_examples.py +0 -20
  105. schemathesis/engine/config.py +0 -59
  106. schemathesis/experimental/__init__.py +0 -72
  107. schemathesis/generation/targets.py +0 -69
  108. schemathesis-4.0.0a10.dist-info/RECORD +0 -153
  109. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/WHEEL +0 -0
  110. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/entry_points.txt +0 -0
  111. {schemathesis-4.0.0a10.dist-info → schemathesis-4.0.0a12.dist-info}/licenses/LICENSE +0 -0
@@ -14,6 +14,7 @@ from schemathesis.engine.recorder import ScenarioRecorder
14
14
  from schemathesis.generation import GenerationMode
15
15
  from schemathesis.generation.case import Case
16
16
  from schemathesis.generation.hypothesis import strategies
17
+ from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
17
18
  from schemathesis.generation.stateful.state_machine import APIStateMachine, StepInput, StepOutput, _normalize_name
18
19
  from schemathesis.schemas import APIOperation
19
20
  from schemathesis.specs.openapi.stateful.control import TransitionController
@@ -32,7 +33,7 @@ class OpenAPIStateMachine(APIStateMachine):
32
33
  _transitions: ApiTransitions
33
34
 
34
35
  def __init__(self) -> None:
35
- self.recorder = ScenarioRecorder(label="Stateful tests")
36
+ self.recorder = ScenarioRecorder(label=STATEFUL_TESTS_LABEL)
36
37
  self.control = TransitionController(self._transitions)
37
38
  super().__init__()
38
39
 
@@ -162,22 +163,25 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
162
163
  f"{link.source.label} -> {link.status_code} -> {link.name} -> {target.label}"
163
164
  )
164
165
  assert name not in rules, name
166
+ config = schema.config.generation_for(operation=target, phase="stateful")
165
167
  rules[name] = precondition(is_transition_allowed(bundle_name, link.source.label, target.label))(
166
168
  transition(
167
169
  name=name,
168
170
  target=catch_all,
169
171
  input=bundles[bundle_name].flatmap(
170
- into_step_input(target=target, link=link, modes=schema.generation_config.modes)
172
+ into_step_input(target=target, link=link, modes=config.modes)
171
173
  ),
172
174
  )
173
175
  )
174
176
  if target.label in roots.reliable or (not roots.reliable and target.label in roots.fallback):
175
177
  name = _normalize_name(f"RANDOM -> {target.label}")
176
- if len(schema.generation_config.modes) == 1:
177
- case_strategy = target.as_strategy(generation_mode=schema.generation_config.modes[0])
178
+ config = schema.config.generation_for(operation=target, phase="stateful")
179
+ if len(config.modes) == 1:
180
+ case_strategy = target.as_strategy(generation_mode=config.modes[0], __is_stateful_phase=True)
178
181
  else:
179
182
  _strategies = {
180
- method: target.as_strategy(generation_mode=method) for method in schema.generation_config.modes
183
+ method: target.as_strategy(generation_mode=method, __is_stateful_phase=True)
184
+ for method in config.modes
181
185
  }
182
186
 
183
187
  @st.composite # type: ignore[misc]
@@ -262,7 +266,9 @@ def into_step_input(
262
266
  and not link.merge_body
263
267
  ):
264
268
  kwargs["body"] = transition.request_body.value.ok()
265
- cases = strategies.combine([target.as_strategy(generation_mode=mode, **kwargs) for mode in modes])
269
+ cases = strategies.combine(
270
+ [target.as_strategy(generation_mode=mode, __is_stateful_phase=True, **kwargs) for mode in modes]
271
+ )
266
272
  case = draw(cases)
267
273
  if (
268
274
  transition.request_body is not None
@@ -2,11 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from inspect import iscoroutinefunction
5
- from typing import Any, Callable, Generic, Iterator, TypeVar
5
+ from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, TypeVar, Union
6
6
 
7
7
  from schemathesis.core import media_types
8
8
  from schemathesis.core.errors import SerializationNotPossible
9
9
 
10
+ if TYPE_CHECKING:
11
+ from schemathesis.core.transport import Response
12
+ from schemathesis.generation.case import Case
13
+
10
14
 
11
15
  def get(app: Any) -> BaseTransport:
12
16
  """Get transport to send the data to the application."""
@@ -23,41 +27,43 @@ def get(app: Any) -> BaseTransport:
23
27
  return WSGI_TRANSPORT
24
28
 
25
29
 
26
- C = TypeVar("C", contravariant=True)
27
- R = TypeVar("R", covariant=True)
28
30
  S = TypeVar("S", contravariant=True)
29
31
 
30
32
 
31
33
  @dataclass
32
- class SerializationContext(Generic[C]):
33
- """Generic context for serialization process."""
34
+ class SerializationContext:
35
+ """Context object passed to serializer functions.
36
+
37
+ It provides access to the generated test case and any related metadata.
38
+ """
34
39
 
35
- case: C
40
+ case: Case
41
+ """The generated test case."""
36
42
 
37
43
  __slots__ = ("case",)
38
44
 
39
45
 
40
- Serializer = Callable[[SerializationContext[C], Any], Any]
46
+ Serializer = Callable[[SerializationContext, Any], Any]
41
47
 
42
48
 
43
- class BaseTransport(Generic[C, R, S]):
49
+ class BaseTransport(Generic[S]):
44
50
  """Base implementation with serializer registration."""
45
51
 
46
52
  def __init__(self) -> None:
47
- self._serializers: dict[str, Serializer[C]] = {}
53
+ self._serializers: dict[str, Serializer] = {}
48
54
 
49
- def serialize_case(self, case: C, **kwargs: Any) -> dict[str, Any]:
55
+ def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
50
56
  """Prepare the case for sending."""
51
57
  raise NotImplementedError
52
58
 
53
- def send(self, case: C, *, session: S | None = None, **kwargs: Any) -> R:
59
+ def send(self, case: Case, *, session: S | None = None, **kwargs: Any) -> Response:
54
60
  """Send the case using this transport."""
55
61
  raise NotImplementedError
56
62
 
57
- def serializer(self, *media_types: str) -> Callable[[Serializer[C]], Serializer[C]]:
63
+ def serializer(self, *media_types: str) -> Callable[[Serializer], Serializer]:
58
64
  """Register a serializer for given media types."""
59
65
 
60
- def decorator(func: Serializer[C]) -> Serializer[C]:
66
+ def decorator(func: Serializer) -> Serializer:
61
67
  for media_type in media_types:
62
68
  self._serializers[media_type] = func
63
69
  return func
@@ -71,10 +77,10 @@ class BaseTransport(Generic[C, R, S]):
71
77
  def _copy_serializers_from(self, transport: BaseTransport) -> None:
72
78
  self._serializers.update(transport._serializers)
73
79
 
74
- def get_first_matching_media_type(self, media_type: str) -> tuple[str, Serializer[C]] | None:
80
+ def get_first_matching_media_type(self, media_type: str) -> tuple[str, Serializer] | None:
75
81
  return next(self.get_matching_media_types(media_type), None)
76
82
 
77
- def get_matching_media_types(self, media_type: str) -> Iterator[tuple[str, Serializer[C]]]:
83
+ def get_matching_media_types(self, media_type: str) -> Iterator[tuple[str, Serializer]]:
78
84
  """Get all registered media types matching the given media type."""
79
85
  if media_type == "*/*":
80
86
  # Shortcut to avoid comparing all values
@@ -96,9 +102,41 @@ class BaseTransport(Generic[C, R, S]):
96
102
  if main in ("*", target_main) and sub in ("*", target_sub):
97
103
  yield registered_media_type, serializer
98
104
 
99
- def _get_serializer(self, input_media_type: str) -> Serializer[C]:
105
+ def _get_serializer(self, input_media_type: str) -> Serializer:
100
106
  pair = self.get_first_matching_media_type(input_media_type)
101
107
  if pair is None:
102
108
  # This media type is set manually. Otherwise, it should have been rejected during the data generation
103
109
  raise SerializationNotPossible.for_media_type(input_media_type)
104
110
  return pair[1]
111
+
112
+
113
+ _Serializer = Callable[[SerializationContext, Any], Union[bytes, None]]
114
+
115
+
116
+ def serializer(*media_types: str) -> Callable[[_Serializer], None]:
117
+ """Register a serializer for specified media types on HTTP, ASGI, and WSGI transports.
118
+
119
+ Args:
120
+ *media_types: One or more MIME types (e.g., "application/json") this serializer handles.
121
+
122
+ Returns:
123
+ A decorator that wraps a function taking `(ctx: SerializationContext, value: Any)` and returning `bytes` for serialized body and `None` for omitting request body.
124
+
125
+ """
126
+
127
+ def register(func: _Serializer) -> None:
128
+ from schemathesis.transport.asgi import ASGI_TRANSPORT
129
+ from schemathesis.transport.requests import REQUESTS_TRANSPORT
130
+ from schemathesis.transport.wsgi import WSGI_TRANSPORT
131
+
132
+ @ASGI_TRANSPORT.serializer(*media_types)
133
+ @REQUESTS_TRANSPORT.serializer(*media_types)
134
+ @WSGI_TRANSPORT.serializer(*media_types)
135
+ def inner(ctx: SerializationContext, value: Any) -> dict[str, bytes]:
136
+ result = {}
137
+ serialized = func(ctx, value)
138
+ if serialized is not None:
139
+ result["data"] = serialized
140
+ return result
141
+
142
+ return register
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from functools import lru_cache
3
4
  from typing import TYPE_CHECKING, Any, Mapping, cast
4
5
  from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
5
6
 
7
+ from schemathesis.config import SanitizationConfig
6
8
  from schemathesis.core import SCHEMATHESIS_TEST_CASE_HEADER, NotSet
7
9
  from schemathesis.core.errors import InvalidSchema
8
10
  from schemathesis.core.output.sanitization import sanitize_url, sanitize_value
9
11
  from schemathesis.core.transport import USER_AGENT
12
+ from schemathesis.generation.meta import CoveragePhaseData
10
13
 
11
14
  if TYPE_CHECKING:
12
15
  from requests import PreparedRequest
@@ -15,15 +18,37 @@ if TYPE_CHECKING:
15
18
  from schemathesis.generation.case import Case
16
19
 
17
20
 
18
- def prepare_headers(case: Case, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
19
- from requests.structures import CaseInsensitiveDict
21
+ @lru_cache()
22
+ def get_default_headers() -> CaseInsensitiveDict:
23
+ from requests.utils import default_headers
24
+
25
+ headers = default_headers()
26
+ headers["User-Agent"] = USER_AGENT
27
+ return headers
28
+
20
29
 
21
- final_headers = case.headers.copy() if case.headers is not None else CaseInsensitiveDict()
30
+ def prepare_headers(case: Case, headers: dict[str, str] | None = None) -> CaseInsensitiveDict:
31
+ default_headers = get_default_headers().copy()
32
+ if case.headers:
33
+ default_headers.update(case.headers)
34
+ default_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, case.id)
22
35
  if headers:
23
- final_headers.update(headers)
24
- final_headers.setdefault("User-Agent", USER_AGENT)
25
- final_headers.setdefault(SCHEMATHESIS_TEST_CASE_HEADER, case.id)
26
- return final_headers
36
+ default_headers.update(headers)
37
+ for header in get_exclude_headers(case):
38
+ default_headers.pop(header, None)
39
+ return default_headers
40
+
41
+
42
+ def get_exclude_headers(case: Case) -> list[str]:
43
+ if (
44
+ case.meta is not None
45
+ and isinstance(case.meta.phase.data, CoveragePhaseData)
46
+ and case.meta.phase.data.description.startswith("Missing")
47
+ and case.meta.phase.data.description.endswith("at header")
48
+ and case.meta.phase.data.parameter is not None
49
+ ):
50
+ return [case.meta.phase.data.parameter]
51
+ return []
27
52
 
28
53
 
29
54
  def prepare_url(case: Case, base_url: str | None) -> str:
@@ -81,19 +106,19 @@ def prepare_path(path: str, parameters: dict[str, Any] | None) -> str:
81
106
  raise InvalidSchema(f"Malformed path template: `{path}`\n\n {exc}") from exc
82
107
 
83
108
 
84
- def prepare_request(case: Case, headers: Mapping[str, Any] | None, sanitize: bool) -> PreparedRequest:
109
+ def prepare_request(case: Case, headers: Mapping[str, Any] | None, *, config: SanitizationConfig) -> PreparedRequest:
85
110
  import requests
86
111
 
87
112
  from schemathesis.transport.requests import REQUESTS_TRANSPORT
88
113
 
89
114
  base_url = normalize_base_url(case.operation.base_url)
90
115
  kwargs = REQUESTS_TRANSPORT.serialize_case(case, base_url=base_url, headers=headers)
91
- if sanitize:
92
- kwargs["url"] = sanitize_url(kwargs["url"])
93
- sanitize_value(kwargs["headers"])
116
+ if config.enabled:
117
+ kwargs["url"] = sanitize_url(kwargs["url"], config=config)
118
+ sanitize_value(kwargs["headers"], config=config)
94
119
  if kwargs["cookies"]:
95
- sanitize_value(kwargs["cookies"])
120
+ sanitize_value(kwargs["cookies"], config=config)
96
121
  if kwargs["params"]:
97
- sanitize_value(kwargs["params"])
122
+ sanitize_value(kwargs["params"], config=config)
98
123
 
99
124
  return requests.Request(**kwargs).prepare()
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
21
21
  from schemathesis.generation.case import Case
22
22
 
23
23
 
24
- class RequestsTransport(BaseTransport["Case", Response, "requests.Session"]):
24
+ class RequestsTransport(BaseTransport["requests.Session"]):
25
25
  def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
26
26
  base_url = kwargs.get("base_url")
27
27
  headers = kwargs.get("headers")
@@ -92,6 +92,7 @@ class RequestsTransport(BaseTransport["Case", Response, "requests.Session"]):
92
92
  if session is None:
93
93
  validate_vanilla_requests_kwargs(data)
94
94
  session = requests.Session()
95
+ session.headers = {}
95
96
  close_session = True
96
97
  else:
97
98
  close_session = False
@@ -99,7 +100,9 @@ class RequestsTransport(BaseTransport["Case", Response, "requests.Session"]):
99
100
  verify = data.get("verify", True)
100
101
 
101
102
  try:
102
- with ratelimit(case.operation.schema.rate_limiter, case.operation.schema.base_url):
103
+ config = case.operation.schema.config
104
+ rate_limit = config.rate_limit_for(operation=case.operation)
105
+ with ratelimit(rate_limit, config.base_url):
103
106
  response = session.request(**data) # type: ignore
104
107
  return Response.from_requests(response, verify=verify)
105
108
  finally:
@@ -133,14 +136,14 @@ REQUESTS_TRANSPORT = RequestsTransport()
133
136
 
134
137
 
135
138
  @REQUESTS_TRANSPORT.serializer("application/json", "text/json")
136
- def json_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
139
+ def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
137
140
  return serialize_json(value)
138
141
 
139
142
 
140
143
  @REQUESTS_TRANSPORT.serializer(
141
144
  "text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
142
145
  )
143
- def yaml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
146
+ def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
144
147
  return serialize_yaml(value)
145
148
 
146
149
 
@@ -186,7 +189,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
186
189
 
187
190
 
188
191
  @REQUESTS_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
189
- def multipart_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
192
+ def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
190
193
  if isinstance(value, bytes):
191
194
  return {"data": value}
192
195
  if isinstance(value, dict):
@@ -202,7 +205,7 @@ def multipart_serializer(ctx: SerializationContext[Case], value: Any) -> dict[st
202
205
 
203
206
 
204
207
  @REQUESTS_TRANSPORT.serializer("application/xml", "text/xml")
205
- def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
208
+ def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
206
209
  media_type = ctx.case.media_type
207
210
 
208
211
  assert media_type is not None
@@ -214,17 +217,17 @@ def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any
214
217
 
215
218
 
216
219
  @REQUESTS_TRANSPORT.serializer("application/x-www-form-urlencoded")
217
- def urlencoded_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
220
+ def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
218
221
  return {"data": value}
219
222
 
220
223
 
221
224
  @REQUESTS_TRANSPORT.serializer("text/plain")
222
- def text_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
225
+ def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
223
226
  if isinstance(value, bytes):
224
227
  return {"data": value}
225
228
  return {"data": str(value).encode("utf8")}
226
229
 
227
230
 
228
231
  @REQUESTS_TRANSPORT.serializer("application/octet-stream")
229
- def binary_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
232
+ def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
230
233
  return {"data": serialize_binary(value)}
@@ -19,7 +19,7 @@ if TYPE_CHECKING:
19
19
  import werkzeug
20
20
 
21
21
 
22
- class WSGITransport(BaseTransport["Case", Response, "werkzeug.Client"]):
22
+ class WSGITransport(BaseTransport["werkzeug.Client"]):
23
23
  def serialize_case(self, case: Case, **kwargs: Any) -> dict[str, Any]:
24
24
  headers = kwargs.get("headers")
25
25
  params = kwargs.get("params")
@@ -75,10 +75,9 @@ class WSGITransport(BaseTransport["Case", Response, "werkzeug.Client"]):
75
75
  client = session or wsgi.get_client(application)
76
76
  cookies = {**(case.cookies or {}), **(cookies or {})}
77
77
 
78
- with (
79
- cookie_handler(client, cookies),
80
- ratelimit(case.operation.schema.rate_limiter, case.operation.schema.base_url),
81
- ):
78
+ config = case.operation.schema.config
79
+ rate_limit = config.rate_limit_for(operation=case.operation)
80
+ with cookie_handler(client, cookies), ratelimit(rate_limit, config.base_url):
82
81
  start = time.monotonic()
83
82
  response = client.open(**data)
84
83
  elapsed = time.monotonic() - start
@@ -120,24 +119,24 @@ WSGI_TRANSPORT = WSGITransport()
120
119
 
121
120
 
122
121
  @WSGI_TRANSPORT.serializer("application/json", "text/json")
123
- def json_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
122
+ def json_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
124
123
  return serialize_json(value)
125
124
 
126
125
 
127
126
  @WSGI_TRANSPORT.serializer(
128
127
  "text/yaml", "text/x-yaml", "text/vnd.yaml", "text/yml", "application/yaml", "application/x-yaml"
129
128
  )
130
- def yaml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
129
+ def yaml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
131
130
  return serialize_yaml(value)
132
131
 
133
132
 
134
133
  @WSGI_TRANSPORT.serializer("multipart/form-data", "multipart/mixed")
135
- def multipart_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
134
+ def multipart_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
136
135
  return {"data": value}
137
136
 
138
137
 
139
138
  @WSGI_TRANSPORT.serializer("application/xml", "text/xml")
140
- def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
139
+ def xml_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
141
140
  media_type = ctx.case.media_type
142
141
 
143
142
  assert media_type is not None
@@ -149,17 +148,17 @@ def xml_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any
149
148
 
150
149
 
151
150
  @WSGI_TRANSPORT.serializer("application/x-www-form-urlencoded")
152
- def urlencoded_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
151
+ def urlencoded_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
153
152
  return {"data": value}
154
153
 
155
154
 
156
155
  @WSGI_TRANSPORT.serializer("text/plain")
157
- def text_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
156
+ def text_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
158
157
  if isinstance(value, bytes):
159
158
  return {"data": value}
160
159
  return {"data": str(value)}
161
160
 
162
161
 
163
162
  @WSGI_TRANSPORT.serializer("application/octet-stream")
164
- def binary_serializer(ctx: SerializationContext[Case], value: Any) -> dict[str, Any]:
163
+ def binary_serializer(ctx: SerializationContext, value: Any) -> dict[str, Any]:
165
164
  return {"data": serialize_binary(value)}