airbyte.cloud.connections

Cloud Connections.

   1# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
   2"""Cloud Connections."""
   3
   4from __future__ import annotations
   5
   6import logging
   7from typing import TYPE_CHECKING, Any, Literal, overload
   8
   9from typing_extensions import deprecated
  10
  11from airbyte._util import api_util
  12from airbyte.cloud._connection_catalog import (
  13    _denormalize_catalog_to_api,
  14    _is_protocol_catalog_format,
  15    _normalize_catalog_to_protocol,
  16)
  17from airbyte.cloud._connection_state import (
  18    ConnectionStateResponse,
  19    _denormalize_protocol_state_to_api,
  20    _get_stream_list,
  21    _is_protocol_state_format,
  22    _match_stream,
  23    _normalize_state_to_protocol,
  24)
  25from airbyte.cloud.connectors import CloudDestination, CloudSource
  26from airbyte.cloud.constants import FINAL_STATUSES
  27from airbyte.cloud.models import (
  28    CloudConnectionInfo,
  29    CloudJobInfo,
  30    JobTypeEnum,
  31    _ConnectionResponseLike,
  32)
  33from airbyte.cloud.sync_results import SyncResult
  34from airbyte.exceptions import AirbyteWorkspaceMismatchError, PyAirbyteInputError
  35
  36
  37logger = logging.getLogger(__name__)
  38
  39
  40if TYPE_CHECKING:
  41    from airbyte.cloud.workspaces import CloudWorkspace
  42
  43
  44class CloudConnection:  # noqa: PLR0904  # Too many public methods
  45    """A connection is an extract-load (EL) pairing of a source and destination in Airbyte Cloud.
  46
  47    You can use a connection object to run sync jobs, retrieve logs, and manage the connection.
  48    """
  49
  50    def __init__(
  51        self,
  52        workspace: CloudWorkspace,
  53        connection_id: str,
  54        source: str | None = None,
  55        destination: str | None = None,
  56    ) -> None:
  57        """It is not recommended to create a `CloudConnection` object directly.
  58
  59        Instead, use `CloudWorkspace.get_connection()` to create a connection object.
  60        """
  61        self.connection_id = connection_id
  62        """The ID of the connection."""
  63
  64        self.workspace = workspace
  65        """The workspace that the connection belongs to."""
  66
  67        self._source_id = source
  68        """The ID of the source."""
  69
  70        self._destination_id = destination
  71        """The ID of the destination."""
  72
  73        self._connection_info: CloudConnectionInfo | None = None
  74        """The connection info object. (Cached.)"""
  75
  76        self._cloud_source_object: CloudSource | None = None
  77        """The source object. (Cached.)"""
  78
  79        self._cloud_destination_object: CloudDestination | None = None
  80        """The destination object. (Cached.)"""
  81
  82    def _fetch_connection_info(
  83        self,
  84        *,
  85        force_refresh: bool = False,
  86        verify: bool = True,
  87    ) -> CloudConnectionInfo:
  88        """Fetch and cache connection info from the API.
  89
  90        By default, this method will only fetch from the API if connection info is not
  91        already cached. It also verifies that the connection belongs to the expected
  92        workspace unless verification is explicitly disabled.
  93
  94        Args:
  95            force_refresh: If True, always fetch from the API even if cached.
  96                If False (default), only fetch if not already cached.
  97            verify: If True (default), verify that the connection is valid (e.g., that
  98                the workspace_id matches this object's workspace). Raises an error if
  99                validation fails.
 100
 101        Returns:
 102            Information about the connection from the API.
 103
 104        Raises:
 105            AirbyteWorkspaceMismatchError: If verify is True and the connection's
 106                workspace_id doesn't match the expected workspace.
 107            AirbyteMissingResourceError: If the connection doesn't exist.
 108        """
 109        if not force_refresh and self._connection_info is not None:
 110            # Use cached info, but still verify if requested
 111            if verify:
 112                self._verify_workspace_match(self._connection_info)
 113            return self._connection_info
 114
 115        # Fetch from API
 116        connection_info = api_util.get_connection(
 117            workspace_id=self.workspace.workspace_id,
 118            connection_id=self.connection_id,
 119            api_root=self.workspace.api_root,
 120            client_id=self.workspace.client_id,
 121            client_secret=self.workspace.client_secret,
 122            bearer_token=self.workspace.bearer_token,
 123        )
 124        result = CloudConnectionInfo.from_api_response(connection_info)
 125
 126        self._connection_info = result
 127
 128        # Verify if requested
 129        if verify:
 130            self._verify_workspace_match(result)
 131
 132        return result
 133
 134    def _verify_workspace_match(self, connection_info: CloudConnectionInfo) -> None:
 135        """Verify that the connection belongs to the expected workspace.
 136
 137        Raises:
 138            AirbyteWorkspaceMismatchError: If the workspace IDs don't match.
 139        """
 140        if connection_info.workspace_id != self.workspace.workspace_id:
 141            raise AirbyteWorkspaceMismatchError(
 142                resource_type="connection",
 143                resource_id=self.connection_id,
 144                workspace=self.workspace,
 145                expected_workspace_id=self.workspace.workspace_id,
 146                actual_workspace_id=connection_info.workspace_id,
 147                message=(
 148                    f"Connection '{self.connection_id}' belongs to workspace "
 149                    f"'{connection_info.workspace_id}', not '{self.workspace.workspace_id}'."
 150                ),
 151            )
 152
 153    def check_is_valid(self) -> bool:
 154        """Check if this connection exists and belongs to the expected workspace.
 155
 156        This method fetches connection info from the API (if not already cached) and
 157        verifies that the connection's workspace_id matches the workspace associated
 158        with this CloudConnection object.
 159
 160        Returns:
 161            True if the connection exists and belongs to the expected workspace.
 162
 163        Raises:
 164            AirbyteWorkspaceMismatchError: If the connection belongs to a different workspace.
 165            AirbyteMissingResourceError: If the connection doesn't exist.
 166        """
 167        self._fetch_connection_info(force_refresh=False, verify=True)
 168        return True
 169
 170    @classmethod
 171    def _from_connection_response(
 172        cls,
 173        workspace: CloudWorkspace,
 174        connection_response: _ConnectionResponseLike,
 175    ) -> CloudConnection:
 176        """Create a CloudConnection from an API connection response."""
 177        connection_info = CloudConnectionInfo.from_api_response(connection_response)
 178        result = cls(
 179            workspace=workspace,
 180            connection_id=connection_info.connection_id,
 181            source=connection_info.source_id,
 182            destination=connection_info.destination_id,
 183        )
 184        result._connection_info = connection_info  # noqa: SLF001 # Accessing Non-Public API
 185        return result
 186
 187    # Properties
 188
 189    @property
 190    def name(self) -> str | None:
 191        """Get the display name of the connection, if available.
 192
 193        E.g. "My Postgres to Snowflake", not the connection ID.
 194        """
 195        if not self._connection_info:
 196            self._connection_info = self._fetch_connection_info()
 197
 198        return self._connection_info.name
 199
 200    @property
 201    def source_id(self) -> str:
 202        """The ID of the source."""
 203        if not self._source_id:
 204            if not self._connection_info:
 205                self._connection_info = self._fetch_connection_info()
 206
 207            self._source_id = self._connection_info.source_id
 208
 209        return self._source_id
 210
 211    @property
 212    def source(self) -> CloudSource:
 213        """Get the source object."""
 214        if self._cloud_source_object:
 215            return self._cloud_source_object
 216
 217        self._cloud_source_object = CloudSource(
 218            workspace=self.workspace,
 219            connector_id=self.source_id,
 220        )
 221        return self._cloud_source_object
 222
 223    @property
 224    def destination_id(self) -> str:
 225        """The ID of the destination."""
 226        if not self._destination_id:
 227            if not self._connection_info:
 228                self._connection_info = self._fetch_connection_info()
 229
 230            self._destination_id = self._connection_info.destination_id
 231
 232        return self._destination_id
 233
 234    @property
 235    def destination(self) -> CloudDestination:
 236        """Get the destination object."""
 237        if self._cloud_destination_object:
 238            return self._cloud_destination_object
 239
 240        self._cloud_destination_object = CloudDestination(
 241            workspace=self.workspace,
 242            connector_id=self.destination_id,
 243        )
 244        return self._cloud_destination_object
 245
 246    @property
 247    def stream_names(self) -> list[str]:
 248        """The stream names."""
 249        if not self._connection_info:
 250            self._connection_info = self._fetch_connection_info()
 251
 252        return [stream.name for stream in self._connection_info.configurations.streams or []]
 253
 254    @property
 255    def table_prefix(self) -> str:
 256        """The table prefix."""
 257        if not self._connection_info:
 258            self._connection_info = self._fetch_connection_info()
 259
 260        return self._connection_info.prefix or ""
 261
 262    @property
 263    def connection_url(self) -> str | None:
 264        """The web URL to the connection."""
 265        return f"{self.workspace.workspace_url}/connections/{self.connection_id}"
 266
 267    @property
 268    def job_history_url(self) -> str | None:
 269        """The URL to the job history for the connection."""
 270        return f"{self.connection_url}/timeline"
 271
 272    # Run Sync
 273
 274    def run_sync(
 275        self,
 276        *,
 277        wait: bool = True,
 278        wait_timeout: int = 300,
 279    ) -> SyncResult:
 280        """Run a sync."""
 281        connection_response = api_util.run_connection(
 282            connection_id=self.connection_id,
 283            api_root=self.workspace.api_root,
 284            workspace_id=self.workspace.workspace_id,
 285            client_id=self.workspace.client_id,
 286            client_secret=self.workspace.client_secret,
 287            bearer_token=self.workspace.bearer_token,
 288        )
 289        sync_result = SyncResult(
 290            workspace=self.workspace,
 291            connection=self,
 292            job_id=connection_response.job_id,
 293        )
 294
 295        if wait:
 296            sync_result.wait_for_completion(
 297                wait_timeout=wait_timeout,
 298                raise_failure=True,
 299                raise_timeout=True,
 300            )
 301
 302        return sync_result
 303
 304    def _get_latest_cancellable_sync_job_id(self) -> int:
 305        """Get the latest cancellable sync job ID."""
 306        sync_results = self.get_previous_sync_logs(
 307            limit=1,
 308            job_type=JobTypeEnum.SYNC,
 309        )
 310        sync_result = sync_results[0] if sync_results else None
 311        if sync_result is None:
 312            raise PyAirbyteInputError(
 313                message="No sync jobs found for this connection.",
 314            )
 315        if sync_result.is_job_complete():
 316            raise PyAirbyteInputError(
 317                message=(
 318                    f"The latest sync job is already finished with status "
 319                    f"'{sync_result.get_job_status().value}'. "
 320                    "Pass an explicit job_id to target a different job."
 321                ),
 322            )
 323        return sync_result.job_id
 324
 325    def _validated_cancellable_job_id(self, job_id: int) -> int:
 326        """Validate an explicit cancellable job ID."""
 327        job_info = api_util.get_job_info(
 328            job_id=job_id,
 329            api_root=self.workspace.api_root,
 330            client_id=self.workspace.client_id,
 331            client_secret=self.workspace.client_secret,
 332            bearer_token=self.workspace.bearer_token,
 333        )
 334        if job_info.connection_id != self.connection_id:
 335            raise PyAirbyteInputError(
 336                message=(
 337                    f"Job {job_id} belongs to connection '{job_info.connection_id}', "
 338                    f"not '{self.connection_id}'."
 339                ),
 340            )
 341        job_status = CloudJobInfo.from_api_response(job_info).status
 342        if job_status in FINAL_STATUSES:
 343            raise PyAirbyteInputError(
 344                message=f"Job {job_id} is already finished with status " f"'{job_status.value}'.",
 345            )
 346        return job_id
 347
 348    def cancel_sync(self, job_id: int | None = None) -> SyncResult:
 349        """Cancel a running sync job.
 350
 351        Defaults to the connection's most recent sync job. Other job types must be
 352        targeted with an explicit `job_id`.
 353        """
 354        target_job_id: int = (
 355            self._get_latest_cancellable_sync_job_id()
 356            if job_id is None
 357            else self._validated_cancellable_job_id(job_id)
 358        )
 359
 360        job_response = api_util.cancel_job(
 361            job_id=target_job_id,
 362            api_root=self.workspace.api_root,
 363            client_id=self.workspace.client_id,
 364            client_secret=self.workspace.client_secret,
 365            bearer_token=self.workspace.bearer_token,
 366        )
 367        return SyncResult(
 368            workspace=self.workspace,
 369            connection=self,
 370            job_id=job_response.job_id,
 371            _latest_job_info=CloudJobInfo.from_api_response(job_response),
 372        )
 373
 374    def __repr__(self) -> str:
 375        """String representation of the connection."""
 376        return (
 377            f"CloudConnection(connection_id={self.connection_id}, source_id={self.source_id}, "
 378            f"destination_id={self.destination_id}, connection_url={self.connection_url})"
 379        )
 380
 381    # Logs
 382
 383    def get_previous_sync_logs(
 384        self,
 385        *,
 386        limit: int = 20,
 387        offset: int | None = None,
 388        from_tail: bool = True,
 389        job_type: str | JobTypeEnum | None = None,
 390    ) -> list[SyncResult]:
 391        """Get previous sync jobs for a connection with pagination support.
 392
 393        Returns SyncResult objects containing job metadata (job_id, status, bytes_synced,
 394        rows_synced, start_time). Full log text can be fetched lazily via
 395        `SyncResult.get_full_log_text()`.
 396
 397        Args:
 398            limit: Maximum number of jobs to return. Defaults to 20.
 399            offset: Number of jobs to skip from the beginning. Defaults to None (0).
 400            from_tail: If True, returns jobs ordered newest-first (createdAt DESC).
 401                If False, returns jobs ordered oldest-first (createdAt ASC).
 402                Defaults to True.
 403            job_type: Filter by job type (e.g., `sync`, `refresh`).
 404                If not specified, defaults to sync and reset jobs only (API default behavior).
 405
 406        Returns:
 407            A list of SyncResult objects representing the sync jobs.
 408        """
 409        order_by = (
 410            api_util.JOB_ORDER_BY_CREATED_AT_DESC
 411            if from_tail
 412            else api_util.JOB_ORDER_BY_CREATED_AT_ASC
 413        )
 414        sync_logs = api_util.get_job_logs(
 415            connection_id=self.connection_id,
 416            api_root=self.workspace.api_root,
 417            workspace_id=self.workspace.workspace_id,
 418            limit=limit,
 419            offset=offset,
 420            order_by=order_by,
 421            job_type=job_type,
 422            client_id=self.workspace.client_id,
 423            client_secret=self.workspace.client_secret,
 424            bearer_token=self.workspace.bearer_token,
 425        )
 426        return [
 427            SyncResult(
 428                workspace=self.workspace,
 429                connection=self,
 430                job_id=sync_log.job_id,
 431                _latest_job_info=CloudJobInfo.from_api_response(sync_log),
 432            )
 433            for sync_log in sync_logs
 434        ]
 435
 436    def get_sync_result(
 437        self,
 438        job_id: int | None = None,
 439    ) -> SyncResult | None:
 440        """Get the sync result for the connection.
 441
 442        If `job_id` is not provided, the most recent sync job will be used.
 443
 444        Returns `None` if job_id is omitted and no previous jobs are found.
 445        """
 446        if job_id is None:
 447            # Get the most recent sync job
 448            results = self.get_previous_sync_logs(
 449                limit=1,
 450            )
 451            if results:
 452                return results[0]
 453
 454            return None
 455
 456        # Get the sync job by ID (lazy loaded)
 457        return SyncResult(
 458            workspace=self.workspace,
 459            connection=self,
 460            job_id=job_id,
 461        )
 462
 463    # Artifacts
 464
 465    @deprecated("Use 'dump_raw_state()' instead.")
 466    def get_state_artifacts(self) -> list[dict[str, Any]] | None:
 467        """Deprecated. Use `dump_raw_state()` instead."""
 468        state_response = api_util.get_connection_state(
 469            connection_id=self.connection_id,
 470            api_root=self.workspace.api_root,
 471            client_id=self.workspace.client_id,
 472            client_secret=self.workspace.client_secret,
 473            bearer_token=self.workspace.bearer_token,
 474            config_api_root=self.workspace.config_api_root,
 475        )
 476        if state_response.get("stateType") == "not_set":
 477            return None
 478        return state_response.get("streamState", [])
 479
 480    @overload
 481    def dump_raw_state(self, *, normalize: Literal[True] = True) -> list[dict[str, Any]]: ...
 482
 483    @overload
 484    def dump_raw_state(self, *, normalize: Literal[False]) -> dict[str, Any]: ...
 485
 486    def dump_raw_state(
 487        self,
 488        *,
 489        normalize: bool = True,
 490    ) -> dict[str, Any] | list[dict[str, Any]]:
 491        """Dump the state for this connection.
 492
 493        By default, returns a list of Airbyte protocol `AirbyteStateMessage` dicts
 494        with snake_case keys, suitable for passing to a connector's `--state` flag.
 495
 496        When `normalize` is `False`, returns the raw Config API dict (camelCase keys,
 497        includes `stateType` and `connectionId`). This raw format can be passed
 498        directly to `import_raw_state()` for backup/restore workflows.
 499
 500        Args:
 501            normalize: If `True` (default), convert to Airbyte protocol format.
 502                If `False`, return the raw Config API response.
 503
 504        Returns:
 505            Normalized: list of protocol-format state message dicts (empty list if
 506            no state). Raw: the full Config API state dict.
 507        """
 508        raw = api_util.get_connection_state(
 509            connection_id=self.connection_id,
 510            api_root=self.workspace.api_root,
 511            client_id=self.workspace.client_id,
 512            client_secret=self.workspace.client_secret,
 513            bearer_token=self.workspace.bearer_token,
 514            config_api_root=self.workspace.config_api_root,
 515        )
 516        if normalize:
 517            return _normalize_state_to_protocol(raw)
 518        return raw
 519
 520    def import_raw_state(
 521        self,
 522        connection_state: dict[str, Any] | list[dict[str, Any]],
 523    ) -> dict[str, Any]:
 524        """Import (restore) the full state for this connection.
 525
 526        > ⚠️ **WARNING:** Modifying the state directly is not recommended and
 527        > could result in broken connections, and/or incorrect sync behavior.
 528
 529        Replaces the entire connection state with the provided state blob.
 530        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
 531
 532        This is the counterpart to `dump_raw_state()` for backup/restore workflows.
 533        The `connectionId` in the blob is always overridden with this connection's
 534        ID, making state blobs portable across connections.
 535
 536        Accepts either format:
 537
 538        - **Config API format** (dict with `stateType`): passed through directly.
 539        - **Airbyte protocol format** (list of `AirbyteStateMessage` dicts): automatically
 540          converted to Config API format before sending.
 541
 542        Args:
 543            connection_state: Connection state in either Config API or Airbyte protocol format.
 544
 545        Returns:
 546            The updated connection state as a dictionary.
 547
 548        Raises:
 549            AirbyteConnectionSyncActiveError: If a sync is currently running on this
 550                connection (HTTP 423). Wait for the sync to complete before retrying.
 551        """
 552        api_state: dict[str, Any]
 553        if isinstance(connection_state, list):
 554            if not _is_protocol_state_format(connection_state):
 555                msg = (
 556                    "Expected connection_state list to contain Airbyte protocol state "
 557                    "message dicts (each with a top-level `type` of STREAM, GLOBAL, "
 558                    "or LEGACY). Got a list that does not match protocol format."
 559                )
 560                raise ValueError(msg)
 561            api_state = _denormalize_protocol_state_to_api(
 562                protocol_messages=connection_state,
 563                connection_id=self.connection_id,
 564            )
 565        elif isinstance(connection_state, dict):
 566            if _is_protocol_state_format(connection_state):
 567                api_state = _denormalize_protocol_state_to_api(
 568                    protocol_messages=[connection_state],
 569                    connection_id=self.connection_id,
 570                )
 571            else:
 572                api_state = connection_state
 573        else:
 574            msg = f"Expected a dict or list, got {type(connection_state)}"
 575            raise TypeError(msg)
 576
 577        return api_util.replace_connection_state(
 578            connection_id=self.connection_id,
 579            connection_state_dict=api_state,
 580            api_root=self.workspace.api_root,
 581            client_id=self.workspace.client_id,
 582            client_secret=self.workspace.client_secret,
 583            bearer_token=self.workspace.bearer_token,
 584            config_api_root=self.workspace.config_api_root,
 585        )
 586
 587    def get_stream_state(
 588        self,
 589        stream_name: str,
 590        stream_namespace: str | None = None,
 591    ) -> dict[str, Any] | None:
 592        """Get the state blob for a single stream within this connection.
 593
 594        Returns just the stream's state dictionary (e.g., {"cursor": "2024-01-01"}),
 595        not the full connection state envelope.
 596
 597        This is compatible with `stream`-type state and stream-level entries
 598        within a `global`-type state. It is not compatible with `legacy` state.
 599        To get or set the entire connection-level state artifact, use
 600        `dump_raw_state` and `import_raw_state` instead.
 601
 602        Args:
 603            stream_name: The name of the stream to get state for.
 604            stream_namespace: The source-side stream namespace. This refers to the
 605                namespace from the source (e.g., database schema), not any destination
 606                namespace override set in connection advanced settings.
 607
 608        Returns:
 609            The stream's state blob as a dictionary, or None if the stream is not found.
 610        """
 611        state_data = self.dump_raw_state(normalize=False)
 612        result = ConnectionStateResponse(**state_data)
 613
 614        streams = _get_stream_list(result)
 615        matching = [s for s in streams if _match_stream(s, stream_name, stream_namespace)]
 616
 617        if not matching:
 618            available = [s.stream_descriptor.name for s in streams]
 619            logger.warning(
 620                "Stream '%s' not found in connection state for connection '%s'. "
 621                "Available streams: %s",
 622                stream_name,
 623                self.connection_id,
 624                available,
 625            )
 626            return None
 627
 628        return matching[0].stream_state
 629
 630    def set_stream_state(
 631        self,
 632        stream_name: str,
 633        state_blob_dict: dict[str, Any],
 634        stream_namespace: str | None = None,
 635    ) -> None:
 636        """Set the state for a single stream within this connection.
 637
 638        Fetches the current full state, replaces only the specified stream's state,
 639        then sends the full updated state back to the API. If the stream does not
 640        exist in the current state, it is appended.
 641
 642        This is compatible with `stream`-type state and stream-level entries
 643        within a `global`-type state. It is not compatible with `legacy` state.
 644        To get or set the entire connection-level state artifact, use
 645        `dump_raw_state` and `import_raw_state` instead.
 646
 647        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
 648
 649        Args:
 650            stream_name: The name of the stream to update state for.
 651            state_blob_dict: The state blob dict for this stream (e.g., {"cursor": "2024-01-01"}).
 652            stream_namespace: The source-side stream namespace. This refers to the
 653                namespace from the source (e.g., database schema), not any destination
 654                namespace override set in connection advanced settings.
 655
 656        Raises:
 657            PyAirbyteInputError: If the connection state type is not supported for
 658                stream-level operations (not_set, legacy).
 659            AirbyteConnectionSyncActiveError: If a sync is currently running on this
 660                connection (HTTP 423). Wait for the sync to complete before retrying.
 661        """
 662        state_data = self.dump_raw_state(normalize=False)
 663        current = ConnectionStateResponse(**state_data)
 664
 665        if current.state_type == "not_set":
 666            raise PyAirbyteInputError(
 667                message="Cannot set stream state: connection has no existing state.",
 668                context={"connection_id": self.connection_id},
 669            )
 670
 671        if current.state_type == "legacy":
 672            raise PyAirbyteInputError(
 673                message="Cannot set stream state on a legacy-type connection state.",
 674                context={"connection_id": self.connection_id},
 675            )
 676
 677        new_stream_entry = {
 678            "streamDescriptor": {
 679                "name": stream_name,
 680                **(
 681                    {
 682                        "namespace": stream_namespace,
 683                    }
 684                    if stream_namespace
 685                    else {}
 686                ),
 687            },
 688            "streamState": state_blob_dict,
 689        }
 690
 691        raw_streams: list[dict[str, Any]]
 692        if current.state_type == "stream":
 693            raw_streams = state_data.get("streamState", [])
 694        elif current.state_type == "global":
 695            raw_streams = state_data.get("globalState", {}).get("streamStates", [])
 696        else:
 697            raw_streams = []
 698
 699        streams = _get_stream_list(current)
 700        found = False
 701        updated_streams_raw: list[dict[str, Any]] = []
 702        for raw_s, parsed_s in zip(raw_streams, streams, strict=False):
 703            if _match_stream(parsed_s, stream_name, stream_namespace):
 704                updated_streams_raw.append(new_stream_entry)
 705                found = True
 706            else:
 707                updated_streams_raw.append(raw_s)
 708
 709        if not found:
 710            updated_streams_raw.append(new_stream_entry)
 711
 712        full_state: dict[str, Any] = {
 713            **state_data,
 714        }
 715
 716        if current.state_type == "stream":
 717            full_state["streamState"] = updated_streams_raw
 718        elif current.state_type == "global":
 719            original_global = state_data.get("globalState", {})
 720            full_state["globalState"] = {
 721                **original_global,
 722                "streamStates": updated_streams_raw,
 723            }
 724
 725        self.import_raw_state(full_state)
 726
 727    @deprecated("Use 'dump_raw_catalog()' instead.")
 728    def get_catalog_artifact(self) -> dict[str, Any] | None:
 729        """Get the configured catalog for this connection.
 730
 731        Returns the full configured catalog (syncCatalog) for this connection,
 732        including stream schemas, sync modes, cursor fields, and primary keys.
 733
 734        Uses the Config API endpoint: POST /v1/web_backend/connections/get
 735
 736        Returns:
 737            Dictionary containing the configured catalog, or `None` if not found.
 738        """
 739        return self.dump_raw_catalog()
 740
 741    def dump_raw_catalog(
 742        self,
 743        *,
 744        normalize: bool = True,
 745    ) -> dict[str, Any] | None:
 746        """Dump the configured catalog for this connection.
 747
 748        By default, returns the catalog in Airbyte protocol format
 749        (`ConfiguredAirbyteCatalog` with snake_case keys), suitable for passing
 750        to a connector's `--catalog` flag.
 751
 752        When `normalize` is `False`, returns the raw `syncCatalog` dict from the
 753        Config API (camelCase keys, nested `config` block). This raw format can be
 754        passed directly to `import_raw_catalog()` for backup/restore workflows.
 755
 756        Args:
 757            normalize: If `True` (default), convert to Airbyte protocol format.
 758                If `False`, return the raw Config API catalog.
 759
 760        Returns:
 761            The configured catalog dict, or `None` if not found.
 762        """
 763        connection_response = api_util.get_connection_catalog(
 764            connection_id=self.connection_id,
 765            api_root=self.workspace.api_root,
 766            client_id=self.workspace.client_id,
 767            client_secret=self.workspace.client_secret,
 768            bearer_token=self.workspace.bearer_token,
 769            config_api_root=self.workspace.config_api_root,
 770        )
 771        raw = connection_response.get("syncCatalog")
 772        if raw is None:
 773            return None
 774        if normalize:
 775            return _normalize_catalog_to_protocol(raw)
 776        return raw
 777
 778    def import_raw_catalog(self, catalog: dict[str, Any]) -> None:
 779        """Replace the configured catalog for this connection.
 780
 781        > ⚠️ **WARNING:** Modifying the catalog directly is not recommended and
 782        > could result in broken connections, and/or incorrect sync behavior.
 783
 784        Accepts a configured catalog dict and replaces the connection's entire
 785        catalog with it. All other connection settings remain unchanged.
 786
 787        Accepts either format:
 788
 789        - **Config API format** (`syncCatalog` with camelCase keys and nested `config`):
 790          passed through directly.
 791        - **Airbyte protocol format** (`ConfiguredAirbyteCatalog` with snake_case keys):
 792          automatically converted to Config API format before sending.
 793
 794        Args:
 795            catalog: The configured catalog dict in either format.
 796        """
 797        if _is_protocol_catalog_format(catalog):
 798            catalog = _denormalize_catalog_to_api(catalog)
 799
 800        api_util.replace_connection_catalog(
 801            connection_id=self.connection_id,
 802            configured_catalog_dict=catalog,
 803            api_root=self.workspace.api_root,
 804            client_id=self.workspace.client_id,
 805            client_secret=self.workspace.client_secret,
 806            bearer_token=self.workspace.bearer_token,
 807            config_api_root=self.workspace.config_api_root,
 808        )
 809
 810    def rename(self, name: str) -> CloudConnection:
 811        """Rename the connection.
 812
 813        Args:
 814            name: New name for the connection
 815
 816        Returns:
 817            Updated CloudConnection object with refreshed info
 818        """
 819        updated_response = api_util.patch_connection(
 820            connection_id=self.connection_id,
 821            api_root=self.workspace.api_root,
 822            client_id=self.workspace.client_id,
 823            client_secret=self.workspace.client_secret,
 824            bearer_token=self.workspace.bearer_token,
 825            name=name,
 826        )
 827        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 828        return self
 829
 830    def set_table_prefix(self, prefix: str) -> CloudConnection:
 831        """Set the table prefix for the connection.
 832
 833        Args:
 834            prefix: New table prefix to use when syncing to the destination
 835
 836        Returns:
 837            Updated CloudConnection object with refreshed info
 838        """
 839        updated_response = api_util.patch_connection(
 840            connection_id=self.connection_id,
 841            api_root=self.workspace.api_root,
 842            client_id=self.workspace.client_id,
 843            client_secret=self.workspace.client_secret,
 844            bearer_token=self.workspace.bearer_token,
 845            prefix=prefix,
 846        )
 847        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 848        return self
 849
 850    def set_selected_streams(self, stream_names: list[str]) -> CloudConnection:
 851        """Set the selected streams for the connection.
 852
 853        This is a destructive operation that can break existing connections if the
 854        stream selection is changed incorrectly. Use with caution.
 855
 856        Args:
 857            stream_names: List of stream names to sync
 858
 859        Returns:
 860            Updated CloudConnection object with refreshed info
 861        """
 862        configurations = api_util.build_stream_configurations(stream_names)
 863
 864        updated_response = api_util.patch_connection(
 865            connection_id=self.connection_id,
 866            api_root=self.workspace.api_root,
 867            client_id=self.workspace.client_id,
 868            client_secret=self.workspace.client_secret,
 869            bearer_token=self.workspace.bearer_token,
 870            configurations=configurations,
 871        )
 872        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 873        return self
 874
 875    # Enable/Disable
 876
 877    @property
 878    def enabled(self) -> bool:
 879        """Get the current enabled status of the connection.
 880
 881        This property always fetches fresh data from the API to ensure accuracy,
 882        as another process or user may have toggled the setting.
 883
 884        Returns:
 885            True if the connection status is 'active', False otherwise.
 886        """
 887        connection_info = self._fetch_connection_info(force_refresh=True)
 888        return connection_info.status == "active"
 889
 890    @enabled.setter
 891    def enabled(self, value: bool) -> None:
 892        """Set the enabled status of the connection.
 893
 894        Args:
 895            value: True to enable (set status to 'active'), False to disable
 896                (set status to 'inactive').
 897        """
 898        self.set_enabled(enabled=value)
 899
 900    def set_enabled(
 901        self,
 902        *,
 903        enabled: bool,
 904        ignore_noop: bool = True,
 905    ) -> None:
 906        """Set the enabled status of the connection.
 907
 908        Args:
 909            enabled: True to enable (set status to 'active'), False to disable
 910                (set status to 'inactive').
 911            ignore_noop: If True (default), silently return if the connection is already
 912                in the requested state. If False, raise ValueError when the requested
 913                state matches the current state.
 914
 915        Raises:
 916            ValueError: If ignore_noop is False and the connection is already in the
 917                requested state.
 918        """
 919        # Always fetch fresh data to check current status
 920        connection_info = self._fetch_connection_info(force_refresh=True)
 921        current_status = connection_info.status
 922        desired_status = "active" if enabled else "inactive"
 923
 924        if current_status == desired_status:
 925            if ignore_noop:
 926                return
 927            raise ValueError(
 928                f"Connection is already {'enabled' if enabled else 'disabled'}. "
 929                f"Current status: {current_status}"
 930            )
 931
 932        updated_response = api_util.patch_connection(
 933            connection_id=self.connection_id,
 934            api_root=self.workspace.api_root,
 935            client_id=self.workspace.client_id,
 936            client_secret=self.workspace.client_secret,
 937            bearer_token=self.workspace.bearer_token,
 938            status=desired_status,
 939        )
 940        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 941
 942    # Scheduling
 943
 944    def set_schedule(
 945        self,
 946        cron_expression: str,
 947    ) -> None:
 948        """Set a cron schedule for the connection.
 949
 950        Args:
 951            cron_expression: A cron expression defining when syncs should run.
 952
 953        Examples:
 954            - "0 0 * * *"  # Daily at midnight UTC
 955            - "0 */6 * * *"  # Every 6 hours
 956            - "0 0 * * 0"  # Weekly on Sunday at midnight UTC
 957        """
 958        updated_response = api_util.patch_connection(
 959            connection_id=self.connection_id,
 960            api_root=self.workspace.api_root,
 961            client_id=self.workspace.client_id,
 962            client_secret=self.workspace.client_secret,
 963            bearer_token=self.workspace.bearer_token,
 964            schedule=api_util.build_connection_schedule(
 965                schedule_type="cron",
 966                cron_expression=cron_expression,
 967            ),
 968        )
 969        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 970
 971    def set_manual_schedule(self) -> None:
 972        """Set the connection to manual scheduling.
 973
 974        Disables automatic syncs. Syncs will only run when manually triggered.
 975        """
 976        updated_response = api_util.patch_connection(
 977            connection_id=self.connection_id,
 978            api_root=self.workspace.api_root,
 979            client_id=self.workspace.client_id,
 980            client_secret=self.workspace.client_secret,
 981            bearer_token=self.workspace.bearer_token,
 982            schedule=api_util.build_connection_schedule(schedule_type="manual"),
 983        )
 984        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 985
 986    # Deletions
 987
 988    def permanently_delete(
 989        self,
 990        *,
 991        cascade_delete_source: bool = False,
 992        cascade_delete_destination: bool = False,
 993    ) -> None:
 994        """Delete the connection.
 995
 996        Args:
 997            cascade_delete_source: Whether to also delete the source.
 998            cascade_delete_destination: Whether to also delete the destination.
 999        """
