@@ -180,11 +180,7 @@ def _transform_recursive(
180
180
if isinstance (data , pydantic .BaseModel ):
181
181
return model_dump (data , exclude_unset = True )
182
182
183
- return _transform_value (data , annotation )
184
-
185
-
186
- def _transform_value (data : object , type_ : type ) -> object :
187
- annotated_type = _get_annotated_type (type_ )
183
+ annotated_type = _get_annotated_type (annotation )
188
184
if annotated_type is None :
189
185
return data
190
186
@@ -222,3 +218,125 @@ def _transform_typeddict(
222
218
else :
223
219
result [_maybe_transform_key (key , type_ )] = _transform_recursive (value , annotation = type_ )
224
220
return result
221
+
222
+
223
+ async def async_maybe_transform (
224
+ data : object ,
225
+ expected_type : object ,
226
+ ) -> Any | None :
227
+ """Wrapper over `async_transform()` that allows `None` to be passed.
228
+
229
+ See `async_transform()` for more details.
230
+ """
231
+ if data is None :
232
+ return None
233
+ return await async_transform (data , expected_type )
234
+
235
+
236
+ async def async_transform (
237
+ data : _T ,
238
+ expected_type : object ,
239
+ ) -> _T :
240
+ """Transform dictionaries based off of type information from the given type, for example:
241
+
242
+ ```py
243
+ class Params(TypedDict, total=False):
244
+ card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
245
+
246
+
247
+ transformed = transform({"card_id": "<my card ID>"}, Params)
248
+ # {'cardID': '<my card ID>'}
249
+ ```
250
+
251
+ Any keys / data that does not have type information given will be included as is.
252
+
253
+ It should be noted that the transformations that this function does are not represented in the type system.
254
+ """
255
+ transformed = await _async_transform_recursive (data , annotation = cast (type , expected_type ))
256
+ return cast (_T , transformed )
257
+
258
+
259
+ async def _async_transform_recursive (
260
+ data : object ,
261
+ * ,
262
+ annotation : type ,
263
+ inner_type : type | None = None ,
264
+ ) -> object :
265
+ """Transform the given data against the expected type.
266
+
267
+ Args:
268
+ annotation: The direct type annotation given to the particular piece of data.
269
+ This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
270
+
271
+ inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
272
+ is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
273
+ the list can be transformed using the metadata from the container type.
274
+
275
+ Defaults to the same value as the `annotation` argument.
276
+ """
277
+ if inner_type is None :
278
+ inner_type = annotation
279
+
280
+ stripped_type = strip_annotated_type (inner_type )
281
+ if is_typeddict (stripped_type ) and is_mapping (data ):
282
+ return await _async_transform_typeddict (data , stripped_type )
283
+
284
+ if (
285
+ # List[T]
286
+ (is_list_type (stripped_type ) and is_list (data ))
287
+ # Iterable[T]
288
+ or (is_iterable_type (stripped_type ) and is_iterable (data ) and not isinstance (data , str ))
289
+ ):
290
+ inner_type = extract_type_arg (stripped_type , 0 )
291
+ return [await _async_transform_recursive (d , annotation = annotation , inner_type = inner_type ) for d in data ]
292
+
293
+ if is_union_type (stripped_type ):
294
+ # For union types we run the transformation against all subtypes to ensure that everything is transformed.
295
+ #
296
+ # TODO: there may be edge cases where the same normalized field name will transform to two different names
297
+ # in different subtypes.
298
+ for subtype in get_args (stripped_type ):
299
+ data = await _async_transform_recursive (data , annotation = annotation , inner_type = subtype )
300
+ return data
301
+
302
+ if isinstance (data , pydantic .BaseModel ):
303
+ return model_dump (data , exclude_unset = True )
304
+
305
+ annotated_type = _get_annotated_type (annotation )
306
+ if annotated_type is None :
307
+ return data
308
+
309
+ # ignore the first argument as it is the actual type
310
+ annotations = get_args (annotated_type )[1 :]
311
+ for annotation in annotations :
312
+ if isinstance (annotation , PropertyInfo ) and annotation .format is not None :
313
+ return await _async_format_data (data , annotation .format , annotation .format_template )
314
+
315
+ return data
316
+
317
+
318
+ async def _async_format_data (data : object , format_ : PropertyFormat , format_template : str | None ) -> object :
319
+ if isinstance (data , (date , datetime )):
320
+ if format_ == "iso8601" :
321
+ return data .isoformat ()
322
+
323
+ if format_ == "custom" and format_template is not None :
324
+ return data .strftime (format_template )
325
+
326
+ return data
327
+
328
+
329
+ async def _async_transform_typeddict (
330
+ data : Mapping [str , object ],
331
+ expected_type : type ,
332
+ ) -> Mapping [str , object ]:
333
+ result : dict [str , object ] = {}
334
+ annotations = get_type_hints (expected_type , include_extras = True )
335
+ for key , value in data .items ():
336
+ type_ = annotations .get (key )
337
+ if type_ is None :
338
+ # we do not have a type annotation for this field, leave it as is
339
+ result [key ] = value
340
+ else :
341
+ result [_maybe_transform_key (key , type_ )] = await _async_transform_recursive (value , annotation = type_ )
342
+ return result
0 commit comments