parallel-web 0.1.3__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of parallel-web might be problematic. Click here for more details.

Files changed (77) hide show
  1. parallel/__init__.py +2 -1
  2. parallel/_base_client.py +37 -5
  3. parallel/_client.py +9 -0
  4. parallel/_compat.py +55 -54
  5. parallel/_files.py +4 -4
  6. parallel/_models.py +70 -46
  7. parallel/_types.py +35 -1
  8. parallel/_utils/__init__.py +9 -2
  9. parallel/_utils/_compat.py +45 -0
  10. parallel/_utils/_datetime_parse.py +136 -0
  11. parallel/_utils/_transform.py +11 -1
  12. parallel/_utils/_typing.py +6 -1
  13. parallel/_utils/_utils.py +0 -1
  14. parallel/_version.py +1 -1
  15. parallel/lib/_pydantic.py +4 -3
  16. parallel/resources/__init__.py +14 -0
  17. parallel/resources/beta/__init__.py +47 -0
  18. parallel/resources/beta/beta.py +301 -0
  19. parallel/resources/beta/task_group.py +632 -0
  20. parallel/resources/beta/task_run.py +499 -0
  21. parallel/resources/task_run.py +47 -18
  22. parallel/types/__init__.py +15 -0
  23. parallel/types/auto_schema.py +13 -0
  24. parallel/types/auto_schema_param.py +12 -0
  25. parallel/types/beta/__init__.py +30 -0
  26. parallel/types/beta/beta_run_input.py +63 -0
  27. parallel/types/beta/beta_run_input_param.py +65 -0
  28. parallel/types/beta/beta_search_params.py +48 -0
  29. parallel/types/beta/beta_task_run_result.py +74 -0
  30. parallel/types/beta/error_event.py +16 -0
  31. parallel/types/beta/mcp_server.py +25 -0
  32. parallel/types/beta/mcp_server_param.py +27 -0
  33. parallel/types/beta/mcp_tool_call.py +27 -0
  34. parallel/types/beta/parallel_beta_param.py +12 -0
  35. parallel/types/beta/search_result.py +16 -0
  36. parallel/types/beta/task_group.py +24 -0
  37. parallel/types/beta/task_group_add_runs_params.py +30 -0
  38. parallel/types/beta/task_group_create_params.py +13 -0
  39. parallel/types/beta/task_group_events_params.py +16 -0
  40. parallel/types/beta/task_group_events_response.py +28 -0
  41. parallel/types/beta/task_group_get_runs_params.py +18 -0
  42. parallel/types/beta/task_group_get_runs_response.py +12 -0
  43. parallel/types/beta/task_group_run_response.py +30 -0
  44. parallel/types/beta/task_group_status.py +27 -0
  45. parallel/types/beta/task_run_create_params.py +70 -0
  46. parallel/types/beta/task_run_event.py +32 -0
  47. parallel/types/beta/task_run_events_response.py +58 -0
  48. parallel/types/beta/task_run_result_params.py +18 -0
  49. parallel/types/beta/web_search_result.py +18 -0
  50. parallel/types/beta/webhook.py +16 -0
  51. parallel/types/beta/webhook_param.py +16 -0
  52. parallel/types/citation.py +21 -0
  53. parallel/types/field_basis.py +25 -0
  54. parallel/types/json_schema.py +16 -0
  55. parallel/types/json_schema_param.py +2 -1
  56. parallel/types/parsed_task_run_result.py +13 -4
  57. parallel/types/shared/__init__.py +6 -0
  58. parallel/types/shared/error_object.py +18 -0
  59. parallel/types/shared/error_response.py +16 -0
  60. parallel/types/shared/source_policy.py +21 -0
  61. parallel/types/shared/warning.py +22 -0
  62. parallel/types/shared_params/__init__.py +3 -0
  63. parallel/types/shared_params/source_policy.py +23 -0
  64. parallel/types/task_run.py +17 -18
  65. parallel/types/task_run_create_params.py +12 -3
  66. parallel/types/task_run_json_output.py +46 -0
  67. parallel/types/task_run_result.py +24 -94
  68. parallel/types/task_run_text_output.py +37 -0
  69. parallel/types/task_spec.py +31 -0
  70. parallel/types/task_spec_param.py +3 -2
  71. parallel/types/text_schema.py +16 -0
  72. parallel/types/text_schema_param.py +3 -2
  73. {parallel_web-0.1.3.dist-info → parallel_web-0.2.1.dist-info}/METADATA +23 -159
  74. parallel_web-0.2.1.dist-info/RECORD +96 -0
  75. parallel_web-0.1.3.dist-info/RECORD +0 -47
  76. {parallel_web-0.1.3.dist-info → parallel_web-0.2.1.dist-info}/WHEEL +0 -0
  77. {parallel_web-0.1.3.dist-info → parallel_web-0.2.1.dist-info}/licenses/LICENSE +0 -0