1000        self.workspace.permanently_delete_connection(self)
1001
1002        if cascade_delete_source:
1003            self.workspace.permanently_delete_source(self.source_id)
1004
1005        if cascade_delete_destination:
1006            self.workspace.permanently_delete_destination(self.destination_id)
logger = <Logger airbyte.cloud.connections (INFO)>
class CloudConnection:
  45class CloudConnection:  # noqa: PLR0904  # Too many public methods
  46    """A connection is an extract-load (EL) pairing of a source and destination in Airbyte Cloud.
  47
  48    You can use a connection object to run sync jobs, retrieve logs, and manage the connection.
  49    """
  50
  51    def __init__(
  52        self,
  53        workspace: CloudWorkspace,
  54        connection_id: str,
  55        source: str | None = None,
  56        destination: str | None = None,
  57    ) -> None:
  58        """It is not recommended to create a `CloudConnection` object directly.
  59
  60        Instead, use `CloudWorkspace.get_connection()` to create a connection object.
  61        """
  62        self.connection_id = connection_id
  63        """The ID of the connection."""
  64
  65        self.workspace = workspace
  66        """The workspace that the connection belongs to."""
  67
  68        self._source_id = source
  69        """The ID of the source."""
  70
  71        self._destination_id = destination
  72        """The ID of the destination."""
  73
  74        self._connection_info: CloudConnectionInfo | None = None
  75        """The connection info object. (Cached.)"""
  76
  77        self._cloud_source_object: CloudSource | None = None
  78        """The source object. (Cached.)"""
  79
  80        self._cloud_destination_object: CloudDestination | None = None
  81        """The destination object. (Cached.)"""
  82
  83    def _fetch_connection_info(
  84        self,
  85        *,
  86        force_refresh: bool = False,
  87        verify: bool = True,
  88    ) -> CloudConnectionInfo:
  89        """Fetch and cache connection info from the API.
  90
  91        By default, this method will only fetch from the API if connection info is not
  92        already cached. It also verifies that the connection belongs to the expected
  93        workspace unless verification is explicitly disabled.
  94
  95        Args:
  96            force_refresh: If True, always fetch from the API even if cached.
  97                If False (default), only fetch if not already cached.
  98            verify: If True (default), verify that the connection is valid (e.g., that
  99                the workspace_id matches this object's workspace). Raises an error if
 100                validation fails.
 101
 102        Returns:
 103            Information about the connection from the API.
 104
 105        Raises:
 106            AirbyteWorkspaceMismatchError: If verify is True and the connection's
 107                workspace_id doesn't match the expected workspace.
 108            AirbyteMissingResourceError: If the connection doesn't exist.
 109        """
 110        if not force_refresh and self._connection_info is not None:
 111            # Use cached info, but still verify if requested
 112            if verify:
 113                self._verify_workspace_match(self._connection_info)
 114            return self._connection_info
 115
 116        # Fetch from API
 117        connection_info = api_util.get_connection(
 118            workspace_id=self.workspace.workspace_id,
 119            connection_id=self.connection_id,
 120            api_root=self.workspace.api_root,
 121            client_id=self.workspace.client_id,
 122            client_secret=self.workspace.client_secret,
 123            bearer_token=self.workspace.bearer_token,
 124        )
 125        result = CloudConnectionInfo.from_api_response(connection_info)
 126
 127        self._connection_info = result
 128
 129        # Verify if requested
 130        if verify:
 131            self._verify_workspace_match(result)
 132
 133        return result
 134
 135    def _verify_workspace_match(self, connection_info: CloudConnectionInfo) -> None:
 136        """Verify that the connection belongs to the expected workspace.
 137
 138        Raises:
 139            AirbyteWorkspaceMismatchError: If the workspace IDs don't match.
 140        """
 141        if connection_info.workspace_id != self.workspace.workspace_id:
 142            raise AirbyteWorkspaceMismatchError(
 143                resource_type="connection",
 144                resource_id=self.connection_id,
 145                workspace=self.workspace,
 146                expected_workspace_id=self.workspace.workspace_id,
 147                actual_workspace_id=connection_info.workspace_id,
 148                message=(
 149                    f"Connection '{self.connection_id}' belongs to workspace "
 150                    f"'{connection_info.workspace_id}', not '{self.workspace.workspace_id}'."
 151                ),
 152            )
 153
 154    def check_is_valid(self) -> bool:
 155        """Check if this connection exists and belongs to the expected workspace.
 156
 157        This method fetches connection info from the API (if not already cached) and
 158        verifies that the connection's workspace_id matches the workspace associated
 159        with this CloudConnection object.
 160
 161        Returns:
 162            True if the connection exists and belongs to the expected workspace.
 163
 164        Raises:
 165            AirbyteWorkspaceMismatchError: If the connection belongs to a different workspace.
 166            AirbyteMissingResourceError: If the connection doesn't exist.
 167        """
 168        self._fetch_connection_info(force_refresh=False, verify=True)
 169        return True
 170
 171    @classmethod
 172    def _from_connection_response(
 173        cls,
 174        workspace: CloudWorkspace,
 175        connection_response: _ConnectionResponseLike,
 176    ) -> CloudConnection:
 177        """Create a CloudConnection from an API connection response."""
 178        connection_info = CloudConnectionInfo.from_api_response(connection_response)
 179        result = cls(
 180            workspace=workspace,
 181            connection_id=connection_info.connection_id,
 182            source=connection_info.source_id,
 183            destination=connection_info.destination_id,
 184        )
 185        result._connection_info = connection_info  # noqa: SLF001 # Accessing Non-Public API
 186        return result
 187
 188    # Properties
 189
 190    @property
 191    def name(self) -> str | None:
 192        """Get the display name of the connection, if available.
 193
 194        E.g. "My Postgres to Snowflake", not the connection ID.
 195        """
 196        if not self._connection_info:
 197            self._connection_info = self._fetch_connection_info()
 198
 199        return self._connection_info.name
 200
 201    @property
 202    def source_id(self) -> str:
 203        """The ID of the source."""
 204        if not self._source_id:
 205            if not self._connection_info:
 206                self._connection_info = self._fetch_connection_info()
 207
 208            self._source_id = self._connection_info.source_id
 209
 210        return self._source_id
 211
 212    @property
 213    def source(self) -> CloudSource:
 214        """Get the source object."""
 215        if self._cloud_source_object:
 216            return self._cloud_source_object
 217
 218        self._cloud_source_object = CloudSource(
 219            workspace=self.workspace,
 220            connector_id=self.source_id,
 221        )
 222        return self._cloud_source_object
 223
 224    @property
 225    def destination_id(self) -> str:
 226        """The ID of the destination."""
 227        if not self._destination_id:
 228            if not self._connection_info:
 229                self._connection_info = self._fetch_connection_info()
 230
 231            self._destination_id = self._connection_info.destination_id
 232
 233        return self._destination_id
 234
 235    @property
 236    def destination(self) -> CloudDestination:
 237        """Get the destination object."""
 238        if self._cloud_destination_object:
 239            return self._cloud_destination_object
 240
 241        self._cloud_destination_object = CloudDestination(
 242            workspace=self.workspace,
 243            connector_id=self.destination_id,
 244        )
 245        return self._cloud_destination_object
 246
 247    @property
 248    def stream_names(self) -> list[str]:
 249        """The stream names."""
 250        if not self._connection_info:
 251            self._connection_info = self._fetch_connection_info()
 252
 253        return [stream.name for stream in self._connection_info.configurations.streams or []]
 254
 255    @property
 256    def table_prefix(self) -> str:
 257        """The table prefix."""
 258        if not self._connection_info:
 259            self._connection_info = self._fetch_connection_info()
 260
 261        return self._connection_info.prefix or ""
 262
 263    @property
 264    def connection_url(self) -> str | None:
 265        """The web URL to the connection."""
 266        return f"{self.workspace.workspace_url}/connections/{self.connection_id}"
 267
 268    @property
 269    def job_history_url(self) -> str | None:
 270        """The URL to the job history for the connection."""
 271        return f"{self.connection_url}/timeline"
 272
 273    # Run Sync
 274
 275    def run_sync(
 276        self,
 277        *,
 278        wait: bool = True,
 279        wait_timeout: int = 300,
 280    ) -> SyncResult:
 281        """Run a sync."""
 282        connection_response = api_util.run_connection(
 283            connection_id=self.connection_id,
 284            api_root=self.workspace.api_root,
 285            workspace_id=self.workspace.workspace_id,
 286            client_id=self.workspace.client_id,
 287            client_secret=self.workspace.client_secret,
 288            bearer_token=self.workspace.bearer_token,
 289        )
 290        sync_result = SyncResult(
 291            workspace=self.workspace,
 292            connection=self,
 293            job_id=connection_response.job_id,
 294        )
 295
 296        if wait:
 297            sync_result.wait_for_completion(
 298                wait_timeout=wait_timeout,
 299                raise_failure=True,
 300                raise_timeout=True,
 301            )
 302
 303        return sync_result
 304
 305    def _get_latest_cancellable_sync_job_id(self) -> int:
 306        """Get the latest cancellable sync job ID."""
 307        sync_results = self.get_previous_sync_logs(
 308            limit=1,
 309            job_type=JobTypeEnum.SYNC,
 310        )
 311        sync_result = sync_results[0] if sync_results else None
 312        if sync_result is None:
 313            raise PyAirbyteInputError(
 314                message="No sync jobs found for this connection.",
 315            )
 316        if sync_result.is_job_complete():
 317            raise PyAirbyteInputError(
 318                message=(
 319                    f"The latest sync job is already finished with status "
 320                    f"'{sync_result.get_job_status().value}'. "
 321                    "Pass an explicit job_id to target a different job."
 322                ),
 323            )
 324        return sync_result.job_id
 325
 326    def _validated_cancellable_job_id(self, job_id: int) -> int:
 327        """Validate an explicit cancellable job ID."""
 328        job_info = api_util.get_job_info(
 329            job_id=job_id,
 330            api_root=self.workspace.api_root,
 331            client_id=self.workspace.client_id,
 332            client_secret=self.workspace.client_secret,
 333            bearer_token=self.workspace.bearer_token,
 334        )
 335        if job_info.connection_id != self.connection_id:
 336            raise PyAirbyteInputError(
 337                message=(
 338                    f"Job {job_id} belongs to connection '{job_info.connection_id}', "
 339                    f"not '{self.connection_id}'."
 340                ),
 341            )
 342        job_status = CloudJobInfo.from_api_response(job_info).status
 343        if job_status in FINAL_STATUSES:
 344            raise PyAirbyteInputError(
 345                message=f"Job {job_id} is already finished with status " f"'{job_status.value}'.",
 346            )
 347        return job_id
 348
 349    def cancel_sync(self, job_id: int | None = None) -> SyncResult:
 350        """Cancel a running sync job.
 351
 352        Defaults to the connection's most recent sync job. Other job types must be
 353        targeted with an explicit `job_id`.
 354        """
 355        target_job_id: int = (
 356            self._get_latest_cancellable_sync_job_id()
 357            if job_id is None
 358            else self._validated_cancellable_job_id(job_id)
 359        )
 360
 361        job_response = api_util.cancel_job(
 362            job_id=target_job_id,
 363            api_root=self.workspace.api_root,
 364            client_id=self.workspace.client_id,
 365            client_secret=self.workspace.client_secret,
 366            bearer_token=self.workspace.bearer_token,
 367        )
 368        return SyncResult(
 369            workspace=self.workspace,
 370            connection=self,
 371            job_id=job_response.job_id,
 372            _latest_job_info=CloudJobInfo.from_api_response(job_response),
 373        )
 374
 375    def __repr__(self) -> str:
 376        """String representation of the connection."""
 377        return (
 378            f"CloudConnection(connection_id={self.connection_id}, source_id={self.source_id}, "
 379            f"destination_id={self.destination_id}, connection_url={self.connection_url})"
 380        )
 381
 382    # Logs
 383
 384    def get_previous_sync_logs(
 385        self,
 386        *,
 387        limit: int = 20,
 388        offset: int | None = None,
 389        from_tail: bool = True,
 390        job_type: str | JobTypeEnum | None = None,
 391    ) -> list[SyncResult]:
 392        """Get previous sync jobs for a connection with pagination support.
 393
 394        Returns SyncResult objects containing job metadata (job_id, status, bytes_synced,
 395        rows_synced, start_time). Full log text can be fetched lazily via
 396        `SyncResult.get_full_log_text()`.
 397
 398        Args:
 399            limit: Maximum number of jobs to return. Defaults to 20.
 400            offset: Number of jobs to skip from the beginning. Defaults to None (0).
 401            from_tail: If True, returns jobs ordered newest-first (createdAt DESC).
 402                If False, returns jobs ordered oldest-first (createdAt ASC).
 403                Defaults to True.
 404            job_type: Filter by job type (e.g., `sync`, `refresh`).
 405                If not specified, defaults to sync and reset jobs only (API default behavior).
 406
 407        Returns:
 408            A list of SyncResult objects representing the sync jobs.
 409        """
 410        order_by = (
 411            api_util.JOB_ORDER_BY_CREATED_AT_DESC
 412            if from_tail
 413            else api_util.JOB_ORDER_BY_CREATED_AT_ASC
 414        )
 415        sync_logs = api_util.get_job_logs(
 416            connection_id=self.connection_id,
 417            api_root=self.workspace.api_root,
 418            workspace_id=self.workspace.workspace_id,
 419            limit=limit,
 420            offset=offset,
 421            order_by=order_by,
 422            job_type=job_type,
 423            client_id=self.workspace.client_id,
 424            client_secret=self.workspace.client_secret,
 425            bearer_token=self.workspace.bearer_token,
 426        )
 427        return [
 428            SyncResult(
 429                workspace=self.workspace,
 430                connection=self,
 431                job_id=sync_log.job_id,
 432                _latest_job_info=CloudJobInfo.from_api_response(sync_log),
 433            )
 434            for sync_log in sync_logs
 435        ]
 436
 437    def get_sync_result(
 438        self,
 439        job_id: int | None = None,
 440    ) -> SyncResult | None:
 441        """Get the sync result for the connection.
 442
 443        If `job_id` is not provided, the most recent sync job will be used.
 444
 445        Returns `None` if job_id is omitted and no previous jobs are found.
 446        """
 447        if job_id is None:
 448            # Get the most recent sync job
 449            results = self.get_previous_sync_logs(
 450                limit=1,
 451            )
 452            if results:
 453                return results[0]
 454
 455            return None
 456
 457        # Get the sync job by ID (lazy loaded)
 458        return SyncResult(
 459            workspace=self.workspace,
 460            connection=self,
 461            job_id=job_id,
 462        )
 463
 464    # Artifacts
 465
 466    @deprecated("Use 'dump_raw_state()' instead.")
 467    def get_state_artifacts(self) -> list[dict[str, Any]] | None:
 468        """Deprecated. Use `dump_raw_state()` instead."""
 469        state_response = api_util.get_connection_state(
 470            connection_id=self.connection_id,
 471            api_root=self.workspace.api_root,
 472            client_id=self.workspace.client_id,
 473            client_secret=self.workspace.client_secret,
 474            bearer_token=self.workspace.bearer_token,
 475            config_api_root=self.workspace.config_api_root,
 476        )
 477        if state_response.get("stateType") == "not_set":
 478            return None
 479        return state_response.get("streamState", [])
 480
 481    @overload
 482    def dump_raw_state(self, *, normalize: Literal[True] = True) -> list[dict[str, Any]]: ...
 483
 484    @overload
 485    def dump_raw_state(self, *, normalize: Literal[False]) -> dict[str, Any]: ...
 486
 487    def dump_raw_state(
 488        self,
 489        *,
 490        normalize: bool = True,
 491    ) -> dict[str, Any] | list[dict[str, Any]]:
 492        """Dump the state for this connection.
 493
 494        By default, returns a list of Airbyte protocol `AirbyteStateMessage` dicts
 495        with snake_case keys, suitable for passing to a connector's `--state` flag.
 496
 497        When `normalize` is `False`, returns the raw Config API dict (camelCase keys,
 498        includes `stateType` and `connectionId`). This raw format can be passed
 499        directly to `import_raw_state()` for backup/restore workflows.
 500
 501        Args:
 502            normalize: If `True` (default), convert to Airbyte protocol format.
 503                If `False`, return the raw Config API response.
 504
 505        Returns:
 506            Normalized: list of protocol-format state message dicts (empty list if
 507            no state). Raw: the full Config API state dict.
 508        """
 509        raw = api_util.get_connection_state(
 510            connection_id=self.connection_id,
 511            api_root=self.workspace.api_root,
 512            client_id=self.workspace.client_id,
 513            client_secret=self.workspace.client_secret,
 514            bearer_token=self.workspace.bearer_token,
 515            config_api_root=self.workspace.config_api_root,
 516        )
 517        if normalize:
 518            return _normalize_state_to_protocol(raw)
 519        return raw
 520
 521    def import_raw_state(
 522        self,
 523        connection_state: dict[str, Any] | list[dict[str, Any]],
 524    ) -> dict[str, Any]:
 525        """Import (restore) the full state for this connection.
 526
 527        > ⚠️ **WARNING:** Modifying the state directly is not recommended and
 528        > could result in broken connections, and/or incorrect sync behavior.
 529
 530        Replaces the entire connection state with the provided state blob.
 531        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
 532
 533        This is the counterpart to `dump_raw_state()` for backup/restore workflows.
 534        The `connectionId` in the blob is always overridden with this connection's
 535        ID, making state blobs portable across connections.
 536
 537        Accepts either format:
 538
 539        - **Config API format** (dict with `stateType`): passed through directly.
 540        - **Airbyte protocol format** (list of `AirbyteStateMessage` dicts): automatically
 541          converted to Config API format before sending.
 542
 543        Args:
 544            connection_state: Connection state in either Config API or Airbyte protocol format.
 545
 546        Returns:
 547            The updated connection state as a dictionary.
 548
 549        Raises:
 550            AirbyteConnectionSyncActiveError: If a sync is currently running on this
 551                connection (HTTP 423). Wait for the sync to complete before retrying.
 552        """
 553        api_state: dict[str, Any]
 554        if isinstance(connection_state, list):
 555            if not _is_protocol_state_format(connection_state):
 556                msg = (
 557                    "Expected connection_state list to contain Airbyte protocol state "
 558                    "message dicts (each with a top-level `type` of STREAM, GLOBAL, "
 559                    "or LEGACY). Got a list that does not match protocol format."
 560                )
 561                raise ValueError(msg)
 562            api_state = _denormalize_protocol_state_to_api(
 563                protocol_messages=connection_state,
 564                connection_id=self.connection_id,
 565            )
 566        elif isinstance(connection_state, dict):
 567            if _is_protocol_state_format(connection_state):
 568                api_state = _denormalize_protocol_state_to_api(
 569                    protocol_messages=[connection_state],
 570                    connection_id=self.connection_id,
 571                )
 572            else:
 573                api_state = connection_state
 574        else:
 575            msg = f"Expected a dict or list, got {type(connection_state)}"
 576            raise TypeError(msg)
 577
 578        return api_util.replace_connection_state(
 579            connection_id=self.connection_id,
 580            connection_state_dict=api_state,
 581            api_root=self.workspace.api_root,
 582            client_id=self.workspace.client_id,
 583            client_secret=self.workspace.client_secret,
 584            bearer_token=self.workspace.bearer_token,
 585            config_api_root=self.workspace.config_api_root,
 586        )
 587
 588    def get_stream_state(
 589        self,
 590        stream_name: str,
 591        stream_namespace: str | None = None,
 592    ) -> dict[str, Any] | None:
 593        """Get the state blob for a single stream within this connection.
 594
 595        Returns just the stream's state dictionary (e.g., {"cursor": "2024-01-01"}),
 596        not the full connection state envelope.
 597
 598        This is compatible with `stream`-type state and stream-level entries
 599        within a `global`-type state. It is not compatible with `legacy` state.
 600        To get or set the entire connection-level state artifact, use
 601        `dump_raw_state` and `import_raw_state` instead.
 602
 603        Args:
 604            stream_name: The name of the stream to get state for.
 605            stream_namespace: The source-side stream namespace. This refers to the
 606                namespace from the source (e.g., database schema), not any destination
 607                namespace override set in connection advanced settings.
 608
 609        Returns:
 610            The stream's state blob as a dictionary, or None if the stream is not found.
 611        """
 612        state_data = self.dump_raw_state(normalize=False)
 613        result = ConnectionStateResponse(**state_data)
 614
 615        streams = _get_stream_list(result)
 616        matching = [s for s in streams if _match_stream(s, stream_name, stream_namespace)]
 617
 618        if not matching:
 619            available = [s.stream_descriptor.name for s in streams]
 620            logger.warning(
 621                "Stream '%s' not found in connection state for connection '%s'. "
 622                "Available streams: %s",
 623                stream_name,
 624                self.connection_id,
 625                available,
 626            )
 627            return None
 628
 629        return matching[0].stream_state
 630
 631    def set_stream_state(
 632        self,
 633        stream_name: str,
 634        state_blob_dict: dict[str, Any],
 635        stream_namespace: str | None = None,
 636    ) -> None:
 637        """Set the state for a single stream within this connection.
 638
 639        Fetches the current full state, replaces only the specified stream's state,
 640        then sends the full updated state back to the API. If the stream does not
 641        exist in the current state, it is appended.
 642
 643        This is compatible with `stream`-type state and stream-level entries
 644        within a `global`-type state. It is not compatible with `legacy` state.
 645        To get or set the entire connection-level state artifact, use
 646        `dump_raw_state` and `import_raw_state` instead.
 647
 648        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
 649
 650        Args:
 651            stream_name: The name of the stream to update state for.
 652            state_blob_dict: The state blob dict for this stream (e.g., {"cursor": "2024-01-01"}).
 653            stream_namespace: The source-side stream namespace. This refers to the
 654                namespace from the source (e.g., database schema), not any destination
 655                namespace override set in connection advanced settings.
 656
 657        Raises:
 658            PyAirbyteInputError: If the connection state type is not supported for
 659                stream-level operations (not_set, legacy).
 660            AirbyteConnectionSyncActiveError: If a sync is currently running on this
 661                connection (HTTP 423). Wait for the sync to complete before retrying.
 662        """
 663        state_data = self.dump_raw_state(normalize=False)
 664        current = ConnectionStateResponse(**state_data)
 665
 666        if current.state_type == "not_set":
 667            raise PyAirbyteInputError(
 668                message="Cannot set stream state: connection has no existing state.",
 669                context={"connection_id": self.connection_id},
 670            )
 671
 672        if current.state_type == "legacy":
 673            raise PyAirbyteInputError(
 674                message="Cannot set stream state on a legacy-type connection state.",
 675                context={"connection_id": self.connection_id},
 676            )
 677
 678        new_stream_entry = {
 679            "streamDescriptor": {
 680                "name": stream_name,
 681                **(
 682                    {
 683                        "namespace": stream_namespace,
 684                    }
 685                    if stream_namespace
 686                    else {}
 687                ),
 688            },
 689            "streamState": state_blob_dict,
 690        }
 691
 692        raw_streams: list[dict[str, Any]]
 693        if current.state_type == "stream":
 694            raw_streams = state_data.get("streamState", [])
 695        elif current.state_type == "global":
 696            raw_streams = state_data.get("globalState", {}).get("streamStates", [])
 697        else:
 698            raw_streams = []
 699
 700        streams = _get_stream_list(current)
 701        found = False
 702        updated_streams_raw: list[dict[str, Any]] = []
 703        for raw_s, parsed_s in zip(raw_streams, streams, strict=False):
 704            if _match_stream(parsed_s, stream_name, stream_namespace):
 705                updated_streams_raw.append(new_stream_entry)
 706                found = True
 707            else:
 708                updated_streams_raw.append(raw_s)
 709
 710        if not found:
 711            updated_streams_raw.append(new_stream_entry)
 712
 713        full_state: dict[str, Any] = {
 714            **state_data,
 715        }
 716
 717        if current.state_type == "stream":
 718            full_state["streamState"] = updated_streams_raw
 719        elif current.state_type == "global":
 720            original_global = state_data.get("globalState", {})
 721            full_state["globalState"] = {
 722                **original_global,
 723                "streamStates": updated_streams_raw,
 724            }
 725
 726        self.import_raw_state(full_state)
 727
 728    @deprecated("Use 'dump_raw_catalog()' instead.")
 729    def get_catalog_artifact(self) -> dict[str, Any] | None:
 730        """Get the configured catalog for this connection.
 731
 732        Returns the full configured catalog (syncCatalog) for this connection,
 733        including stream schemas, sync modes, cursor fields, and primary keys.
 734
 735        Uses the Config API endpoint: POST /v1/web_backend/connections/get
 736
 737        Returns:
 738            Dictionary containing the configured catalog, or `None` if not found.
 739        """
 740        return self.dump_raw_catalog()
 741
 742    def dump_raw_catalog(
 743        self,
 744        *,
 745        normalize: bool = True,
 746    ) -> dict[str, Any] | None:
 747        """Dump the configured catalog for this connection.
 748
 749        By default, returns the catalog in Airbyte protocol format
 750        (`ConfiguredAirbyteCatalog` with snake_case keys), suitable for passing
 751        to a connector's `--catalog` flag.
 752
 753        When `normalize` is `False`, returns the raw `syncCatalog` dict from the
 754        Config API (camelCase keys, nested `config` block). This raw format can be
 755        passed directly to `import_raw_catalog()` for backup/restore workflows.
 756
 757        Args:
 758            normalize: If `True` (default), convert to Airbyte protocol format.
 759                If `False`, return the raw Config API catalog.
 760
 761        Returns:
 762            The configured catalog dict, or `None` if not found.
 763        """
 764        connection_response = api_util.get_connection_catalog(
 765            connection_id=self.connection_id,
 766            api_root=self.workspace.api_root,
 767            client_id=self.workspace.client_id,
 768            client_secret=self.workspace.client_secret,
 769            bearer_token=self.workspace.bearer_token,
 770            config_api_root=self.workspace.config_api_root,
 771        )
 772        raw = connection_response.get("syncCatalog")
 773        if raw is None:
 774            return None
 775        if normalize:
 776            return _normalize_catalog_to_protocol(raw)
 777        return raw
 778
 779    def import_raw_catalog(self, catalog: dict[str, Any]) -> None:
 780        """Replace the configured catalog for this connection.
 781
 782        > ⚠️ **WARNING:** Modifying the catalog directly is not recommended and
 783        > could result in broken connections, and/or incorrect sync behavior.
 784
 785        Accepts a configured catalog dict and replaces the connection's entire
 786        catalog with it. All other connection settings remain unchanged.
 787
 788        Accepts either format:
 789
 790        - **Config API format** (`syncCatalog` with camelCase keys and nested `config`):
 791          passed through directly.
 792        - **Airbyte protocol format** (`ConfiguredAirbyteCatalog` with snake_case keys):
 793          automatically converted to Config API format before sending.
 794
 795        Args:
 796            catalog: The configured catalog dict in either format.
 797        """
 798        if _is_protocol_catalog_format(catalog):
 799            catalog = _denormalize_catalog_to_api(catalog)
 800
 801        api_util.replace_connection_catalog(
 802            connection_id=self.connection_id,
 803            configured_catalog_dict=catalog,
 804            api_root=self.workspace.api_root,
 805            client_id=self.workspace.client_id,
 806            client_secret=self.workspace.client_secret,
 807            bearer_token=self.workspace.bearer_token,
 808            config_api_root=self.workspace.config_api_root,
 809        )
 810
 811    def rename(self, name: str) -> CloudConnection:
 812        """Rename the connection.
 813
 814        Args:
 815            name: New name for the connection
 816
 817        Returns:
 818            Updated CloudConnection object with refreshed info
 819        """
 820        updated_response = api_util.patch_connection(
 821            connection_id=self.connection_id,
 822            api_root=self.workspace.api_root,
 823            client_id=self.workspace.client_id,
 824            client_secret=self.workspace.client_secret,
 825            bearer_token=self.workspace.bearer_token,
 826            name=name,
 827        )
 828        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 829        return self
 830
 831    def set_table_prefix(self, prefix: str) -> CloudConnection:
 832        """Set the table prefix for the connection.
 833
 834        Args:
 835            prefix: New table prefix to use when syncing to the destination
 836
 837        Returns:
 838            Updated CloudConnection object with refreshed info
 839        """
 840        updated_response = api_util.patch_connection(
 841            connection_id=self.connection_id,
 842            api_root=self.workspace.api_root,
 843            client_id=self.workspace.client_id,
 844            client_secret=self.workspace.client_secret,
 845            bearer_token=self.workspace.bearer_token,
 846            prefix=prefix,
 847        )
 848        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 849        return self
 850
 851    def set_selected_streams(self, stream_names: list[str]) -> CloudConnection:
 852        """Set the selected streams for the connection.
 853
 854        This is a destructive operation that can break existing connections if the
 855        stream selection is changed incorrectly. Use with caution.
 856
 857        Args:
 858            stream_names: List of stream names to sync
 859
 860        Returns:
 861            Updated CloudConnection object with refreshed info
 862        """
 863        configurations = api_util.build_stream_configurations(stream_names)
 864
 865        updated_response = api_util.patch_connection(
 866            connection_id=self.connection_id,
 867            api_root=self.workspace.api_root,
 868            client_id=self.workspace.client_id,
 869            client_secret=self.workspace.client_secret,
 870            bearer_token=self.workspace.bearer_token,
 871            configurations=configurations,
 872        )
 873        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 874        return self
 875
 876    # Enable/Disable
 877
 878    @property
 879    def enabled(self) -> bool:
 880        """Get the current enabled status of the connection.
 881
 882        This property always fetches fresh data from the API to ensure accuracy,
 883        as another process or user may have toggled the setting.
 884
 885        Returns:
 886            True if the connection status is 'active', False otherwise.
 887        """
 888        connection_info = self._fetch_connection_info(force_refresh=True)
 889        return connection_info.status == "active"
 890
 891    @enabled.setter
 892    def enabled(self, value: bool) -> None:
 893        """Set the enabled status of the connection.
 894
 895        Args:
 896            value: True to enable (set status to 'active'), False to disable
 897                (set status to 'inactive').
 898        """
 899        self.set_enabled(enabled=value)
 900
 901    def set_enabled(
 902        self,
 903        *,
 904        enabled: bool,
 905        ignore_noop: bool = True,
 906    ) -> None:
 907        """Set the enabled status of the connection.
 908
 909        Args:
 910            enabled: True to enable (set status to 'active'), False to disable
 911                (set status to 'inactive').
 912            ignore_noop: If True (default), silently return if the connection is already
 913                in the requested state. If False, raise ValueError when the requested
 914                state matches the current state.
 915
 916        Raises:
 917            ValueError: If ignore_noop is False and the connection is already in the
 918                requested state.
 919        """
 920        # Always fetch fresh data to check current status
 921        connection_info = self._fetch_connection_info(force_refresh=True)
 922        current_status = connection_info.status
 923        desired_status = "active" if enabled else "inactive"
 924
 925        if current_status == desired_status:
 926            if ignore_noop:
 927                return
 928            raise ValueError(
 929                f"Connection is already {'enabled' if enabled else 'disabled'}. "
 930                f"Current status: {current_status}"
 931            )
 932
 933        updated_response = api_util.patch_connection(
 934            connection_id=self.connection_id,
 935            api_root=self.workspace.api_root,
 936            client_id=self.workspace.client_id,
 937            client_secret=self.workspace.client_secret,
 938            bearer_token=self.workspace.bearer_token,
 939            status=desired_status,
 940        )
 941        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 942
 943    # Scheduling
 944
 945    def set_schedule(
 946        self,
 947        cron_expression: str,
 948    ) -> None:
 949        """Set a cron schedule for the connection.
 950
 951        Args:
 952            cron_expression: A cron expression defining when syncs should run.
 953
 954        Examples:
 955            - "0 0 * * *"  # Daily at midnight UTC
 956            - "0 */6 * * *"  # Every 6 hours
 957            - "0 0 * * 0"  # Weekly on Sunday at midnight UTC
 958        """
 959        updated_response = api_util.patch_connection(
 960            connection_id=self.connection_id,
 961            api_root=self.workspace.api_root,
 962            client_id=self.workspace.client_id,
 963            client_secret=self.workspace.client_secret,
 964            bearer_token=self.workspace.bearer_token,
 965            schedule=api_util.build_connection_schedule(
 966                schedule_type="cron",
 967                cron_expression=cron_expression,
 968            ),
 969        )
 970        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 971
 972    def set_manual_schedule(self) -> None:
 973        """Set the connection to manual scheduling.
 974
 975        Disables automatic syncs. Syncs will only run when manually triggered.
 976        """
 977        updated_response = api_util.patch_connection(
 978            connection_id=self.connection_id,
 979            api_root=self.workspace.api_root,
 980            client_id=self.workspace.client_id,
 981            client_secret=self.workspace.client_secret,
 982            bearer_token=self.workspace.bearer_token,
 983            schedule=api_util.build_connection_schedule(schedule_type="manual"),
 984        )
 985        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
 986
 987    # Deletions
 988
 989    def permanently_delete(
 990        self,
 991        *,
 992        cascade_delete_source: bool = False,
 993        cascade_delete_destination: bool = False,
 994    ) -> None:
 995        """Delete the connection.
 996
 997        Args:
 998            cascade_delete_source: Whether to also delete the source.
 999            cascade_delete_destination: Whether to also delete the destination.
1000        """
1001        self.workspace.permanently_delete_connection(self)
1002
1003        if cascade_delete_source:
1004            self.workspace.permanently_delete_source(self.source_id)
1005
1006        if cascade_delete_destination:
1007            self.workspace.permanently_delete_destination(self.destination_id)

