Skip to content

Commit b564be7

Browse files
author
Oleksandr Bazarnov
committed
make path argument optional
1 parent a402288 commit b564be7

File tree

6 files changed

+84
-24
lines changed

6 files changed

+84
-24
lines changed

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,7 +1754,6 @@ definitions:
17541754
type: object
17551755
required:
17561756
- type
1757-
- path
17581757
- url_base
17591758
properties:
17601759
type:
@@ -1766,9 +1765,18 @@ definitions:
17661765
type: string
17671766
interpolation_context:
17681767
- config
1768+
- next_page_token
1769+
- stream_interval
1770+
- stream_partition
1771+
- stream_slice
1772+
- creation_response
1773+
- polling_response
1774+
- download_target
17691775
examples:
17701776
- "https://connect.squareup.com/v2"
1771-
- "{{ config['base_url'] or 'https://app.posthog.com'}}/api/"
1777+
- "{{ config['base_url'] or 'https://app.posthog.com'}}/api"
1778+
- "https://connect.squareup.com/v2/quotes/{{ stream_partition['id'] }}/quote_line_groups"
1779+
- "https://example.com/api/v1/resource/{{ next_page_token['id'] }}"
17721780
path:
17731781
title: URL Path
17741782
description: Path the specific API endpoint that this stream represents. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,12 +2048,14 @@ class HttpRequester(BaseModel):
20482048
description="Base URL of the API source. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.",
20492049
examples=[
20502050
"https://connect.squareup.com/v2",
2051-
"{{ config['base_url'] or 'https://app.posthog.com'}}/api/",
2051+
"{{ config['base_url'] or 'https://app.posthog.com'}}/api",
2052+
"https://connect.squareup.com/v2/quotes/{{ stream_partition['id'] }}/quote_line_groups",
2053+
"https://example.com/api/v1/resource/{{ next_page_token['id'] }}",
20522054
],
20532055
title="API Base URL",
20542056
)
2055-
path: str = Field(
2056-
...,
2057+
path: Optional[str] = Field(
2058+
None,
20572059
description="Path the specific API endpoint that this stream represents. Do not put sensitive information (e.g. API tokens) into this field - Use the Authentication component for this.",
20582060
examples=[
20592061
"/products",

airbyte_cdk/sources/declarative/requesters/http_requester.py

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from airbyte_cdk.sources.streams.call_rate import APIBudget
2626
from airbyte_cdk.sources.streams.http import HttpClient
2727
from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
28-
from airbyte_cdk.sources.types import Config, StreamSlice, StreamState
28+
from airbyte_cdk.sources.types import Config, EmptyString, StreamSlice, StreamState
2929
from airbyte_cdk.utils.mapping_helpers import combine_mappings
3030

3131

@@ -49,9 +49,10 @@ class HttpRequester(Requester):
4949

5050
name: str
5151
url_base: Union[InterpolatedString, str]
52-
path: Union[InterpolatedString, str]
5352
config: Config
5453
parameters: InitVar[Mapping[str, Any]]
54+
55+
path: Optional[Union[InterpolatedString, str]] = None
5556
authenticator: Optional[DeclarativeAuthenticator] = None
5657
http_method: Union[str, HttpMethod] = HttpMethod.GET
5758
request_options_provider: Optional[InterpolatedRequestOptionsProvider] = None
@@ -66,7 +67,9 @@ class HttpRequester(Requester):
6667

6768
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
6869
self._url_base = InterpolatedString.create(self.url_base, parameters=parameters)
69-
self._path = InterpolatedString.create(self.path, parameters=parameters)
70+
self._path = InterpolatedString.create(
71+
self.path if self.path else EmptyString, parameters=parameters
72+
)
7073
if self.request_options_provider is None:
7174
self._request_options_provider = InterpolatedRequestOptionsProvider(
7275
config=self.config, parameters=parameters
@@ -112,27 +115,50 @@ def exit_on_rate_limit(self, value: bool) -> None:
112115
def get_authenticator(self) -> DeclarativeAuthenticator:
113116
return self._authenticator
114117

115-
def get_url_base(self) -> str:
116-
return os.path.join(self._url_base.eval(self.config), "")
117-
118-
def get_path(
118+
def _get_interpolation_context(
119119
self,
120-
*,
121-
stream_state: Optional[StreamState],
122-
stream_slice: Optional[StreamSlice],
123-
next_page_token: Optional[Mapping[str, Any]],
124-
) -> str:
125-
kwargs = {
120+
stream_state: Optional[StreamState] = None,
121+
stream_slice: Optional[StreamSlice] = None,
122+
next_page_token: Optional[Mapping[str, Any]] = None,
123+
) -> Mapping[str, Any]:
124+
return {
126125
"stream_slice": stream_slice,
127126
"next_page_token": next_page_token,
128-
# update the interpolation context with extra fields, if passed.
127+
# update the context with extra fields, if passed.
129128
**(
130129
stream_slice.extra_fields
131130
if stream_slice is not None and hasattr(stream_slice, "extra_fields")
132131
else {}
133132
),
134133
}
135-
path = str(self._path.eval(self.config, **kwargs))
134+
135+
def get_url_base(
136+
self,
137+
*,
138+
stream_state: Optional[StreamState] = None,
139+
stream_slice: Optional[StreamSlice] = None,
140+
next_page_token: Optional[Mapping[str, Any]] = None,
141+
) -> str:
142+
interpolation_context = self._get_interpolation_context(
143+
stream_state=stream_state,
144+
stream_slice=stream_slice,
145+
next_page_token=next_page_token,
146+
)
147+
return os.path.join(self._url_base.eval(self.config, **interpolation_context), EmptyString)
148+
149+
def get_path(
150+
self,
151+
*,
152+
stream_state: Optional[StreamState] = None,
153+
stream_slice: Optional[StreamSlice] = None,
154+
next_page_token: Optional[Mapping[str, Any]] = None,
155+
) -> str:
156+
interpolation_context = self._get_interpolation_context(
157+
stream_state=stream_state,
158+
stream_slice=stream_slice,
159+
next_page_token=next_page_token,
160+
)
161+
path = str(self._path.eval(self.config, **interpolation_context))
136162
return path.lstrip("/")
137163

138164
def get_method(self) -> HttpMethod:
@@ -330,7 +356,20 @@ def _request_body_json(
330356

331357
@classmethod
332358
def _join_url(cls, url_base: str, path: str) -> str:
333-
return urljoin(url_base, path)
359+
"""
360+
Joins a base URL with a given path and returns the resulting URL with any trailing slash removed.
361+
362+
This method ensures that there are no duplicate slashes when concatenating the base URL and the path,
363+
which is useful when the full URL is provided from an interpolation context.
364+
365+
Args:
366+
url_base (str): The base URL to which the path will be appended.
367+
path (str): The path to join with the base URL.
368+
369+
Returns:
370+
str: The concatenated URL with the trailing slash (if any) removed.
371+
"""
372+
return urljoin(url_base, path).rstrip("/")
334373

335374
def send_request(
336375
self,
@@ -347,7 +386,11 @@ def send_request(
347386
request, response = self._http_client.send_request(
348387
http_method=self.get_method().value,
349388
url=self._join_url(
350-
self.get_url_base(),
389+
self.get_url_base(
390+
stream_state=stream_state,
391+
stream_slice=stream_slice,
392+
next_page_token=next_page_token,
393+
),
351394
path
352395
or self.get_path(
353396
stream_state=stream_state,

airbyte_cdk/sources/declarative/requesters/requester.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ def get_authenticator(self) -> DeclarativeAuthenticator:
3535
pass
3636

3737
@abstractmethod
38-
def get_url_base(self) -> str:
38+
def get_url_base(
39+
self,
40+
*,
41+
stream_state: Optional[StreamState],
42+
stream_slice: Optional[StreamSlice],
43+
next_page_token: Optional[Mapping[str, Any]],
44+
) -> str:
3945
"""
4046
:return: URL base for the API endpoint e.g: if you wanted to hit https://myapi.com/v1/some_entity then this should return "https://myapi.com/v1/"
4147
"""

airbyte_cdk/sources/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Config = Mapping[str, Any]
1515
ConnectionDefinition = Mapping[str, Any]
1616
StreamState = Mapping[str, Any]
17+
EmptyString = str()
1718

1819

1920
class Record(Mapping[str, Any]):

unit_tests/sources/declarative/requesters/test_http_requester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ def test_send_request_stream_slice_next_page_token():
825825
"test_trailing_slash_on_path",
826826
"https://airbyte.io",
827827
"/my_endpoint/",
828-
"https://airbyte.io/my_endpoint/",
828+
"https://airbyte.io/my_endpoint",
829829
),
830830
(
831831
"test_nested_path_no_leading_slash",

0 commit comments

Comments
 (0)