parallel/__init__.py CHANGED
@@ -36,7 +36,7 @@ from ._exceptions import (
36
36
  UnprocessableEntityError,
37
37
  APIResponseValidationError,
38
38
  )
39
- from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
39
+ from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
40
40
  from ._utils._logs import setup_logging as _setup_logging
41
41
 
42
42
  __all__ = [
@@ -78,6 +78,7 @@ __all__ = [
78
78
  "DEFAULT_CONNECTION_LIMITS",
79
79
  "DefaultHttpxClient",
80
80
  "DefaultAsyncHttpxClient",
81
+ "DefaultAioHttpClient",
81
82
  ]
82
83
 
83
84
  if not _t.TYPE_CHECKING:
parallel/_base_client.py CHANGED
@@ -59,7 +59,7 @@ from ._types import (
59
59
  ModelBuilderProtocol,
60
60
  )
61
61
  from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
62
- from ._compat import PYDANTIC_V2, model_copy, model_dump
62
+ from ._compat import PYDANTIC_V1, model_copy, model_dump
63
63
  from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
64
64
  from ._response import (
65
65
  APIResponse,
@@ -232,7 +232,7 @@ class BaseSyncPage(BasePage[_T], Generic[_T]):
232
232
  model: Type[_T],
233
233
  options: FinalRequestOptions,
234
234
  ) -> None:
235
- if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
235
+ if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None:
236
236
  self.__pydantic_private__ = {}
237
237
 
238
238
  self._model = model
@@ -320,7 +320,7 @@ class BaseAsyncPage(BasePage[_T], Generic[_T]):
320
320
  client: AsyncAPIClient,
321
321
  options: FinalRequestOptions,
322
322
  ) -> None:
323
- if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
323
+ if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None:
324
324
  self.__pydantic_private__ = {}
325
325
 
326
326
  self._model = model
@@ -529,6 +529,18 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
529
529
  # work around https://github.com/encode/httpx/discussions/2880
530
530
  kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
531
531
 
532
+ is_body_allowed = options.method.lower() != "get"
533
+
534
+ if is_body_allowed:
535
+ if isinstance(json_data, bytes):
536
+ kwargs["content"] = json_data
537
+ else:
538
+ kwargs["json"] = json_data if is_given(json_data) else None
539
+ kwargs["files"] = files
540
+ else:
541
+ headers.pop("Content-Type", None)
542
+ kwargs.pop("data", None)
543
+
532
544
  # TODO: report this error to httpx
533
545
  return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
534
546
  headers=headers,
@@ -540,8 +552,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
540
552
  # so that passing a `TypedDict` doesn't cause an error.
541
553
  # https://github.com/microsoft/pyright/issues/3526#event-6715453066
542
554
  params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
543
- json=json_data if is_given(json_data) else None,
544
- files=files,
545
555
  **kwargs,
546
556
  )
547
557
 