A connection is an extract-load (EL) pairing of a source and destination in Airbyte Cloud.

You can use a connection object to run sync jobs, retrieve logs, and manage the connection.

CloudConnection( workspace: airbyte.cloud.CloudWorkspace, connection_id: str, source: str | None = None, destination: str | None = None)
51    def __init__(
52        self,
53        workspace: CloudWorkspace,
54        connection_id: str,
55        source: str | None = None,
56        destination: str | None = None,
57    ) -> None:
58        """It is not recommended to create a `CloudConnection` object directly.
59
60        Instead, use `CloudWorkspace.get_connection()` to create a connection object.
61        """
62        self.connection_id = connection_id
63        """The ID of the connection."""
64
65        self.workspace = workspace
66        """The workspace that the connection belongs to."""
67
68        self._source_id = source
69        """The ID of the source."""
70
71        self._destination_id = destination
72        """The ID of the destination."""
73
74        self._connection_info: CloudConnectionInfo | None = None
75        """The connection info object. (Cached.)"""
76
77        self._cloud_source_object: CloudSource | None = None
78        """The source object. (Cached.)"""
79
80        self._cloud_destination_object: CloudDestination | None = None
81        """The destination object. (Cached.)"""

It is not recommended to create a CloudConnection object directly.

Instead, use CloudWorkspace.get_connection() to create a connection object.

