Skip to content

Commit 665c4de

Browse files
chore(internal): split up transforms into sync / async (#304)
1 parent a9fa0db commit 665c4de

File tree

15 files changed

+339
-101
lines changed

15 files changed

+339
-101
lines changed

src/finch/_utils/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,7 @@
4444
from ._transform import (
4545
PropertyInfo as PropertyInfo,
4646
transform as transform,
47+
async_transform as async_transform,
4748
maybe_transform as maybe_transform,
49+
async_maybe_transform as async_maybe_transform,
4850
)

src/finch/_utils/_transform.py

+123-5
Original file line numberDiff line numberDiff line change
@@ -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

src/finch/resources/hris/benefits/benefits.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from .... import _legacy_response
1010
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11-
from ...._utils import maybe_transform
11+
from ...._utils import (
12+
maybe_transform,
13+
async_maybe_transform,
14+
)
1215
from ...._compat import cached_property
1316
from .individuals import (
1417
Individuals,
@@ -267,7 +270,7 @@ async def create(
267270
"""
268271
return await self._post(
269272
"/employer/benefits",
270-
body=maybe_transform(
273+
body=await async_maybe_transform(
271274
{
272275
"description": description,
273276
"frequency": frequency,
@@ -348,7 +351,7 @@ async def update(
348351
raise ValueError(f"Expected a non-empty value for `benefit_id` but received {benefit_id!r}")
349352
return await self._post(
350353
f"/employer/benefits/{benefit_id}",
351-
body=maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams),
354+
body=await async_maybe_transform({"description": description}, benefit_update_params.BenefitUpdateParams),
352355
options=make_request_options(
353356
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
354357
),

src/finch/resources/jobs/automated.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from ... import _legacy_response
1010
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11-
from ..._utils import maybe_transform
11+
from ..._utils import (
12+
maybe_transform,
13+
async_maybe_transform,
14+
)
1215
from ..._compat import cached_property
1316
from ..._resource import SyncAPIResource, AsyncAPIResource
1417
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -205,7 +208,7 @@ async def create(
205208
"""
206209
return await self._post(
207210
"/jobs/automated",
208-
body=maybe_transform({"type": type}, automated_create_params.AutomatedCreateParams),
211+
body=await async_maybe_transform({"type": type}, automated_create_params.AutomatedCreateParams),
209212
options=make_request_options(
210213
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
211214
),

src/finch/resources/request_forwarding.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from .. import _legacy_response
1010
from ..types import RequestForwardingForwardResponse, request_forwarding_forward_params
1111
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
12-
from .._utils import maybe_transform
12+
from .._utils import (
13+
maybe_transform,
14+
async_maybe_transform,
15+
)
1316
from .._compat import cached_property
1417
from .._resource import SyncAPIResource, AsyncAPIResource
1518
from .._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -155,7 +158,7 @@ async def forward(
155158
"""
156159
return await self._post(
157160
"/forward",
158-
body=maybe_transform(
161+
body=await async_maybe_transform(
159162
{
160163
"method": method,
161164
"route": route,

src/finch/resources/sandbox/company.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from ... import _legacy_response
1010
from ...types import LocationParam
1111
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
12-
from ..._utils import maybe_transform
12+
from ..._utils import (
13+
maybe_transform,
14+
async_maybe_transform,
15+
)
1316
from ..._compat import cached_property
1417
from ..._resource import SyncAPIResource, AsyncAPIResource
1518
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -151,7 +154,7 @@ async def update(
151154
"""
152155
return await self._put(
153156
"/sandbox/company",
154-
body=maybe_transform(
157+
body=await async_maybe_transform(
155158
{
156159
"accounts": accounts,
157160
"departments": departments,

src/finch/resources/sandbox/connections/accounts.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from .... import _legacy_response
1111
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
12-
from ...._utils import maybe_transform
12+
from ...._utils import (
13+
maybe_transform,
14+
async_maybe_transform,
15+
)
1316
from ...._compat import cached_property
1417
from ...._resource import SyncAPIResource, AsyncAPIResource
1518
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -157,7 +160,7 @@ async def create(
157160
"""
158161
return await self._post(
159162
"/sandbox/connections/accounts",
160-
body=maybe_transform(
163+
body=await async_maybe_transform(
161164
{
162165
"company_id": company_id,
163166
"provider_id": provider_id,
@@ -199,7 +202,9 @@ async def update(
199202
"""
200203
return await self._put(
201204
"/sandbox/connections/accounts",
202-
body=maybe_transform({"connection_status": connection_status}, account_update_params.AccountUpdateParams),
205+
body=await async_maybe_transform(
206+
{"connection_status": connection_status}, account_update_params.AccountUpdateParams
207+
),
203208
options=make_request_options(
204209
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
205210
),

src/finch/resources/sandbox/connections/connections.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
AsyncAccountsWithStreamingResponse,
1818
)
1919
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
20-
from ...._utils import maybe_transform
20+
from ...._utils import (
21+
maybe_transform,
22+
async_maybe_transform,
23+
)
2124
from ...._compat import cached_property
2225
from ...._resource import SyncAPIResource, AsyncAPIResource
2326
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -133,7 +136,7 @@ async def create(
133136
"""
134137
return await self._post(
135138
"/sandbox/connections",
136-
body=maybe_transform(
139+
body=await async_maybe_transform(
137140
{
138141
"provider_id": provider_id,
139142
"authentication_type": authentication_type,

src/finch/resources/sandbox/directory.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from ... import _legacy_response
1010
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11-
from ..._utils import maybe_transform
11+
from ..._utils import (
12+
maybe_transform,
13+
async_maybe_transform,
14+
)
1215
from ..._compat import cached_property
1316
from ..._resource import SyncAPIResource, AsyncAPIResource
1417
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -102,7 +105,7 @@ async def create(
102105
"""
103106
return await self._post(
104107
"/sandbox/directory",
105-
body=maybe_transform(body, directory_create_params.DirectoryCreateParams),
108+
body=await async_maybe_transform(body, directory_create_params.DirectoryCreateParams),
106109
options=make_request_options(
107110
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
108111
),

src/finch/resources/sandbox/employment.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from ... import _legacy_response
1010
from ...types import IncomeParam, LocationParam
1111
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
12-
from ..._utils import maybe_transform
12+
from ..._utils import (
13+
maybe_transform,
14+
async_maybe_transform,
15+
)
1316
from ..._compat import cached_property
1417
from ..._resource import SyncAPIResource, AsyncAPIResource
1518
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -217,7 +220,7 @@ async def update(
217220
raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}")
218221
return await self._put(
219222
f"/sandbox/employment/{individual_id}",
220-
body=maybe_transform(
223+
body=await async_maybe_transform(
221224
{
222225
"class_code": class_code,
223226
"custom_fields": custom_fields,

src/finch/resources/sandbox/individual.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from ... import _legacy_response
1111
from ...types import LocationParam
1212
from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven
13-
from ..._utils import maybe_transform
13+
from ..._utils import (
14+
maybe_transform,
15+
async_maybe_transform,
16+
)
1417
from ..._compat import cached_property
1518
from ..._resource import SyncAPIResource, AsyncAPIResource
1619
from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -210,7 +213,7 @@ async def update(
210213
raise ValueError(f"Expected a non-empty value for `individual_id` but received {individual_id!r}")
211214
return await self._put(
212215
f"/sandbox/individual/{individual_id}",
213-
body=maybe_transform(
216+
body=await async_maybe_transform(
214217
{
215218
"dob": dob,
216219
"emails": emails,

src/finch/resources/sandbox/jobs/configuration.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from .... import _legacy_response
1010
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11-
from ...._utils import maybe_transform
11+
from ...._utils import (
12+
maybe_transform,
13+
async_maybe_transform,
14+
)
1215
from ...._compat import cached_property
1316
from ...._resource import SyncAPIResource, AsyncAPIResource
1417
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -142,7 +145,7 @@ async def update(
142145
"""
143146
return await self._put(
144147
"/sandbox/jobs/configuration",
145-
body=maybe_transform(
148+
body=await async_maybe_transform(
146149
{
147150
"completion_status": completion_status,
148151
"type": type,

src/finch/resources/sandbox/jobs/jobs.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
from .... import _legacy_response
1010
from ...._types import NOT_GIVEN, Body, Query, Headers, NotGiven
11-
from ...._utils import maybe_transform
11+
from ...._utils import (
12+
maybe_transform,
13+
async_maybe_transform,
14+
)
1215
from ...._compat import cached_property
1316
from ...._resource import SyncAPIResource, AsyncAPIResource
1417
from ...._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper
@@ -118,7 +121,7 @@ async def create(
118121
"""
119122
return await self._post(
120123
"/sandbox/jobs",
121-
body=maybe_transform({"type": type}, job_create_params.JobCreateParams),
124+
body=await async_maybe_transform({"type": type}, job_create_params.JobCreateParams),
122125
options=make_request_options(
123126
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
124127
),

0 commit comments

Comments
 (0)