@@ -1289,6 +1299,24 @@ class _DefaultAsyncHttpxClient(httpx.AsyncClient):
1289
1299
  super().__init__(**kwargs)
1290
1300
 
1291
1301
 
1302
+ try:
1303
+ import httpx_aiohttp
1304
+ except ImportError:
1305
+
1306
+ class _DefaultAioHttpClient(httpx.AsyncClient):
1307
+ def __init__(self, **_kwargs: Any) -> None:
1308
+ raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
1309
+ else:
1310
+
1311
+ class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
1312
+ def __init__(self, **kwargs: Any) -> None:
1313
+ kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
1314
+ kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
1315
+ kwargs.setdefault("follow_redirects", True)
1316
+
1317
+ super().__init__(**kwargs)
1318
+
1319
+
1292
1320
  if TYPE_CHECKING:
1293
1321
  DefaultAsyncHttpxClient = httpx.AsyncClient
1294
1322
  """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1297,8 +1325,12 @@ if TYPE_CHECKING:
1297
1325
  This is useful because overriding the `http_client` with your own instance of
1298
1326
  `httpx.AsyncClient` will result in httpx's defaults being used, not ours.
1299
1327
  """
1328
+
1329
+ DefaultAioHttpClient = httpx.AsyncClient
1330
+ """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
1300
1331
  else:
1301
1332
  DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
1333
+ DefaultAioHttpClient = _DefaultAioHttpClient
1302
1334
 
1303
1335
 
1304
1336
  class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
parallel/_client.py CHANGED
@@ -29,6 +29,7 @@ from ._base_client import (
29
29
  SyncAPIClient,
30
30
  AsyncAPIClient,
31
31
  )
32
+ from .resources.beta import beta
32
33
 
33
34
  __all__ = [
34
35
  "Timeout",
@@ -44,6 +45,7 @@ __all__ = [
44
45
 
45
46
  class Parallel(SyncAPIClient):
46
47
  task_run: task_run.TaskRunResource
48
+ beta: beta.BetaResource
47
49
  with_raw_response: ParallelWithRawResponse
48
50
  with_streaming_response: ParallelWithStreamedResponse
49
51
 
@@ -102,6 +104,7 @@ class Parallel(SyncAPIClient):
102
104
  )
103
105
 
104
106
  self.task_run = task_run.TaskRunResource(self)
107
+ self.beta = beta.BetaResource(self)
105
108
  self.with_raw_response = ParallelWithRawResponse(self)
106
109
  self.with_streaming_response = ParallelWithStreamedResponse(self)
107
110
 
@@ -212,6 +215,7 @@ class Parallel(SyncAPIClient):
212
215
 
213
216
  class AsyncParallel(AsyncAPIClient):
214
217
  task_run: task_run.AsyncTaskRunResource
218
+ beta: beta.AsyncBetaResource
215
219
  with_raw_response: AsyncParallelWithRawResponse
216
220
  with_streaming_response: AsyncParallelWithStreamedResponse
217
221
 
@@ -270,6 +274,7 @@ class AsyncParallel(AsyncAPIClient):
270
274
  )
271
275
 
272
276
  self.task_run = task_run.AsyncTaskRunResource(self)
277
+ self.beta = beta.AsyncBetaResource(self)
273
278
  self.with_raw_response = AsyncParallelWithRawResponse(self)
274
279
  self.with_streaming_response = AsyncParallelWithStreamedResponse(self)
275
280
 
@@ -381,21 +386,25 @@ class AsyncParallel(AsyncAPIClient):
381
386
  class ParallelWithRawResponse:
382
387
  def __init__(self, client: Parallel) -> None:
383
388
  self.task_run = task_run.TaskRunResourceWithRawResponse(client.task_run)
389
+ self.beta = beta.BetaResourceWithRawResponse(client.beta)
384
390
 
385
391
 
386
392
  class AsyncParallelWithRawResponse:
387
393
  def __init__(self, client: AsyncParallel) -> None:
388
394
  self.task_run = task_run.AsyncTaskRunResourceWithRawResponse(client.task_run)
395
+ self.beta = beta.AsyncBetaResourceWithRawResponse(client.beta)
389
396
 
390
397
 
391
398
  class ParallelWithStreamedResponse:
392
399
  def __init__(self, client: Parallel) -> None:
393
400
  self.task_run = task_run.TaskRunResourceWithStreamingResponse(client.task_run)
401
+ self.beta = beta.BetaResourceWithStreamingResponse(client.beta)
394
402
 
395
403
 
396
404
  class AsyncParallelWithStreamedResponse:
397
405
  def __init__(self, client: AsyncParallel) -> None:
398
406
  self.task_run = task_run.AsyncTaskRunResourceWithStreamingResponse(client.task_run)
407
+ self.beta = beta.AsyncBetaResourceWithStreamingResponse(client.beta)
399
408
 
400
409
 
401
410
  Client = Parallel
parallel/_compat.py CHANGED
@@ -12,14 +12,13 @@ from ._types import IncEx, StrBytesIntFloat
12
12
  _T = TypeVar("_T")
13
13
  _ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel)
14
14
 
15
- # --------------- Pydantic v2 compatibility ---------------
15
+ # --------------- Pydantic v2, v3 compatibility ---------------
16
16
 
17
17
  # Pyright incorrectly reports some of our functions as overriding a method when they don't
18
18
  # pyright: reportIncompatibleMethodOverride=false
19
19
 
20
- PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
20
+ PYDANTIC_V1 = pydantic.VERSION.startswith("1.")
21
21
 
22
- # v1 re-exports
23
22
  if TYPE_CHECKING:
24
23
 
25
24
  def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001
@@ -44,90 +43,92 @@ if TYPE_CHECKING:
44
43
  ...
45
44
 
46
45
  else:
47
- if PYDANTIC_V2:
48
- from pydantic.v1.typing import (
46
+ # v1 re-exports
47
+ if PYDANTIC_V1:
48
+ from pydantic.typing import (
49
49
  get_args as get_args,
50
50
  is_union as is_union,
51
51
  get_origin as get_origin,
52
52
  is_typeddict as is_typeddict,
53
53
  is_literal_type as is_literal_type,
54
54
  )
55
- from pydantic.v1.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
55
+ from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
56
56
  else:
57
- from pydantic.typing import (
57
+ from ._utils import (
58
58
  get_args as get_args,
59
59
  is_union as is_union,
60
60
  get_origin as get_origin,
61
+ parse_date as parse_date,
61
62
  is_typeddict as is_typeddict,
63
+ parse_datetime as parse_datetime,
62
64
  is_literal_type as is_literal_type,
63
65
  )
64
- from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime
65
66
 
66
67
 
67
68
  # refactored config
68
69
  if TYPE_CHECKING:
69
70
  from pydantic import ConfigDict as ConfigDict
70
71
  else:
71
- if PYDANTIC_V2:
72
- from pydantic import ConfigDict
73
- else:
72
+ if PYDANTIC_V1:
74
73
  # TODO: provide an error message here?
75
74
  ConfigDict = None
75
+ else:
76
+ from pydantic import ConfigDict as ConfigDict
76
77
 
77
78
 
78
79
  # renamed methods / properties
79
80
  def parse_obj(model: type[_ModelT], value: object) -> _ModelT:
80
- if PYDANTIC_V2:
81
- return model.model_validate(value)
82
- else:
81
+ if PYDANTIC_V1:
83
82
  return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
83
+ else:
84
+ return model.model_validate(value)
84
85
 
85
86
 
86
87
  def field_is_required(field: FieldInfo) -> bool:
87
- if PYDANTIC_V2:
88
- return field.is_required()
89
- return field.required # type: ignore
88
+ if PYDANTIC_V1:
89
+ return field.required # type: ignore
90
+ return field.is_required()
90
91
 
91
92
 
92
93
  def field_get_default(field: FieldInfo) -> Any:
93
94
  value = field.get_default()
94
- if PYDANTIC_V2:
95
- from pydantic_core import PydanticUndefined
96
-
97
- if value == PydanticUndefined:
98
- return None
95
+ if PYDANTIC_V1:
99
96
  return value
97
+ from pydantic_core import PydanticUndefined
98
+
99
+ if value == PydanticUndefined:
100
+ return None
100
101
  return value
101
102
 
102
103
 
103
104
  def field_outer_type(field: FieldInfo) -> Any:
104
- if PYDANTIC_V2:
105
- return field.annotation
106
- return field.outer_type_ # type: ignore
105
+ if PYDANTIC_V1:
106
+ return field.outer_type_ # type: ignore
107
+ return field.annotation
107
108
 
108
109
 
109
110
  def get_model_config(model: type[pydantic.BaseModel]) -> Any:
110
- if PYDANTIC_V2:
111
- return model.model_config
112
- return model.__config__ # type: ignore
111
+ if PYDANTIC_V1:
112
+ return model.__config__ # type: ignore
113
+ return model.model_config
113
114
 
114
115
 
115
116
  def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]:
116
- if PYDANTIC_V2:
117
- return model.model_fields
118
- return model.__fields__ # type: ignore
117
+ if PYDANTIC_V1:
118
+ return model.__fields__ # type: ignore
119
+ return model.model_fields
119
120
 
120
121
 
121
122
  def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
122
- if PYDANTIC_V2:
123
- return model.model_copy(deep=deep)
124
- return model.copy(deep=deep) # type: ignore
123
+ if PYDANTIC_V1:
124
+ return model.copy(deep=deep) # type: ignore
125
+ return model.model_copy(deep=deep)
125
126
 
126
127
 
127
128
  def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
128
- if PYDANTIC_V2:
129
- return model.model_dump_json(indent=indent)
130
- return model.json(indent=indent) # type: ignore
129
+ if PYDANTIC_V1:
130
+ return model.json(indent=indent) # type: ignore
131
+ return model.model_dump_json(indent=indent)
131
132
 
132
133
 
133
134
  def model_dump(
@@ -139,14 +140,14 @@ def model_dump(
139
140
  warnings: bool = True,
140
141
  mode: Literal["json", "python"] = "python",
141
142
  ) -> dict[str, Any]:
142
- if PYDANTIC_V2 or hasattr(model, "model_dump"):
143
+ if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
143
144
  return model.model_dump(
144
145
  mode=mode,
145
146
  exclude=exclude,
146
147
  exclude_unset=exclude_unset,
147
148
  exclude_defaults=exclude_defaults,
148
149
  # warnings are not supported in Pydantic v1
149
- warnings=warnings if PYDANTIC_V2 else True,
150
+ warnings=True if PYDANTIC_V1 else warnings,
150
151
  )
151
152
  return cast(
152
153
  "dict[str, Any]",
@@ -159,21 +160,22 @@ def model_dump(
159
160
 
160
161
 
161
162
  def model_parse(model: type[_ModelT], data: Any) -> _ModelT:
162
- if PYDANTIC_V2:
163
- return model.model_validate(data)
164
- return model.parse_obj(data) # pyright: ignore[reportDeprecated]
163
+ if PYDANTIC_V1:
164
+ return model.parse_obj(data) # pyright: ignore[reportDeprecated]
165
+ return model.model_validate(data)
165
166
 
166
167
 
167
168
  def model_parse_json(model: type[_ModelT], data: str | bytes) -> _ModelT:
168
- if PYDANTIC_V2:
169
- return model.model_validate_json(data)
170
- return model.parse_raw(data) # pyright: ignore[reportDeprecated]
169
+ if PYDANTIC_V1:
170
+ return model.parse_raw(data) # pyright: ignore[reportDeprecated]
171
+ return model.model_validate_json(data)
171
172
 
172
173
 
173
174
  def model_json_schema(model: type[_ModelT]) -> dict[str, Any]:
174
- if PYDANTIC_V2:
175
- return model.model_json_schema()
176
- return model.schema() # pyright: ignore[reportDeprecated]
175
+ if PYDANTIC_V1:
176
+ return model.schema() # pyright: ignore[reportDeprecated]
177
+ return model.model_json_schema()
178
+
177
179
 
178
180
  # generic models
179
181
  if TYPE_CHECKING:
@@ -181,17 +183,16 @@ if TYPE_CHECKING:
181
183
  class GenericModel(pydantic.BaseModel): ...
182
184
 
183
185
  else:
184
- if PYDANTIC_V2:
186
+ if PYDANTIC_V1:
187
+ import pydantic.generics
188
+
189
+ class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
190
+ else:
185
191
  # there no longer needs to be a distinction in v2 but
186
192
  # we still have to create our own subclass to avoid
187
193
  # inconsistent MRO ordering errors
188
194
  class GenericModel(pydantic.BaseModel): ...
189
195
 
190
- else:
191
- import pydantic.generics
192
-
193
- class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ...
194
-
195
196
 
196
197
  # cached properties
197
198
  if TYPE_CHECKING:
parallel/_files.py CHANGED
@@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
69
69
  return file
70
70
 
71
71
  if is_tuple_t(file):
72
- return (file[0], _read_file_content(file[1]), *file[2:])
72
+ return (file[0], read_file_content(file[1]), *file[2:])
73
73
 
74
74
  raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
75
75
 
76
76
 
77
- def _read_file_content(file: FileContent) -> HttpxFileContent:
77
+ def read_file_content(file: FileContent) -> HttpxFileContent:
78
78
  if isinstance(file, os.PathLike):
79
79
  return pathlib.Path(file).read_bytes()
80
80
  return file
@@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
111
111
  return file
112
112
 
113
113
  if is_tuple_t(file):
114
- return (file[0], await _async_read_file_content(file[1]), *file[2:])
114
+ return (file[0], await async_read_file_content(file[1]), *file[2:])
115
115
 
116
116
  raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
117
117
 
118
118
 
119
- async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
119
+ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
120
120
  if isinstance(file, os.PathLike):
121
121
  return await anyio.Path(file).read_bytes()
122
122
 
parallel/_models.py CHANGED
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import inspect
5
- from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
5
+ from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
6
6
  from datetime import date, datetime
7
7
  from typing_extensions import (
8
+ List,
8
9
  Unpack,
9
10
  Literal,
10
11
  ClassVar,
@@ -49,7 +50,7 @@ from ._utils import (
49
50
  strip_annotated_type,
50
51
  )
51
52
  from ._compat import (
52
- PYDANTIC_V2,
53
+ PYDANTIC_V1,
53
54
  ConfigDict,
54
55
  GenericModel as BaseGenericModel,
55
56
  get_args,
@@ -80,11 +81,7 @@ class _ConfigProtocol(Protocol):
80
81
 
81
82
 
82
83
  class BaseModel(pydantic.BaseModel):
83
- if PYDANTIC_V2:
84
- model_config: ClassVar[ConfigDict] = ConfigDict(
85
- extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
86
- )
87
- else:
84
+ if PYDANTIC_V1:
88
85
 
89
86
  @property
90
87
  @override
@@ -94,6 +91,10 @@ class BaseModel(pydantic.BaseModel):
94
91
 
95
92
  class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
96
93
  extra: Any = pydantic.Extra.allow # type: ignore
94
+ else:
95
+ model_config: ClassVar[ConfigDict] = ConfigDict(
96
+ extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true"))
97
+ )
97
98
 
98
99
  def to_dict(
99
100
  self,
@@ -207,28 +208,32 @@ class BaseModel(pydantic.BaseModel):
207
208
  else:
208
209
  fields_values[name] = field_get_default(field)
209
210
 
211
+ extra_field_type = _get_extra_fields_type(__cls)
212
+
210
213
  _extra = {}
211
214
  for key, value in values.items():
212
215
  if key not in model_fields:
213
- if PYDANTIC_V2:
214
- _extra[key] = value
215
- else:
216
+ parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value
217
+
218
+ if PYDANTIC_V1:
216
219
  _fields_set.add(key)
217
- fields_values[key] = value
220
+ fields_values[key] = parsed
221
+ else:
222
+ _extra[key] = parsed
218
223
 
219
224
  object.__setattr__(m, "__dict__", fields_values)
220
225
 
221
- if PYDANTIC_V2:
222
- # these properties are copied from Pydantic's `model_construct()` method
223
- object.__setattr__(m, "__pydantic_private__", None)
224
- object.__setattr__(m, "__pydantic_extra__", _extra)
225
- object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
226
- else:
226
+ if PYDANTIC_V1:
227
227
  # init_private_attributes() does not exist in v2
228
228
  m._init_private_attributes() # type: ignore
229
229
 
230
230
  # copied from Pydantic v1's `construct()` method
231
231
  object.__setattr__(m, "__fields_set__", _fields_set)
232
+ else:
233
+ # these properties are copied from Pydantic's `model_construct()` method
234
+ object.__setattr__(m, "__pydantic_private__", None)
235
+ object.__setattr__(m, "__pydantic_extra__", _extra)
236
+ object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
232
237
 
233
238
  return m
234
239
 
@@ -238,7 +243,7 @@ class BaseModel(pydantic.BaseModel):
238
243
  # although not in practice
239
244
  model_construct = construct
240
245
 
241
- if not PYDANTIC_V2:
246
+ if PYDANTIC_V1:
242
247
  # we define aliases for some of the new pydantic v2 methods so
243
248
  # that we can just document these methods without having to specify
244
249
  # a specific pydantic version as some users may not know which
@@ -299,7 +304,7 @@ class BaseModel(pydantic.BaseModel):
299
304
  exclude_none=exclude_none,
300
305
  )
301
306
 
302
- return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped
307
+ return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped
303
308
 
304
309
  @override
305
310
  def model_dump_json(
@@ -358,15 +363,32 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
358
363
  if value is None:
359
364
  return field_get_default(field)
360
365
 
361
- if PYDANTIC_V2:
362
- type_ = field.annotation
363
- else:
366
+ if PYDANTIC_V1:
364
367
  type_ = cast(type, field.outer_type_) # type: ignore
368
+ else:
369
+ type_ = field.annotation # type: ignore
365
370
 
366
371
  if type_ is None:
367
372
  raise RuntimeError(f"Unexpected field type is None for {key}")
368
373
 
369
- return construct_type(value=value, type_=type_)
374
+ return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
375
+
376
+
377
+ def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
378
+ if PYDANTIC_V1:
379
+ # TODO
380
+ return None
381
+
382
+ schema = cls.__pydantic_core_schema__
383
+ if schema["type"] == "model":
384
+ fields = schema["schema"]
385
+ if fields["type"] == "model-fields":
386
+ extras = fields.get("extras_schema")
387
+ if extras and "cls" in extras:
388
+ # mypy can't narrow the type
389
+ return extras["cls"] # type: ignore[no-any-return]
390
+
391
+ return None
370
392
 
371
393
 
372
394
  def is_basemodel(type_: type) -> bool:
@@ -420,7 +442,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
420
442
  return cast(_T, construct_type(value=value, type_=type_))
421
443
 
422
444
 
423
- def construct_type(*, value: object, type_: object) -> object:
445
+ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
424
446
  """Loose coercion to the expected type with construction of nested values.