connection_id

The ID of the connection.

workspace

The workspace that the connection belongs to.

def check_is_valid(self) -> bool:
154    def check_is_valid(self) -> bool:
155        """Check if this connection exists and belongs to the expected workspace.
156
157        This method fetches connection info from the API (if not already cached) and
158        verifies that the connection's workspace_id matches the workspace associated
159        with this CloudConnection object.
160
161        Returns:
162            True if the connection exists and belongs to the expected workspace.
163
164        Raises:
165            AirbyteWorkspaceMismatchError: If the connection belongs to a different workspace.
166            AirbyteMissingResourceError: If the connection doesn't exist.
167        """
168        self._fetch_connection_info(force_refresh=False, verify=True)
169        return True

Check if this connection exists and belongs to the expected workspace.

This method fetches connection info from the API (if not already cached) and verifies that the connection's workspace_id matches the workspace associated with this CloudConnection object.

Returns:

True if the connection exists and belongs to the expected workspace.

Raises:
  • AirbyteWorkspaceMismatchError: If the connection belongs to a different workspace.
  • AirbyteMissingResourceError: If the connection doesn't exist.
name: str | None
190    @property
191    def name(self) -> str | None:
192        """Get the display name of the connection, if available.
193
194        E.g. "My Postgres to Snowflake", not the connection ID.
195        """
196        if not self._connection_info:
197            self._connection_info = self._fetch_connection_info()
198
199        return self._connection_info.name

