pyrestkit 1.2.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 (115) hide show
  1. pyrestkit/__init__.py +35 -0
  2. pyrestkit/ai/__init__.py +17 -0
  3. pyrestkit/ai/analyzer.py +137 -0
  4. pyrestkit/ai/client.py +101 -0
  5. pyrestkit/ai/config/__init__.py +5 -0
  6. pyrestkit/ai/config/ai_config.py +200 -0
  7. pyrestkit/ai/exceptions.py +22 -0
  8. pyrestkit/ai/generators/__init__.py +0 -0
  9. pyrestkit/ai/models.py +44 -0
  10. pyrestkit/ai/parsers/__init__.py +0 -0
  11. pyrestkit/ai/prompts/failure_analysis.md +21 -0
  12. pyrestkit/ai/provider.py +58 -0
  13. pyrestkit/ai/providers/__init__.py +21 -0
  14. pyrestkit/ai/providers/anthropic.py +85 -0
  15. pyrestkit/ai/providers/azure_openai.py +84 -0
  16. pyrestkit/ai/providers/base.py +39 -0
  17. pyrestkit/ai/providers/bedrock.py +70 -0
  18. pyrestkit/ai/providers/cohere.py +82 -0
  19. pyrestkit/ai/providers/gemini.py +113 -0
  20. pyrestkit/ai/providers/groq.py +81 -0
  21. pyrestkit/ai/providers/mistral.py +88 -0
  22. pyrestkit/ai/providers/ollama.py +82 -0
  23. pyrestkit/ai/providers/openai.py +124 -0
  24. pyrestkit/ai/utils/__init__.py +0 -0
  25. pyrestkit/ai/utils/prompt_loader.py +52 -0
  26. pyrestkit/assertions/__init__.py +7 -0
  27. pyrestkit/assertions/assertion_exception.py +4 -0
  28. pyrestkit/assertions/response_assertions.py +181 -0
  29. pyrestkit/auth/__init__.py +11 -0
  30. pyrestkit/auth/auth_strategy.py +14 -0
  31. pyrestkit/auth/authentication_manager.py +35 -0
  32. pyrestkit/auth/strategies/__init__.py +5 -0
  33. pyrestkit/auth/strategies/api_key_auth.py +18 -0
  34. pyrestkit/auth/strategies/basic_auth.py +24 -0
  35. pyrestkit/auth/strategies/bearer_auth.py +17 -0
  36. pyrestkit/auth/token_cache.py +44 -0
  37. pyrestkit/auth/token_manager.py +32 -0
  38. pyrestkit/auth/token_provider.py +12 -0
  39. pyrestkit/auth/token_response.py +13 -0
  40. pyrestkit/builder/__init__.py +5 -0
  41. pyrestkit/builder/fluent_request_builder.py +167 -0
  42. pyrestkit/clients/__init__.py +7 -0
  43. pyrestkit/clients/base_client.py +68 -0
  44. pyrestkit/clients/user_client.py +66 -0
  45. pyrestkit/config/__init__.py +5 -0
  46. pyrestkit/config/config.py +97 -0
  47. pyrestkit/constants/__init__.py +0 -0
  48. pyrestkit/constants/content_types.py +0 -0
  49. pyrestkit/constants/headers.py +0 -0
  50. pyrestkit/constants/status_codes.py +0 -0
  51. pyrestkit/core/__init__.py +13 -0
  52. pyrestkit/core/api_client.py +129 -0
  53. pyrestkit/core/logger.py +41 -0
  54. pyrestkit/core/request_builder.py +45 -0
  55. pyrestkit/core/request_executor.py +64 -0
  56. pyrestkit/core/request_logger.py +0 -0
  57. pyrestkit/core/response_logger.py +0 -0
  58. pyrestkit/core/session_manager.py +19 -0
  59. pyrestkit/database/__init__.py +0 -0
  60. pyrestkit/endpoints/__init__.py +5 -0
  61. pyrestkit/endpoints/base_endpoints.py +32 -0
  62. pyrestkit/endpoints/order_endpoints.py +9 -0
  63. pyrestkit/endpoints/payment_endpoints.py +5 -0
  64. pyrestkit/endpoints/user_endpoints.py +48 -0
  65. pyrestkit/exceptions/__init__.py +21 -0
  66. pyrestkit/exceptions/api_exception.py +8 -0
  67. pyrestkit/exceptions/authentication_exception.py +10 -0
  68. pyrestkit/exceptions/configuration_exception.py +10 -0
  69. pyrestkit/exceptions/exception_mapper.py +32 -0
  70. pyrestkit/exceptions/network_exception.py +10 -0
  71. pyrestkit/exceptions/response_exception.py +10 -0
  72. pyrestkit/exceptions/serialization_exception.py +10 -0
  73. pyrestkit/exceptions/validation_exception.py +10 -0
  74. pyrestkit/factories/__init__.py +5 -0
  75. pyrestkit/factories/base_factory.py +25 -0
  76. pyrestkit/factories/user_factory.py +37 -0
  77. pyrestkit/hooks/__init__.py +5 -0
  78. pyrestkit/hooks/hook.py +27 -0
  79. pyrestkit/hooks/hook_manager.py +39 -0
  80. pyrestkit/hooks/request_hook.py +18 -0
  81. pyrestkit/hooks/response_hook.py +17 -0
  82. pyrestkit/hooks/timing_hook.py +32 -0
  83. pyrestkit/models/__init__.py +8 -0
  84. pyrestkit/models/base_response.py +11 -0
  85. pyrestkit/models/request/__init__.py +7 -0
  86. pyrestkit/models/request/create_user_request.py +11 -0
  87. pyrestkit/models/request/update_user_request.py +10 -0
  88. pyrestkit/models/response/__init__.py +5 -0
  89. pyrestkit/models/response/create_user_response.py +26 -0
  90. pyrestkit/models/response/get_user_response.py +28 -0
  91. pyrestkit/models/response/user_response.py +12 -0
  92. pyrestkit/pipeline/__init__.py +5 -0
  93. pyrestkit/pipeline/middleware.py +18 -0
  94. pyrestkit/pipeline/middleware_chain.py +11 -0
  95. pyrestkit/pipeline/pipeline.py +27 -0
  96. pyrestkit/pipeline/request_context.py +26 -0
  97. pyrestkit/response/__init__.py +8 -0
  98. pyrestkit/response/framework_response.py +271 -0
  99. pyrestkit/response/response_body.py +124 -0
  100. pyrestkit/retry/__init__.py +5 -0
  101. pyrestkit/retry/backoff.py +32 -0
  102. pyrestkit/retry/retry_handler.py +52 -0
  103. pyrestkit/retry/retry_policy.py +33 -0
  104. pyrestkit/serializers/__init__.py +0 -0
  105. pyrestkit/serializers/response_mapper.py +25 -0
  106. pyrestkit/types/__init__.py +0 -0
  107. pyrestkit/types/model_protocol.py +17 -0
  108. pyrestkit/utils/__init__.py +0 -0
  109. pyrestkit/validators/__init__.py +7 -0
  110. pyrestkit/validators/response_validator.py +57 -0
  111. pyrestkit/validators/schema_validator.py +33 -0
  112. pyrestkit-1.2.0.dist-info/METADATA +741 -0
  113. pyrestkit-1.2.0.dist-info/RECORD +115 -0
  114. pyrestkit-1.2.0.dist-info/WHEEL +5 -0
  115. pyrestkit-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class UserResponse:
8
+ id: int
9
+ email: str
10
+ first_name: str
11
+ last_name: str
12
+ avatar: str
@@ -0,0 +1,5 @@
1
+ from .pipeline import RequestPipeline
2
+
3
+ __all__ = [
4
+ "RequestPipeline",
5
+ ]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from pyrestkit.pipeline.request_context import RequestContext
6
+
7
+
8
+ class Middleware(ABC):
9
+ """
10
+ Base class for all middleware.
11
+ """
12
+
13
+ @abstractmethod
14
+ def process(
15
+ self,
16
+ context: RequestContext,
17
+ ) -> RequestContext:
18
+ raise NotImplementedError
@@ -0,0 +1,11 @@
1
+ from pyrestkit.pipeline.pipeline import RequestPipeline
2
+
3
+
4
+ class MiddlewareChain:
5
+ """
6
+ Factory responsible for creating request pipelines.
7
+ """
8
+
9
+ @staticmethod
10
+ def build() -> RequestPipeline:
11
+ return RequestPipeline()
@@ -0,0 +1,27 @@
1
+ from pyrestkit.pipeline.middleware import Middleware
2
+ from pyrestkit.pipeline.request_context import RequestContext
3
+
4
+
5
+ class RequestPipeline:
6
+ """
7
+ Executes middleware sequentially.
8
+ """
9
+
10
+ def __init__(self) -> None:
11
+ self._middlewares: list[Middleware] = []
12
+
13
+ def add(
14
+ self,
15
+ middleware: Middleware,
16
+ ) -> None:
17
+ self._middlewares.append(middleware)
18
+
19
+ def execute(
20
+ self,
21
+ context: RequestContext,
22
+ ) -> RequestContext:
23
+
24
+ for middleware in self._middlewares:
25
+ context = middleware.process(context)
26
+
27
+ return context
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class RequestContext:
9
+ """
10
+ Represents a single HTTP request flowing through the middleware pipeline.
11
+ """
12
+
13
+ method: str
14
+ url: str
15
+
16
+ headers: dict[str, str] = field(default_factory=dict)
17
+
18
+ params: dict[str, Any] = field(default_factory=dict)
19
+
20
+ json: dict[str, Any] | None = None
21
+
22
+ data: Any = None
23
+
24
+ timeout: int = 30
25
+
26
+ kwargs: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,8 @@
1
+ from .framework_response import FrameworkResponse, ResponseAIHelper
2
+ from .response_body import ResponseBody
3
+
4
+ __all__ = [
5
+ "FrameworkResponse",
6
+ "ResponseAIHelper",
7
+ "ResponseBody",
8
+ ]
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import is_dataclass
5
+ from datetime import timedelta
6
+ from typing import TYPE_CHECKING, Any, TypeVar
7
+
8
+ import requests
9
+
10
+ from pyrestkit.ai.analyzer import FailureAnalyzer
11
+ from pyrestkit.ai.client import AIClient
12
+ from pyrestkit.ai.config import AIConfig
13
+ from pyrestkit.ai.exceptions import AIConfigurationError
14
+ from pyrestkit.ai.models import FailureAnalysis, FailureContext
15
+ from pyrestkit.response.response_body import ResponseBody
16
+
17
+ if TYPE_CHECKING:
18
+ from pyrestkit.assertions.response_assertions import ResponseAssertions
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ class ResponseAIHelper:
24
+ """
25
+ Optional AI helper attached to a FrameworkResponse.
26
+
27
+ It is lazy-loaded and degrades gracefully by raising a framework-specific
28
+ configuration error when no AI client is configured.
29
+ """
30
+
31
+ def __init__(self, response: FrameworkResponse) -> None:
32
+ self._response = response
33
+
34
+ def explain_failure(
35
+ self,
36
+ *,
37
+ client: AIClient | None = None,
38
+ config: AIConfig | None = None,
39
+ prompt_loader: Any | None = None,
40
+ ) -> FailureAnalysis:
41
+ if client is None:
42
+ if config is None:
43
+ raise AIConfigurationError("AI is not configured.")
44
+
45
+ client = AIClient(config=config)
46
+
47
+ analyzer = FailureAnalyzer(client, prompt_loader=prompt_loader)
48
+ context = FailureContext(
49
+ test_name="response_failure",
50
+ error_type="HTTPError",
51
+ error_message=self._response._response.reason,
52
+ request_method=None,
53
+ request_url=str(self._response._response.url or ""),
54
+ response_status=self._response.status,
55
+ response_body=self._response.text,
56
+ )
57
+
58
+ return analyzer.analyze(context)
59
+
60
+
61
+ class FrameworkResponse:
62
+ """
63
+ Wrapper around requests.Response.
64
+
65
+ Adds framework features while still exposing
66
+ the original response.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ response: requests.Response,
72
+ ) -> None:
73
+ self._response = response
74
+ self._body: ResponseBody | None = None
75
+ self._ai_helper: ResponseAIHelper | None = None
76
+
77
+ @property
78
+ def raw(
79
+ self,
80
+ ) -> requests.Response:
81
+ return self._response
82
+
83
+ @property
84
+ def should(
85
+ self,
86
+ ) -> ResponseAssertions:
87
+ from pyrestkit.assertions.response_assertions import (
88
+ ResponseAssertions,
89
+ )
90
+
91
+ return ResponseAssertions(self)
92
+
93
+ @property
94
+ def status(
95
+ self,
96
+ ) -> int:
97
+ return self._response.status_code
98
+
99
+ @property
100
+ def status_code(
101
+ self,
102
+ ) -> int:
103
+ return self._response.status_code
104
+
105
+ @property
106
+ def headers(
107
+ self,
108
+ ) -> Mapping[str, str]:
109
+ return self._response.headers
110
+
111
+ @property
112
+ def ai(
113
+ self,
114
+ ) -> ResponseAIHelper:
115
+ if self._ai_helper is None:
116
+ self._ai_helper = ResponseAIHelper(self)
117
+
118
+ return self._ai_helper
119
+
120
+ @property
121
+ def text(
122
+ self,
123
+ ) -> str:
124
+ return self._response.text
125
+
126
+ @property
127
+ def elapsed(
128
+ self,
129
+ ) -> timedelta:
130
+ return self._response.elapsed
131
+
132
+ @property
133
+ def body(
134
+ self,
135
+ ) -> ResponseBody:
136
+ if self._body is None:
137
+ self._body = ResponseBody(
138
+ self._response.json(),
139
+ )
140
+
141
+ return self._body
142
+
143
+ def json(
144
+ self,
145
+ ) -> Any:
146
+ return self._response.json()
147
+
148
+ def as_model(
149
+ self,
150
+ model: type[T],
151
+ path: str | None = None,
152
+ ) -> T:
153
+ """
154
+ Deserialize a JSON object into a dataclass.
155
+
156
+ Examples:
157
+
158
+ response.as_model(User)
159
+
160
+ response.as_model(User, path="data")
161
+
162
+ response.as_model(User, path="result.user")
163
+ """
164
+ if not is_dataclass(model):
165
+ raise TypeError(
166
+ f"{model.__name__} is not a dataclass.",
167
+ )
168
+
169
+ data = self._get_value(path)
170
+
171
+ if not isinstance(data, dict):
172
+ raise TypeError(
173
+ "Expected a JSON object.",
174
+ )
175
+
176
+ return model(**data)
177
+
178
+ def _get_value(
179
+ self,
180
+ path: str | None = None,
181
+ ) -> Any:
182
+ """
183
+ Returns a value from the response JSON using dot notation.
184
+
185
+ Examples:
186
+
187
+ None
188
+ -> whole JSON
189
+
190
+ "data"
191
+ -> json()["data"]
192
+
193
+ "data.user"
194
+ -> json()["data"]["user"]
195
+ """
196
+ data = self.json()
197
+
198
+ if path is None:
199
+ return data
200
+
201
+ for key in path.split("."):
202
+ if not isinstance(data, dict):
203
+ raise TypeError(
204
+ f"Cannot access '{key}' as the current value is not a JSON object.",
205
+ )
206
+
207
+ if key not in data:
208
+ raise KeyError(
209
+ f"Path '{path}' not found in response.",
210
+ )
211
+
212
+ data = data[key]
213
+
214
+ return data
215
+
216
+ def as_list(
217
+ self,
218
+ model: type[T],
219
+ path: str | None = None,
220
+ ) -> list[T]:
221
+ """
222
+ Deserialize a JSON array into a list of dataclasses.
223
+
224
+ Examples:
225
+
226
+ response.as_list(User)
227
+
228
+ response.as_list(User, path="data")
229
+
230
+ response.as_list(User, path="result.users")
231
+ """
232
+ if not is_dataclass(model):
233
+ raise TypeError(
234
+ f"{model.__name__} is not a dataclass.",
235
+ )
236
+
237
+ data = self._get_value(path)
238
+
239
+ if not isinstance(data, list):
240
+ raise TypeError(
241
+ "Expected a JSON array.",
242
+ )
243
+
244
+ return [model(**item) for item in data]
245
+
246
+ def raise_for_status(
247
+ self,
248
+ ) -> None:
249
+ self._response.raise_for_status()
250
+
251
+ def __getattr__(
252
+ self,
253
+ name: str,
254
+ ) -> Any:
255
+ """
256
+ Delegate unknown attributes to requests.Response.
257
+ """
258
+ return getattr(
259
+ self._response,
260
+ name,
261
+ )
262
+
263
+ def __repr__(
264
+ self,
265
+ ) -> str:
266
+ return repr(self._response)
267
+
268
+ def __str__(
269
+ self,
270
+ ) -> str:
271
+ return str(self._response)
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Any
5
+
6
+
7
+ class ResponseBody:
8
+ """
9
+ Provides attribute-style access to JSON responses.
10
+
11
+ Example:
12
+
13
+ body.first_name
14
+ body.data.email
15
+ body.users[0].email
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ data: Any,
21
+ ) -> None:
22
+ self._data = data
23
+
24
+ def __getattr__(
25
+ self,
26
+ name: str,
27
+ ) -> Any:
28
+ if not isinstance(self._data, dict):
29
+ raise AttributeError(
30
+ f"'{type(self._data).__name__}' has no attribute '{name}'."
31
+ )
32
+
33
+ if name not in self._data:
34
+ raise AttributeError(f"Field '{name}' not found.")
35
+
36
+ return self._wrap(
37
+ self._data[name],
38
+ )
39
+
40
+ def __getitem__(
41
+ self,
42
+ key: Any,
43
+ ) -> Any:
44
+ return self._wrap(
45
+ self._data[key],
46
+ )
47
+
48
+ # def __iter__(
49
+ # self,
50
+ # ):
51
+ # if isinstance(
52
+ # self._data,
53
+ # list,
54
+ # ):
55
+ # for item in self._data:
56
+ # yield self._wrap(item)
57
+ # else:
58
+ # raise TypeError("ResponseBody is not iterable.")
59
+
60
+ # from collections.abc import Iterator
61
+
62
+ # ...
63
+
64
+ def __iter__(
65
+ self,
66
+ ) -> Iterator[Any]:
67
+ if isinstance(
68
+ self._data,
69
+ list,
70
+ ):
71
+ for item in self._data:
72
+ yield self._wrap(item)
73
+ else:
74
+ raise TypeError("ResponseBody is not iterable.")
75
+
76
+ @staticmethod
77
+ def _wrap(
78
+ value: Any,
79
+ ) -> Any:
80
+ if isinstance(
81
+ value,
82
+ dict,
83
+ ):
84
+ return ResponseBody(value)
85
+
86
+ if isinstance(
87
+ value,
88
+ list,
89
+ ):
90
+ return [ResponseBody._wrap(item) for item in value]
91
+
92
+ return value
93
+
94
+ def to_dict(
95
+ self,
96
+ ) -> Any:
97
+ """
98
+ Returns the underlying JSON object.
99
+ """
100
+ return self._data
101
+
102
+ def get(
103
+ self,
104
+ path: str,
105
+ ) -> Any:
106
+ """
107
+ Returns a JSON value using dot notation.
108
+
109
+ Example:
110
+
111
+ body.get("data.users")
112
+ """
113
+
114
+ current: Any = self._data
115
+
116
+ for part in path.split("."):
117
+ if isinstance(current, dict):
118
+ if part not in current:
119
+ raise AttributeError(f"Field '{path}' not found.")
120
+ current = current[part]
121
+ else:
122
+ raise AttributeError(f"Field '{path}' not found.")
123
+
124
+ return current
@@ -0,0 +1,5 @@
1
+ from .retry_handler import RetryHandler
2
+
3
+ __all__ = [
4
+ "RetryHandler",
5
+ ]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+
6
+ class ExponentialBackoff:
7
+ """
8
+ Implements exponential backoff.
9
+
10
+ Example:
11
+
12
+ attempt=1 -> 1 sec
13
+
14
+ attempt=2 -> 2 sec
15
+
16
+ attempt=3 -> 4 sec
17
+
18
+ attempt=4 -> 8 sec
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ factor: float = 1.0,
24
+ ) -> None:
25
+ self._factor = factor
26
+
27
+ def sleep(
28
+ self,
29
+ attempt: int,
30
+ ) -> None:
31
+ delay = self._factor * (2 ** (attempt - 1))
32
+ time.sleep(delay)
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from pyrestkit.retry.backoff import ExponentialBackoff
9
+ from pyrestkit.retry.retry_policy import RetryPolicy
10
+
11
+
12
+ class RetryHandler:
13
+ """
14
+ Executes HTTP requests with retry support.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ policy: RetryPolicy,
20
+ ) -> None:
21
+ self._policy = policy
22
+ self._backoff = ExponentialBackoff(
23
+ factor=policy.backoff_factor,
24
+ )
25
+
26
+ def execute(
27
+ self,
28
+ request: Callable[..., requests.Response],
29
+ **kwargs: Any,
30
+ ) -> requests.Response:
31
+ last_exception: Exception | None = None
32
+
33
+ for attempt in range(
34
+ 1,
35
+ self._policy.retries + 1,
36
+ ):
37
+ try:
38
+ response = request(**kwargs)
39
+
40
+ if response.status_code not in self._policy.retry_on_status:
41
+ return response
42
+
43
+ except self._policy.retry_on_exceptions as exc:
44
+ last_exception = exc
45
+
46
+ if attempt < self._policy.retries:
47
+ self._backoff.sleep(attempt)
48
+
49
+ if last_exception is not None:
50
+ raise last_exception
51
+
52
+ return response
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import requests
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class RetryPolicy:
10
+ """
11
+ Retry configuration.
12
+ """
13
+
14
+ retries: int = 3
15
+
16
+ backoff_factor: float = 1.0
17
+
18
+ retry_on_status: set[int] = field(
19
+ default_factory=lambda: {
20
+ 500,
21
+ 502,
22
+ 503,
23
+ 504,
24
+ },
25
+ )
26
+
27
+ retry_on_exceptions: tuple[
28
+ type[Exception],
29
+ ...,
30
+ ] = (
31
+ requests.exceptions.Timeout,
32
+ requests.exceptions.ConnectionError,
33
+ )
File without changes
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypeVar, cast
4
+
5
+ from pyrestkit.exceptions.serialization_exception import SerializationException
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class ResponseMapper:
11
+ """
12
+ Maps API response dictionaries to model objects.
13
+ """
14
+
15
+ @staticmethod
16
+ def map(
17
+ data: dict[str, Any],
18
+ model: type[T],
19
+ ) -> T:
20
+ try:
21
+ return cast(T, model.from_dict(data)) # type: ignore[attr-defined]
22
+ except Exception as exc:
23
+ raise SerializationException(
24
+ f"Unable to map response to {model.__name__}"
25
+ ) from exc
File without changes
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol, TypeVar
4
+
5
+
6
+ class ModelProtocol(Protocol):
7
+ @classmethod
8
+ def from_dict(
9
+ cls,
10
+ data: dict[str, Any],
11
+ ) -> ModelProtocol: ...
12
+
13
+
14
+ T = TypeVar(
15
+ "T",
16
+ bound=ModelProtocol,
17
+ )
File without changes
@@ -0,0 +1,7 @@
1
+ from .response_validator import ResponseValidator
2
+ from .schema_validator import SchemaValidator
3
+
4
+ __all__ = [
5
+ "ResponseValidator",
6
+ "SchemaValidator",
7
+ ]