425
447
 
426
448
  If the given value does not match the expected type then it is returned as-is.
@@ -438,8 +460,10 @@ def construct_type(*, value: object, type_: object) -> object:
438
460
  type_ = type_.__value__ # type: ignore[unreachable]
439
461
 
440
462
  # unwrap `Annotated[T, ...]` -> `T`
441
- if is_annotated_type(type_):
442
- meta: tuple[Any, ...] = get_args(type_)[1:]
463
+ if metadata is not None and len(metadata) > 0:
464
+ meta: tuple[Any, ...] = tuple(metadata)
465
+ elif is_annotated_type(type_):
466
+ meta = get_args(type_)[1:]
443
467
  type_ = extract_type_arg(type_, 0)
444
468
  else:
445
469
  meta = tuple()
@@ -604,30 +628,30 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any,
604
628
  for variant in get_args(union):
605
629
  variant = strip_annotated_type(variant)
606
630
  if is_basemodel_type(variant):
607
- if PYDANTIC_V2:
608
- field = _extract_field_schema_pv2(variant, discriminator_field_name)
609
- if not field:
631
+ if PYDANTIC_V1:
632
+ field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
633
+ if not field_info:
610
634
  continue
611
635
 
612
636
  # Note: if one variant defines an alias then they all should
613
- discriminator_alias = field.get("serialization_alias")
614
-
615
- field_schema = field["schema"]
637
+ discriminator_alias = field_info.alias
616
638
 