Get the display name of the connection, if available.

E.g. "My Postgres to Snowflake", not the connection ID.

source_id: str
201    @property
202    def source_id(self) -> str:
203        """The ID of the source."""
204        if not self._source_id:
205            if not self._connection_info:
206                self._connection_info = self._fetch_connection_info()
207
208            self._source_id = self._connection_info.source_id
209
210        return self._source_id

The ID of the source.

source: airbyte.cloud.connectors.CloudSource
212    @property
213    def source(self) -> CloudSource:
214        """Get the source object."""
215        if self._cloud_source_object:
216            return self._cloud_source_object
217
218        self._cloud_source_object = CloudSource(
219            workspace=self.workspace,
220            connector_id=self.source_id,
221        )
222        return self._cloud_source_object

Get the source object.

destination_id: str
224    @property
225    def destination_id(self) -> str:
226        """The ID of the destination."""
227        if not self._destination_id:
228            if not self._connection_info:
229                self._connection_info = self._fetch_connection_info()
230
231            self._destination_id = self._connection_info.destination_id
232
233        return self._destination_id

The ID of the destination.

destination: airbyte.cloud.connectors.CloudDestination
235    @property
236    def destination(self) -> CloudDestination:
237        """Get the destination object."""
238        if self._cloud_destination_object:
239            return self._cloud_destination_object
240
241        self._cloud_destination_object = CloudDestination(
242            workspace=self.workspace,
243            connector_id=self.destination_id,
244        )
245        return self._cloud_destination_object

