isaacus 0.1.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.
- isaacus/__init__.py +84 -0
- isaacus/_base_client.py +1969 -0
- isaacus/_client.py +397 -0
- isaacus/_compat.py +219 -0
- isaacus/_constants.py +14 -0
- isaacus/_exceptions.py +108 -0
- isaacus/_files.py +123 -0
- isaacus/_models.py +801 -0
- isaacus/_qs.py +150 -0
- isaacus/_resource.py +43 -0
- isaacus/_response.py +830 -0
- isaacus/_streaming.py +333 -0
- isaacus/_types.py +217 -0
- isaacus/_utils/__init__.py +57 -0
- isaacus/_utils/_logs.py +25 -0
- isaacus/_utils/_proxy.py +62 -0
- isaacus/_utils/_reflection.py +42 -0
- isaacus/_utils/_streams.py +12 -0
- isaacus/_utils/_sync.py +86 -0
- isaacus/_utils/_transform.py +402 -0
- isaacus/_utils/_typing.py +149 -0
- isaacus/_utils/_utils.py +414 -0
- isaacus/_version.py +4 -0
- isaacus/lib/.keep +4 -0
- isaacus/py.typed +0 -0
- isaacus/resources/__init__.py +19 -0
- isaacus/resources/classifications/__init__.py +33 -0
- isaacus/resources/classifications/classifications.py +102 -0
- isaacus/resources/classifications/universal.py +263 -0
- isaacus/types/__init__.py +3 -0
- isaacus/types/classifications/__init__.py +6 -0
- isaacus/types/classifications/universal_classification.py +55 -0
- isaacus/types/classifications/universal_create_params.py +64 -0
- isaacus-0.1.0a1.dist-info/METADATA +391 -0
- isaacus-0.1.0a1.dist-info/RECORD +37 -0
- isaacus-0.1.0a1.dist-info/WHEEL +4 -0
- isaacus-0.1.0a1.dist-info/licenses/LICENSE +201 -0
isaacus/_models.py
ADDED
@@ -0,0 +1,801 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import inspect
|
5
|
+
from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
|
6
|
+
from datetime import date, datetime
|
7
|
+
from typing_extensions import (
|
8
|
+
Unpack,
|
9
|
+
Literal,
|
10
|
+
ClassVar,
|
11
|
+
Protocol,
|
12
|
+
Required,
|
13
|
+
ParamSpec,
|
14
|
+
TypedDict,
|
15
|
+
TypeGuard,
|
16
|
+
final,
|
17
|
+
override,
|
18
|
+
runtime_checkable,
|
19
|
+
)
|
20
|
+
|
21
|
+
import pydantic
|
22
|
+
import pydantic.generics
|
23
|
+
from pydantic.fields import FieldInfo
|
24
|
+
|
25
|
+
from ._types import (
|
26
|
+
Body,
|
27
|
+
IncEx,
|
28
|
+
Query,
|
29
|
+
ModelT,
|
30
|
+
Headers,
|
31
|
+
Timeout,
|
32
|
+
NotGiven,
|
33
|
+
AnyMapping,
|
34
|
+
HttpxRequestFiles,
|
35
|
+
)
|
36
|
+
from ._utils import (
|
37
|
+
PropertyInfo,
|
38
|
+
is_list,
|
39
|
+
is_given,
|
40
|
+
json_safe,
|
41
|
+
lru_cache,
|
42
|
+
is_mapping,
|
43
|
+
parse_date,
|
44
|
+
coerce_boolean,
|
45
|
+
parse_datetime,
|
46
|
+
strip_not_given,
|
47
|
+
extract_type_arg,
|
48
|
+
is_annotated_type,
|
49
|
+
is_type_alias_type,
|
50
|
+
strip_annotated_type,
|
51
|
+
)
|
52
|
+
from ._compat import (
|
53
|
+
PYDANTIC_V2,
|
54
|
+
ConfigDict,
|
55
|
+
GenericModel as BaseGenericModel,
|
56
|
+
get_args,
|
57
|
+
is_union,
|
58
|
+
parse_obj,
|
59
|
+
get_origin,
|
60
|
+
is_literal_type,
|
61
|
+
get_model_config,
|
62
|
+
get_model_fields,
|
63
|
+
field_get_default,
|
64
|
+
)
|
65
|
+
from ._constants import RAW_RESPONSE_HEADER
|
66
|
+
|
67
|
+
if TYPE_CHECKING:
|
68
|
+
from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema
|
69
|
+
|
70
|
+
__all__ = ["BaseModel", "GenericModel"]
|
71
|
+
|
72
|
+
_T = TypeVar("_T")
|
73
|
+
_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
|
74
|
+
|
75
|
+
P = ParamSpec("P")
|
76
|
+
|
77
|
+
|
78
|
+
@runtime_checkable
|
79
|
+
class _ConfigProtocol(Protocol):
|
80
|
+
allow_population_by_field_name: bool
|
81
|
+
|
82
|
+
|
83
|
+
class BaseModel(pydantic.BaseModel):
|
84
|
+
if PYDANTIC_V2:
|
85
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(
|
86
|
+
extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
|
87
|
+
)
|
88
|
+
else:
|
89
|
+
|
90
|
+
@property
|
91
|
+
@override
|
92
|
+
def model_fields_set(self) -> set[str]:
|
93
|
+
# a forwards-compat shim for pydantic v2
|
94
|
+
return self.__fields_set__ # type: ignore
|
95
|
+
|
96
|
+
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
|
97
|
+
extra: Any = pydantic.Extra.allow # type: ignore
|
98
|
+
|
99
|
+
def to_dict(
|
100
|
+
self,
|
101
|
+
*,
|
102
|
+
mode: Literal["json", "python"] = "python",
|
103
|
+
use_api_names: bool = True,
|
104
|
+
exclude_unset: bool = True,
|
105
|
+
exclude_defaults: bool = False,
|
106
|
+
exclude_none: bool = False,
|
107
|
+
warnings: bool = True,
|
108
|
+
) -> dict[str, object]:
|
109
|
+
"""Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
110
|
+
|
111
|
+
By default, fields that were not set by the API will not be included,
|
112
|
+
and keys will match the API response, *not* the property names from the model.
|
113
|
+
|
114
|
+
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
|
115
|
+
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
|
116
|
+
|
117
|
+
Args:
|
118
|
+
mode:
|
119
|
+
If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`.
|
120
|
+
If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)`
|
121
|
+
|
122
|
+
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
|
123
|
+
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
124
|
+
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
|
125
|
+
exclude_none: Whether to exclude fields that have a value of `None` from the output.
|
126
|
+
warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2.
|
127
|
+
"""
|
128
|
+
return self.model_dump(
|
129
|
+
mode=mode,
|
130
|
+
by_alias=use_api_names,
|
131
|
+
exclude_unset=exclude_unset,
|
132
|
+
exclude_defaults=exclude_defaults,
|
133
|
+
exclude_none=exclude_none,
|
134
|
+
warnings=warnings,
|
135
|
+
)
|
136
|
+
|
137
|
+
def to_json(
|
138
|
+
self,
|
139
|
+
*,
|
140
|
+
indent: int | None = 2,
|
141
|
+
use_api_names: bool = True,
|
142
|
+
exclude_unset: bool = True,
|
143
|
+
exclude_defaults: bool = False,
|
144
|
+
exclude_none: bool = False,
|
145
|
+
warnings: bool = True,
|
146
|
+
) -> str:
|
147
|
+
"""Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation).
|
148
|
+
|
149
|
+
By default, fields that were not set by the API will not be included,
|
150
|
+
and keys will match the API response, *not* the property names from the model.
|
151
|
+
|
152
|
+
For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property,
|
153
|
+
the output will use the `"fooBar"` key (unless `use_api_names=False` is passed).
|
154
|
+
|
155
|
+
Args:
|
156
|
+
indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2`
|
157
|
+
use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`.
|
158
|
+
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
159
|
+
exclude_defaults: Whether to exclude fields that have the default value.
|
160
|
+
exclude_none: Whether to exclude fields that have a value of `None`.
|
161
|
+
warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2.
|
162
|
+
"""
|
163
|
+
return self.model_dump_json(
|
164
|
+
indent=indent,
|
165
|
+
by_alias=use_api_names,
|
166
|
+
exclude_unset=exclude_unset,
|
167
|
+
exclude_defaults=exclude_defaults,
|
168
|
+
exclude_none=exclude_none,
|
169
|
+
warnings=warnings,
|
170
|
+
)
|
171
|
+
|
172
|
+
@override
|
173
|
+
def __str__(self) -> str:
|
174
|
+
# mypy complains about an invalid self arg
|
175
|
+
return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
|
176
|
+
|
177
|
+
# Override the 'construct' method in a way that supports recursive parsing without validation.
|
178
|
+
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
|
179
|
+
@classmethod
|
180
|
+
@override
|
181
|
+
def construct( # pyright: ignore[reportIncompatibleMethodOverride]
|
182
|
+
__cls: Type[ModelT],
|
183
|
+
_fields_set: set[str] | None = None,
|
184
|
+
**values: object,
|
185
|
+
) -> ModelT:
|
186
|
+
m = __cls.__new__(__cls)
|
187
|
+
fields_values: dict[str, object] = {}
|
188
|
+
|
189
|
+
config = get_model_config(__cls)
|
190
|
+
populate_by_name = (
|
191
|
+
config.allow_population_by_field_name
|
192
|
+
if isinstance(config, _ConfigProtocol)
|
193
|
+
else config.get("populate_by_name")
|
194
|
+
)
|
195
|
+
|
196
|
+
if _fields_set is None:
|
197
|
+
_fields_set = set()
|
198
|
+
|
199
|
+
model_fields = get_model_fields(__cls)
|
200
|
+
for name, field in model_fields.items():
|
201
|
+
key = field.alias
|
202
|
+
if key is None or (key not in values and populate_by_name):
|
203
|
+
key = name
|
204
|
+
|
205
|
+
if key in values:
|
206
|
+
fields_values[name] = _construct_field(value=values[key], field=field, key=key)
|
207
|
+
_fields_set.add(name)
|
208
|
+
else:
|
209
|
+
fields_values[name] = field_get_default(field)
|
210
|
+
|
211
|
+
_extra = {}
|
212
|
+
for key, value in values.items():
|
213
|
+
if key not in model_fields:
|
214
|
+
if PYDANTIC_V2:
|
215
|
+
_extra[key] = value
|
216
|
+
else:
|
217
|
+
_fields_set.add(key)
|
218
|
+
fields_values[key] = value
|
219
|
+
|
220
|
+
object.__setattr__(m, "__dict__", fields_values)
|
221
|
+
|
222
|
+
if PYDANTIC_V2:
|
223
|
+
# these properties are copied from Pydantic's `model_construct()` method
|
224
|
+
object.__setattr__(m, "__pydantic_private__", None)
|
225
|
+
object.__setattr__(m, "__pydantic_extra__", _extra)
|
226
|
+
object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
|
227
|
+
else:
|
228
|
+
# init_private_attributes() does not exist in v2
|
229
|
+
m._init_private_attributes() # type: ignore
|
230
|
+
|
231
|
+
# copied from Pydantic v1's `construct()` method
|
232
|
+
object.__setattr__(m, "__fields_set__", _fields_set)
|
233
|
+
|
234
|
+
return m
|
235
|
+
|
236
|
+
if not TYPE_CHECKING:
|
237
|
+
# type checkers incorrectly complain about this assignment
|
238
|
+
# because the type signatures are technically different
|
239
|
+
# although not in practice
|
240
|
+
model_construct = construct
|
241
|
+
|
242
|
+
if not PYDANTIC_V2:
|
243
|
+
# we define aliases for some of the new pydantic v2 methods so
|
244
|
+
# that we can just document these methods without having to specify
|
245
|
+
# a specific pydantic version as some users may not know which
|
246
|
+
# pydantic version they are currently using
|
247
|
+
|
248
|
+
@override
|
249
|
+
def model_dump(
|
250
|
+
self,
|
251
|
+
*,
|
252
|
+
mode: Literal["json", "python"] | str = "python",
|
253
|
+
include: IncEx | None = None,
|
254
|
+
exclude: IncEx | None = None,
|
255
|
+
by_alias: bool = False,
|
256
|
+
exclude_unset: bool = False,
|
257
|
+
exclude_defaults: bool = False,
|
258
|
+
exclude_none: bool = False,
|
259
|
+
round_trip: bool = False,
|
260
|
+
warnings: bool | Literal["none", "warn", "error"] = True,
|
261
|
+
context: dict[str, Any] | None = None,
|
262
|
+
serialize_as_any: bool = False,
|
263
|
+
) -> dict[str, Any]:
|
264
|
+
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump
|
265
|
+
|
266
|
+
Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
mode: The mode in which `to_python` should run.
|
270
|
+
If mode is 'json', the dictionary will only contain JSON serializable types.
|
271
|
+
If mode is 'python', the dictionary may contain any Python objects.
|
272
|
+
include: A list of fields to include in the output.
|
273
|
+
exclude: A list of fields to exclude from the output.
|
274
|
+
by_alias: Whether to use the field's alias in the dictionary key if defined.
|
275
|
+
exclude_unset: Whether to exclude fields that are unset or None from the output.
|
276
|
+
exclude_defaults: Whether to exclude fields that are set to their default value from the output.
|
277
|
+
exclude_none: Whether to exclude fields that have a value of `None` from the output.
|
278
|
+
round_trip: Whether to enable serialization and deserialization round-trip support.
|
279
|
+
warnings: Whether to log warnings when invalid fields are encountered.
|
280
|
+
|
281
|
+
Returns:
|
282
|
+
A dictionary representation of the model.
|
283
|
+
"""
|
284
|
+
if mode not in {"json", "python"}:
|
285
|
+
raise ValueError("mode must be either 'json' or 'python'")
|
286
|
+
if round_trip != False:
|
287
|
+
raise ValueError("round_trip is only supported in Pydantic v2")
|
288
|
+
if warnings != True:
|
289
|
+
raise ValueError("warnings is only supported in Pydantic v2")
|
290
|
+
if context is not None:
|
291
|
+
raise ValueError("context is only supported in Pydantic v2")
|
292
|
+
if serialize_as_any != False:
|
293
|
+
raise ValueError("serialize_as_any is only supported in Pydantic v2")
|
294
|
+
dumped = super().dict( # pyright: ignore[reportDeprecated]
|
295
|
+
include=include,
|
296
|
+
exclude=exclude,
|
297
|
+
by_alias=by_alias,
|
298
|
+
exclude_unset=exclude_unset,
|
299
|
+
exclude_defaults=exclude_defaults,
|
300
|
+
exclude_none=exclude_none,
|
301
|
+
)
|
302
|
+
|
303
|
+
return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
|
304
|
+
|
305
|
+
@override
|
306
|
+
def model_dump_json(
|
307
|
+
self,
|
308
|
+
*,
|
309
|
+
indent: int | None = None,
|
310
|
+
include: IncEx | None = None,
|
311
|
+
exclude: IncEx | None = None,
|
312
|
+
by_alias: bool = False,
|
313
|
+
exclude_unset: bool = False,
|
314
|
+
exclude_defaults: bool = False,
|
315
|
+
exclude_none: bool = False,
|
316
|
+
round_trip: bool = False,
|
317
|
+
warnings: bool | Literal["none", "warn", "error"] = True,
|
318
|
+
context: dict[str, Any] | None = None,
|
319
|
+
serialize_as_any: bool = False,
|
320
|
+
) -> str:
|
321
|
+
"""Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json
|
322
|
+
|
323
|
+
Generates a JSON representation of the model using Pydantic's `to_json` method.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.
|
327
|
+
include: Field(s) to include in the JSON output. Can take either a string or set of strings.
|
328
|
+
exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings.
|
329
|
+
by_alias: Whether to serialize using field aliases.
|
330
|
+
exclude_unset: Whether to exclude fields that have not been explicitly set.
|
331
|
+
exclude_defaults: Whether to exclude fields that have the default value.
|
332
|
+
exclude_none: Whether to exclude fields that have a value of `None`.
|
333
|
+
round_trip: Whether to use serialization/deserialization between JSON and class instance.
|
334
|
+
warnings: Whether to show any warnings that occurred during serialization.
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
A JSON string representation of the model.
|
338
|
+
"""
|
339
|
+
if round_trip != False:
|
340
|
+
raise ValueError("round_trip is only supported in Pydantic v2")
|
341
|
+
if warnings != True:
|
342
|
+
raise ValueError("warnings is only supported in Pydantic v2")
|
343
|
+
if context is not None:
|
344
|
+
raise ValueError("context is only supported in Pydantic v2")
|
345
|
+
if serialize_as_any != False:
|
346
|
+
raise ValueError("serialize_as_any is only supported in Pydantic v2")
|
347
|
+
return super().json( # type: ignore[reportDeprecated]
|
348
|
+
indent=indent,
|
349
|
+
include=include,
|
350
|
+
exclude=exclude,
|
351
|
+
by_alias=by_alias,
|
352
|
+
exclude_unset=exclude_unset,
|
353
|
+
exclude_defaults=exclude_defaults,
|
354
|
+
exclude_none=exclude_none,
|
355
|
+
)
|
356
|
+
|
357
|
+
|
358
|
+
def _construct_field(value: object, field: FieldInfo, key: str) -> object:
|
359
|
+
if value is None:
|
360
|
+
return field_get_default(field)
|
361
|
+
|
362
|
+
if PYDANTIC_V2:
|
363
|
+
type_ = field.annotation
|
364
|
+
else:
|
365
|
+
type_ = cast(type, field.outer_type_) # type: ignore
|
366
|
+
|
367
|
+
if type_ is None:
|
368
|
+
raise RuntimeError(f"Unexpected field type is None for {key}")
|
369
|
+
|
370
|
+
return construct_type(value=value, type_=type_)
|
371
|
+
|
372
|
+
|
373
|
+
def is_basemodel(type_: type) -> bool:
|
374
|
+
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
|
375
|
+
if is_union(type_):
|
376
|
+
for variant in get_args(type_):
|
377
|
+
if is_basemodel(variant):
|
378
|
+
return True
|
379
|
+
|
380
|
+
return False
|
381
|
+
|
382
|
+
return is_basemodel_type(type_)
|
383
|
+
|
384
|
+
|
385
|
+
def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]:
|
386
|
+
origin = get_origin(type_) or type_
|
387
|
+
if not inspect.isclass(origin):
|
388
|
+
return False
|
389
|
+
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
|
390
|
+
|
391
|
+
|
392
|
+
def build(
|
393
|
+
base_model_cls: Callable[P, _BaseModelT],
|
394
|
+
*args: P.args,
|
395
|
+
**kwargs: P.kwargs,
|
396
|
+
) -> _BaseModelT:
|
397
|
+
"""Construct a BaseModel class without validation.
|
398
|
+
|
399
|
+
This is useful for cases where you need to instantiate a `BaseModel`
|
400
|
+
from an API response as this provides type-safe params which isn't supported
|
401
|
+
by helpers like `construct_type()`.
|
402
|
+
|
403
|
+
```py
|
404
|
+
build(MyModel, my_field_a="foo", my_field_b=123)
|
405
|
+
```
|
406
|
+
"""
|
407
|
+
if args:
|
408
|
+
raise TypeError(
|
409
|
+
"Received positional arguments which are not supported; Keyword arguments must be used instead",
|
410
|
+
)
|
411
|
+
|
412
|
+
return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
|
413
|
+
|
414
|
+
|
415
|
+
def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
|
416
|
+
"""Loose coercion to the expected type with construction of nested values.
|
417
|
+
|
418
|
+
Note: the returned value from this function is not guaranteed to match the
|
419
|
+
given type.
|
420
|
+
"""
|
421
|
+
return cast(_T, construct_type(value=value, type_=type_))
|
422
|
+
|
423
|
+
|
424
|
+
def construct_type(*, value: object, type_: object) -> object:
|
425
|
+
"""Loose coercion to the expected type with construction of nested values.
|
426
|
+
|
427
|
+
If the given value does not match the expected type then it is returned as-is.
|
428
|
+
"""
|
429
|
+
|
430
|
+
# store a reference to the original type we were given before we extract any inner
|
431
|
+
# types so that we can properly resolve forward references in `TypeAliasType` annotations
|
432
|
+
original_type = None
|
433
|
+
|
434
|
+
# we allow `object` as the input type because otherwise, passing things like
|
435
|
+
# `Literal['value']` will be reported as a type error by type checkers
|
436
|
+
type_ = cast("type[object]", type_)
|
437
|
+
if is_type_alias_type(type_):
|
438
|
+
original_type = type_ # type: ignore[unreachable]
|
439
|
+
type_ = type_.__value__ # type: ignore[unreachable]
|
440
|
+
|
441
|
+
# unwrap `Annotated[T, ...]` -> `T`
|
442
|
+
if is_annotated_type(type_):
|
443
|
+
meta: tuple[Any, ...] = get_args(type_)[1:]
|
444
|
+
type_ = extract_type_arg(type_, 0)
|
445
|
+
else:
|
446
|
+
meta = tuple()
|
447
|
+
|
448
|
+
# we need to use the origin class for any types that are subscripted generics
|
449
|
+
# e.g. Dict[str, object]
|
450
|
+
origin = get_origin(type_) or type_
|
451
|
+
args = get_args(type_)
|
452
|
+
|
453
|
+
if is_union(origin):
|
454
|
+
try:
|
455
|
+
return validate_type(type_=cast("type[object]", original_type or type_), value=value)
|
456
|
+
except Exception:
|
457
|
+
pass
|
458
|
+
|
459
|
+
# if the type is a discriminated union then we want to construct the right variant
|
460
|
+
# in the union, even if the data doesn't match exactly, otherwise we'd break code
|
461
|
+
# that relies on the constructed class types, e.g.
|
462
|
+
#
|
463
|
+
# class FooType:
|
464
|
+
# kind: Literal['foo']
|
465
|
+
# value: str
|
466
|
+
#
|
467
|
+
# class BarType:
|
468
|
+
# kind: Literal['bar']
|
469
|
+
# value: int
|
470
|
+
#
|
471
|
+
# without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then
|
472
|
+
# we'd end up constructing `FooType` when it should be `BarType`.
|
473
|
+
discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta)
|
474
|
+
if discriminator and is_mapping(value):
|
475
|
+
variant_value = value.get(discriminator.field_alias_from or discriminator.field_name)
|
476
|
+
if variant_value and isinstance(variant_value, str):
|
477
|
+
variant_type = discriminator.mapping.get(variant_value)
|
478
|
+
if variant_type:
|
479
|
+
return construct_type(type_=variant_type, value=value)
|
480
|
+
|
481
|
+
# if the data is not valid, use the first variant that doesn't fail while deserializing
|
482
|
+
for variant in args:
|
483
|
+
try:
|
484
|
+
return construct_type(value=value, type_=variant)
|
485
|
+
except Exception:
|
486
|
+
continue
|
487
|
+
|
488
|
+
raise RuntimeError(f"Could not convert data into a valid instance of {type_}")
|
489
|
+
|
490
|
+
if origin == dict:
|
491
|
+
if not is_mapping(value):
|
492
|
+
return value
|
493
|
+
|
494
|
+
_, items_type = get_args(type_) # Dict[_, items_type]
|
495
|
+
return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
|
496
|
+
|
497
|
+
if (
|
498
|
+
not is_literal_type(type_)
|
499
|
+
and inspect.isclass(origin)
|
500
|
+
and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
|
501
|
+
):
|
502
|
+
if is_list(value):
|
503
|
+
return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
|
504
|
+
|
505
|
+
if is_mapping(value):
|
506
|
+
if issubclass(type_, BaseModel):
|
507
|
+
return type_.construct(**value) # type: ignore[arg-type]
|
508
|
+
|
509
|
+
return cast(Any, type_).construct(**value)
|
510
|
+
|
511
|
+
if origin == list:
|
512
|
+
if not is_list(value):
|
513
|
+
return value
|
514
|
+
|
515
|
+
inner_type = args[0] # List[inner_type]
|
516
|
+
return [construct_type(value=entry, type_=inner_type) for entry in value]
|
517
|
+
|
518
|
+
if origin == float:
|
519
|
+
if isinstance(value, int):
|
520
|
+
coerced = float(value)
|
521
|
+
if coerced != value:
|
522
|
+
return value
|
523
|
+
return coerced
|
524
|
+
|
525
|
+
return value
|
526
|
+
|
527
|
+
if type_ == datetime:
|
528
|
+
try:
|
529
|
+
return parse_datetime(value) # type: ignore
|
530
|
+
except Exception:
|
531
|
+
return value
|
532
|
+
|
533
|
+
if type_ == date:
|
534
|
+
try:
|
535
|
+
return parse_date(value) # type: ignore
|
536
|
+
except Exception:
|
537
|
+
return value
|
538
|
+
|
539
|
+
return value
|
540
|
+
|
541
|
+
|
542
|
+
@runtime_checkable
|
543
|
+
class CachedDiscriminatorType(Protocol):
|
544
|
+
__discriminator__: DiscriminatorDetails
|
545
|
+
|
546
|
+
|
547
|
+
class DiscriminatorDetails:
|
548
|
+
field_name: str
|
549
|
+
"""The name of the discriminator field in the variant class, e.g.
|
550
|
+
|
551
|
+
```py
|
552
|
+
class Foo(BaseModel):
|
553
|
+
type: Literal['foo']
|
554
|
+
```
|
555
|
+
|
556
|
+
Will result in field_name='type'
|
557
|
+
"""
|
558
|
+
|
559
|
+
field_alias_from: str | None
|
560
|
+
"""The name of the discriminator field in the API response, e.g.
|
561
|
+
|
562
|
+
```py
|
563
|
+
class Foo(BaseModel):
|
564
|
+
type: Literal['foo'] = Field(alias='type_from_api')
|
565
|
+
```
|
566
|
+
|
567
|
+
Will result in field_alias_from='type_from_api'
|
568
|
+
"""
|
569
|
+
|
570
|
+
mapping: dict[str, type]
|
571
|
+
"""Mapping of discriminator value to variant type, e.g.
|
572
|
+
|
573
|
+
{'foo': FooVariant, 'bar': BarVariant}
|
574
|
+
"""
|
575
|
+
|
576
|
+
def __init__(
|
577
|
+
self,
|
578
|
+
*,
|
579
|
+
mapping: dict[str, type],
|
580
|
+
discriminator_field: str,
|
581
|
+
discriminator_alias: str | None,
|
582
|
+
) -> None:
|
583
|
+
self.mapping = mapping
|
584
|
+
self.field_name = discriminator_field
|
585
|
+
self.field_alias_from = discriminator_alias
|
586
|
+
|
587
|
+
|
588
|
+
def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None:
|
589
|
+
if isinstance(union, CachedDiscriminatorType):
|
590
|
+
return union.__discriminator__
|
591
|
+
|
592
|
+
discriminator_field_name: str | None = None
|
593
|
+
|
594
|
+
for annotation in meta_annotations:
|
595
|
+
if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None:
|
596
|
+
discriminator_field_name = annotation.discriminator
|
597
|
+
break
|
598
|
+
|
599
|
+
if not discriminator_field_name:
|
600
|
+
return None
|
601
|
+
|
602
|
+
mapping: dict[str, type] = {}
|
603
|
+
discriminator_alias: str | None = None
|
604
|
+
|
605
|
+
for variant in get_args(union):
|
606
|
+
variant = strip_annotated_type(variant)
|
607
|
+
if is_basemodel_type(variant):
|
608
|
+
if PYDANTIC_V2:
|
609
|
+
field = _extract_field_schema_pv2(variant, discriminator_field_name)
|
610
|
+
if not field:
|
611
|
+
continue
|
612
|
+
|
613
|
+
# Note: if one variant defines an alias then they all should
|
614
|
+
discriminator_alias = field.get("serialization_alias")
|
615
|
+
|
616
|
+
field_schema = field["schema"]
|
617
|
+
|
618
|
+
if field_schema["type"] == "literal":
|
619
|
+
for entry in cast("LiteralSchema", field_schema)["expected"]:
|
620
|
+
if isinstance(entry, str):
|
621
|
+
mapping[entry] = variant
|
622
|
+
else:
|
623
|
+
field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
|
624
|
+
if not field_info:
|
625
|
+
continue
|
626
|
+
|
627
|
+
# Note: if one variant defines an alias then they all should
|
628
|
+
discriminator_alias = field_info.alias
|
629
|
+
|
630
|
+
if field_info.annotation and is_literal_type(field_info.annotation):
|
631
|
+
for entry in get_args(field_info.annotation):
|
632
|
+
if isinstance(entry, str):
|
633
|
+
mapping[entry] = variant
|
634
|
+
|
635
|
+
if not mapping:
|
636
|
+
return None
|
637
|
+
|
638
|
+
details = DiscriminatorDetails(
|
639
|
+
mapping=mapping,
|
640
|
+
discriminator_field=discriminator_field_name,
|
641
|
+
discriminator_alias=discriminator_alias,
|
642
|
+
)
|
643
|
+
cast(CachedDiscriminatorType, union).__discriminator__ = details
|
644
|
+
return details
|
645
|
+
|
646
|
+
|
647
|
+
def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None:
|
648
|
+
schema = model.__pydantic_core_schema__
|
649
|
+
if schema["type"] != "model":
|
650
|
+
return None
|
651
|
+
|
652
|
+
fields_schema = schema["schema"]
|
653
|
+
if fields_schema["type"] != "model-fields":
|
654
|
+
return None
|
655
|
+
|
656
|
+
fields_schema = cast("ModelFieldsSchema", fields_schema)
|
657
|
+
|
658
|
+
field = fields_schema["fields"].get(field_name)
|
659
|
+
if not field:
|
660
|
+
return None
|
661
|
+
|
662
|
+
return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast]
|
663
|
+
|
664
|
+
|
665
|
+
def validate_type(*, type_: type[_T], value: object) -> _T:
|
666
|
+
"""Strict validation that the given value matches the expected type"""
|
667
|
+
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
|
668
|
+
return cast(_T, parse_obj(type_, value))
|
669
|
+
|
670
|
+
return cast(_T, _validate_non_model_type(type_=type_, value=value))
|
671
|
+
|
672
|
+
|
673
|
+
def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None:
|
674
|
+
"""Add a pydantic config for the given type.
|
675
|
+
|
676
|
+
Note: this is a no-op on Pydantic v1.
|
677
|
+
"""
|
678
|
+
setattr(typ, "__pydantic_config__", config) # noqa: B010
|
679
|
+
|
680
|
+
|
681
|
+
# our use of subclasssing here causes weirdness for type checkers,
|
682
|
+
# so we just pretend that we don't subclass
|
683
|
+
if TYPE_CHECKING:
|
684
|
+
GenericModel = BaseModel
|
685
|
+
else:
|
686
|
+
|
687
|
+
class GenericModel(BaseGenericModel, BaseModel):
|
688
|
+
pass
|
689
|
+
|
690
|
+
|
691
|
+
if PYDANTIC_V2:
|
692
|
+
from pydantic import TypeAdapter as _TypeAdapter
|
693
|
+
|
694
|
+
_CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
|
695
|
+
|
696
|
+
if TYPE_CHECKING:
|
697
|
+
from pydantic import TypeAdapter
|
698
|
+
else:
|
699
|
+
TypeAdapter = _CachedTypeAdapter
|
700
|
+
|
701
|
+
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
|
702
|
+
return TypeAdapter(type_).validate_python(value)
|
703
|
+
|
704
|
+
elif not TYPE_CHECKING: # TODO: condition is weird
|
705
|
+
|
706
|
+
class RootModel(GenericModel, Generic[_T]):
|
707
|
+
"""Used as a placeholder to easily convert runtime types to a Pydantic format
|
708
|
+
to provide validation.
|
709
|
+
|
710
|
+
For example:
|
711
|
+
```py
|
712
|
+
validated = RootModel[int](__root__="5").__root__
|
713
|
+
# validated: 5
|
714
|
+
```
|
715
|
+
"""
|
716
|
+
|
717
|
+
__root__: _T
|
718
|
+
|
719
|
+
def _validate_non_model_type(*, type_: type[_T], value: object) -> _T:
|
720
|
+
model = _create_pydantic_model(type_).validate(value)
|
721
|
+
return cast(_T, model.__root__)
|
722
|
+
|
723
|
+
def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
|
724
|
+
return RootModel[type_] # type: ignore
|
725
|
+
|
726
|
+
|
727
|
+
class FinalRequestOptionsInput(TypedDict, total=False):
|
728
|
+
method: Required[str]
|
729
|
+
url: Required[str]
|
730
|
+
params: Query
|
731
|
+
headers: Headers
|
732
|
+
max_retries: int
|
733
|
+
timeout: float | Timeout | None
|
734
|
+
files: HttpxRequestFiles | None
|
735
|
+
idempotency_key: str
|
736
|
+
json_data: Body
|
737
|
+
extra_json: AnyMapping
|
738
|
+
|
739
|
+
|
740
|
+
@final
|
741
|
+
class FinalRequestOptions(pydantic.BaseModel):
|
742
|
+
method: str
|
743
|
+
url: str
|
744
|
+
params: Query = {}
|
745
|
+
headers: Union[Headers, NotGiven] = NotGiven()
|
746
|
+
max_retries: Union[int, NotGiven] = NotGiven()
|
747
|
+
timeout: Union[float, Timeout, None, NotGiven] = NotGiven()
|
748
|
+
files: Union[HttpxRequestFiles, None] = None
|
749
|
+
idempotency_key: Union[str, None] = None
|
750
|
+
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
|
751
|
+
|
752
|
+
# It should be noted that we cannot use `json` here as that would override
|
753
|
+
# a BaseModel method in an incompatible fashion.
|
754
|
+
json_data: Union[Body, None] = None
|
755
|
+
extra_json: Union[AnyMapping, None] = None
|
756
|
+
|
757
|
+
if PYDANTIC_V2:
|
758
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
|
759
|
+
else:
|
760
|
+
|
761
|
+
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
|
762
|
+
arbitrary_types_allowed: bool = True
|
763
|
+
|
764
|
+
def get_max_retries(self, max_retries: int) -> int:
|
765
|
+
if isinstance(self.max_retries, NotGiven):
|
766
|
+
return max_retries
|
767
|
+
return self.max_retries
|
768
|
+
|
769
|
+
def _strip_raw_response_header(self) -> None:
|
770
|
+
if not is_given(self.headers):
|
771
|
+
return
|
772
|
+
|
773
|
+
if self.headers.get(RAW_RESPONSE_HEADER):
|
774
|
+
self.headers = {**self.headers}
|
775
|
+
self.headers.pop(RAW_RESPONSE_HEADER)
|
776
|
+
|
777
|
+
# override the `construct` method so that we can run custom transformations.
|
778
|
+
# this is necessary as we don't want to do any actual runtime type checking
|
779
|
+
# (which means we can't use validators) but we do want to ensure that `NotGiven`
|
780
|
+
# values are not present
|
781
|
+
#
|
782
|
+
# type ignore required because we're adding explicit types to `**values`
|
783
|
+
@classmethod
|
784
|
+
def construct( # type: ignore
|
785
|
+
cls,
|
786
|
+
_fields_set: set[str] | None = None,
|
787
|
+
**values: Unpack[FinalRequestOptionsInput],
|
788
|
+
) -> FinalRequestOptions:
|
789
|
+
kwargs: dict[str, Any] = {
|
790
|
+
# we unconditionally call `strip_not_given` on any value
|
791
|
+
# as it will just ignore any non-mapping types
|
792
|
+
key: strip_not_given(value)
|
793
|
+
for key, value in values.items()
|
794
|
+
}
|
795
|
+
if PYDANTIC_V2:
|
796
|
+
return super().model_construct(_fields_set, **kwargs)
|
797
|
+
return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
|
798
|
+
|
799
|
+
if not TYPE_CHECKING:
|
800
|
+
# type checkers incorrectly complain about this assignment
|
801
|
+
model_construct = construct
|