Skip to content

Commit 5d81b46

Browse files
committed
add check/discover
1 parent 4d1c03a commit 5d81b46

File tree

6 files changed

+553
-5
lines changed

6 files changed

+553
-5
lines changed

airbyte_cdk/manifest_runner/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ Test reading from a specific stream in the manifest.
3838

3939
**POST** - Test stream reading with configurable limits for records, pages, and slices.
4040

41+
### `/manifest/check`
42+
Check configuration against a manifest.
43+
44+
**POST** - Validates connector configuration and returns success/failure status with message.
45+
46+
### `/manifest/discover`
47+
Discover streams from a manifest.
48+
49+
**POST** - Returns the catalog of available streams from the manifest.
50+
4151
### `/manifest/resolve`
4252
Resolve a manifest to its final configuration.
4353

airbyte_cdk/manifest_runner/api_models/manifest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from typing import Any, List, Optional
99

10+
from airbyte_protocol_dataclasses.models import AirbyteCatalog
1011
from pydantic import BaseModel, Field
1112

1213
from .dicts import ConnectorConfig, Manifest
@@ -25,6 +26,33 @@ class StreamTestReadRequest(BaseModel):
2526
slice_limit: int = Field(default=5, ge=1, le=20)
2627

2728

29+
class CheckRequest(BaseModel):
30+
"""Request to check a manifest."""
31+
32+
manifest: Manifest
33+
config: ConnectorConfig
34+
35+
36+
class CheckResponse(BaseModel):
37+
"""Response to check a manifest."""
38+
39+
success: bool
40+
message: Optional[str] = None
41+
42+
43+
class DiscoverRequest(BaseModel):
44+
"""Request to discover a manifest."""
45+
46+
manifest: Manifest
47+
config: ConnectorConfig
48+
49+
50+
class DiscoverResponse(BaseModel):
51+
"""Response to discover a manifest."""
52+
53+
catalog: AirbyteCatalog
54+
55+
2856
class ResolveRequest(BaseModel):
2957
"""Request to resolve a manifest."""
3058

airbyte_cdk/manifest_runner/manifest_runner/runner.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
from typing import Any, List, Mapping
1+
import logging
2+
from typing import Any, Iterable, List, Mapping, Tuple
3+
4+
from airbyte_protocol_dataclasses.models import (
5+
AirbyteCatalog,
6+
AirbyteConnectionStatus,
7+
AirbyteMessage,
8+
Status,
9+
TraceType,
10+
)
11+
from airbyte_protocol_dataclasses.models import Type as AirbyteMessageType
212

313
from airbyte_cdk.connector_builder.models import StreamRead
414
from airbyte_cdk.connector_builder.test_reader import TestReader
15+
from airbyte_cdk.entrypoint import AirbyteEntrypoint
516
from airbyte_cdk.models.airbyte_protocol import (
617
AirbyteStateMessage,
718
ConfiguredAirbyteCatalog,
@@ -13,6 +24,7 @@
1324

1425
class ManifestRunner:
1526
_source: ManifestDeclarativeSource
27+
_logger = logging.getLogger("airbyte.manifest-runner")
1628

1729
def __init__(self, source: ManifestDeclarativeSource) -> None:
1830
self._source = source
@@ -44,3 +56,85 @@ def test_read(
4456
)
4557

4658
return stream_read
59+
60+
def check_connection(
61+
self,
62+
config: Mapping[str, Any],
63+
) -> Tuple[bool, str]:
64+
"""
65+
Check the connection to the source.
66+
"""
67+
68+
spec = self._source.spec(self._logger)
69+
messages = AirbyteEntrypoint(source=self._source).check(spec, config)
70+
messages_by_type = self._get_messages_by_type(messages)
71+
self._raise_on_trace_message(messages_by_type)
72+
connection_status = self._get_connection_status(messages_by_type)
73+
74+
if connection_status:
75+
return connection_status.status == Status.SUCCEEDED, connection_status.message
76+
return False, "Connection check failed"
77+
78+
def discover(
79+
self,
80+
config: Mapping[str, Any],
81+
) -> AirbyteCatalog | None:
82+
"""
83+
Discover the catalog from the source.
84+
"""
85+
spec = self._source.spec(self._logger)
86+
messages = AirbyteEntrypoint(source=self._source).discover(spec, config)
87+
messages_by_type = self._get_messages_by_type(messages)
88+
self._raise_on_trace_message(messages_by_type)
89+
return self._get_catalog(messages_by_type)
90+
91+
def _get_messages_by_type(
92+
self,
93+
messages: Iterable[AirbyteMessage],
94+
) -> Mapping[str, Iterable[AirbyteMessage]]:
95+
"""
96+
Group messages by type.
97+
"""
98+
grouped = {}
99+
for message in messages:
100+
msg_type = message.type
101+
if msg_type not in grouped:
102+
grouped[msg_type] = []
103+
grouped[msg_type].append(message)
104+
return grouped
105+
106+
def _get_connection_status(
107+
self,
108+
messages_by_type: Mapping[str, List[AirbyteMessage]],
109+
) -> AirbyteConnectionStatus | None:
110+
"""
111+
Get the connection status from the messages.
112+
"""
113+
messages = messages_by_type.get(AirbyteMessageType.CONNECTION_STATUS)
114+
return messages[-1].connectionStatus if messages else None
115+
116+
def _get_catalog(
117+
self,
118+
messages_by_type: Mapping[str, List[AirbyteMessage]],
119+
) -> AirbyteCatalog:
120+
"""
121+
Get the catalog from the messages.
122+
"""
123+
messages = messages_by_type.get(AirbyteMessageType.CATALOG)
124+
return messages[-1].catalog if messages else None
125+
126+
def _raise_on_trace_message(
127+
self,
128+
messages_by_type: Mapping[str, List[AirbyteMessage]],
129+
) -> None:
130+
"""
131+
Raise an exception if a trace message is found.
132+
"""
133+
messages = [
134+
message
135+
for message in messages_by_type.get(AirbyteMessageType.TRACE, [])
136+
if message.trace.type == TraceType.ERROR
137+
]
138+
if messages:
139+
# TODO: raise a better exception
140+
raise Exception(messages[-1].trace.error.message)

0 commit comments

Comments
 (0)