Get the destination object.

stream_names: list[str]
247    @property
248    def stream_names(self) -> list[str]:
249        """The stream names."""
250        if not self._connection_info:
251            self._connection_info = self._fetch_connection_info()
252
253        return [stream.name for stream in self._connection_info.configurations.streams or []]

The stream names.

table_prefix: str
255    @property
256    def table_prefix(self) -> str:
257        """The table prefix."""
258        if not self._connection_info:
259            self._connection_info = self._fetch_connection_info()
260
261        return self._connection_info.prefix or ""

The table prefix.

connection_url: str | None
263    @property
264    def connection_url(self) -> str | None:
265        """The web URL to the connection."""
266        return f"{self.workspace.workspace_url}/connections/{self.connection_id}"

The web URL to the connection.

job_history_url: str | None
268    @property
269    def job_history_url(self) -> str | None:
270        """The URL to the job history for the connection."""
271        return f"{self.connection_url}/timeline"

The URL to the job history for the connection.

def run_sync( self, *, wait: bool = True, wait_timeout: int = 300) -> airbyte.cloud.SyncResult:
275    def run_sync(
276        self,
277        *,
278        wait: bool = True,
279        wait_timeout: int = 300,
280    ) -> SyncResult:
281        """Run a sync."""
282        connection_response = api_util.run_connection(
283            connection_id=self.connection_id,
284            api_root=self.workspace.api_root,
285            workspace_id=self.workspace.workspace_id,
286            client_id=self.workspace.client_id,
287            client_secret=self.workspace.client_secret,
288            bearer_token=self.workspace.bearer_token,
289        )
290        sync_result = SyncResult(
291            workspace=self.workspace,
292            connection=self,
293            job_id=connection_response.job_id,
294        )
295
296        if wait:
297            sync_result.wait_for_completion(
298                wait_timeout=wait_timeout,
299                raise_failure=True,
300                raise_timeout=True,
301            )
302
303        return sync_result

Run a sync.

def cancel_sync(self, job_id: int | None = None) -> airbyte.cloud.SyncResult:
349    def cancel_sync(self, job_id: int | None = None) -> SyncResult:
350        """Cancel a running sync job.
351
352        Defaults to the connection's most recent sync job. Other job types must be
353        targeted with an explicit `job_id`.
354        """
355        target_job_id: int = (
356            self._get_latest_cancellable_sync_job_id()
357            if job_id is None
358            else self._validated_cancellable_job_id(job_id)
359        )
360
361        job_response = api_util.cancel_job(
362            job_id=target_job_id,
363            api_root=self.workspace.api_root,
364            client_id=self.workspace.client_id,
365            client_secret=self.workspace.client_secret,
366            bearer_token=self.workspace.bearer_token,
367        )
368        return SyncResult(
369            workspace=self.workspace,
370            connection=self,
371            job_id=job_response.job_id,
372            _latest_job_info=CloudJobInfo.from_api_response(job_response),
373        )

Cancel a running sync job.

Defaults to the connection's most recent sync job. Other job types must be targeted with an explicit job_id.

def get_previous_sync_logs( self, *, limit: int = 20, offset: int | None = None, from_tail: bool = True, job_type: str | airbyte.cloud.JobTypeEnum | None = None) -> list[airbyte.cloud.SyncResult]:
384    def get_previous_sync_logs(
385        self,
386        *,
387        limit: int = 20,
388        offset: int | None = None,
389        from_tail: bool = True,
390        job_type: str | JobTypeEnum | None = None,
391    ) -> list[SyncResult]:
392        """Get previous sync jobs for a connection with pagination support.
393
394        Returns SyncResult objects containing job metadata (job_id, status, bytes_synced,
395        rows_synced, start_time). Full log text can be fetched lazily via
396        `SyncResult.get_full_log_text()`.
397
398        Args:
399            limit: Maximum number of jobs to return. Defaults to 20.
400            offset: Number of jobs to skip from the beginning. Defaults to None (0).
401            from_tail: If True, returns jobs ordered newest-first (createdAt DESC).
402                If False, returns jobs ordered oldest-first (createdAt ASC).
403                Defaults to True.
404            job_type: Filter by job type (e.g., `sync`, `refresh`).
405                If not specified, defaults to sync and reset jobs only (API default behavior).
406
407        Returns:
408            A list of SyncResult objects representing the sync jobs.
409        """
410        order_by = (
411            api_util.JOB_ORDER_BY_CREATED_AT_DESC
412            if from_tail
413            else api_util.JOB_ORDER_BY_CREATED_AT_ASC
414        )
415        sync_logs = api_util.get_job_logs(
416            connection_id=self.connection_id,
417            api_root=self.workspace.api_root,
418            workspace_id=self.workspace.workspace_id,
419            limit=limit,
420            offset=offset,
421            order_by=order_by,
422            job_type=job_type,
423            client_id=self.workspace.client_id,
424            client_secret=self.workspace.client_secret,
425            bearer_token=self.workspace.bearer_token,
426        )
427        return [
428            SyncResult(
429                workspace=self.workspace,
430                connection=self,
431                job_id=sync_log.job_id,
432                _latest_job_info=CloudJobInfo.from_api_response(sync_log),
433            )
434            for sync_log in sync_logs
435        ]

Get previous sync jobs for a connection with pagination support.

Returns SyncResult objects containing job metadata (job_id, status, bytes_synced, rows_synced, start_time). Full log text can be fetched lazily via SyncResult.get_full_log_text().

