agenta 0.26.0__py3-none-any.whl → 0.27.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.
Potentially problematic release.
This version of agenta might be problematic. Click here for more details.
- agenta/__init__.py +29 -10
- agenta/cli/helper.py +5 -1
- agenta/client/backend/__init__.py +14 -0
- agenta/client/backend/apps/client.py +28 -20
- agenta/client/backend/client.py +47 -16
- agenta/client/backend/containers/client.py +5 -1
- agenta/client/backend/core/__init__.py +2 -1
- agenta/client/backend/core/client_wrapper.py +6 -6
- agenta/client/backend/core/file.py +33 -11
- agenta/client/backend/core/http_client.py +45 -31
- agenta/client/backend/core/pydantic_utilities.py +144 -29
- agenta/client/backend/core/request_options.py +3 -0
- agenta/client/backend/core/serialization.py +139 -42
- agenta/client/backend/evaluations/client.py +7 -2
- agenta/client/backend/evaluators/client.py +349 -1
- agenta/client/backend/observability/client.py +11 -2
- agenta/client/backend/testsets/client.py +10 -10
- agenta/client/backend/types/__init__.py +14 -0
- agenta/client/backend/types/app.py +1 -0
- agenta/client/backend/types/app_variant_response.py +3 -1
- agenta/client/backend/types/config_dto.py +32 -0
- agenta/client/backend/types/config_response_model.py +32 -0
- agenta/client/backend/types/create_span.py +3 -2
- agenta/client/backend/types/environment_output.py +1 -0
- agenta/client/backend/types/environment_output_extended.py +1 -0
- agenta/client/backend/types/evaluation.py +1 -2
- agenta/client/backend/types/evaluator.py +2 -0
- agenta/client/backend/types/evaluator_config.py +1 -0
- agenta/client/backend/types/evaluator_mapping_output_interface.py +21 -0
- agenta/client/backend/types/evaluator_output_interface.py +21 -0
- agenta/client/backend/types/human_evaluation.py +1 -2
- agenta/client/backend/types/lifecycle_dto.py +24 -0
- agenta/client/backend/types/llm_tokens.py +2 -2
- agenta/client/backend/types/reference_dto.py +23 -0
- agenta/client/backend/types/reference_request_model.py +23 -0
- agenta/client/backend/types/span.py +1 -0
- agenta/client/backend/types/span_detail.py +7 -1
- agenta/client/backend/types/test_set_output_response.py +5 -2
- agenta/client/backend/types/trace_detail.py +7 -1
- agenta/client/backend/types/with_pagination.py +4 -2
- agenta/client/backend/variants/client.py +1565 -272
- agenta/docker/docker-assets/Dockerfile.cloud.template +1 -1
- agenta/sdk/__init__.py +44 -7
- agenta/sdk/agenta_init.py +85 -33
- agenta/sdk/context/__init__.py +0 -0
- agenta/sdk/context/routing.py +26 -0
- agenta/sdk/context/tracing.py +3 -0
- agenta/sdk/decorators/__init__.py +0 -0
- agenta/sdk/decorators/{llm_entrypoint.py → routing.py} +216 -191
- agenta/sdk/decorators/tracing.py +218 -99
- agenta/sdk/litellm/__init__.py +1 -0
- agenta/sdk/litellm/litellm.py +288 -0
- agenta/sdk/managers/__init__.py +6 -0
- agenta/sdk/managers/config.py +318 -0
- agenta/sdk/managers/deployment.py +45 -0
- agenta/sdk/managers/shared.py +639 -0
- agenta/sdk/managers/variant.py +182 -0
- agenta/sdk/router.py +0 -7
- agenta/sdk/tracing/__init__.py +1 -0
- agenta/sdk/tracing/attributes.py +141 -0
- agenta/sdk/tracing/context.py +24 -0
- agenta/sdk/tracing/conventions.py +49 -0
- agenta/sdk/tracing/exporters.py +65 -0
- agenta/sdk/tracing/inline.py +1252 -0
- agenta/sdk/tracing/processors.py +117 -0
- agenta/sdk/tracing/spans.py +136 -0
- agenta/sdk/tracing/tracing.py +233 -0
- agenta/sdk/types.py +49 -2
- agenta/sdk/utils/{helper/openai_cost.py → costs.py} +3 -0
- agenta/sdk/utils/debug.py +5 -5
- agenta/sdk/utils/exceptions.py +52 -0
- agenta/sdk/utils/globals.py +3 -5
- agenta/sdk/{tracing/logger.py → utils/logging.py} +3 -5
- agenta/sdk/utils/singleton.py +13 -0
- {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/METADATA +5 -1
- {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/RECORD +78 -57
- agenta/sdk/config_manager.py +0 -205
- agenta/sdk/context.py +0 -41
- agenta/sdk/decorators/base.py +0 -10
- agenta/sdk/tracing/callbacks.py +0 -187
- agenta/sdk/tracing/llm_tracing.py +0 -617
- agenta/sdk/tracing/tasks_manager.py +0 -129
- agenta/sdk/tracing/tracing_context.py +0 -27
- {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/WHEEL +0 -0
- {agenta-0.26.0.dist-info → agenta-0.27.0.dist-info}/entry_points.txt +0 -0
|
@@ -148,9 +148,9 @@ def get_request_body(
|
|
|
148
148
|
json_body = maybe_filter_request_body(json, request_options, omit)
|
|
149
149
|
|
|
150
150
|
# If you have an empty JSON body, you should just send None
|
|
151
|
-
return (
|
|
152
|
-
|
|
153
|
-
)
|
|
151
|
+
return (json_body if json_body != {} else None), (
|
|
152
|
+
data_body if data_body != {} else None
|
|
153
|
+
)
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
class HttpClient:
|
|
@@ -158,9 +158,9 @@ class HttpClient:
|
|
|
158
158
|
self,
|
|
159
159
|
*,
|
|
160
160
|
httpx_client: httpx.Client,
|
|
161
|
-
base_timeout: typing.Optional[float],
|
|
162
|
-
base_headers: typing.Dict[str, str],
|
|
163
|
-
base_url: typing.Optional[str] = None,
|
|
161
|
+
base_timeout: typing.Callable[[], typing.Optional[float]],
|
|
162
|
+
base_headers: typing.Callable[[], typing.Dict[str, str]],
|
|
163
|
+
base_url: typing.Optional[typing.Callable[[], str]] = None,
|
|
164
164
|
):
|
|
165
165
|
self.base_url = base_url
|
|
166
166
|
self.base_timeout = base_timeout
|
|
@@ -168,7 +168,10 @@ class HttpClient:
|
|
|
168
168
|
self.httpx_client = httpx_client
|
|
169
169
|
|
|
170
170
|
def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
|
|
171
|
-
base_url =
|
|
171
|
+
base_url = maybe_base_url
|
|
172
|
+
if self.base_url is not None and base_url is None:
|
|
173
|
+
base_url = self.base_url()
|
|
174
|
+
|
|
172
175
|
if base_url is None:
|
|
173
176
|
raise ValueError(
|
|
174
177
|
"A base_url is required to make this request, please provide one and try again."
|
|
@@ -200,7 +203,7 @@ class HttpClient:
|
|
|
200
203
|
request_options.get("timeout_in_seconds")
|
|
201
204
|
if request_options is not None
|
|
202
205
|
and request_options.get("timeout_in_seconds") is not None
|
|
203
|
-
else self.base_timeout
|
|
206
|
+
else self.base_timeout()
|
|
204
207
|
)
|
|
205
208
|
|
|
206
209
|
json_body, data_body = get_request_body(
|
|
@@ -213,7 +216,7 @@ class HttpClient:
|
|
|
213
216
|
headers=jsonable_encoder(
|
|
214
217
|
remove_none_from_dict(
|
|
215
218
|
{
|
|
216
|
-
**self.base_headers,
|
|
219
|
+
**self.base_headers(),
|
|
217
220
|
**(headers if headers is not None else {}),
|
|
218
221
|
**(
|
|
219
222
|
request_options.get("additional_headers", {}) or {}
|
|
@@ -246,9 +249,11 @@ class HttpClient:
|
|
|
246
249
|
json=json_body,
|
|
247
250
|
data=data_body,
|
|
248
251
|
content=content,
|
|
249
|
-
files=
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
files=(
|
|
253
|
+
convert_file_dict_to_httpx_tuples(remove_none_from_dict(files))
|
|
254
|
+
if (files is not None and files is not omit)
|
|
255
|
+
else None
|
|
256
|
+
),
|
|
252
257
|
timeout=timeout,
|
|
253
258
|
)
|
|
254
259
|
|
|
@@ -300,7 +305,7 @@ class HttpClient:
|
|
|
300
305
|
request_options.get("timeout_in_seconds")
|
|
301
306
|
if request_options is not None
|
|
302
307
|
and request_options.get("timeout_in_seconds") is not None
|
|
303
|
-
else self.base_timeout
|
|
308
|
+
else self.base_timeout()
|
|
304
309
|
)
|
|
305
310
|
|
|
306
311
|
json_body, data_body = get_request_body(
|
|
@@ -313,7 +318,7 @@ class HttpClient:
|
|
|
313
318
|
headers=jsonable_encoder(
|
|
314
319
|
remove_none_from_dict(
|
|
315
320
|
{
|
|
316
|
-
**self.base_headers,
|
|
321
|
+
**self.base_headers(),
|
|
317
322
|
**(headers if headers is not None else {}),
|
|
318
323
|
**(
|
|
319
324
|
request_options.get("additional_headers", {})
|
|
@@ -345,9 +350,11 @@ class HttpClient:
|
|
|
345
350
|
json=json_body,
|
|
346
351
|
data=data_body,
|
|
347
352
|
content=content,
|
|
348
|
-
files=
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
files=(
|
|
354
|
+
convert_file_dict_to_httpx_tuples(remove_none_from_dict(files))
|
|
355
|
+
if (files is not None and files is not omit)
|
|
356
|
+
else None
|
|
357
|
+
),
|
|
351
358
|
timeout=timeout,
|
|
352
359
|
) as stream:
|
|
353
360
|
yield stream
|
|
@@ -358,9 +365,9 @@ class AsyncHttpClient:
|
|
|
358
365
|
self,
|
|
359
366
|
*,
|
|
360
367
|
httpx_client: httpx.AsyncClient,
|
|
361
|
-
base_timeout: typing.Optional[float],
|
|
362
|
-
base_headers: typing.Dict[str, str],
|
|
363
|
-
base_url: typing.Optional[str] = None,
|
|
368
|
+
base_timeout: typing.Callable[[], typing.Optional[float]],
|
|
369
|
+
base_headers: typing.Callable[[], typing.Dict[str, str]],
|
|
370
|
+
base_url: typing.Optional[typing.Callable[[], str]] = None,
|
|
364
371
|
):
|
|
365
372
|
self.base_url = base_url
|
|
366
373
|
self.base_timeout = base_timeout
|
|
@@ -368,7 +375,10 @@ class AsyncHttpClient:
|
|
|
368
375
|
self.httpx_client = httpx_client
|
|
369
376
|
|
|
370
377
|
def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str:
|
|
371
|
-
base_url =
|
|
378
|
+
base_url = maybe_base_url
|
|
379
|
+
if self.base_url is not None and base_url is None:
|
|
380
|
+
base_url = self.base_url()
|
|
381
|
+
|
|
372
382
|
if base_url is None:
|
|
373
383
|
raise ValueError(
|
|
374
384
|
"A base_url is required to make this request, please provide one and try again."
|
|
@@ -400,7 +410,7 @@ class AsyncHttpClient:
|
|
|
400
410
|
request_options.get("timeout_in_seconds")
|
|
401
411
|
if request_options is not None
|
|
402
412
|
and request_options.get("timeout_in_seconds") is not None
|
|
403
|
-
else self.base_timeout
|
|
413
|
+
else self.base_timeout()
|
|
404
414
|
)
|
|
405
415
|
|
|
406
416
|
json_body, data_body = get_request_body(
|
|
@@ -414,7 +424,7 @@ class AsyncHttpClient:
|
|
|
414
424
|
headers=jsonable_encoder(
|
|
415
425
|
remove_none_from_dict(
|
|
416
426
|
{
|
|
417
|
-
**self.base_headers,
|
|
427
|
+
**self.base_headers(),
|
|
418
428
|
**(headers if headers is not None else {}),
|
|
419
429
|
**(
|
|
420
430
|
request_options.get("additional_headers", {}) or {}
|
|
@@ -447,9 +457,11 @@ class AsyncHttpClient:
|
|
|
447
457
|
json=json_body,
|
|
448
458
|
data=data_body,
|
|
449
459
|
content=content,
|
|
450
|
-
files=
|
|
451
|
-
|
|
452
|
-
|
|
460
|
+
files=(
|
|
461
|
+
convert_file_dict_to_httpx_tuples(remove_none_from_dict(files))
|
|
462
|
+
if files is not None
|
|
463
|
+
else None
|
|
464
|
+
),
|
|
453
465
|
timeout=timeout,
|
|
454
466
|
)
|
|
455
467
|
|
|
@@ -500,7 +512,7 @@ class AsyncHttpClient:
|
|
|
500
512
|
request_options.get("timeout_in_seconds")
|
|
501
513
|
if request_options is not None
|
|
502
514
|
and request_options.get("timeout_in_seconds") is not None
|
|
503
|
-
else self.base_timeout
|
|
515
|
+
else self.base_timeout()
|
|
504
516
|
)
|
|
505
517
|
|
|
506
518
|
json_body, data_body = get_request_body(
|
|
@@ -513,7 +525,7 @@ class AsyncHttpClient:
|
|
|
513
525
|
headers=jsonable_encoder(
|
|
514
526
|
remove_none_from_dict(
|
|
515
527
|
{
|
|
516
|
-
**self.base_headers,
|
|
528
|
+
**self.base_headers(),
|
|
517
529
|
**(headers if headers is not None else {}),
|
|
518
530
|
**(
|
|
519
531
|
request_options.get("additional_headers", {})
|
|
@@ -545,9 +557,11 @@ class AsyncHttpClient:
|
|
|
545
557
|
json=json_body,
|
|
546
558
|
data=data_body,
|
|
547
559
|
content=content,
|
|
548
|
-
files=
|
|
549
|
-
|
|
550
|
-
|
|
560
|
+
files=(
|
|
561
|
+
convert_file_dict_to_httpx_tuples(remove_none_from_dict(files))
|
|
562
|
+
if files is not None
|
|
563
|
+
else None
|
|
564
|
+
),
|
|
551
565
|
timeout=timeout,
|
|
552
566
|
) as stream:
|
|
553
567
|
yield stream
|
|
@@ -10,6 +10,7 @@ import typing_extensions
|
|
|
10
10
|
import pydantic
|
|
11
11
|
|
|
12
12
|
from .datetime_utils import serialize_datetime
|
|
13
|
+
from .serialization import convert_and_respect_annotation_metadata
|
|
13
14
|
|
|
14
15
|
IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
|
|
15
16
|
|
|
@@ -56,11 +57,14 @@ Model = typing.TypeVar("Model", bound=pydantic.BaseModel)
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T:
|
|
60
|
+
dealiased_object = convert_and_respect_annotation_metadata(
|
|
61
|
+
object_=object_, annotation=type_, direction="read"
|
|
62
|
+
)
|
|
59
63
|
if IS_PYDANTIC_V2:
|
|
60
64
|
adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2
|
|
61
|
-
return adapter.validate_python(
|
|
65
|
+
return adapter.validate_python(dealiased_object)
|
|
62
66
|
else:
|
|
63
|
-
return pydantic.parse_obj_as(type_,
|
|
67
|
+
return pydantic.parse_obj_as(type_, dealiased_object)
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def to_jsonable_with_fallback(
|
|
@@ -75,9 +79,53 @@ def to_jsonable_with_fallback(
|
|
|
75
79
|
|
|
76
80
|
|
|
77
81
|
class UniversalBaseModel(pydantic.BaseModel):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if IS_PYDANTIC_V2:
|
|
83
|
+
model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
|
|
84
|
+
# Allow fields begining with `model_` to be used in the model
|
|
85
|
+
protected_namespaces=(),
|
|
86
|
+
) # type: ignore # Pydantic v2
|
|
87
|
+
|
|
88
|
+
@pydantic.model_serializer(mode="wrap", when_used="json") # type: ignore # Pydantic v2
|
|
89
|
+
def serialize_model(
|
|
90
|
+
self, handler: pydantic.SerializerFunctionWrapHandler
|
|
91
|
+
) -> typing.Any: # type: ignore # Pydantic v2
|
|
92
|
+
serialized = handler(self)
|
|
93
|
+
data = {
|
|
94
|
+
k: serialize_datetime(v) if isinstance(v, dt.datetime) else v
|
|
95
|
+
for k, v in serialized.items()
|
|
96
|
+
}
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
else:
|
|
100
|
+
|
|
101
|
+
class Config:
|
|
102
|
+
smart_union = True
|
|
103
|
+
json_encoders = {dt.datetime: serialize_datetime}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def model_construct(
|
|
107
|
+
cls: typing.Type["Model"],
|
|
108
|
+
_fields_set: typing.Optional[typing.Set[str]] = None,
|
|
109
|
+
**values: typing.Any,
|
|
110
|
+
) -> "Model":
|
|
111
|
+
dealiased_object = convert_and_respect_annotation_metadata(
|
|
112
|
+
object_=values, annotation=cls, direction="read"
|
|
113
|
+
)
|
|
114
|
+
return cls.construct(_fields_set, **dealiased_object)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def construct(
|
|
118
|
+
cls: typing.Type["Model"],
|
|
119
|
+
_fields_set: typing.Optional[typing.Set[str]] = None,
|
|
120
|
+
**values: typing.Any,
|
|
121
|
+
) -> "Model":
|
|
122
|
+
dealiased_object = convert_and_respect_annotation_metadata(
|
|
123
|
+
object_=values, annotation=cls, direction="read"
|
|
124
|
+
)
|
|
125
|
+
if IS_PYDANTIC_V2:
|
|
126
|
+
return super().model_construct(_fields_set, **dealiased_object) # type: ignore # Pydantic v2
|
|
127
|
+
else:
|
|
128
|
+
return super().construct(_fields_set, **dealiased_object)
|
|
81
129
|
|
|
82
130
|
def json(self, **kwargs: typing.Any) -> str:
|
|
83
131
|
kwargs_with_defaults: typing.Any = {
|
|
@@ -95,30 +143,97 @@ class UniversalBaseModel(pydantic.BaseModel):
|
|
|
95
143
|
Override the default dict method to `exclude_unset` by default. This function patches
|
|
96
144
|
`exclude_unset` to work include fields within non-None default values.
|
|
97
145
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for
|
|
102
|
-
|
|
103
|
-
default = _get_field_default(field)
|
|
104
|
-
|
|
105
|
-
# If the default values are non-null act like they've been set
|
|
106
|
-
# This effectively allows exclude_unset to work like exclude_none where
|
|
107
|
-
# the latter passes through intentionally set none values.
|
|
108
|
-
if default != None:
|
|
109
|
-
_fields_set.add(name)
|
|
110
|
-
|
|
111
|
-
kwargs_with_defaults_exclude_unset: typing.Any = {
|
|
112
|
-
"by_alias": True,
|
|
113
|
-
"exclude_unset": True,
|
|
114
|
-
"include": _fields_set,
|
|
115
|
-
**kwargs,
|
|
116
|
-
}
|
|
117
|
-
|
|
146
|
+
# Note: the logic here is multi-plexed given the levers exposed in Pydantic V1 vs V2
|
|
147
|
+
# Pydantic V1's .dict can be extremely slow, so we do not want to call it twice.
|
|
148
|
+
#
|
|
149
|
+
# We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models
|
|
150
|
+
# that we have less control over, and this is less intrusive than custom serializers for now.
|
|
118
151
|
if IS_PYDANTIC_V2:
|
|
119
|
-
|
|
152
|
+
kwargs_with_defaults_exclude_unset: typing.Any = {
|
|
153
|
+
**kwargs,
|
|
154
|
+
"by_alias": True,
|
|
155
|
+
"exclude_unset": True,
|
|
156
|
+
"exclude_none": False,
|
|
157
|
+
}
|
|
158
|
+
kwargs_with_defaults_exclude_none: typing.Any = {
|
|
159
|
+
**kwargs,
|
|
160
|
+
"by_alias": True,
|
|
161
|
+
"exclude_none": True,
|
|
162
|
+
"exclude_unset": False,
|
|
163
|
+
}
|
|
164
|
+
dict_dump = deep_union_pydantic_dicts(
|
|
165
|
+
super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore # Pydantic v2
|
|
166
|
+
super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore # Pydantic v2
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
_fields_set = self.__fields_set__.copy()
|
|
171
|
+
|
|
172
|
+
fields = _get_model_fields(self.__class__)
|
|
173
|
+
for name, field in fields.items():
|
|
174
|
+
if name not in _fields_set:
|
|
175
|
+
default = _get_field_default(field)
|
|
176
|
+
|
|
177
|
+
# If the default values are non-null act like they've been set
|
|
178
|
+
# This effectively allows exclude_unset to work like exclude_none where
|
|
179
|
+
# the latter passes through intentionally set none values.
|
|
180
|
+
if default is not None or (
|
|
181
|
+
"exclude_unset" in kwargs and not kwargs["exclude_unset"]
|
|
182
|
+
):
|
|
183
|
+
_fields_set.add(name)
|
|
184
|
+
|
|
185
|
+
if default is not None:
|
|
186
|
+
self.__fields_set__.add(name)
|
|
187
|
+
|
|
188
|
+
kwargs_with_defaults_exclude_unset_include_fields: typing.Any = {
|
|
189
|
+
"by_alias": True,
|
|
190
|
+
"exclude_unset": True,
|
|
191
|
+
"include": _fields_set,
|
|
192
|
+
**kwargs,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dict_dump = super().dict(
|
|
196
|
+
**kwargs_with_defaults_exclude_unset_include_fields
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return convert_and_respect_annotation_metadata(
|
|
200
|
+
object_=dict_dump, annotation=self.__class__, direction="write"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _union_list_of_pydantic_dicts(
|
|
205
|
+
source: typing.List[typing.Any], destination: typing.List[typing.Any]
|
|
206
|
+
) -> typing.List[typing.Any]:
|
|
207
|
+
converted_list: typing.List[typing.Any] = []
|
|
208
|
+
for i, item in enumerate(source):
|
|
209
|
+
destination_value = destination[i] # type: ignore
|
|
210
|
+
if isinstance(item, dict):
|
|
211
|
+
converted_list.append(deep_union_pydantic_dicts(item, destination_value))
|
|
212
|
+
elif isinstance(item, list):
|
|
213
|
+
converted_list.append(
|
|
214
|
+
_union_list_of_pydantic_dicts(item, destination_value)
|
|
215
|
+
)
|
|
120
216
|
else:
|
|
121
|
-
|
|
217
|
+
converted_list.append(item)
|
|
218
|
+
return converted_list
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def deep_union_pydantic_dicts(
|
|
222
|
+
source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any]
|
|
223
|
+
) -> typing.Dict[str, typing.Any]:
|
|
224
|
+
for key, value in source.items():
|
|
225
|
+
node = destination.setdefault(key, {})
|
|
226
|
+
if isinstance(value, dict):
|
|
227
|
+
deep_union_pydantic_dicts(value, node)
|
|
228
|
+
# Note: we do not do this same processing for sets given we do not have sets of models
|
|
229
|
+
# and given the sets are unordered, the processing of the set and matching objects would
|
|
230
|
+
# be non-trivial.
|
|
231
|
+
elif isinstance(value, list):
|
|
232
|
+
destination[key] = _union_list_of_pydantic_dicts(value, node)
|
|
233
|
+
else:
|
|
234
|
+
destination[key] = value
|
|
235
|
+
|
|
236
|
+
return destination
|
|
122
237
|
|
|
123
238
|
|
|
124
239
|
if IS_PYDANTIC_V2:
|
|
@@ -145,11 +260,11 @@ def encode_by_type(o: typing.Any) -> typing.Any:
|
|
|
145
260
|
return encoder(o)
|
|
146
261
|
|
|
147
262
|
|
|
148
|
-
def update_forward_refs(model: typing.Type["Model"]) -> None:
|
|
263
|
+
def update_forward_refs(model: typing.Type["Model"], **localns: typing.Any) -> None:
|
|
149
264
|
if IS_PYDANTIC_V2:
|
|
150
265
|
model.model_rebuild(raise_errors=False) # type: ignore # Pydantic v2
|
|
151
266
|
else:
|
|
152
|
-
model.update_forward_refs()
|
|
267
|
+
model.update_forward_refs(**localns)
|
|
153
268
|
|
|
154
269
|
|
|
155
270
|
# Mirrors Pydantic's internal typing
|
|
@@ -23,6 +23,8 @@ class RequestOptions(typing.TypedDict, total=False):
|
|
|
23
23
|
- additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict
|
|
24
24
|
|
|
25
25
|
- additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict
|
|
26
|
+
|
|
27
|
+
- chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads.
|
|
26
28
|
"""
|
|
27
29
|
|
|
28
30
|
timeout_in_seconds: NotRequired[int]
|
|
@@ -30,3 +32,4 @@ class RequestOptions(typing.TypedDict, total=False):
|
|
|
30
32
|
additional_headers: NotRequired[typing.Dict[str, typing.Any]]
|
|
31
33
|
additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]]
|
|
32
34
|
additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]]
|
|
35
|
+
chunk_size: NotRequired[int]
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# This file was auto-generated by Fern from our API Definition.
|
|
2
2
|
|
|
3
3
|
import collections
|
|
4
|
+
import inspect
|
|
4
5
|
import typing
|
|
5
6
|
|
|
6
7
|
import typing_extensions
|
|
7
8
|
|
|
9
|
+
import pydantic
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class FieldMetadata:
|
|
10
13
|
"""
|
|
@@ -29,6 +32,7 @@ def convert_and_respect_annotation_metadata(
|
|
|
29
32
|
object_: typing.Any,
|
|
30
33
|
annotation: typing.Any,
|
|
31
34
|
inner_type: typing.Optional[typing.Any] = None,
|
|
35
|
+
direction: typing.Literal["read", "write"],
|
|
32
36
|
) -> typing.Any:
|
|
33
37
|
"""
|
|
34
38
|
Respect the metadata annotations on a field, such as aliasing. This function effectively
|
|
@@ -56,49 +60,79 @@ def convert_and_respect_annotation_metadata(
|
|
|
56
60
|
inner_type = annotation
|
|
57
61
|
|
|
58
62
|
clean_type = _remove_annotations(inner_type)
|
|
63
|
+
# Pydantic models
|
|
64
|
+
if (
|
|
65
|
+
inspect.isclass(clean_type)
|
|
66
|
+
and issubclass(clean_type, pydantic.BaseModel)
|
|
67
|
+
and isinstance(object_, typing.Mapping)
|
|
68
|
+
):
|
|
69
|
+
return _convert_mapping(object_, clean_type, direction)
|
|
70
|
+
# TypedDicts
|
|
59
71
|
if typing_extensions.is_typeddict(clean_type) and isinstance(
|
|
60
72
|
object_, typing.Mapping
|
|
61
73
|
):
|
|
62
|
-
return
|
|
74
|
+
return _convert_mapping(object_, clean_type, direction)
|
|
63
75
|
|
|
64
76
|
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
typing_extensions.get_origin(clean_type) == typing.Set
|
|
79
|
-
or typing_extensions.get_origin(clean_type) == set
|
|
80
|
-
or clean_type == typing.Set
|
|
81
|
-
)
|
|
82
|
-
and isinstance(object_, typing.Set)
|
|
77
|
+
typing_extensions.get_origin(clean_type) == typing.Dict
|
|
78
|
+
or typing_extensions.get_origin(clean_type) == dict
|
|
79
|
+
or clean_type == typing.Dict
|
|
80
|
+
) and isinstance(object_, typing.Dict):
|
|
81
|
+
key_type = typing_extensions.get_args(clean_type)[0]
|
|
82
|
+
value_type = typing_extensions.get_args(clean_type)[1]
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
key: convert_and_respect_annotation_metadata(
|
|
86
|
+
object_=value,
|
|
87
|
+
annotation=annotation,
|
|
88
|
+
inner_type=value_type,
|
|
89
|
+
direction=direction,
|
|
83
90
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
for key, value in object_.items()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# If you're iterating on a string, do not bother to coerce it to a sequence.
|
|
95
|
+
if not isinstance(object_, str):
|
|
96
|
+
if (
|
|
97
|
+
typing_extensions.get_origin(clean_type) == typing.Set
|
|
98
|
+
or typing_extensions.get_origin(clean_type) == set
|
|
99
|
+
or clean_type == typing.Set
|
|
100
|
+
) and isinstance(object_, typing.Set):
|
|
101
|
+
inner_type = typing_extensions.get_args(clean_type)[0]
|
|
102
|
+
return {
|
|
103
|
+
convert_and_respect_annotation_metadata(
|
|
104
|
+
object_=item,
|
|
105
|
+
annotation=annotation,
|
|
106
|
+
inner_type=inner_type,
|
|
107
|
+
direction=direction,
|
|
90
108
|
)
|
|
91
|
-
|
|
109
|
+
for item in object_
|
|
110
|
+
}
|
|
111
|
+
elif (
|
|
112
|
+
(
|
|
113
|
+
typing_extensions.get_origin(clean_type) == typing.List
|
|
114
|
+
or typing_extensions.get_origin(clean_type) == list
|
|
115
|
+
or clean_type == typing.List
|
|
92
116
|
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
117
|
+
and isinstance(object_, typing.List)
|
|
118
|
+
) or (
|
|
119
|
+
(
|
|
120
|
+
typing_extensions.get_origin(clean_type) == typing.Sequence
|
|
121
|
+
or typing_extensions.get_origin(clean_type) == collections.abc.Sequence
|
|
122
|
+
or clean_type == typing.Sequence
|
|
99
123
|
)
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
and isinstance(object_, typing.Sequence)
|
|
125
|
+
):
|
|
126
|
+
inner_type = typing_extensions.get_args(clean_type)[0]
|
|
127
|
+
return [
|
|
128
|
+
convert_and_respect_annotation_metadata(
|
|
129
|
+
object_=item,
|
|
130
|
+
annotation=annotation,
|
|
131
|
+
inner_type=inner_type,
|
|
132
|
+
direction=direction,
|
|
133
|
+
)
|
|
134
|
+
for item in object_
|
|
135
|
+
]
|
|
102
136
|
|
|
103
137
|
if typing_extensions.get_origin(clean_type) == typing.Union:
|
|
104
138
|
# We should be able to ~relatively~ safely try to convert keys against all
|
|
@@ -107,7 +141,10 @@ def convert_and_respect_annotation_metadata(
|
|
|
107
141
|
# Or if another member aliases a field of the same name that another member does not.
|
|
108
142
|
for member in typing_extensions.get_args(clean_type):
|
|
109
143
|
object_ = convert_and_respect_annotation_metadata(
|
|
110
|
-
object_=object_,
|
|
144
|
+
object_=object_,
|
|
145
|
+
annotation=annotation,
|
|
146
|
+
inner_type=member,
|
|
147
|
+
direction=direction,
|
|
111
148
|
)
|
|
112
149
|
return object_
|
|
113
150
|
|
|
@@ -120,19 +157,37 @@ def convert_and_respect_annotation_metadata(
|
|
|
120
157
|
return object_
|
|
121
158
|
|
|
122
159
|
|
|
123
|
-
def
|
|
124
|
-
object_: typing.Mapping[str, object],
|
|
160
|
+
def _convert_mapping(
|
|
161
|
+
object_: typing.Mapping[str, object],
|
|
162
|
+
expected_type: typing.Any,
|
|
163
|
+
direction: typing.Literal["read", "write"],
|
|
125
164
|
) -> typing.Mapping[str, object]:
|
|
126
165
|
converted_object: typing.Dict[str, object] = {}
|
|
127
166
|
annotations = typing_extensions.get_type_hints(expected_type, include_extras=True)
|
|
167
|
+
aliases_to_field_names = _get_alias_to_field_name(annotations)
|
|
128
168
|
for key, value in object_.items():
|
|
129
|
-
|
|
169
|
+
if direction == "read" and key in aliases_to_field_names:
|
|
170
|
+
dealiased_key = aliases_to_field_names.get(key)
|
|
171
|
+
if dealiased_key is not None:
|
|
172
|
+
type_ = annotations.get(dealiased_key)
|
|
173
|
+
else:
|
|
174
|
+
type_ = annotations.get(key)
|
|
175
|
+
# Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map
|
|
176
|
+
#
|
|
177
|
+
# So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias
|
|
178
|
+
# then we can just pass the value through as is
|
|
130
179
|
if type_ is None:
|
|
131
180
|
converted_object[key] = value
|
|
181
|
+
elif direction == "read" and key not in aliases_to_field_names:
|
|
182
|
+
converted_object[key] = convert_and_respect_annotation_metadata(
|
|
183
|
+
object_=value, annotation=type_, direction=direction
|
|
184
|
+
)
|
|
132
185
|
else:
|
|
133
186
|
converted_object[
|
|
134
|
-
_alias_key(key, type_)
|
|
135
|
-
] = convert_and_respect_annotation_metadata(
|
|
187
|
+
_alias_key(key, type_, direction, aliases_to_field_names)
|
|
188
|
+
] = convert_and_respect_annotation_metadata(
|
|
189
|
+
object_=value, annotation=type_, direction=direction
|
|
190
|
+
)
|
|
136
191
|
return converted_object
|
|
137
192
|
|
|
138
193
|
|
|
@@ -165,7 +220,39 @@ def _remove_annotations(type_: typing.Any) -> typing.Any:
|
|
|
165
220
|
return type_
|
|
166
221
|
|
|
167
222
|
|
|
168
|
-
def
|
|
223
|
+
def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]:
|
|
224
|
+
annotations = typing_extensions.get_type_hints(type_, include_extras=True)
|
|
225
|
+
return _get_alias_to_field_name(annotations)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]:
|
|
229
|
+
annotations = typing_extensions.get_type_hints(type_, include_extras=True)
|
|
230
|
+
return _get_field_to_alias_name(annotations)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _get_alias_to_field_name(
|
|
234
|
+
field_to_hint: typing.Dict[str, typing.Any],
|
|
235
|
+
) -> typing.Dict[str, str]:
|
|
236
|
+
aliases = {}
|
|
237
|
+
for field, hint in field_to_hint.items():
|
|
238
|
+
maybe_alias = _get_alias_from_type(hint)
|
|
239
|
+
if maybe_alias is not None:
|
|
240
|
+
aliases[maybe_alias] = field
|
|
241
|
+
return aliases
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _get_field_to_alias_name(
|
|
245
|
+
field_to_hint: typing.Dict[str, typing.Any],
|
|
246
|
+
) -> typing.Dict[str, str]:
|
|
247
|
+
aliases = {}
|
|
248
|
+
for field, hint in field_to_hint.items():
|
|
249
|
+
maybe_alias = _get_alias_from_type(hint)
|
|
250
|
+
if maybe_alias is not None:
|
|
251
|
+
aliases[field] = maybe_alias
|
|
252
|
+
return aliases
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]:
|
|
169
256
|
maybe_annotated_type = _get_annotation(type_)
|
|
170
257
|
|
|
171
258
|
if maybe_annotated_type is not None:
|
|
@@ -175,5 +262,15 @@ def _alias_key(key: str, type_: typing.Any) -> str:
|
|
|
175
262
|
for annotation in annotations:
|
|
176
263
|
if isinstance(annotation, FieldMetadata) and annotation.alias is not None:
|
|
177
264
|
return annotation.alias
|
|
265
|
+
return None
|
|
266
|
+
|
|
178
267
|
|
|
179
|
-
|
|
268
|
+
def _alias_key(
|
|
269
|
+
key: str,
|
|
270
|
+
type_: typing.Any,
|
|
271
|
+
direction: typing.Literal["read", "write"],
|
|
272
|
+
aliases_to_field_names: typing.Dict[str, str],
|
|
273
|
+
) -> str:
|
|
274
|
+
if direction == "read":
|
|
275
|
+
return aliases_to_field_names.get(key, key)
|
|
276
|
+
return _get_alias_from_type(type_=type_) or key
|