Skip to content

Commit d08f1ae

Browse files
lazebnyioctavia-squidington-iii
and
octavia-squidington-iii
authored
feat(low-code): add check dynamic stream (#223)
Co-authored-by: octavia-squidington-iii <contact@airbyte.com>
1 parent 2185bd9 commit d08f1ae

File tree

8 files changed

+272
-10
lines changed

8 files changed

+272
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
#
2-
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
33
#
44

5+
from typing import Mapping
6+
7+
from pydantic.v1 import BaseModel
8+
9+
from airbyte_cdk.sources.declarative.checks.check_dynamic_stream import CheckDynamicStream
510
from airbyte_cdk.sources.declarative.checks.check_stream import CheckStream
611
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
12+
from airbyte_cdk.sources.declarative.models import (
13+
CheckDynamicStream as CheckDynamicStreamModel,
14+
)
15+
from airbyte_cdk.sources.declarative.models import (
16+
CheckStream as CheckStreamModel,
17+
)
18+
19+
COMPONENTS_CHECKER_TYPE_MAPPING: Mapping[str, type[BaseModel]] = {
20+
"CheckStream": CheckStreamModel,
21+
"CheckDynamicStream": CheckDynamicStreamModel,
22+
}
723

8-
__all__ = ["CheckStream", "ConnectionChecker"]
24+
__all__ = ["CheckStream", "CheckDynamicStream", "ConnectionChecker"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#
2+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import logging
6+
import traceback
7+
from dataclasses import InitVar, dataclass
8+
from typing import Any, List, Mapping, Tuple
9+
10+
from airbyte_cdk import AbstractSource
11+
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
12+
from airbyte_cdk.sources.streams.http.availability_strategy import HttpAvailabilityStrategy
13+
14+
15+
@dataclass
16+
class CheckDynamicStream(ConnectionChecker):
17+
"""
18+
Checks the connections by checking availability of one or many dynamic streams
19+
20+
Attributes:
21+
stream_count (int): numbers of streams to check
22+
"""
23+
24+
stream_count: int
25+
parameters: InitVar[Mapping[str, Any]]
26+
27+
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
28+
self._parameters = parameters
29+
30+
def check_connection(
31+
self, source: AbstractSource, logger: logging.Logger, config: Mapping[str, Any]
32+
) -> Tuple[bool, Any]:
33+
streams = source.streams(config=config)
34+
if len(streams) == 0:
35+
return False, f"No streams to connect to from source {source}"
36+
37+
for stream_index in range(min(self.stream_count, len(streams))):
38+
stream = streams[stream_index]
39+
availability_strategy = HttpAvailabilityStrategy()
40+
try:
41+
stream_is_available, reason = availability_strategy.check_availability(
42+
stream, logger
43+
)
44+
if not stream_is_available:
45+
return False, reason
46+
except Exception as error:
47+
logger.error(
48+
f"Encountered an error trying to connect to stream {stream.name}. Error: \n {traceback.format_exc()}"
49+
)
50+
return False, f"Unable to connect to stream {stream.name} - {error}"
51+
return True, None

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

+18-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ properties:
1818
type: string
1919
enum: [DeclarativeSource]
2020
check:
21-
"$ref": "#/definitions/CheckStream"
21+
anyOf:
22+
- "$ref": "#/definitions/CheckStream"
23+
- "$ref": "#/definitions/CheckDynamicStream"
2224
streams:
2325
type: array
2426
items:
@@ -303,6 +305,21 @@ definitions:
303305
examples:
304306
- ["users"]
305307
- ["users", "contacts"]
308+
CheckDynamicStream:
309+
title: Dynamic Streams to Check
310+
description: (This component is experimental. Use at your own risk.) Defines the dynamic streams to try reading when running a check operation.
311+
type: object
312+
required:
313+
- type
314+
- stream_count
315+
properties:
316+
type:
317+
type: string
318+
enum: [CheckDynamicStream]
319+
stream_count:
320+
title: Stream Count
321+
description: Numbers of the streams to try reading from when running a check operation.
322+
type: integer
306323
CompositeErrorHandler:
307324
title: Composite Error Handler
308325
description: Error handler that sequentially iterates over a list of error handlers.

airbyte_cdk/sources/declarative/manifest_declarative_source.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ConnectorSpecification,
2323
FailureType,
2424
)
25+
from airbyte_cdk.sources.declarative.checks import COMPONENTS_CHECKER_TYPE_MAPPING
2526
from airbyte_cdk.sources.declarative.checks.connection_checker import ConnectionChecker
2627
from airbyte_cdk.sources.declarative.declarative_source import DeclarativeSource
2728
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
@@ -107,7 +108,7 @@ def connection_checker(self) -> ConnectionChecker:
107108
if "type" not in check:
108109
check["type"] = "CheckStream"
109110
check_stream = self._constructor.create_component(
110-
CheckStreamModel,
111+
COMPONENTS_CHECKER_TYPE_MAPPING[check["type"]],
111112
check,
112113
dict(),
113114
emit_connector_builder_messages=self._emit_connector_builder_messages,

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ class CheckStream(BaseModel):
5252
)
5353

5454

55+
class CheckDynamicStream(BaseModel):
56+
type: Literal["CheckDynamicStream"]
57+
stream_count: int = Field(
58+
...,
59+
description="Numbers of the streams to try reading from when running a check operation.",
60+
title="Stream Count",
61+
)
62+
63+
5564
class ConcurrencyLevel(BaseModel):
5665
type: Optional[Literal["ConcurrencyLevel"]] = None
5766
default_concurrency: Union[int, str] = Field(
@@ -1661,7 +1670,7 @@ class Config:
16611670
extra = Extra.forbid
16621671

16631672
type: Literal["DeclarativeSource"]
1664-
check: CheckStream
1673+
check: Union[CheckStream, CheckDynamicStream]
16651674
streams: List[DeclarativeStream]
16661675
dynamic_streams: Optional[List[DynamicDeclarativeStream]] = None
16671676
version: str = Field(
@@ -1687,7 +1696,7 @@ class Config:
16871696
extra = Extra.forbid
16881697

16891698
type: Literal["DeclarativeSource"]
1690-
check: CheckStream
1699+
check: Union[CheckStream, CheckDynamicStream]
16911700
streams: Optional[List[DeclarativeStream]] = None
16921701
dynamic_streams: List[DynamicDeclarativeStream]
16931702
version: str = Field(

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
SessionTokenProvider,
5555
TokenProvider,
5656
)
57-
from airbyte_cdk.sources.declarative.checks import CheckStream
57+
from airbyte_cdk.sources.declarative.checks import CheckDynamicStream, CheckStream
5858
from airbyte_cdk.sources.declarative.concurrency_level import ConcurrencyLevel
5959
from airbyte_cdk.sources.declarative.datetime import MinMaxDatetime
6060
from airbyte_cdk.sources.declarative.declarative_stream import DeclarativeStream
@@ -123,6 +123,9 @@
123123
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
124124
BearerAuthenticator as BearerAuthenticatorModel,
125125
)
126+
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
127+
CheckDynamicStream as CheckDynamicStreamModel,
128+
)
126129
from airbyte_cdk.sources.declarative.models.declarative_component_schema import (
127130
CheckStream as CheckStreamModel,
128131
)
@@ -493,6 +496,7 @@ def _init_mappings(self) -> None:
493496
BasicHttpAuthenticatorModel: self.create_basic_http_authenticator,
494497
BearerAuthenticatorModel: self.create_bearer_authenticator,
495498
CheckStreamModel: self.create_check_stream,
499+
CheckDynamicStreamModel: self.create_check_dynamic_stream,
496500
CompositeErrorHandlerModel: self.create_composite_error_handler,
497501
CompositeRawDecoderModel: self.create_composite_raw_decoder,
498502
ConcurrencyLevelModel: self.create_concurrency_level,
@@ -846,6 +850,12 @@ def create_bearer_authenticator(
846850
def create_check_stream(model: CheckStreamModel, config: Config, **kwargs: Any) -> CheckStream:
847851
return CheckStream(stream_names=model.stream_names, parameters={})
848852

853+
@staticmethod
854+
def create_check_dynamic_stream(
855+
model: CheckDynamicStreamModel, config: Config, **kwargs: Any
856+
) -> CheckDynamicStream:
857+
return CheckDynamicStream(stream_count=model.stream_count, parameters={})
858+
849859
def create_composite_error_handler(
850860
self, model: CompositeErrorHandlerModel, config: Config, **kwargs: Any
851861
) -> CompositeErrorHandler:

airbyte_cdk/sources/declarative/requesters/README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
- Components marked as optional are not required and can be ignored.
44
- if `url_requester` is not provided, `urls_extractor` will get urls from the `polling_job_response`
55
- interpolation_context, e.g. `create_job_response` or `polling_job_response` can be obtained from stream_slice
6-
76

87
```mermaid
98
---
109
title: AsyncHttpJobRepository Sequence Diagram
1110
---
12-
sequenceDiagram
11+
sequenceDiagram
1312
participant AsyncHttpJobRepository as AsyncOrchestrator
1413
participant CreationRequester as creation_requester
1514
participant PollingRequester as polling_requester
@@ -54,4 +53,4 @@ sequenceDiagram
5453
DeleteRequester -->> AsyncHttpJobRepository: Confirmation
5554
5655
57-
```
56+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#
2+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import json
6+
import logging
7+
8+
import pytest
9+
10+
from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
11+
ConcurrentDeclarativeSource,
12+
)
13+
from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse
14+
15+
logger = logging.getLogger("test")
16+
17+
_CONFIG = {"start_date": "2024-07-01T00:00:00.000Z"}
18+
19+
_MANIFEST = {
20+
"version": "6.7.0",
21+
"type": "DeclarativeSource",
22+
"check": {"type": "CheckDynamicStream", "stream_count": 1},
23+
"dynamic_streams": [
24+
{
25+
"type": "DynamicDeclarativeStream",
26+
"stream_template": {
27+
"type": "DeclarativeStream",
28+
"name": "",
29+
"primary_key": [],
30+
"schema_loader": {
31+
"type": "InlineSchemaLoader",
32+
"schema": {
33+
"$schema": "http://json-schema.org/schema#",
34+
"properties": {
35+
"ABC": {"type": "number"},
36+
"AED": {"type": "number"},
37+
},
38+
"type": "object",
39+
},
40+
},
41+
"retriever": {
42+
"type": "SimpleRetriever",
43+
"requester": {
44+
"type": "HttpRequester",
45+
"$parameters": {"item_id": ""},
46+
"url_base": "https://api.test.com",
47+
"path": "/items/{{parameters['item_id']}}",
48+
"http_method": "GET",
49+
"authenticator": {
50+
"type": "ApiKeyAuthenticator",
51+
"header": "apikey",
52+
"api_token": "{{ config['api_key'] }}",
53+
},
54+
},
55+
"record_selector": {
56+
"type": "RecordSelector",
57+
"extractor": {"type": "DpathExtractor", "field_path": []},
58+
},
59+
"paginator": {"type": "NoPagination"},
60+
},
61+
},
62+
"components_resolver": {
63+
"type": "HttpComponentsResolver",
64+
"retriever": {
65+
"type": "SimpleRetriever",
66+
"requester": {
67+
"type": "HttpRequester",
68+
"url_base": "https://api.test.com",
69+
"path": "items",
70+
"http_method": "GET",
71+
"authenticator": {
72+
"type": "ApiKeyAuthenticator",
73+
"header": "apikey",
74+
"api_token": "{{ config['api_key'] }}",
75+
},
76+
},
77+
"record_selector": {
78+
"type": "RecordSelector",
79+
"extractor": {"type": "DpathExtractor", "field_path": []},
80+
},
81+
"paginator": {"type": "NoPagination"},
82+
},
83+
"components_mapping": [
84+
{
85+
"type": "ComponentMappingDefinition",
86+
"field_path": ["name"],
87+
"value": "{{components_values['name']}}",
88+
},
89+
{
90+
"type": "ComponentMappingDefinition",
91+
"field_path": [
92+
"retriever",
93+
"requester",
94+
"$parameters",
95+
"item_id",
96+
],
97+
"value": "{{components_values['id']}}",
98+
},
99+
],
100+
},
101+
}
102+
],
103+
}
104+
105+
106+
@pytest.mark.parametrize(
107+
"response_code, available_expectation, expected_messages",
108+
[
109+
pytest.param(
110+
404,
111+
False,
112+
["Not found. The requested resource was not found on the server."],
113+
id="test_stream_unavailable_unhandled_error",
114+
),
115+
pytest.param(
116+
403,
117+
False,
118+
["Forbidden. You don't have permission to access this resource."],
119+
id="test_stream_unavailable_handled_error",
120+
),
121+
pytest.param(200, True, [], id="test_stream_available"),
122+
pytest.param(
123+
401,
124+
False,
125+
["Unauthorized. Please ensure you are authenticated correctly."],
126+
id="test_stream_unauthorized_error",
127+
),
128+
],
129+
)
130+
def test_check_dynamic_stream(response_code, available_expectation, expected_messages):
131+
with HttpMocker() as http_mocker:
132+
http_mocker.get(
133+
HttpRequest(url="https://api.test.com/items"),
134+
HttpResponse(
135+
body=json.dumps(
136+
[
137+
{"id": 1, "name": "item_1"},
138+
{"id": 2, "name": "item_2"},
139+
]
140+
)
141+
),
142+
)
143+
http_mocker.get(
144+
HttpRequest(url="https://api.test.com/items/1"),
145+
HttpResponse(body=json.dumps(expected_messages), status_code=response_code),
146+
)
147+
148+
source = ConcurrentDeclarativeSource(
149+
source_config=_MANIFEST,
150+
config=_CONFIG,
151+
catalog=None,
152+
state=None,
153+
)
154+
155+
stream_is_available, reason = source.check_connection(logger, _CONFIG)
156+
157+
assert stream_is_available == available_expectation
158+
for message in expected_messages:
159+
assert message in reason

0 commit comments

Comments
 (0)