Arguments:
  • limit: Maximum number of jobs to return. Defaults to 20.
  • offset: Number of jobs to skip from the beginning. Defaults to None (0).
  • from_tail: If True, returns jobs ordered newest-first (createdAt DESC). If False, returns jobs ordered oldest-first (createdAt ASC). Defaults to True.
  • job_type: Filter by job type (e.g., sync, refresh). If not specified, defaults to sync and reset jobs only (API default behavior).
Returns:

A list of SyncResult objects representing the sync jobs.

def get_sync_result( self, job_id: int | None = None) -> airbyte.cloud.SyncResult | None:
437    def get_sync_result(
438        self,
439        job_id: int | None = None,
440    ) -> SyncResult | None:
441        """Get the sync result for the connection.
442
443        If `job_id` is not provided, the most recent sync job will be used.
444
445        Returns `None` if job_id is omitted and no previous jobs are found.
446        """
447        if job_id is None:
448            # Get the most recent sync job
449            results = self.get_previous_sync_logs(
450                limit=1,
451            )
452            if results:
453                return results[0]
454
455            return None
456
457        # Get the sync job by ID (lazy loaded)
458        return SyncResult(
459            workspace=self.workspace,
460            connection=self,
461            job_id=job_id,
462        )

Get the sync result for the connection.

If job_id is not provided, the most recent sync job will be used.

Returns None if job_id is omitted and no previous jobs are found.

@deprecated("Use 'dump_raw_state()' instead.")
def get_state_artifacts(self) -> list[dict[str, typing.Any]] | None:
466    @deprecated("Use 'dump_raw_state()' instead.")
467    def get_state_artifacts(self) -> list[dict[str, Any]] | None:
468        """Deprecated. Use `dump_raw_state()` instead."""
469        state_response = api_util.get_connection_state(
470            connection_id=self.connection_id,
471            api_root=self.workspace.api_root,
472            client_id=self.workspace.client_id,
473            client_secret=self.workspace.client_secret,
474            bearer_token=self.workspace.bearer_token,
475            config_api_root=self.workspace.config_api_root,
476        )
477        if state_response.get("stateType") == "not_set":
478            return None
479        return state_response.get("streamState", [])

Deprecated. Use dump_raw_state() instead.

def dump_raw_state( self, *, normalize: bool = True) -> dict[str, typing.Any] | list[dict[str, typing.Any]]:
487    def dump_raw_state(
488        self,
489        *,
490        normalize: bool = True,
491    ) -> dict[str, Any] | list[dict[str, Any]]:
492        """Dump the state for this connection.
493
494        By default, returns a list of Airbyte protocol `AirbyteStateMessage` dicts
495        with snake_case keys, suitable for passing to a connector's `--state` flag.
496
497        When `normalize` is `False`, returns the raw Config API dict (camelCase keys,
498        includes `stateType` and `connectionId`). This raw format can be passed
499        directly to `import_raw_state()` for backup/restore workflows.
500
501        Args:
502            normalize: If `True` (default), convert to Airbyte protocol format.
503                If `False`, return the raw Config API response.
504
505        Returns:
506            Normalized: list of protocol-format state message dicts (empty list if
507            no state). Raw: the full Config API state dict.
508        """
509        raw = api_util.get_connection_state(
510            connection_id=self.connection_id,
511            api_root=self.workspace.api_root,
512            client_id=self.workspace.client_id,
513            client_secret=self.workspace.client_secret,
514            bearer_token=self.workspace.bearer_token,
515            config_api_root=self.workspace.config_api_root,
516        )
517        if normalize:
518            return _normalize_state_to_protocol(raw)
519        return raw

Dump the state for this connection.

By default, returns a list of Airbyte protocol AirbyteStateMessage dicts with snake_case keys, suitable for passing to a connector's --state flag.

When normalize is False, returns the raw Config API dict (camelCase keys, includes stateType and connectionId). This raw format can be passed directly to import_raw_state() for backup/restore workflows.

Arguments:
  • normalize: If True (default), convert to Airbyte protocol format. If False, return the raw Config API response.
Returns:

Normalized: list of protocol-format state message dicts (empty list if no state). Raw: the full Config API state dict.

def import_raw_state( self, connection_state: dict[str, typing.Any] | list[dict[str, typing.Any]]) -> dict[str, typing.Any]:
521    def import_raw_state(
522        self,
523        connection_state: dict[str, Any] | list[dict[str, Any]],
524    ) -> dict[str, Any]:
525        """Import (restore) the full state for this connection.
526
527        > ⚠️ **WARNING:** Modifying the state directly is not recommended and
528        > could result in broken connections, and/or incorrect sync behavior.
529
530        Replaces the entire connection state with the provided state blob.
531        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
532
533        This is the counterpart to `dump_raw_state()` for backup/restore workflows.
534        The `connectionId` in the blob is always overridden with this connection's
535        ID, making state blobs portable across connections.
536
537        Accepts either format:
538
539        - **Config API format** (dict with `stateType`): passed through directly.
540        - **Airbyte protocol format** (list of `AirbyteStateMessage` dicts): automatically
541          converted to Config API format before sending.
542
543        Args:
544            connection_state: Connection state in either Config API or Airbyte protocol format.
545
546        Returns:
547            The updated connection state as a dictionary.
548
549        Raises:
550            AirbyteConnectionSyncActiveError: If a sync is currently running on this
551                connection (HTTP 423). Wait for the sync to complete before retrying.
552        """
553        api_state: dict[str, Any]
554        if isinstance(connection_state, list):
555            if not _is_protocol_state_format(connection_state):
556                msg = (
557                    "Expected connection_state list to contain Airbyte protocol state "
558                    "message dicts (each with a top-level `type` of STREAM, GLOBAL, "
559                    "or LEGACY). Got a list that does not match protocol format."
560                )
561                raise ValueError(msg)
562            api_state = _denormalize_protocol_state_to_api(
563                protocol_messages=connection_state,
564                connection_id=self.connection_id,
565            )
566        elif isinstance(connection_state, dict):
567            if _is_protocol_state_format(connection_state):
568                api_state = _denormalize_protocol_state_to_api(
569                    protocol_messages=[connection_state],
570                    connection_id=self.connection_id,
571                )
572            else:
573                api_state = connection_state
574        else:
575            msg = f"Expected a dict or list, got {type(connection_state)}"
576            raise TypeError(msg)
577
578        return api_util.replace_connection_state(
579            connection_id=self.connection_id,
580            connection_state_dict=api_state,
581            api_root=self.workspace.api_root,
582            client_id=self.workspace.client_id,
583            client_secret=self.workspace.client_secret,
584            bearer_token=self.workspace.bearer_token,
585            config_api_root=self.workspace.config_api_root,
586        )

Import (restore) the full state for this connection.

⚠️ WARNING: Modifying the state directly is not recommended and could result in broken connections, and/or incorrect sync behavior.

Replaces the entire connection state with the provided state blob. Uses the safe variant that prevents updates while a sync is running (HTTP 423).

This is the counterpart to dump_raw_state() for backup/restore workflows. The connectionId in the blob is always overridden with this connection's ID, making state blobs portable across connections.

Accepts either format:

  • Config API format (dict with stateType): passed through directly.
  • Airbyte protocol format (list of AirbyteStateMessage dicts): automatically converted to Config API format before sending.
Arguments:
  • connection_state: Connection state in either Config API or Airbyte protocol format.
Returns:

The updated connection state as a dictionary.

Raises:
  • AirbyteConnectionSyncActiveError: If a sync is currently running on this connection (HTTP 423). Wait for the sync to complete before retrying.
def get_stream_state( self, stream_name: str, stream_namespace: str | None = None) -> dict[str, typing.Any] | None:
588    def get_stream_state(
589        self,
590        stream_name: str,
591        stream_namespace: str | None = None,
592    ) -> dict[str, Any] | None:
593        """Get the state blob for a single stream within this connection.
594
595        Returns just the stream's state dictionary (e.g., {"cursor": "2024-01-01"}),
596        not the full connection state envelope.
597
598        This is compatible with `stream`-type state and stream-level entries
599        within a `global`-type state. It is not compatible with `legacy` state.
600        To get or set the entire connection-level state artifact, use
601        `dump_raw_state` and `import_raw_state` instead.
602
603        Args:
604            stream_name: The name of the stream to get state for.
605            stream_namespace: The source-side stream namespace. This refers to the
606                namespace from the source (e.g., database schema), not any destination
607                namespace override set in connection advanced settings.
608
609        Returns:
610            The stream's state blob as a dictionary, or None if the stream is not found.
611        """
612        state_data = self.dump_raw_state(normalize=False)
613        result = ConnectionStateResponse(**state_data)
614
615        streams = _get_stream_list(result)
616        matching = [s for s in streams if _match_stream(s, stream_name, stream_namespace)]
617
618        if not matching:
619            available = [s.stream_descriptor.name for s in streams]
620            logger.warning(
621                "Stream '%s' not found in connection state for connection '%s'. "
622                "Available streams: %s",
623                stream_name,
624                self.connection_id,
625                available,
626            )
627            return None
628
629        return matching[0].stream_state

Get the state blob for a single stream within this connection.

Returns just the stream's state dictionary (e.g., {"cursor": "2024-01-01"}), not the full connection state envelope.

This is compatible with stream-type state and stream-level entries within a global-type state. It is not compatible with legacy state. To get or set the entire connection-level state artifact, use dump_raw_state and import_raw_state instead.

Arguments:
  • stream_name: The name of the stream to get state for.
  • stream_namespace: The source-side stream namespace. This refers to the namespace from the source (e.g., database schema), not any destination namespace override set in connection advanced settings.
Returns:

The stream's state blob as a dictionary, or None if the stream is not found.

def set_stream_state( self, stream_name: str, state_blob_dict: dict[str, typing.Any], stream_namespace: str | None = None) -> None:
631    def set_stream_state(
632        self,
633        stream_name: str,
634        state_blob_dict: dict[str, Any],
635        stream_namespace: str | None = None,
636    ) -> None:
637        """Set the state for a single stream within this connection.
638
639        Fetches the current full state, replaces only the specified stream's state,
640        then sends the full updated state back to the API. If the stream does not
641        exist in the current state, it is appended.
642
643        This is compatible with `stream`-type state and stream-level entries
644        within a `global`-type state. It is not compatible with `legacy` state.
645        To get or set the entire connection-level state artifact, use
646        `dump_raw_state` and `import_raw_state` instead.
647
648        Uses the safe variant that prevents updates while a sync is running (HTTP 423).
649
650        Args:
651            stream_name: The name of the stream to update state for.
652            state_blob_dict: The state blob dict for this stream (e.g., {"cursor": "2024-01-01"}).
653            stream_namespace: The source-side stream namespace. This refers to the
654                namespace from the source (e.g., database schema), not any destination
655                namespace override set in connection advanced settings.
656
657        Raises:
658            PyAirbyteInputError: If the connection state type is not supported for
659                stream-level operations (not_set, legacy).
660            AirbyteConnectionSyncActiveError: If a sync is currently running on this
661                connection (HTTP 423). Wait for the sync to complete before retrying.
662        """
663        state_data = self.dump_raw_state(normalize=False)
664        current = ConnectionStateResponse(**state_data)
665
666        if current.state_type == "not_set":
667            raise PyAirbyteInputError(
668                message="Cannot set stream state: connection has no existing state.",
669                context={"connection_id": self.connection_id},
670            )
671
672        if current.state_type == "legacy":
673            raise PyAirbyteInputError(
674                message="Cannot set stream state on a legacy-type connection state.",
675                context={"connection_id": self.connection_id},
676            )
677
678        new_stream_entry = {
679            "streamDescriptor": {
680                "name": stream_name,
681                **(
682                    {
683                        "namespace": stream_namespace,
684                    }
685                    if stream_namespace
686                    else {}
687                ),
688            },
689            "streamState": state_blob_dict,
690        }
691
692        raw_streams: list[dict[str, Any]]
693        if current.state_type == "stream":
694            raw_streams = state_data.get("streamState", [])
695        elif current.state_type == "global":
696            raw_streams = state_data.get("globalState", {}).get("streamStates", [])
697        else:
698            raw_streams = []
699
700        streams = _get_stream_list(current)
701        found = False
702        updated_streams_raw: list[dict[str, Any]] = []
703        for raw_s, parsed_s in zip(raw_streams, streams, strict=False):
704            if _match_stream(parsed_s, stream_name, stream_namespace):
705                updated_streams_raw.append(new_stream_entry)
706                found = True
707            else:
708                updated_streams_raw.append(raw_s)
709
710        if not found:
711            updated_streams_raw.append(new_stream_entry)
712
713        full_state: dict[str, Any] = {
714            **state_data,
715        }
716
717        if current.state_type == "stream":
718            full_state["streamState"] = updated_streams_raw
719        elif current.state_type == "global":
720            original_global = state_data.get("globalState", {})
721            full_state["globalState"] = {
722                **original_global,
723                "streamStates": updated_streams_raw,
724            }
725
726        self.import_raw_state(full_state)

Set the state for a single stream within this connection.

Fetches the current full state, replaces only the specified stream's state, then sends the full updated state back to the API. If the stream does not exist in the current state, it is appended.

