-
Notifications
You must be signed in to change notification settings - Fork 75
feat(cloud): add connection state and catalog management to CloudConnection #975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Aaron ("AJ") Steers (aaronsteers)
merged 28 commits into
main
from
devin/1770419251-connection-state-management
Feb 7, 2026
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
961217d
feat(cloud): add connection state management to CloudConnection class
devin-ai-integration[bot] eccc0be
style: fix ruff formatting in test file
devin-ai-integration[bot] a892ca4
fix: return None instead of raising when stream not found in get_state
devin-ai-integration[bot] 5d813d6
docs: clarify create_or_update_safe replaces full state, no per-strea…
devin-ai-integration[bot] 762a327
refactor: return raw dict from CloudConnection state methods instead …
devin-ai-integration[bot] 9fad1da
refactor: make connection_state private, rename get/set to dump/load …
devin-ai-integration[bot] f272414
refactor: rename dump_state/load_state to dump_raw_state/import_raw_s…
devin-ai-integration[bot] 50e49ba
refactor: rename create_or_update_connection_state_safe to create_or_…
devin-ai-integration[bot] c052867
feat: force-override connectionId in import_raw_state for portability
devin-ai-integration[bot] 3fb4a10
refactor: rename to replace_connection_state, inject connectionId in …
devin-ai-integration[bot] eb0b2b1
refactor: use @deprecated decorator from typing_extensions
devin-ai-integration[bot] 24bb62d
feat: catch HTTP 423 in replace_connection_state, raise AirbyteConnec…
devin-ai-integration[bot] b7543ac
docs: document AirbyteConnectionSyncActiveError in import_raw_state a…
devin-ai-integration[bot] e59ceda
refactor: namespace matching treats None and empty string as equivale…
devin-ai-integration[bot] cfc735b
docs: clarify namespace param refers to source-side, not destination …
devin-ai-integration[bot] abb85d8
refactor: use dump_raw_state() in MCP get_connection_artifact tool
devin-ai-integration[bot] db81a4f
feat: add dump_raw_catalog/import_raw_catalog for catalog backup/restore
devin-ai-integration[bot] d405591
fix: preserve raw dicts in set_stream_state, add warnings to import m…
devin-ai-integration[bot] 4a1a4a1
Apply suggestion from @aaronsteers
aaronsteers 5f22a04
refactor: rename sync_catalog to configured_catalog_dict for clarity
devin-ai-integration[bot] 22dd7ad
Apply suggestion from @aaronsteers
aaronsteers 62c4cbc
Merge branch 'devin/1770419251-connection-state-management' of https:…
devin-ai-integration[bot] e4816d4
refactor: rename dict params to *_dict convention (connection_state_d…
devin-ai-integration[bot] cacacd3
docs: add compatibility notes to get/set_stream_state docstrings
devin-ai-integration[bot] 29fac04
style: replace double-backticks with single-backticks in docstrings
devin-ai-integration[bot] b648857
fix: change skipReset to True in replace_connection_catalog to preven…
devin-ai-integration[bot] c2c4e77
Apply suggestion from @aaronsteers
aaronsteers 5065c74
style: fix trailing whitespace in api_util.py
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| # Copyright (c) 2024 Airbyte, Inc., all rights reserved. | ||
| """Pydantic models for connection state management. | ||
|
|
||
| These models represent the state of an Airbyte connection, which tracks sync progress | ||
| for incremental syncs. The state can be one of several types: | ||
| - stream: Per-stream state | ||
| - global: Global state with optional per-stream states | ||
| - legacy: Legacy state blob | ||
| - not_set: No state has been set yet | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Literal | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
|
|
||
| class StreamDescriptor(BaseModel): | ||
| """Descriptor for a single stream within a connection state.""" | ||
|
|
||
| name: str = Field(description="The stream name") | ||
| namespace: str | None = Field( | ||
| default=None, | ||
| description="The stream namespace (optional)", | ||
| ) | ||
|
|
||
|
|
||
| class StreamState(BaseModel): | ||
| """State for a single stream.""" | ||
|
|
||
| model_config = ConfigDict(populate_by_name=True) | ||
|
|
||
| stream_descriptor: StreamDescriptor = Field( | ||
| alias="streamDescriptor", | ||
| description="The stream descriptor (name and namespace)", | ||
| ) | ||
| stream_state: dict[str, Any] | None = Field( | ||
| default=None, | ||
| alias="streamState", | ||
| description="The state blob for this stream", | ||
| ) | ||
|
|
||
|
|
||
| class GlobalState(BaseModel): | ||
| """Global state containing shared state and per-stream states.""" | ||
|
|
||
| model_config = ConfigDict(populate_by_name=True) | ||
|
|
||
| shared_state: dict[str, Any] | None = Field( | ||
| default=None, | ||
| alias="sharedState", | ||
| description="Shared state across all streams", | ||
| ) | ||
| stream_states: list[StreamState] = Field( | ||
| default_factory=list, | ||
| alias="streamStates", | ||
| description="Per-stream states within the global state", | ||
| ) | ||
|
|
||
|
|
||
| class ConnectionStateResponse(BaseModel): | ||
| """Response model for connection state operations. | ||
|
|
||
| Represents the state of a connection, which can be one of: | ||
| - stream: Per-stream state (streamState field populated) | ||
| - global: Global state with optional per-stream states (globalState field populated) | ||
| - legacy: Legacy state blob (state field populated) | ||
| - not_set: No state has been set yet | ||
| """ | ||
|
|
||
| model_config = ConfigDict(populate_by_name=True) | ||
|
|
||
| state_type: Literal["global", "stream", "legacy", "not_set"] = Field( | ||
| alias="stateType", | ||
| description="The type of state: global, stream, legacy, or not_set", | ||
| ) | ||
| connection_id: str = Field( | ||
| alias="connectionId", | ||
| description="The connection ID (UUID)", | ||
| ) | ||
| state: dict[str, Any] | None = Field( | ||
| default=None, | ||
| description="Legacy state blob (populated when stateType is 'legacy')", | ||
| ) | ||
| stream_state: list[StreamState] | None = Field( | ||
| default=None, | ||
| alias="streamState", | ||
| description="Per-stream states (populated when stateType is 'stream')", | ||
| ) | ||
| global_state: GlobalState | None = Field( | ||
| default=None, | ||
| alias="globalState", | ||
| description="Global state (populated when stateType is 'global')", | ||
| ) | ||
|
|
||
| def __str__(self) -> str: | ||
| """Return a string representation of the connection state.""" | ||
| stream_count = 0 | ||
| if self.stream_state: | ||
| stream_count = len(self.stream_state) | ||
| elif self.global_state and self.global_state.stream_states: | ||
| stream_count = len(self.global_state.stream_states) | ||
| return ( | ||
| f"Connection {self.connection_id}: " | ||
| f"stateType={self.state_type}, streams={stream_count}" | ||
| ) | ||
|
|
||
|
|
||
| def _match_stream( | ||
| stream: StreamState, | ||
| stream_name: str, | ||
| stream_namespace: str | None = None, | ||
| ) -> bool: | ||
| """Check if a StreamState matches the given name and namespace. | ||
|
|
||
| Namespace matching treats None and "" as equivalent (both mean "no namespace"). | ||
| There is no wildcard behavior: the caller must match the actual namespace. | ||
| """ | ||
| if stream.stream_descriptor.name != stream_name: | ||
| return False | ||
| return (stream.stream_descriptor.namespace or None) == (stream_namespace or None) | ||
|
|
||
|
|
||
| def _get_stream_list( | ||
| state: ConnectionStateResponse, | ||
| ) -> list[StreamState]: | ||
| """Extract the stream list from a ConnectionStateResponse regardless of state type.""" | ||
| if state.state_type == "stream" and state.stream_state: | ||
| return state.stream_state | ||
| if state.state_type == "global" and state.global_state: | ||
| return state.global_state.stream_states | ||
| return [] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.