617
- if field_schema["type"] == "literal":
618
- for entry in cast("LiteralSchema", field_schema)["expected"]:
639
+ if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
640
+ for entry in get_args(annotation):
619
641
  if isinstance(entry, str):
620
642
  mapping[entry] = variant
621
643
  else:
622
- field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
623
- if not field_info:
644
+ field = _extract_field_schema_pv2(variant, discriminator_field_name)
645
+ if not field:
624
646
  continue
625
647
 
626
648
  # Note: if one variant defines an alias then they all should
627
- discriminator_alias = field_info.alias
649
+ discriminator_alias = field.get("serialization_alias")
628
650
 
629
- if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation):
630
- for entry in get_args(annotation):
651
+ field_schema = field["schema"]
652
+
653
+ if field_schema["type"] == "literal":
654
+ for entry in cast("LiteralSchema", field_schema)["expected"]:
631
655
  if isinstance(entry, str):
632
656
  mapping[entry] = variant
633
657
 
@@ -690,7 +714,7 @@ else:
690
714
  pass
691
715
 
692
716
 
693
- if PYDANTIC_V2:
717
+ if not PYDANTIC_V1:
694
718
  from pydantic import TypeAdapter as _TypeAdapter
695
719
 
696
720
  _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter))
@@ -758,12 +782,12 @@ class FinalRequestOptions(pydantic.BaseModel):
758
782
  json_data: Union[Body, None] = None
759
783
  extra_json: Union[AnyMapping, None] = None
760
784
 
761
- if PYDANTIC_V2:
762
- model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
763
- else:
785
+ if PYDANTIC_V1:
764
786
 
765
787
  class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
766
788
  arbitrary_types_allowed: bool = True
789
+ else:
790
+ model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True)
767
791
 
768
792
  def get_max_retries(self, max_retries: int) -> int:
769
793
  if isinstance(self.max_retries, NotGiven):
@@ -796,9 +820,9 @@ class FinalRequestOptions(pydantic.BaseModel):
796
820
  key: strip_not_given(value)
797
821
  for key, value in values.items()
798
822
  }
799
- if PYDANTIC_V2:
800
- return super().model_construct(_fields_set, **kwargs)
801
- return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
823
+ if PYDANTIC_V1:
824
+ return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated]
825
+ return super().model_construct(_fields_set, **kwargs)
802
826
 
803
827
  if not TYPE_CHECKING:
804
828
  # type checkers incorrectly complain about this assignment