Skip to content

Commit 6ecf85d

Browse files
author
Oleksandr Bazarnov
committed
add request_body_json/data deprecation + tests the new RequestBody provides the same functionality
1 parent d0983e5 commit 6ecf85d

File tree

6 files changed

+218
-3
lines changed

6 files changed

+218
-3
lines changed

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,6 +2000,8 @@ definitions:
20002000
- "$ref": "#/definitions/CustomAuthenticator"
20012001
- "$ref": "#/definitions/LegacySessionTokenAuthenticator"
20022002
request_body_data:
2003+
deprecated: true
2004+
deprecation_message: "Use `request_body` field instead."
20032005
title: Request Body Payload (Non-JSON)
20042006
description: Specifies how to populate the body of the request with a non-JSON payload. Plain text will be sent as is, whereas objects will be converted to a urlencoded form.
20052007
anyOf:
@@ -2018,6 +2020,8 @@ definitions:
20182020
[{"value": {{ stream_interval['start_time'] | int * 1000 }} }]
20192021
}, "orderBy": 1, "columnName": "Timestamp"}]/
20202022
request_body_json:
2023+
deprecated: true
2024+
deprecation_message: "Use `request_body` field instead."
20212025
title: Request Body JSON Payload
20222026
description: Specifies how to populate the body of the request with a JSON payload. Can contain nested objects.
20232027
anyOf:
@@ -2036,6 +2040,35 @@ definitions:
20362040
- sort:
20372041
field: "updated_at"
20382042
order: "ascending"
2043+
request_body:
2044+
title: Request Body Payload to be send as a part of the API request.
2045+
description: Specifies how to populate the body of the request with a payload. Can contain nested objects.
2046+
anyOf:
2047+
- "$ref": "#/definitions/RequestBody"
2048+
interpolation_context:
2049+
- next_page_token
2050+
- stream_interval
2051+
- stream_partition
2052+
- stream_slice
2053+
examples:
2054+
- type: RequestBodyJson
2055+
value:
2056+
sort_order: "ASC"
2057+
sort_field: "CREATED_AT"
2058+
- type: RequestBodyJson
2059+
value:
2060+
key: "{{ config['value'] }}"
2061+
- type: RequestBodyJson
2062+
value:
2063+
sort:
2064+
field: "updated_at"
2065+
order: "ascending"
2066+
- type: RequestBodyData
2067+
value: "plain_text_body"
2068+
- type: RequestBodyData
2069+
value:
2070+
param1: "value1"
2071+
param2: "{{ config['param2_value'] }}"
20392072
request_headers:
20402073
title: Request Headers
20412074
description: Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method.
@@ -4036,6 +4069,27 @@ definitions:
40364069
- type
40374070
- stream_template
40384071
- components_resolver
4072+
RequestBody:
4073+
type: object
4074+
description: The request body payload. Can be either URL encoded data or JSON.
4075+
properties:
4076+
type:
4077+
anyOf:
4078+
- type: string
4079+
enum: [RequestBodyData]
4080+
- type: string
4081+
enum: [RequestBodyJson]
4082+
value:
4083+
anyOf:
4084+
- type: string
4085+
description: The request body payload as a string.
4086+
- type: object
4087+
description: The request body payload as a Non-JSON object (url-encoded data).
4088+
additionalProperties:
4089+
type: string
4090+
- type: object
4091+
description: The request body payload as a JSON object (json-encoded data).
4092+
additionalProperties: true
40394093
interpolation:
40404094
variables:
40414095
- title: config

airbyte_cdk/sources/declarative/interpolation/interpolated_nested_mapping.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
NestedMappingEntry = Union[
1313
dict[str, "NestedMapping"], list["NestedMapping"], str, int, float, bool, None
1414
]
15-
NestedMapping = Union[dict[str, NestedMappingEntry], str]
15+
NestedMapping = Union[dict[str, NestedMappingEntry], str, dict[str, Any]]
1616

