airbyte-cdk 6.60.16__py3-none-any.whl → 6.60.16.post40.dev17219503797__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airbyte_cdk/connector_builder/connector_builder_handler.py +32 -36
- airbyte_cdk/connector_builder/main.py +3 -3
- airbyte_cdk/connector_builder/test_reader/helpers.py +24 -2
- airbyte_cdk/connector_builder/test_reader/message_grouper.py +1 -1
- airbyte_cdk/manifest_server/Dockerfile +45 -0
- airbyte_cdk/manifest_server/README.md +142 -0
- airbyte_cdk/manifest_server/__init__.py +3 -0
- airbyte_cdk/manifest_server/api_models/__init__.py +41 -0
- airbyte_cdk/manifest_server/api_models/capabilities.py +7 -0
- airbyte_cdk/manifest_server/api_models/dicts.py +17 -0
- airbyte_cdk/manifest_server/api_models/manifest.py +73 -0
- airbyte_cdk/manifest_server/api_models/stream.py +76 -0
- airbyte_cdk/manifest_server/app.py +17 -0
- airbyte_cdk/manifest_server/auth.py +43 -0
- airbyte_cdk/manifest_server/cli/__init__.py +5 -0
- airbyte_cdk/manifest_server/cli/_common.py +28 -0
- airbyte_cdk/manifest_server/cli/_info.py +30 -0
- airbyte_cdk/manifest_server/cli/_openapi.py +43 -0
- airbyte_cdk/manifest_server/cli/_start.py +38 -0
- airbyte_cdk/manifest_server/cli/run.py +59 -0
- airbyte_cdk/manifest_server/command_processor/__init__.py +0 -0
- airbyte_cdk/manifest_server/command_processor/processor.py +151 -0
- airbyte_cdk/manifest_server/command_processor/utils.py +76 -0
- airbyte_cdk/manifest_server/main.py +24 -0
- airbyte_cdk/manifest_server/openapi.yaml +641 -0
- airbyte_cdk/manifest_server/routers/__init__.py +0 -0
- airbyte_cdk/manifest_server/routers/capabilities.py +25 -0
- airbyte_cdk/manifest_server/routers/health.py +13 -0
- airbyte_cdk/manifest_server/routers/manifest.py +137 -0
- airbyte_cdk/sources/concurrent_source/concurrent_read_processor.py +15 -22
- airbyte_cdk/sources/concurrent_source/concurrent_source.py +30 -18
- airbyte_cdk/sources/declarative/concurrent_declarative_source.py +73 -3
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +4 -0
- airbyte_cdk/sources/declarative/stream_slicers/declarative_partition_generator.py +42 -4
- airbyte_cdk/sources/declarative/stream_slicers/stream_slicer_test_read_decorator.py +2 -2
- airbyte_cdk/sources/message/concurrent_repository.py +47 -0
- airbyte_cdk/sources/streams/concurrent/cursor.py +23 -7
- airbyte_cdk/sources/streams/concurrent/partition_reader.py +46 -5
- airbyte_cdk/sources/streams/concurrent/partitions/types.py +7 -1
- airbyte_cdk/sources/streams/http/http_client.py +4 -1
- airbyte_cdk/sources/utils/slice_logger.py +4 -0
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/METADATA +4 -1
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/RECORD +47 -21
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/entry_points.txt +1 -0
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/LICENSE_SHORT +0 -0
- {airbyte_cdk-6.60.16.dist-info → airbyte_cdk-6.60.16.post40.dev17219503797.dist-info}/WHEEL +0 -0
@@ -3,8 +3,8 @@
|
|
3
3
|
#
|
4
4
|
|
5
5
|
|
6
|
-
from dataclasses import asdict
|
7
|
-
from typing import Any,
|
6
|
+
from dataclasses import asdict
|
7
|
+
from typing import Any, Dict, List, Mapping, Optional
|
8
8
|
|
9
9
|
from airbyte_cdk.connector_builder.test_reader import TestReader
|
10
10
|
from airbyte_cdk.models import (
|
@@ -15,45 +15,32 @@ from airbyte_cdk.models import (
|
|
15
15
|
Type,
|
16
16
|
)
|
17
17
|
from airbyte_cdk.models import Type as MessageType
|
18
|
+
from airbyte_cdk.sources.declarative.concurrent_declarative_source import (
|
19
|
+
ConcurrentDeclarativeSource,
|
20
|
+
TestLimits,
|
21
|
+
)
|
18
22
|
from airbyte_cdk.sources.declarative.declarative_source import DeclarativeSource
|
19
23
|
from airbyte_cdk.sources.declarative.manifest_declarative_source import ManifestDeclarativeSource
|
20
|
-
from airbyte_cdk.sources.declarative.parsers.model_to_component_factory import (
|
21
|
-
ModelToComponentFactory,
|
22
|
-
)
|
23
24
|
from airbyte_cdk.utils.airbyte_secrets_utils import filter_secrets
|
24
25
|
from airbyte_cdk.utils.datetime_helpers import ab_datetime_now
|
25
26
|
from airbyte_cdk.utils.traced_exception import AirbyteTracedException
|
26
27
|
|
27
|
-
DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE = 5
|
28
|
-
DEFAULT_MAXIMUM_NUMBER_OF_SLICES = 5
|
29
|
-
DEFAULT_MAXIMUM_RECORDS = 100
|
30
|
-
DEFAULT_MAXIMUM_STREAMS = 100
|
31
|
-
|
32
28
|
MAX_PAGES_PER_SLICE_KEY = "max_pages_per_slice"
|
33
29
|
MAX_SLICES_KEY = "max_slices"
|
34
30
|
MAX_RECORDS_KEY = "max_records"
|
35
31
|
MAX_STREAMS_KEY = "max_streams"
|
36
32
|
|
37
33
|
|
38
|
-
@dataclass
|
39
|
-
class TestLimits:
|
40
|
-
__test__: ClassVar[bool] = False # Tell Pytest this is not a Pytest class, despite its name
|
41
|
-
|
42
|
-
max_records: int = field(default=DEFAULT_MAXIMUM_RECORDS)
|
43
|
-
max_pages_per_slice: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_PAGES_PER_SLICE)
|
44
|
-
max_slices: int = field(default=DEFAULT_MAXIMUM_NUMBER_OF_SLICES)
|
45
|
-
max_streams: int = field(default=DEFAULT_MAXIMUM_STREAMS)
|
46
|
-
|
47
|
-
|
48
34
|
def get_limits(config: Mapping[str, Any]) -> TestLimits:
|
49
35
|
command_config = config.get("__test_read_config", {})
|
50
|
-
|
51
|
-
command_config.get(
|
36
|
+
return TestLimits(
|
37
|
+
max_records=command_config.get(MAX_RECORDS_KEY, TestLimits.DEFAULT_MAX_RECORDS),
|
38
|
+
max_pages_per_slice=command_config.get(
|
39
|
+
MAX_PAGES_PER_SLICE_KEY, TestLimits.DEFAULT_MAX_PAGES_PER_SLICE
|
40
|
+
),
|
41
|
+
max_slices=command_config.get(MAX_SLICES_KEY, TestLimits.DEFAULT_MAX_SLICES),
|
42
|
+
max_streams=command_config.get(MAX_STREAMS_KEY, TestLimits.DEFAULT_MAX_STREAMS),
|
52
43
|
)
|
53
|
-
max_slices = command_config.get(MAX_SLICES_KEY) or DEFAULT_MAXIMUM_NUMBER_OF_SLICES
|
54
|
-
max_records = command_config.get(MAX_RECORDS_KEY) or DEFAULT_MAXIMUM_RECORDS
|
55
|
-
max_streams = command_config.get(MAX_STREAMS_KEY) or DEFAULT_MAXIMUM_STREAMS
|
56
|
-
return TestLimits(max_records, max_pages_per_slice, max_slices, max_streams)
|
57
44
|
|
58
45
|
|
59
46
|
def should_migrate_manifest(config: Mapping[str, Any]) -> bool:
|
@@ -75,21 +62,30 @@ def should_normalize_manifest(config: Mapping[str, Any]) -> bool:
|
|
75
62
|
return config.get("__should_normalize", False)
|
76
63
|
|
77
64
|
|
78
|
-
def create_source(
|
65
|
+
def create_source(
|
66
|
+
config: Mapping[str, Any],
|
67
|
+
limits: TestLimits,
|
68
|
+
catalog: Optional[ConfiguredAirbyteCatalog],
|
69
|
+
state: Optional[List[AirbyteStateMessage]],
|
70
|
+
) -> ConcurrentDeclarativeSource[Optional[List[AirbyteStateMessage]]]:
|
79
71
|
manifest = config["__injected_declarative_manifest"]
|
80
|
-
|
72
|
+
|
73
|
+
# We enforce a concurrency level of 1 so that the stream is processed on a single thread
|
74
|
+
# to retain ordering for the grouping of the builder message responses.
|
75
|
+
if "concurrency_level" in manifest:
|
76
|
+
manifest["concurrency_level"]["default_concurrency"] = 1
|
77
|
+
else:
|
78
|
+
manifest["concurrency_level"] = {"type": "ConcurrencyLevel", "default_concurrency": 1}
|
79
|
+
|
80
|
+
return ConcurrentDeclarativeSource(
|
81
|
+
catalog=catalog,
|
81
82
|
config=config,
|
82
|
-
|
83
|
+
state=state,
|
83
84
|
source_config=manifest,
|
85
|
+
emit_connector_builder_messages=True,
|
84
86
|
migrate_manifest=should_migrate_manifest(config),
|
85
87
|
normalize_manifest=should_normalize_manifest(config),
|
86
|
-
|
87
|
-
emit_connector_builder_messages=True,
|
88
|
-
limit_pages_fetched_per_slice=limits.max_pages_per_slice,
|
89
|
-
limit_slices_fetched=limits.max_slices,
|
90
|
-
disable_retries=True,
|
91
|
-
disable_cache=True,
|
92
|
-
),
|
88
|
+
limits=limits,
|
93
89
|
)
|
94
90
|
|
95
91
|
|
@@ -91,12 +91,12 @@ def handle_connector_builder_request(
|
|
91
91
|
def handle_request(args: List[str]) -> str:
|
92
92
|
command, config, catalog, state = get_config_and_catalog_from_args(args)
|
93
93
|
limits = get_limits(config)
|
94
|
-
source = create_source(config, limits)
|
95
|
-
return orjson.dumps(
|
94
|
+
source = create_source(config=config, limits=limits, catalog=catalog, state=state)
|
95
|
+
return orjson.dumps( # type: ignore[no-any-return] # Serializer.dump() always returns AirbyteMessage
|
96
96
|
AirbyteMessageSerializer.dump(
|
97
97
|
handle_connector_builder_request(source, command, config, catalog, state, limits)
|
98
98
|
)
|
99
|
-
).decode()
|
99
|
+
).decode()
|
100
100
|
|
101
101
|
|
102
102
|
if __name__ == "__main__":
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import json
|
6
6
|
from copy import deepcopy
|
7
7
|
from json import JSONDecodeError
|
8
|
-
from typing import Any, Dict, List, Mapping, Optional
|
8
|
+
from typing import Any, Dict, List, Mapping, Optional, Union
|
9
9
|
|
10
10
|
from airbyte_cdk.connector_builder.models import (
|
11
11
|
AuxiliaryRequest,
|
@@ -17,6 +17,8 @@ from airbyte_cdk.connector_builder.models import (
|
|
17
17
|
from airbyte_cdk.models import (
|
18
18
|
AirbyteLogMessage,
|
19
19
|
AirbyteMessage,
|
20
|
+
AirbyteStateBlob,
|
21
|
+
AirbyteStateMessage,
|
20
22
|
OrchestratorType,
|
21
23
|
TraceType,
|
22
24
|
)
|
@@ -466,7 +468,7 @@ def handle_current_slice(
|
|
466
468
|
return StreamReadSlices(
|
467
469
|
pages=current_slice_pages,
|
468
470
|
slice_descriptor=current_slice_descriptor,
|
469
|
-
state=[latest_state_message] if latest_state_message else [],
|
471
|
+
state=[convert_state_blob_to_mapping(latest_state_message)] if latest_state_message else [],
|
470
472
|
auxiliary_requests=auxiliary_requests if auxiliary_requests else [],
|
471
473
|
)
|
472
474
|
|
@@ -718,3 +720,23 @@ def get_auxiliary_request_type(stream: dict, http: dict) -> str: # type: ignore
|
|
718
720
|
Determines the type of the auxiliary request based on the stream and HTTP properties.
|
719
721
|
"""
|
720
722
|
return "PARENT_STREAM" if stream.get("is_substream", False) else str(http.get("type", None))
|
723
|
+
|
724
|
+
|
725
|
+
def convert_state_blob_to_mapping(
|
726
|
+
state_message: Union[AirbyteStateMessage, Dict[str, Any]],
|
727
|
+
) -> Dict[str, Any]:
|
728
|
+
"""
|
729
|
+
The AirbyteStreamState stores state as an AirbyteStateBlob which deceivingly is not
|
730
|
+
a dictionary, but rather a list of kwargs fields. This in turn causes it to not be
|
731
|
+
properly turned into a dictionary when translating this back into response output
|
732
|
+
by the connector_builder_handler using asdict()
|
733
|
+
"""
|
734
|
+
|
735
|
+
if isinstance(state_message, AirbyteStateMessage) and state_message.stream:
|
736
|
+
state_value = state_message.stream.stream_state
|
737
|
+
if isinstance(state_value, AirbyteStateBlob):
|
738
|
+
state_value_mapping = {k: v for k, v in state_value.__dict__.items()}
|
739
|
+
state_message.stream.stream_state = state_value_mapping # type: ignore # we intentionally set this as a Dict so that StreamReadSlices is translated properly in the resulting HTTP response
|
740
|
+
return state_message # type: ignore # See above, but when this is an AirbyteStateMessage we must convert AirbyteStateBlob to a Dict
|
741
|
+
else:
|
742
|
+
return state_message # type: ignore # This is guaranteed to be a Dict since we check isinstance AirbyteStateMessage above
|
@@ -95,7 +95,7 @@ def get_message_groups(
|
|
95
95
|
latest_state_message: Optional[Dict[str, Any]] = None
|
96
96
|
slice_auxiliary_requests: List[AuxiliaryRequest] = []
|
97
97
|
|
98
|
-
while
|
98
|
+
while message := next(messages, None):
|
99
99
|
json_message = airbyte_message_to_json(message)
|
100
100
|
|
101
101
|
if is_page_http_request_for_different_stream(json_message, stream_name):
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Dockerfile for the Airbyte Manifest Server.
|
2
|
+
#
|
3
|
+
# This Dockerfile should be built from the root of the repository.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# docker build -f airbyte_cdk/manifest_server/Dockerfile -t airbyte/manifest-server .
|
7
|
+
|
8
|
+
FROM python:3.12-slim-bookworm
|
9
|
+
|
10
|
+
# Install git (needed for dynamic versioning) and poetry
|
11
|
+
RUN apt-get update && \
|
12
|
+
apt-get install -y git && \
|
13
|
+
rm -rf /var/lib/apt/lists/* && \
|
14
|
+
pip install poetry==1.8.3
|
15
|
+
|
16
|
+
# Configure poetry to not create virtual environments and disable interactive mode
|
17
|
+
ENV POETRY_NO_INTERACTION=1 \
|
18
|
+
POETRY_CACHE_DIR=/tmp/poetry_cache
|
19
|
+
|
20
|
+
WORKDIR /app
|
21
|
+
|
22
|
+
# Copy poetry files (build from project root)
|
23
|
+
COPY pyproject.toml poetry.lock ./
|
24
|
+
|
25
|
+
# Copy the project source code (needed for dynamic versioning)
|
26
|
+
COPY . /app
|
27
|
+
|
28
|
+
# Install dependencies and package directly to system Python
|
29
|
+
RUN --mount=type=cache,target=$POETRY_CACHE_DIR \
|
30
|
+
poetry config virtualenvs.create false && \
|
31
|
+
poetry install --extras manifest-server
|
32
|
+
|
33
|
+
# Create a non-root user and group
|
34
|
+
RUN groupadd --gid 1000 airbyte && \
|
35
|
+
useradd --uid 1000 --gid airbyte --shell /bin/bash --create-home airbyte
|
36
|
+
|
37
|
+
# Change ownership
|
38
|
+
RUN chown -R airbyte:airbyte /app
|
39
|
+
|
40
|
+
# Run app as non-root user
|
41
|
+
USER airbyte:airbyte
|
42
|
+
|
43
|
+
EXPOSE 8080
|
44
|
+
|
45
|
+
CMD ["uvicorn", "airbyte_cdk.manifest_server.app:app", "--host", "0.0.0.0", "--port", "8080"]
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# Manifest Server
|
2
|
+
|
3
|
+
An HTTP server for running Airbyte declarative connectors via their manifest files.
|
4
|
+
|
5
|
+
## Quick Start
|
6
|
+
|
7
|
+
### Installation
|
8
|
+
|
9
|
+
The manifest server is available as an extra dependency:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
# Using Poetry (preferred)
|
13
|
+
poetry install --extras manifest-server
|
14
|
+
|
15
|
+
# Using pip
|
16
|
+
pip install airbyte-cdk[manifest-server]
|
17
|
+
|
18
|
+
# Using uv
|
19
|
+
uv pip install 'airbyte-cdk[manifest-runner]'
|
20
|
+
```
|
21
|
+
|
22
|
+
### Running the Server
|
23
|
+
|
24
|
+
```bash
|
25
|
+
# Start the server (default port 8000)
|
26
|
+
manifest-server start
|
27
|
+
|
28
|
+
# Start on a specific port
|
29
|
+
manifest-server start --port 8080
|
30
|
+
|
31
|
+
# Or using Python module
|
32
|
+
python -m airbyte_cdk.manifest_server.cli.run start
|
33
|
+
```
|
34
|
+
|
35
|
+
The server will start on `http://localhost:8000` by default.
|
36
|
+
|
37
|
+
## API Endpoints
|
38
|
+
|
39
|
+
### `/manifest/test_read`
|
40
|
+
Test reading from a specific stream in the manifest.
|
41
|
+
|
42
|
+
**POST** - Test stream reading with configurable limits for records, pages, and slices.
|
43
|
+
|
44
|
+
### `/manifest/check`
|
45
|
+
Check configuration against a manifest.
|
46
|
+
|
47
|
+
**POST** - Validates connector configuration and returns success/failure status with message.
|
48
|
+
|
49
|
+
### `/manifest/discover`
|
50
|
+
Discover streams from a manifest.
|
51
|
+
|
52
|
+
**POST** - Returns the catalog of available streams from the manifest.
|
53
|
+
|
54
|
+
### `/manifest/resolve`
|
55
|
+
Resolve a manifest to its final configuration.
|
56
|
+
|
57
|
+
**POST** - Returns the resolved manifest without dynamic stream generation.
|
58
|
+
|
59
|
+
### `/manifest/full_resolve`
|
60
|
+
Fully resolve a manifest including dynamic streams.
|
61
|
+
|
62
|
+
**POST** - Generates dynamic streams up to specified limits and includes them in the resolved manifest.
|
63
|
+
|
64
|
+
## Custom Components
|
65
|
+
|
66
|
+
The manifest server supports custom Python components, but this feature is **disabled by default** for security reasons.
|
67
|
+
|
68
|
+
### Enabling Custom Components
|
69
|
+
|
70
|
+
To allow custom Python components in your manifest files, set the environment variable:
|
71
|
+
```bash
|
72
|
+
export AIRBYTE_ENABLE_UNSAFE_CODE=true
|
73
|
+
```
|
74
|
+
|
75
|
+
## Authentication
|
76
|
+
|
77
|
+
The manifest server supports optional JWT bearer token authentication:
|
78
|
+
|
79
|
+
### Configuration
|
80
|
+
Set the environment variable to enable authentication:
|
81
|
+
```bash
|
82
|
+
export AB_JWT_SIGNATURE_SECRET="your-jwt-secret-key"
|
83
|
+
```
|
84
|
+
|
85
|
+
### Usage
|
86
|
+
When authentication is enabled, include a valid JWT token in the Authorization header:
|
87
|
+
```bash
|
88
|
+
curl -H "Authorization: Bearer <your-jwt-token>" \
|
89
|
+
http://localhost:8000/manifest/test_read
|
90
|
+
```
|
91
|
+
|
92
|
+
### Behavior
|
93
|
+
- **Without `AB_JWT_SIGNATURE_SECRET`**: All requests pass through
|
94
|
+
- **With `AB_JWT_SIGNATURE_SECRET`**: Requires valid JWT bearer token using HS256 algorithm
|
95
|
+
|
96
|
+
## OpenAPI Specification
|
97
|
+
|
98
|
+
The manifest server provides an OpenAPI specification for API client generation:
|
99
|
+
|
100
|
+
### Generating the OpenAPI Spec
|
101
|
+
```bash
|
102
|
+
# Generate OpenAPI YAML (default location)
|
103
|
+
manifest-server generate-openapi
|
104
|
+
|
105
|
+
# Generate to custom location
|
106
|
+
manifest-server generate-openapi --output /path/to/openapi.yaml
|
107
|
+
```
|
108
|
+
|
109
|
+
The generated OpenAPI specification is consumed by other applications and tools to:
|
110
|
+
- Generate API clients in various programming languages
|
111
|
+
- Create SDK bindings for the manifest server
|
112
|
+
- Provide API documentation and validation
|
113
|
+
- Enable integration with API development tools
|
114
|
+
|
115
|
+
### Interactive API Documentation
|
116
|
+
|
117
|
+
When running, interactive API documentation is available at:
|
118
|
+
- Swagger UI: `http://localhost:8000/docs`
|
119
|
+
- ReDoc: `http://localhost:8000/redoc`
|
120
|
+
|
121
|
+
## Testing
|
122
|
+
|
123
|
+
Run the manifest server tests from the repository root:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
# Run all manifest server tests
|
127
|
+
poetry run pytest unit_tests/manifest_server/ -v
|
128
|
+
```
|
129
|
+
|
130
|
+
## Docker
|
131
|
+
|
132
|
+
The manifest server can be containerized using the included Dockerfile. Build from the repository root:
|
133
|
+
|
134
|
+
```bash
|
135
|
+
# Build from repository root (not from manifest_server subdirectory)
|
136
|
+
docker build -f airbyte_cdk/manifest_server/Dockerfile -t manifest-server .
|
137
|
+
|
138
|
+
# Run the container
|
139
|
+
docker run -p 8080:8080 manifest-server
|
140
|
+
```
|
141
|
+
|
142
|
+
Note: The container runs on port 8080 by default.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""
|
2
|
+
API Models for the Manifest Server Service.
|
3
|
+
|
4
|
+
This package contains all Pydantic models used for API requests and responses.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .dicts import ConnectorConfig, Manifest
|
8
|
+
from .manifest import (
|
9
|
+
FullResolveRequest,
|
10
|
+
ManifestResponse,
|
11
|
+
ResolveRequest,
|
12
|
+
StreamTestReadRequest,
|
13
|
+
)
|
14
|
+
from .stream import (
|
15
|
+
AuxiliaryRequest,
|
16
|
+
HttpRequest,
|
17
|
+
HttpResponse,
|
18
|
+
LogMessage,
|
19
|
+
StreamRead,
|
20
|
+
StreamReadPages,
|
21
|
+
StreamReadSlices,
|
22
|
+
)
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
# Typed Dicts
|
26
|
+
"ConnectorConfig",
|
27
|
+
"Manifest",
|
28
|
+
# Manifest request/response models
|
29
|
+
"FullResolveRequest",
|
30
|
+
"ManifestResponse",
|
31
|
+
"StreamTestReadRequest",
|
32
|
+
"ResolveRequest",
|
33
|
+
# Stream models
|
34
|
+
"AuxiliaryRequest",
|
35
|
+
"HttpRequest",
|
36
|
+
"HttpResponse",
|
37
|
+
"LogMessage",
|
38
|
+
"StreamRead",
|
39
|
+
"StreamReadPages",
|
40
|
+
"StreamReadSlices",
|
41
|
+
]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
Common API models shared across different endpoints.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
6
|
+
|
7
|
+
|
8
|
+
class Manifest(BaseModel):
|
9
|
+
"""Base manifest model. Allows client generation to replace with proper JsonNode types."""
|
10
|
+
|
11
|
+
model_config = ConfigDict(extra="allow")
|
12
|
+
|
13
|
+
|
14
|
+
class ConnectorConfig(BaseModel):
|
15
|
+
"""Base connector configuration model. Allows client generation to replace with proper JsonNode types."""
|
16
|
+
|
17
|
+
model_config = ConfigDict(extra="allow")
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""
|
2
|
+
Manifest-related API models.
|
3
|
+
|
4
|
+
These models define the request and response structures for manifest operations
|
5
|
+
like reading, resolving, and full resolution.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, List, Optional
|
9
|
+
|
10
|
+
from airbyte_protocol_dataclasses.models import AirbyteCatalog
|
11
|
+
from pydantic import BaseModel, Field
|
12
|
+
|
13
|
+
from .dicts import ConnectorConfig, Manifest
|
14
|
+
|
15
|
+
|
16
|
+
class StreamTestReadRequest(BaseModel):
|
17
|
+
"""Request to test read from a specific stream."""
|
18
|
+
|
19
|
+
manifest: Manifest
|
20
|
+
config: ConnectorConfig
|
21
|
+
stream_name: str
|
22
|
+
state: List[Any] = []
|
23
|
+
custom_components_code: Optional[str] = None
|
24
|
+
record_limit: int = Field(default=100, ge=1, le=5000)
|
25
|
+
page_limit: int = Field(default=5, ge=1, le=20)
|
26
|
+
slice_limit: int = Field(default=5, ge=1, le=20)
|
27
|
+
|
28
|
+
|
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
|
+
|
56
|
+
class ResolveRequest(BaseModel):
|
57
|
+
"""Request to resolve a manifest."""
|
58
|
+
|
59
|
+
manifest: Manifest
|
60
|
+
|
61
|
+
|
62
|
+
class ManifestResponse(BaseModel):
|
63
|
+
"""Response containing a manifest."""
|
64
|
+
|
65
|
+
manifest: Manifest
|
66
|
+
|
67
|
+
|
68
|
+
class FullResolveRequest(BaseModel):
|
69
|
+
"""Request to fully resolve a manifest."""
|
70
|
+
|
71
|
+
manifest: Manifest
|
72
|
+
config: ConnectorConfig
|
73
|
+
stream_limit: int = Field(default=100, ge=1, le=100)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
"""
|
2
|
+
Stream-related API models.
|
3
|
+
|
4
|
+
These models define the structure for stream reading operations and responses.
|
5
|
+
They accurately reflect the runtime types returned by the CDK, particularly
|
6
|
+
fixing type mismatches like slice_descriptor being a string rather than an object.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Any, Dict, List, Optional
|
10
|
+
|
11
|
+
from pydantic import BaseModel
|
12
|
+
|
13
|
+
|
14
|
+
class HttpRequest(BaseModel):
|
15
|
+
"""HTTP request details."""
|
16
|
+
|
17
|
+
url: str
|
18
|
+
headers: Optional[Dict[str, Any]]
|
19
|
+
http_method: str
|
20
|
+
body: Optional[str] = None
|
21
|
+
|
22
|
+
|
23
|
+
class HttpResponse(BaseModel):
|
24
|
+
"""HTTP response details."""
|
25
|
+
|
26
|
+
status: int
|
27
|
+
body: Optional[str] = None
|
28
|
+
headers: Optional[Dict[str, Any]] = None
|
29
|
+
|
30
|
+
|
31
|
+
class LogMessage(BaseModel):
|
32
|
+
"""Log message from stream processing."""
|
33
|
+
|
34
|
+
message: str
|
35
|
+
level: str
|
36
|
+
internal_message: Optional[str] = None
|
37
|
+
stacktrace: Optional[str] = None
|
38
|
+
|
39
|
+
|
40
|
+
class AuxiliaryRequest(BaseModel):
|
41
|
+
"""Auxiliary HTTP request made during stream processing."""
|
42
|
+
|
43
|
+
title: str
|
44
|
+
type: str
|
45
|
+
description: str
|
46
|
+
request: HttpRequest
|
47
|
+
response: HttpResponse
|
48
|
+
|
49
|
+
|
50
|
+
class StreamReadPages(BaseModel):
|
51
|
+
"""Pages of data read from a stream slice."""
|
52
|
+
|
53
|
+
records: List[object]
|
54
|
+
request: Optional[HttpRequest] = None
|
55
|
+
response: Optional[HttpResponse] = None
|
56
|
+
|
57
|
+
|
58
|
+
class StreamReadSlices(BaseModel):
|
59
|
+
"""Slices of data read from a stream."""
|
60
|
+
|
61
|
+
pages: List[StreamReadPages]
|
62
|
+
slice_descriptor: Optional[str] # This is actually a string at runtime, not Dict[str, Any]
|
63
|
+
state: Optional[List[Dict[str, Any]]] = None
|
64
|
+
auxiliary_requests: Optional[List[AuxiliaryRequest]] = None
|
65
|
+
|
66
|
+
|
67
|
+
class StreamRead(BaseModel):
|
68
|
+
"""Complete stream read response with properly typed fields."""
|
69
|
+
|
70
|
+
logs: List[LogMessage]
|
71
|
+
slices: List[StreamReadSlices]
|
72
|
+
test_read_limit_reached: bool
|
73
|
+
auxiliary_requests: List[AuxiliaryRequest]
|
74
|
+
inferred_schema: Optional[Dict[str, Any]]
|
75
|
+
inferred_datetime_formats: Optional[Dict[str, str]]
|
76
|
+
latest_config_update: Optional[Dict[str, Any]]
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
|
3
|
+
from ..manifest_server.routers import capabilities, health, manifest
|
4
|
+
|
5
|
+
app = FastAPI(
|
6
|
+
title="Manifest Server Service",
|
7
|
+
description="A service for running low-code Airbyte connectors",
|
8
|
+
version="0.1.0",
|
9
|
+
contact={
|
10
|
+
"name": "Airbyte",
|
11
|
+
"url": "https://airbyte.com",
|
12
|
+
},
|
13
|
+
)
|
14
|
+
|
15
|
+
app.include_router(health.router)
|
16
|
+
app.include_router(capabilities.router)
|
17
|
+
app.include_router(manifest.router, prefix="/v1")
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import os
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import jwt
|
5
|
+
from fastapi import HTTPException, Security, status
|
6
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
7
|
+
|
8
|
+
security = HTTPBearer(auto_error=False)
|
9
|
+
|
10
|
+
|
11
|
+
def verify_jwt_token(
|
12
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
|
13
|
+
) -> None:
|
14
|
+
"""
|
15
|
+
Verify JWT token if AB_JWT_SIGNATURE_SECRET is set, otherwise allow through.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
credentials: Bearer token credentials from request header
|
19
|
+
|
20
|
+
Raises:
|
21
|
+
HTTPException: If token is invalid or missing when secret is configured
|
22
|
+
"""
|
23
|
+
jwt_secret = os.getenv("AB_JWT_SIGNATURE_SECRET")
|
24
|
+
|
25
|
+
# If no secret is configured, allow all requests through
|
26
|
+
if not jwt_secret:
|
27
|
+
return
|
28
|
+
|
29
|
+
if not credentials:
|
30
|
+
raise HTTPException(
|
31
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
32
|
+
detail="Bearer token required",
|
33
|
+
headers={"WWW-Authenticate": "Bearer"},
|
34
|
+
)
|
35
|
+
|
36
|
+
try:
|
37
|
+
jwt.decode(credentials.credentials, jwt_secret, algorithms=["HS256"])
|
38
|
+
except jwt.InvalidTokenError:
|
39
|
+
raise HTTPException(
|
40
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
41
|
+
detail="Invalid token",
|
42
|
+
headers={"WWW-Authenticate": "Bearer"},
|
43
|
+
)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
2
|
+
"""The `airbyte_cdk.manifest_server.cli` module provides a standalone CLI for the Airbyte CDK Manifest Server.
|
3
|
+
|
4
|
+
This CLI enables running a FastAPI server for managing and executing Airbyte declarative manifests.
|
5
|
+
"""
|