This is compatible with stream-type state and stream-level entries within a global-type state. It is not compatible with legacy state. To get or set the entire connection-level state artifact, use dump_raw_state and import_raw_state instead.

Uses the safe variant that prevents updates while a sync is running (HTTP 423).

Arguments:
  • stream_name: The name of the stream to update state for.
  • state_blob_dict: The state blob dict for this stream (e.g., {"cursor": "2024-01-01"}).
  • stream_namespace: The source-side stream namespace. This refers to the namespace from the source (e.g., database schema), not any destination namespace override set in connection advanced settings.
Raises:
  • PyAirbyteInputError: If the connection state type is not supported for stream-level operations (not_set, legacy).
  • AirbyteConnectionSyncActiveError: If a sync is currently running on this connection (HTTP 423). Wait for the sync to complete before retrying.
@deprecated("Use 'dump_raw_catalog()' instead.")
def get_catalog_artifact(self) -> dict[str, typing.Any] | None:
728    @deprecated("Use 'dump_raw_catalog()' instead.")
729    def get_catalog_artifact(self) -> dict[str, Any] | None:
730        """Get the configured catalog for this connection.
731
732        Returns the full configured catalog (syncCatalog) for this connection,
733        including stream schemas, sync modes, cursor fields, and primary keys.
734
735        Uses the Config API endpoint: POST /v1/web_backend/connections/get
736
737        Returns:
738            Dictionary containing the configured catalog, or `None` if not found.
739        """
740        return self.dump_raw_catalog()

Get the configured catalog for this connection.

Returns the full configured catalog (syncCatalog) for this connection, including stream schemas, sync modes, cursor fields, and primary keys.

Uses the Config API endpoint: POST /v1/web_backend/connections/get

Returns:

Dictionary containing the configured catalog, or None if not found.

def dump_raw_catalog(self, *, normalize: bool = True) -> dict[str, typing.Any] | None:
742    def dump_raw_catalog(
743        self,
744        *,
745        normalize: bool = True,
746    ) -> dict[str, Any] | None:
747        """Dump the configured catalog for this connection.
748
749        By default, returns the catalog in Airbyte protocol format
750        (`ConfiguredAirbyteCatalog` with snake_case keys), suitable for passing
751        to a connector's `--catalog` flag.
752
753        When `normalize` is `False`, returns the raw `syncCatalog` dict from the
754        Config API (camelCase keys, nested `config` block). This raw format can be
755        passed directly to `import_raw_catalog()` for backup/restore workflows.
756
757        Args:
758            normalize: If `True` (default), convert to Airbyte protocol format.
759                If `False`, return the raw Config API catalog.
760
761        Returns:
762            The configured catalog dict, or `None` if not found.
763        """
764        connection_response = api_util.get_connection_catalog(
765            connection_id=self.connection_id,
766            api_root=self.workspace.api_root,
767            client_id=self.workspace.client_id,
768            client_secret=self.workspace.client_secret,
769            bearer_token=self.workspace.bearer_token,
770            config_api_root=self.workspace.config_api_root,
771        )
772        raw = connection_response.get("syncCatalog")
773        if raw is None:
774            return None
775        if normalize:
776            return _normalize_catalog_to_protocol(raw)
777        return raw

Dump the configured catalog for this connection.

By default, returns the catalog in Airbyte protocol format (ConfiguredAirbyteCatalog with snake_case keys), suitable for passing to a connector's --catalog flag.

When normalize is False, returns the raw syncCatalog dict from the Config API (camelCase keys, nested config block). This raw format can be passed directly to import_raw_catalog() for backup/restore workflows.

Arguments:
  • normalize: If True (default), convert to Airbyte protocol format. If False, return the raw Config API catalog.
Returns:

The configured catalog dict, or None if not found.

def import_raw_catalog(self, catalog: dict[str, typing.Any]) -> None:
779    def import_raw_catalog(self, catalog: dict[str, Any]) -> None:
780        """Replace the configured catalog for this connection.
781
782        > ⚠️ **WARNING:** Modifying the catalog directly is not recommended and
783        > could result in broken connections, and/or incorrect sync behavior.
784
785        Accepts a configured catalog dict and replaces the connection's entire
786        catalog with it. All other connection settings remain unchanged.
787
788        Accepts either format:
789
790        - **Config API format** (`syncCatalog` with camelCase keys and nested `config`):
791          passed through directly.
792        - **Airbyte protocol format** (`ConfiguredAirbyteCatalog` with snake_case keys):
793          automatically converted to Config API format before sending.
794
795        Args:
796            catalog: The configured catalog dict in either format.
797        """
798        if _is_protocol_catalog_format(catalog):
799            catalog = _denormalize_catalog_to_api(catalog)
800
801        api_util.replace_connection_catalog(
802            connection_id=self.connection_id,
803            configured_catalog_dict=catalog,
804            api_root=self.workspace.api_root,
805            client_id=self.workspace.client_id,
806            client_secret=self.workspace.client_secret,
807            bearer_token=self.workspace.bearer_token,
808            config_api_root=self.workspace.config_api_root,
809        )

Replace the configured catalog for this connection.

⚠️ WARNING: Modifying the catalog directly is not recommended and could result in broken connections, and/or incorrect sync behavior.

Accepts a configured catalog dict and replaces the connection's entire catalog with it. All other connection settings remain unchanged.

Accepts either format:

  • Config API format (syncCatalog with camelCase keys and nested config): passed through directly.
  • Airbyte protocol format (ConfiguredAirbyteCatalog with snake_case keys): automatically converted to Config API format before sending.
Arguments:
  • catalog: The configured catalog dict in either format.
def rename(self, name: str) -> CloudConnection:
811    def rename(self, name: str) -> CloudConnection:
812        """Rename the connection.
813
814        Args:
815            name: New name for the connection
816
817        Returns:
818            Updated CloudConnection object with refreshed info
819        """
820        updated_response = api_util.patch_connection(
821            connection_id=self.connection_id,
822            api_root=self.workspace.api_root,
823            client_id=self.workspace.client_id,
824            client_secret=self.workspace.client_secret,
825            bearer_token=self.workspace.bearer_token,
826            name=name,
827        )
828        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
829        return self

Rename the connection.

Arguments:
  • name: New name for the connection
Returns:

Updated CloudConnection object with refreshed info

def set_table_prefix(self, prefix: str) -> CloudConnection:
831    def set_table_prefix(self, prefix: str) -> CloudConnection:
832        """Set the table prefix for the connection.
833
834        Args:
835            prefix: New table prefix to use when syncing to the destination
836
837        Returns:
838            Updated CloudConnection object with refreshed info
839        """
840        updated_response = api_util.patch_connection(
841            connection_id=self.connection_id,
842            api_root=self.workspace.api_root,
843            client_id=self.workspace.client_id,
844            client_secret=self.workspace.client_secret,
845            bearer_token=self.workspace.bearer_token,
846            prefix=prefix,
847        )
848        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
849        return self

Set the table prefix for the connection.

Arguments:
  • prefix: New table prefix to use when syncing to the destination
Returns:

Updated CloudConnection object with refreshed info

def set_selected_streams( self, stream_names: list[str]) -> CloudConnection:
851    def set_selected_streams(self, stream_names: list[str]) -> CloudConnection:
852        """Set the selected streams for the connection.
853
854        This is a destructive operation that can break existing connections if the
855        stream selection is changed incorrectly. Use with caution.
856
857        Args:
858            stream_names: List of stream names to sync
859
860        Returns:
861            Updated CloudConnection object with refreshed info
862        """
863        configurations = api_util.build_stream_configurations(stream_names)
864
865        updated_response = api_util.patch_connection(
866            connection_id=self.connection_id,
867            api_root=self.workspace.api_root,
868            client_id=self.workspace.client_id,
869            client_secret=self.workspace.client_secret,
870            bearer_token=self.workspace.bearer_token,
871            configurations=configurations,
872        )
873        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)
874        return self

Set the selected streams for the connection.

This is a destructive operation that can break existing connections if the stream selection is changed incorrectly. Use with caution.

Arguments:
  • stream_names: List of stream names to sync
Returns:

Updated CloudConnection object with refreshed info

enabled: bool
878    @property
879    def enabled(self) -> bool:
880        """Get the current enabled status of the connection.
881
882        This property always fetches fresh data from the API to ensure accuracy,
883        as another process or user may have toggled the setting.
884
885        Returns:
886            True if the connection status is 'active', False otherwise.
887        """
888        connection_info = self._fetch_connection_info(force_refresh=True)
889        return connection_info.status == "active"

Get the current enabled status of the connection.

This property always fetches fresh data from the API to ensure accuracy, as another process or user may have toggled the setting.

Returns:

True if the connection status is 'active', False otherwise.

def set_enabled(self, *, enabled: bool, ignore_noop: bool = True) -> None:
901    def set_enabled(
902        self,
903        *,
904        enabled: bool,
905        ignore_noop: bool = True,
906    ) -> None:
907        """Set the enabled status of the connection.
908
909        Args:
910            enabled: True to enable (set status to 'active'), False to disable
911                (set status to 'inactive').
912            ignore_noop: If True (default), silently return if the connection is already
913                in the requested state. If False, raise ValueError when the requested
914                state matches the current state.
915
916        Raises:
917            ValueError: If ignore_noop is False and the connection is already in the
918                requested state.
919        """
920        # Always fetch fresh data to check current status
921        connection_info = self._fetch_connection_info(force_refresh=True)
922        current_status = connection_info.status
923        desired_status = "active" if enabled else "inactive"
924
925        if current_status == desired_status:
926            if ignore_noop:
927                return
928            raise ValueError(
929                f"Connection is already {'enabled' if enabled else 'disabled'}. "
930                f"Current status: {current_status}"
931            )
932
933        updated_response = api_util.patch_connection(
934            connection_id=self.connection_id,
935            api_root=self.workspace.api_root,
936            client_id=self.workspace.client_id,
937            client_secret=self.workspace.client_secret,
938            bearer_token=self.workspace.bearer_token,
939            status=desired_status,
940        )
941        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)

Set the enabled status of the connection.

Arguments:
  • enabled: True to enable (set status to 'active'), False to disable (set status to 'inactive').
  • ignore_noop: If True (default), silently return if the connection is already in the requested state. If False, raise ValueError when the requested state matches the current state.
Raises:
  • ValueError: If ignore_noop is False and the connection is already in the requested state.
def set_schedule(self, cron_expression: str) -> None:
945    def set_schedule(
946        self,
947        cron_expression: str,
948    ) -> None:
949        """Set a cron schedule for the connection.
950
951        Args:
952            cron_expression: A cron expression defining when syncs should run.
953
954        Examples:
955            - "0 0 * * *"  # Daily at midnight UTC
956            - "0 */6 * * *"  # Every 6 hours
957            - "0 0 * * 0"  # Weekly on Sunday at midnight UTC
958        """
959        updated_response = api_util.patch_connection(
960            connection_id=self.connection_id,
961            api_root=self.workspace.api_root,
962            client_id=self.workspace.client_id,
963            client_secret=self.workspace.client_secret,
964            bearer_token=self.workspace.bearer_token,
965            schedule=api_util.build_connection_schedule(
966                schedule_type="cron",
967                cron_expression=cron_expression,
968            ),
969        )
970        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)

Set a cron schedule for the connection.

Arguments:
  • cron_expression: A cron expression defining when syncs should run.
Examples:
  • "0 0 * * *" # Daily at midnight UTC
  • "0 */6 * * *" # Every 6 hours
  • "0 0 * * 0" # Weekly on Sunday at midnight UTC
def set_manual_schedule(self) -> None:
972    def set_manual_schedule(self) -> None:
973        """Set the connection to manual scheduling.
974
975        Disables automatic syncs. Syncs will only run when manually triggered.
976        """
977        updated_response = api_util.patch_connection(
978            connection_id=self.connection_id,
979            api_root=self.workspace.api_root,
980            client_id=self.workspace.client_id,
981            client_secret=self.workspace.client_secret,
982            bearer_token=self.workspace.bearer_token,
983            schedule=api_util.build_connection_schedule(schedule_type="manual"),
984        )
985        self._connection_info = CloudConnectionInfo.from_api_response(updated_response)

Set the connection to manual scheduling.

Disables automatic syncs. Syncs will only run when manually triggered.

def permanently_delete( self, *, cascade_delete_source: bool = False, cascade_delete_destination: bool = False) -> None:
 989    def permanently_delete(
 990        self,
 991        *,
 992        cascade_delete_source: bool = False,
 993        cascade_delete_destination: bool = False,
 994    ) -> None:
 995        """Delete the connection.
 996
 997        Args:
 998            cascade_delete_source: Whether to also delete the source.
 999            cascade_delete_destination: Whether to also delete the destination.
1000        """
1001        self.workspace.permanently_delete_connection(self)
1002
1003        if cascade_delete_source:
1004            self.workspace.permanently_delete_source(self.source_id)
1005
1006        if cascade_delete_destination:
1007            self.workspace.permanently_delete_destination(self.destination_id)

Delete the connection.

Arguments:
  • cascade_delete_source: Whether to also delete the source.
  • cascade_delete_destination: Whether to also delete the destination.