1717

1818
@dataclass

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,11 @@ class ConfigComponentsResolver(BaseModel):
14991499
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
15001500

15011501

1502+
class RequestBody(BaseModel):
1503+
type: Optional[Union[Literal["RequestBodyData"], Literal["RequestBodyJson"]]] = None
1504+
value: Optional[Union[str, Dict[str, str], Dict[str, Any]]] = None
1505+
1506+
15021507
class AddedFieldDefinition(BaseModel):
15031508
type: Literal["AddedFieldDefinition"]
15041509
path: List[str] = Field(
@@ -2273,6 +2278,8 @@ class HttpRequester(BaseModelWithDeprecations):
22732278
)
22742279
request_body_data: Optional[Union[Dict[str, str], str]] = Field(
22752280
None,
2281+
deprecated=True,
2282+
deprecation_message="Use `request_body` field instead.",
22762283
description="Specifies how to populate the body of the request with a non-JSON payload. Plain text will be sent as is, whereas objects will be converted to a urlencoded form.",
22772284
examples=[
22782285
'[{"clause": {"type": "timestamp", "operator": 10, "parameters":\n [{"value": {{ stream_interval[\'start_time\'] | int * 1000 }} }]\n }, "orderBy": 1, "columnName": "Timestamp"}]/\n'
@@ -2281,6 +2288,8 @@ class HttpRequester(BaseModelWithDeprecations):
22812288
)
22822289
request_body_json: Optional[Union[Dict[str, Any], str]] = Field(
22832290
None,
2291+
deprecated=True,
2292+
deprecation_message="Use `request_body` field instead.",
22842293
description="Specifies how to populate the body of the request with a JSON payload. Can contain nested objects.",
22852294
examples=[
22862295
{"sort_order": "ASC", "sort_field": "CREATED_AT"},
@@ -2289,6 +2298,27 @@ class HttpRequester(BaseModelWithDeprecations):
22892298
],
22902299
title="Request Body JSON Payload",
22912300
)
2301+
request_body: Optional[RequestBody] = Field(
2302+
None,
2303+
description="Specifies how to populate the body of the request with a payload. Can contain nested objects.",
2304+
examples=[
2305+
{
2306+
"type": "RequestBodyJson",
2307+
"value": {"sort_order": "ASC", "sort_field": "CREATED_AT"},
2308+
},
2309+
{"type": "RequestBodyJson", "value": {"key": "{{ config['value'] }}"}},
2310+
{
2311+
"type": "RequestBodyJson",
2312+
"value": {"sort": {"field": "updated_at", "order": "ascending"}},
2313+
},
2314+
{"type": "RequestBodyData", "value": "plain_text_body"},
2315+
{
2316+
"type": "RequestBodyData",
2317+
"value": {"param1": "value1", "param2": "{{ config['param2_value'] }}"},
2318+
},
2319+
],
2320+
title="Request Body Payload to be send as a part of the API request.",
2321+
)
22922322
request_headers: Optional[Union[Dict[str, str], str]] = Field(
22932323
None,
22942324
description="Return any non-auth headers. Authentication headers will overwrite any overlapping headers returned from this method.",

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,6 +2239,7 @@ def create_http_requester(
22392239
request_parameters = model.request_parameters
22402240

22412241
request_options_provider = InterpolatedRequestOptionsProvider(
2242+
request_body=model.request_body,
22422243
request_body_data=model.request_body_data,
22432244
request_body_json=model.request_body_json,
22442245
request_headers=model.request_headers,

airbyte_cdk/sources/declarative/requesters/request_options/interpolated_request_options_provider.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from typing import Any, List, Mapping, MutableMapping, Optional, Union
77

88
from airbyte_cdk.sources.declarative.interpolation.interpolated_nested_mapping import NestedMapping
9+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
10+
RequestBody,
11+
)
912
from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_nested_request_input_provider import (
1013
InterpolatedNestedRequestInputProvider,
1114
)
@@ -38,6 +41,7 @@ class InterpolatedRequestOptionsProvider(RequestOptionsProvider):
3841
config: Config = field(default_factory=dict)
3942
request_parameters: Optional[RequestInput] = None
4043
request_headers: Optional[RequestInput] = None
44+
request_body: Optional[RequestBody] = None
4145
request_body_data: Optional[RequestInput] = None
4246
request_body_json: Optional[NestedMapping] = None
4347
query_properties_key: Optional[str] = None
@@ -47,16 +51,19 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
4751
self.request_parameters = {}
4852
if self.request_headers is None:
4953
self.request_headers = {}
54+
# resolve the request body to either data or json
55+
self._resolve_request_body()
56+
# If request_body is not provided, set request_body_data and request_body_json to empty dicts
5057
if self.request_body_data is None:
5158
self.request_body_data = {}
5259
if self.request_body_json is None:
5360
self.request_body_json = {}
54-
61+
# If both request_body_data and request_body_json are provided, raise an error
5562
if self.request_body_json and self.request_body_data:
5663
raise ValueError(
5764
"RequestOptionsProvider should only contain either 'request_body_data' or 'request_body_json' not both"
5865
)
59-
66+
# set interpolators
6067
self._parameter_interpolator = InterpolatedRequestInputProvider(
6168
config=self.config, request_inputs=self.request_parameters, parameters=parameters
6269
)
@@ -70,6 +77,21 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
7077
config=self.config, request_inputs=self.request_body_json, parameters=parameters
7178
)
7279

80+
def _resolve_request_body(self) -> None:
81+
"""
82+
Resolves the request body configuration by setting either `request_body_data` or `request_body_json`
83+
based on the type specified in `self.request_body`. If neither is provided, both are initialized as empty
84+
dictionaries. Raises a ValueError if both `request_body_data` and `request_body_json` are set simultaneously.
85+
Raises:
86+
ValueError: If both `request_body_data` and `request_body_json` are provided.
87+
"""
88+
# Resolve the request body to either data or json
89+
if self.request_body is not None and self.request_body.type is not None:
90+
if self.request_body.type == "RequestBodyData":
91+
self.request_body_data = self.request_body.value
92+
elif self.request_body.type == "RequestBodyJson":
93+
self.request_body_json = self.request_body.value
94+
7395
def get_request_params(
7496
self,
7597
*,

unit_tests/sources/declarative/requesters/request_options/test_interpolated_request_options_provider.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66

7+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import RequestBody
78
from airbyte_cdk.sources.declarative.requesters.request_options.interpolated_request_options_provider import (
89
InterpolatedRequestOptionsProvider,
910
)
@@ -131,6 +132,71 @@ def test_interpolated_request_json(test_name, input_request_json, expected_reque
131132
assert actual_request_json == expected_request_json
132133

133134

135+
@pytest.mark.parametrize(
136+
"test_name, input_request_json, expected_request_json",
137+
[
138+
(
139+
"test_static_json",
140+
{"a_static_request_param": "a_static_value"},
141+
{"a_static_request_param": "a_static_value"},
142+
),
143+
(
144+
"test_value_depends_on_stream_slice",
145+
{"read_from_slice": "{{ stream_slice['start_date'] }}"},
146+
{"read_from_slice": "2020-01-01"},
147+
),
148+
(
149+
"test_value_depends_on_next_page_token",
150+
{"read_from_token": "{{ next_page_token['offset'] }}"},
151+
{"read_from_token": 12345},
152+
),
153+
(
154+
"test_value_depends_on_config",
155+
{"read_from_config": "{{ config['option'] }}"},
156+
{"read_from_config": "OPTION"},
157+
),
158+
(
159+
"test_interpolated_keys",
160+
{"{{ stream_interval['start_date'] }}": 123, "{{ config['option'] }}": "ABC"},
161+
{"2020-01-01": 123, "OPTION": "ABC"},
162+
),
163+
("test_boolean_false_value", {"boolean_false": "{{ False }}"}, {"boolean_false": False}),
164+
("test_integer_falsy_value", {"integer_falsy": "{{ 0 }}"}, {"integer_falsy": 0}),
165+
("test_number_falsy_value", {"number_falsy": "{{ 0.0 }}"}, {"number_falsy": 0.0}),
166+
("test_string_falsy_value", {"string_falsy": "{{ '' }}"}, {}),
167+
("test_none_value", {"none_value": "{{ None }}"}, {}),
168+
(
169+
"test_string",
170+
"""{"nested": { "key": "{{ config['option'] }}" }}""",
171+
{"nested": {"key": "OPTION"}},
172+
),
173+
(
174+
"test_nested_objects",
175+
{"nested": {"key": "{{ config['option'] }}"}},
176+
{"nested": {"key": "OPTION"}},
177+
),
178+
(
179+
"test_nested_objects_interpolated keys",
180+
{"nested": {"{{ stream_interval['start_date'] }}": "{{ config['option'] }}"}},
181+
{"nested": {"2020-01-01": "OPTION"}},
182+
),
183+
],
184+
)
185+
def test_interpolated_request_json_using_request_body(
186+
test_name, input_request_json, expected_request_json
187+
):
188+
provider = InterpolatedRequestOptionsProvider(
189+
config=config,
190+
request_body=RequestBody(type="RequestBodyJson", value=input_request_json),
191+
parameters={},
192+
)
193+
actual_request_json = provider.get_request_body_json(
194+
stream_state=state, stream_slice=stream_slice, next_page_token=next_page_token
195+
)
196+
197+
assert actual_request_json == expected_request_json
198+
199+
134200
@pytest.mark.parametrize(
135201
"test_name, input_request_data, expected_request_data",
136202
[
@@ -169,6 +235,48 @@ def test_interpolated_request_data(test_name, input_request_data, expected_reque
169235
assert actual_request_data == expected_request_data
170236

171237

238+
@pytest.mark.parametrize(
239+
"test_name, input_request_data, expected_request_data",
240+
[
241+
(
242+
"test_static_map_data",
243+
{"a_static_request_param": "a_static_value"},
244+
{"a_static_request_param": "a_static_value"},
245+
),
246+
(
247+
"test_map_depends_on_stream_slice",
248+
{"read_from_slice": "{{ stream_slice['start_date'] }}"},
249+
{"read_from_slice": "2020-01-01"},
250+
),
251+
(
252+
"test_map_depends_on_config",
253+
{"read_from_config": "{{ config['option'] }}"},
254+
{"read_from_config": "OPTION"},
255+
),
256+
("test_defaults_to_empty_dict", None, {}),
257+
(
258+
"test_interpolated_keys",
259+
{"{{ stream_interval['start_date'] }} - {{ next_page_token['offset'] }}": "ABC"},
260+
{"2020-01-01 - 12345": "ABC"},
261+
),
262+
],
263+
)
264+
def test_interpolated_request_data_using_request_body(
265+
test_name, input_request_data, expected_request_data
266+
):
267+
provider = InterpolatedRequestOptionsProvider(
268+
config=config,
269+
request_body=RequestBody(type="RequestBodyData", value=input_request_data),
270+
parameters={},
271+
)
272+
273+
actual_request_data = provider.get_request_body_data(
274+
stream_state=state, stream_slice=stream_slice, next_page_token=next_page_token
275+
)
276+
277+
assert actual_request_data == expected_request_data
278+
279+
172280
def test_error_on_create_for_both_request_json_and_data():
173281
request_json = {"body_key": "{{ stream_slice['start_date'] }}"}
174282
request_data = "interpolate_me=5&invalid={{ config['option'] }}"

0 commit comments

Comments
 (0)