@@ -180,11 +180,7 @@ def _transform_recursive(
180180 if isinstance (data , pydantic .BaseModel ):
181181 return model_dump (data , exclude_unset = True )
182182
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 )
188184 if annotated_type is None :
189185 return data
190186
@@ -222,3 +218,125 @@ def _transform_typeddict(
222218 else :
223219 result [_maybe_transform_key (key , type_ )] = _transform_recursive (value , annotation = type_ )
224220 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