airbyte

PyAirbyte brings the power of Airbyte to every Python developer.

PyPI version PyPI - Downloads PyPI - Python Version Star on GitHub

Getting Started

Reading Data

You can connect to any of hundreds of sources using the get_source method. You can then read data from sources using Source.read method.

from airbyte import get_source

source = get_source(
    "source-faker",
    config={},
)
read_result = source.read()

for record in read_result["users"]airbyte.records:
    print(record)

For more information, see the airbyte.sources module.

Writing to SQL Caches

Data can be written to caches using a number of SQL-based cache implementations, including Postgres, BigQuery, Snowflake, DuckDB, and MotherDuck. If you do not specify a cache, PyAirbyte will automatically use a local DuckDB cache by default.

For more information, see the airbyte.caches module.

Writing to Destination Connectors

Data can be written to destinations using the Destination.write method. You can connect to destinations using the get_destination method. PyAirbyte supports all Airbyte destinations, but Docker is required on your machine in order to run Java-based destinations.

Note: When loading to a SQL database, we recommend using SQL cache (where available, see above) instead of a destination connector. This is because SQL caches are Python-native and therefor more portable when run from different Python-based environments which might not have Docker container support. Destinations in PyAirbyte are uniquely suited for loading to non-SQL platforms such as vector stores and other reverse ETL-type use cases.

For more information, see the airbyte.destinations module and the full list of destination connectors here.

PyAirbyte API

Importing as ab

Most examples in the PyAirbyte documentation use the import airbyte as ab convention. The ab alias is recommended, making code more concise and readable. When getting started, this also saves you from digging in submodules to find the classes and functions you need, since frequently-used classes and functions are available at the top level of the airbyte module.

While many PyAirbyte classes and functions are available at the top level of the airbyte module, you can also import classes and functions from submodules directly. For example, while you can import the Source class from airbyte, you can also import it from the sources submodule like this:

from airbyte.sources import Source

Whether you import from the top level or from a submodule, the classes and functions are the same. We expect that most users will import from the top level when getting started, and then import from submodules when they are deploying more complex implementations.

For quick reference, top-Level modules are listed in the left sidebar of this page.

Other Resources


API Reference

Below is a list of all classes, functions, and modules available in the top-level airbyte module. (This is a long list!) If you are just starting out, we recommend beginning by selecting a submodule to navigate to from the left sidebar or from the list below:

Each module has its own documentation and code samples related to effectively using the related capabilities.


  1# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
  2"""***PyAirbyte brings the power of Airbyte to every Python developer.***
  3
  4[![PyPI version](https://badge.fury.io/py/airbyte.svg)](https://badge.fury.io/py/airbyte)
  5[![PyPI - Downloads](https://img.shields.io/pypi/dm/airbyte)](https://pypi.org/project/airbyte/)
  6[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/airbyte)](https://pypi.org/project/airbyte/)
  7[![Star on GitHub](https://img.shields.io/github/stars/airbytehq/pyairbyte.svg?style=social&label=★%20on%20GitHub)](https://github.com/airbytehq/pyairbyte)
  8
  9# Getting Started
 10
 11## Reading Data
 12
 13You can connect to any of [hundreds of sources](https://docs.airbyte.com/integrations/sources/)
 14using the `get_source` method. You can then read data from sources using `Source.read` method.
 15
 16```python
 17from airbyte import get_source
 18
 19source = get_source(
 20    "source-faker",
 21    config={},
 22)
 23read_result = source.read()
 24
 25for record in read_result["users"].records:
 26    print(record)
 27```
 28
 29For more information, see the `airbyte.sources` module.
 30
 31## Writing to SQL Caches
 32
 33Data can be written to caches using a number of SQL-based cache implementations, including
 34Postgres, BigQuery, Snowflake, DuckDB, and MotherDuck. If you do not specify a cache, PyAirbyte
 35will automatically use a local DuckDB cache by default.
 36
 37For more information, see the `airbyte.caches` module.
 38
 39## Writing to Destination Connectors
 40
 41Data can be written to destinations using the `Destination.write` method. You can connect to
 42destinations using the `get_destination` method. PyAirbyte supports all Airbyte destinations, but
 43Docker is required on your machine in order to run Java-based destinations.
 44
 45**Note:** When loading to a SQL database, we recommend using SQL cache (where available,
 46[see above](#writing-to-sql-caches)) instead of a destination connector. This is because SQL caches
 47are Python-native and therefor more portable when run from different Python-based environments which
 48might not have Docker container support. Destinations in PyAirbyte are uniquely suited for loading
 49to non-SQL platforms such as vector stores and other reverse ETL-type use cases.
 50
 51For more information, see the `airbyte.destinations` module and the full list of destination
 52connectors [here](https://docs.airbyte.com/integrations/destinations/).
 53
 54# PyAirbyte API
 55
 56## Importing as `ab`
 57
 58Most examples in the PyAirbyte documentation use the `import airbyte as ab` convention. The `ab`
 59alias is recommended, making code more concise and readable. When getting started, this
 60also saves you from digging in submodules to find the classes and functions you need, since
 61frequently-used classes and functions are available at the top level of the `airbyte` module.
 62
 63## Navigating the API
 64
 65While many PyAirbyte classes and functions are available at the top level of the `airbyte` module,
 66you can also import classes and functions from submodules directly. For example, while you can
 67import the `Source` class from `airbyte`, you can also import it from the `sources` submodule like
 68this:
 69
 70```python
 71from airbyte.sources import Source
 72```
 73
 74Whether you import from the top level or from a submodule, the classes and functions are the same.
 75We expect that most users will import from the top level when getting started, and then import from
 76submodules when they are deploying more complex implementations.
 77
 78For quick reference, top-Level modules are listed in the left sidebar of this page.
 79
 80# Other Resources
 81
 82- [PyAirbyte GitHub Readme](https://github.com/airbytehq/pyairbyte)
 83- [PyAirbyte Issue Tracker](https://github.com/airbytehq/pyairbyte/issues)
 84- [Frequently Asked Questions](https://github.com/airbytehq/PyAirbyte/blob/main/docs/faq.md)
 85- [PyAirbyte Contributors Guide](https://github.com/airbytehq/PyAirbyte/blob/main/docs/CONTRIBUTING.md)
 86- [GitHub Releases](https://github.com/airbytehq/PyAirbyte/releases)
 87
 88----------------------
 89
 90# API Reference
 91
 92Below is a list of all classes, functions, and modules available in the top-level `airbyte`
 93module. (This is a long list!) If you are just starting out, we recommend beginning by selecting a
 94submodule to navigate to from the left sidebar or from the list below:
 95
 96Each module
 97has its own documentation and code samples related to effectively using the related capabilities.
 98
 99- **`airbyte.cloud`** - Working with Airbyte Cloud, including running jobs remotely.
100- **`airbyte.caches`** - Working with caches, including how to inspect a cache and get data from it.
101- **`airbyte.datasets`** - Working with datasets, including how to read from datasets and convert to
102    other formats, such as Pandas, Arrow, and LLM Document formats.
103- **`airbyte.destinations`** - Working with destinations, including how to write to Airbyte
104    destinations connectors.
105- **`airbyte.documents`** - Working with LLM documents, including how to convert records into
106    document formats, for instance, when working with AI libraries like LangChain.
107- **`airbyte.exceptions`** - Definitions of all exception and warning classes used in PyAirbyte.
108- **`airbyte.experimental`** - Experimental features and utilities that do not yet have a stable
109    API.
110- **`airbyte.logs`** - Logging functionality and configuration.
111- **`airbyte.records`** - Internal record handling classes.
112- **`airbyte.results`** - Documents the classes returned when working with results from
113    `Source.read` and `Destination.write`
114- **`airbyte.secrets`** - Tools for managing secrets in PyAirbyte.
115- **`airbyte.sources`** - Tools for creating and reading from Airbyte sources. This includes
116    `airbyte.source.get_source` to declare a source, `airbyte.source.Source.read` for reading data,
117    and `airbyte.source.Source.get_records()` to peek at records without caching or writing them
118    directly.
119
120----------------------
121
122"""  # noqa: D415
123
124from __future__ import annotations
125
126from typing import TYPE_CHECKING
127
128from airbyte import registry
129from airbyte.caches.bigquery import BigQueryCache
130from airbyte.caches.duckdb import DuckDBCache
131from airbyte.caches.util import get_colab_cache, get_default_cache, new_local_cache
132from airbyte.datasets import CachedDataset
133from airbyte.destinations.base import Destination
134from airbyte.destinations.util import get_destination
135from airbyte.records import StreamRecord
136from airbyte.registry import get_available_connectors
137from airbyte.results import ReadResult, WriteResult
138from airbyte.secrets import SecretSourceEnum, get_secret
139from airbyte.sources.base import Source
140from airbyte.sources.util import get_source
141
142
143# Submodules imported here for documentation reasons: https://github.com/mitmproxy/pdoc/issues/757
144if TYPE_CHECKING:
145    # ruff: noqa: TC004  # imports used for more than type checking
146    from airbyte import (
147        caches,
148        callbacks,
149        cli,
150        cloud,
151        constants,
152        datasets,
153        destinations,
154        documents,
155        exceptions,  # noqa: ICN001  # No 'exc' alias for top-level module
156        experimental,
157        logs,
158        mcp,
159        records,
160        results,
161        secrets,
162        sources,
163    )
164
165
166__all__ = [
167    # Modules
168    "caches",
169    "callbacks",
170    "cli",
171    "cloud",
172    "constants",
173    "datasets",
174    "destinations",
175    "documents",
176    "exceptions",
177    "experimental",
178    "logs",
179    "mcp",
180    "records",
181    "registry",
182    "results",
183    "secrets",
184    "sources",
185    # Factories
186    "get_available_connectors",
187    "get_colab_cache",
188    "get_default_cache",
189    "get_destination",
190    "get_secret",
191    "get_source",
192    "new_local_cache",
193    # Classes
194    "BigQueryCache",
195    "CachedDataset",
196    "Destination",
197    "DuckDBCache",
198    "ReadResult",
199    "SecretSourceEnum",
200    "Source",
201    "StreamRecord",
202    "WriteResult",
203]
204
205__docformat__ = "google"
def get_available_connectors( install_type: airbyte.registry.InstallType | str | None = <InstallType.INSTALLABLE: 'installable'>) -> list[str]:
293def get_available_connectors(
294    install_type: InstallType | str | None = InstallType.INSTALLABLE,
295) -> list[str]:
296    """Return a list of all available connectors.
297
298    Connectors will be returned in alphabetical order, with the standard prefix "source-".
299
300    Args:
301        install_type: The type of installation for the connector.
302            Defaults to `InstallType.INSTALLABLE`.
303    """
304    if install_type is None or install_type == InstallType.INSTALLABLE:
305        # Filter for installable connectors (default behavior).
306        if is_docker_installed():
307            logger.info("Docker is detected. Returning all connectors.")
308            return sorted(_get_registry_cache().keys())
309
310        logger.info("Docker was not detected. Returning only Python and Manifest-only connectors.")
311        return sorted(
312            [
313                connector_name
314                for connector_name, conn_info in _get_registry_cache().items()
315                if conn_info.language in {Language.PYTHON, Language.MANIFEST_ONLY}
316            ]
317        )
318
319    if not isinstance(install_type, InstallType):
320        install_type = InstallType(install_type)
321
322    if install_type == InstallType.PYTHON:
323        return sorted(
324            connector_name
325            for connector_name, conn_info in _get_registry_cache().items()
326            if conn_info.pypi_package_name is not None
327        )
328
329    if install_type == InstallType.JAVA:
330        warnings.warn(
331            message="Java connectors are not yet supported.",
332            stacklevel=2,
333        )
334        return sorted(
335            connector_name
336            for connector_name, conn_info in _get_registry_cache().items()
337            if conn_info.language == Language.JAVA
338        )
339
340    if install_type in {InstallType.DOCKER, InstallType.ANY}:
341        return sorted(_get_registry_cache().keys())
342
343    if install_type == InstallType.YAML:
344        return sorted(
345            conn.name
346            for conn in _get_registry_cache().values()
347            if InstallType.YAML in conn.install_types
348        )
349
350    # pragma: no cover  # Should never be reached.
351    raise exc.PyAirbyteInputError(
352        message="Invalid install type.",
353        context={
354            "install_type": install_type,
355        },
356    )

Return a list of all available connectors.

Connectors will be returned in alphabetical order, with the standard prefix "source-".

Arguments:
  • install_type: The type of installation for the connector. Defaults to InstallType.INSTALLABLE.
def get_colab_cache( cache_name: str = 'default_cache', sub_dir: str = 'Airbyte/cache', schema_name: str = 'main', table_prefix: str | None = '', drive_name: str = 'MyDrive', mount_path: str = '/content/drive') -> DuckDBCache:
 85def get_colab_cache(
 86    cache_name: str = "default_cache",
 87    sub_dir: str = "Airbyte/cache",
 88    schema_name: str = "main",
 89    table_prefix: str | None = "",
 90    drive_name: str = _MY_DRIVE,
 91    mount_path: str = _GOOGLE_DRIVE_DEFAULT_MOUNT_PATH,
 92) -> DuckDBCache:
 93    """Get a local cache for storing data, using the default database path.
 94
 95    Unlike the default `DuckDBCache`, this implementation will easily persist data across multiple
 96    Colab sessions.
 97
 98    Please note that Google Colab may prompt you to authenticate with your Google account to access
 99    your Google Drive. When prompted, click the link and follow the instructions.
100
101    Colab will require access to read and write files in your Google Drive, so please be sure to
102    grant the necessary permissions when prompted.
103
104    All arguments are optional and have default values that are suitable for most use cases.
105
106    Args:
107        cache_name: The name to use for the cache. Defaults to "colab_cache". Override this if you
108            want to use a different database for different projects.
109        sub_dir: The subdirectory to store the cache in. Defaults to "Airbyte/cache". Override this
110            if you want to store the cache in a different subdirectory than the default.
111        schema_name: The name of the schema to write to. Defaults to "main". Override this if you
112            want to write to a different schema.
113        table_prefix: The prefix to use for all tables in the cache. Defaults to "". Override this
114            if you want to use a different prefix for all tables.
115        drive_name: The name of the Google Drive to use. Defaults to "MyDrive". Override this if you
116            want to store data in a shared drive instead of your personal drive.
117        mount_path: The path to mount Google Drive to. Defaults to "/content/drive". Override this
118            if you want to mount Google Drive to a different path (not recommended).
119
120    ## Usage Examples
121
122    The default `get_colab_cache` arguments are suitable for most use cases:
123
124    ```python
125    from airbyte.caches.colab import get_colab_cache
126
127    colab_cache = get_colab_cache()
128    ```
129
130    Or you can call `get_colab_cache` with custom arguments:
131
132    ```python
133    custom_cache = get_colab_cache(
134        cache_name="my_custom_cache",
135        sub_dir="Airbyte/custom_cache",
136        drive_name="My Company Drive",
137    )
138    ```
139    """
140    try:
141        from google.colab import drive  # noqa: PLC0415 # type: ignore[reportMissingImports]
142    except ImportError:
143        drive = None
144        msg = (
145            "The `google.colab` interface is only available in Google Colab. "
146            "Please run this code in a Google Colab notebook."
147        )
148        raise ImportError(msg) from None
149
150    drive.mount(mount_path)
151    drive_root = (
152        Path(mount_path) / drive_name
153        if drive_name == _MY_DRIVE
154        else Path(mount_path) / "Shareddrives" / drive_name
155    )
156
157    cache_dir = drive_root / sub_dir
158    cache_dir.mkdir(parents=True, exist_ok=True)
159    db_file_path = cache_dir / f"{cache_name}.duckdb"
160
161    print(f"Using persistent PyAirbyte cache in Google Drive: `{db_file_path}`.")
162    return DuckDBCache(
163        db_path=db_file_path,
164        cache_dir=cache_dir,
165        schema_name=schema_name,
166        table_prefix=table_prefix,
167    )

Get a local cache for storing data, using the default database path.

Unlike the default DuckDBCache, this implementation will easily persist data across multiple Colab sessions.

Please note that Google Colab may prompt you to authenticate with your Google account to access your Google Drive. When prompted, click the link and follow the instructions.

Colab will require access to read and write files in your Google Drive, so please be sure to grant the necessary permissions when prompted.

All arguments are optional and have default values that are suitable for most use cases.

Arguments:
  • cache_name: The name to use for the cache. Defaults to "colab_cache". Override this if you want to use a different database for different projects.
  • sub_dir: The subdirectory to store the cache in. Defaults to "Airbyte/cache". Override this if you want to store the cache in a different subdirectory than the default.
  • schema_name: The name of the schema to write to. Defaults to "main". Override this if you want to write to a different schema.
  • table_prefix: The prefix to use for all tables in the cache. Defaults to "". Override this if you want to use a different prefix for all tables.
  • drive_name: The name of the Google Drive to use. Defaults to "MyDrive". Override this if you want to store data in a shared drive instead of your personal drive.
  • mount_path: The path to mount Google Drive to. Defaults to "/content/drive". Override this if you want to mount Google Drive to a different path (not recommended).

Usage Examples

The default get_colab_cache arguments are suitable for most use cases:

from airbyte.caches.colab import get_colab_cache

colab_cache = get_colab_cache()

Or you can call get_colab_cache with custom arguments:

custom_cache = get_colab_cache(
    cache_name="my_custom_cache",
    sub_dir="Airbyte/custom_cache",
    drive_name="My Company Drive",
)
def get_default_cache() -> DuckDBCache:
31def get_default_cache() -> DuckDBCache:
32    """Get a local cache for storing data, using the default database path.
33
34    Cache files are stored in the `.cache` directory, relative to the current
35    working directory.
36    """
37    cache_dir = DEFAULT_CACHE_ROOT / "default_cache"
38    return DuckDBCache(
39        db_path=cache_dir / "default_cache.duckdb",
40        cache_dir=cache_dir,
41    )

Get a local cache for storing data, using the default database path.

Cache files are stored in the .cache directory, relative to the current working directory.

def get_destination( name: str, config: dict[str, typing.Any] | None = None, *, config_change_callback: Callable[[dict[str, typing.Any]], None] | None = None, version: str | None = None, use_python: bool | pathlib.Path | str | None = None, pip_url: str | None = None, local_executable: pathlib.Path | str | None = None, docker_image: str | bool | None = None, use_host_network: bool = False, install_if_missing: bool = True, install_root: pathlib.Path | None = None, no_executor: bool = False) -> Destination:
22def get_destination(  # noqa: PLR0913 # Too many arguments
23    name: str,
24    config: dict[str, Any] | None = None,
25    *,
26    config_change_callback: ConfigChangeCallback | None = None,
27    version: str | None = None,
28    use_python: bool | Path | str | None = None,
29    pip_url: str | None = None,
30    local_executable: Path | str | None = None,
31    docker_image: str | bool | None = None,
32    use_host_network: bool = False,
33    install_if_missing: bool = True,
34    install_root: Path | None = None,
35    no_executor: bool = False,
36) -> Destination:
37    """Get a connector by name and version.
38
39    Args:
40        name: connector name
41        config: connector config - if not provided, you need to set it later via the set_config
42            method.
43        config_change_callback: callback function to be called when the connector config changes.
44        streams: list of stream names to select for reading. If set to "*", all streams will be
45            selected. If not provided, you can set it later via the `select_streams()` or
46            `select_all_streams()` method.
47        version: connector version - if not provided, the currently installed version will be used.
48            If no version is installed, the latest available version will be used. The version can
49            also be set to "latest" to force the use of the latest available version.
50        use_python: (Optional.) Python interpreter specification:
51            - True: Use current Python interpreter. (Inferred if `pip_url` is set.)
52            - False: Use Docker instead.
53            - Path: Use interpreter at this path.
54            - str: Use specific Python version. E.g. "3.11" or "3.11.10". If the version is not yet
55                installed, it will be installed by uv. (This generally adds less than 3 seconds
56                to install times.)
57        pip_url: connector pip URL - if not provided, the pip url will be inferred from the
58            connector name.
59        local_executable: If set, the connector will be assumed to already be installed and will be
60            executed using this path or executable name. Otherwise, the connector will be installed
61            automatically in a virtual environment.
62        docker_image: If set, the connector will be executed using Docker. You can specify `True`
63            to use the default image for the connector, or you can specify a custom image name.
64            If `version` is specified and your image name does not already contain a tag
65            (e.g. `my-image:latest`), the version will be appended as a tag (e.g. `my-image:0.1.0`).
66        use_host_network: If set, along with docker_image, the connector will be executed using
67            the host network. This is useful for connectors that need to access resources on
68            the host machine, such as a local database. This parameter is ignored when
69            `docker_image` is not set.
70        install_if_missing: Whether to install the connector if it is not available locally. This
71            parameter is ignored when local_executable is set.
72        install_root: (Optional.) The root directory where the virtual environment will be
73            created. If not provided, the current working directory will be used.
74        no_executor: If True, use NoOpExecutor which fetches specs from the registry without
75            local installation. This is useful for scenarios where you need to validate
76            configurations but don't need to run the connector locally (e.g., deploying to Cloud).
77    """
78    executor = get_connector_executor(
79        name=name,
80        version=version,
81        use_python=use_python,
82        pip_url=pip_url,
83        local_executable=local_executable,
84        docker_image=docker_image,
85        use_host_network=use_host_network,
86        install_if_missing=install_if_missing,
87        install_root=install_root,
88        no_executor=no_executor,
89    )
90
91    return Destination(
92        name=name,
93        config=config,
94        config_change_callback=config_change_callback,
95        executor=executor,
96    )

Get a connector by name and version.

Arguments:
  • name: connector name
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • config_change_callback: callback function to be called when the connector config changes.
  • streams: list of stream names to select for reading. If set to "*", all streams will be selected. If not provided, you can set it later via the select_streams() or select_all_streams() method.
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • use_python: (Optional.) Python interpreter specification:
    • True: Use current Python interpreter. (Inferred if pip_url is set.)
    • False: Use Docker instead.
    • Path: Use interpreter at this path.
    • str: Use specific Python version. E.g. "3.11" or "3.11.10". If the version is not yet installed, it will be installed by uv. (This generally adds less than 3 seconds to install times.)
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • local_executable: If set, the connector will be assumed to already be installed and will be executed using this path or executable name. Otherwise, the connector will be installed automatically in a virtual environment.
  • docker_image: If set, the connector will be executed using Docker. You can specify True to use the default image for the connector, or you can specify a custom image name. If version is specified and your image name does not already contain a tag (e.g. my-image:latest), the version will be appended as a tag (e.g. my-image:0.1.0).
  • use_host_network: If set, along with docker_image, the connector will be executed using the host network. This is useful for connectors that need to access resources on the host machine, such as a local database. This parameter is ignored when docker_image is not set.
  • install_if_missing: Whether to install the connector if it is not available locally. This parameter is ignored when local_executable is set.
  • install_root: (Optional.) The root directory where the virtual environment will be created. If not provided, the current working directory will be used.
  • no_executor: If True, use NoOpExecutor which fetches specs from the registry without local installation. This is useful for scenarios where you need to validate configurations but don't need to run the connector locally (e.g., deploying to Cloud).
def get_secret( secret_name: str, /, *, sources: list[airbyte.secrets.SecretManager | SecretSourceEnum] | None = None, default: str | airbyte.secrets.SecretString | None = None, allow_prompt: bool = True, **kwargs: dict[str, typing.Any]) -> airbyte.secrets.SecretString:
 64def get_secret(
 65    secret_name: str,
 66    /,
 67    *,
 68    sources: list[SecretManager | SecretSourceEnum] | None = None,
 69    default: str | SecretString | None = None,
 70    allow_prompt: bool = True,
 71    **kwargs: dict[str, Any],
 72) -> SecretString:
 73    """Get a secret from the environment.
 74
 75    The optional `sources` argument of enum type `SecretSourceEnum` or list of `SecretSourceEnum`
 76    options. If left blank, all available sources will be checked. If a list of `SecretSourceEnum`
 77    entries is passed, then the sources will be checked using the provided ordering.
 78
 79    If `allow_prompt` is `True` or if SecretSourceEnum.PROMPT is declared in the `source` arg, then
 80    the user will be prompted to enter the secret if it is not found in any of the other sources.
 81
 82    Raises:
 83        PyAirbyteSecretNotFoundError: If the secret is not found in any of the configured sources,
 84            and if no default value is provided.
 85        PyAirbyteInputError: If an invalid source name is provided in the `sources` argument.
 86    """
 87    if secret_name.startswith(SECRETS_HYDRATION_PREFIX):
 88        # If the secret name starts with the hydration prefix, we assume it's a secret reference.
 89        # We strip the prefix and get the actual secret name.
 90        secret_name = secret_name.removeprefix(SECRETS_HYDRATION_PREFIX).lstrip()
 91
 92    if "source" in kwargs:
 93        warnings.warn(
 94            message="The `source` argument is deprecated. Use the `sources` argument instead.",
 95            category=DeprecationWarning,
 96            stacklevel=2,
 97        )
 98        sources = kwargs.pop("source")  # type: ignore [assignment]
 99
100    available_sources: dict[str, SecretManager] = {}
101    for available_source in _get_secret_sources():
102        # Add available sources to the dict. Order matters.
103        available_sources[available_source.name] = available_source
104
105    if sources is None:
106        # If ANY is in the list, then we don't need to check any other sources.
107        # This is the default behavior.
108        sources = list(available_sources.values())
109
110    elif not isinstance(sources, list):
111        sources = [sources]  # type: ignore [unreachable]  # This is a 'just in case' catch.
112
113    # Replace any SecretSourceEnum strings with the matching SecretManager object
114    for source in list(sources):
115        if isinstance(source, SecretSourceEnum):
116            if source not in available_sources:
117                raise exc.PyAirbyteInputError(
118                    guidance="Invalid secret source name.",
119                    input_value=source,
120                    context={
121                        "Available Sources": list(available_sources.keys()),
122                    },
123                )
124
125            sources[sources.index(source)] = available_sources[source]
126
127    secret_managers = cast("list[SecretManager]", sources)
128
129    if SecretSourceEnum.PROMPT in secret_managers:
130        prompt_source = secret_managers.pop(
131            # Mis-typed, but okay here since we have equality logic for the enum comparison:
132            secret_managers.index(SecretSourceEnum.PROMPT),  # type: ignore [arg-type]
133        )
134
135        if allow_prompt:
136            # Always check prompt last. Add it to the end of the list.
137            secret_managers.append(prompt_source)
138
139    for secret_mgr in secret_managers:
140        val = secret_mgr.get_secret(secret_name)
141        if val:
142            return SecretString(val)
143
144    if default:
145        return SecretString(default)
146
147    raise exc.PyAirbyteSecretNotFoundError(
148        secret_name=secret_name,
149        sources=[str(s) for s in available_sources],
150    )

Get a secret from the environment.

The optional sources argument of enum type SecretSourceEnum or list of SecretSourceEnum options. If left blank, all available sources will be checked. If a list of SecretSourceEnum entries is passed, then the sources will be checked using the provided ordering.

If allow_prompt is True or if SecretSourceEnum.PROMPT is declared in the source arg, then the user will be prompted to enter the secret if it is not found in any of the other sources.

Raises:
  • PyAirbyteSecretNotFoundError: If the secret is not found in any of the configured sources, and if no default value is provided.
  • PyAirbyteInputError: If an invalid source name is provided in the sources argument.
def get_source( name: str, config: dict[str, typing.Any] | None = None, *, config_change_callback: Callable[[dict[str, typing.Any]], None] | None = None, streams: str | list[str] | None = None, version: str | None = None, use_python: bool | pathlib.Path | str | None = None, pip_url: str | None = None, local_executable: pathlib.Path | str | None = None, docker_image: bool | str | None = None, use_host_network: bool = False, source_manifest: bool | dict | pathlib.Path | str | None = None, install_if_missing: bool = True, install_root: pathlib.Path | None = None, no_executor: bool = False) -> Source:
 48def get_source(  # noqa: PLR0913 # Too many arguments
 49    name: str,
 50    config: dict[str, Any] | None = None,
 51    *,
 52    config_change_callback: ConfigChangeCallback | None = None,
 53    streams: str | list[str] | None = None,
 54    version: str | None = None,
 55    use_python: bool | Path | str | None = None,
 56    pip_url: str | None = None,
 57    local_executable: Path | str | None = None,
 58    docker_image: bool | str | None = None,
 59    use_host_network: bool = False,
 60    source_manifest: bool | dict | Path | str | None = None,
 61    install_if_missing: bool = True,
 62    install_root: Path | None = None,
 63    no_executor: bool = False,
 64) -> Source:
 65    """Get a connector by name and version.
 66
 67    If an explicit install or execution method is requested (e.g. `local_executable`,
 68    `docker_image`, `pip_url`, `source_manifest`), the connector will be executed using this method.
 69
 70    Otherwise, an appropriate method will be selected based on the available connector metadata:
 71    1. If the connector is registered and has a YAML source manifest is available, the YAML manifest
 72       will be downloaded and used to to execute the connector.
 73    2. Else, if the connector is registered and has a PyPI package, it will be installed via pip.
 74    3. Else, if the connector is registered and has a Docker image, and if Docker is available, it
 75       will be executed using Docker.
 76
 77    Args:
 78        name: connector name
 79        config: connector config - if not provided, you need to set it later via the set_config
 80            method.
 81        config_change_callback: callback function to be called when the connector config changes.
 82        streams: list of stream names to select for reading. If set to "*", all streams will be
 83            selected. If not provided, you can set it later via the `select_streams()` or
 84            `select_all_streams()` method.
 85        version: connector version - if not provided, the currently installed version will be used.
 86            If no version is installed, the latest available version will be used. The version can
 87            also be set to "latest" to force the use of the latest available version.
 88        use_python: (Optional.) Python interpreter specification:
 89            - True: Use current Python interpreter. (Inferred if `pip_url` is set.)
 90            - False: Use Docker instead.
 91            - Path: Use interpreter at this path.
 92            - str: Use specific Python version. E.g. "3.11" or "3.11.10". If the version is not yet
 93                installed, it will be installed by uv. (This generally adds less than 3 seconds
 94                to install times.)
 95        pip_url: connector pip URL - if not provided, the pip url will be inferred from the
 96            connector name.
 97        local_executable: If set, the connector will be assumed to already be installed and will be
 98            executed using this path or executable name. Otherwise, the connector will be installed
 99            automatically in a virtual environment.
100        docker_image: If set, the connector will be executed using Docker. You can specify `True`
101            to use the default image for the connector, or you can specify a custom image name.
102            If `version` is specified and your image name does not already contain a tag
103            (e.g. `my-image:latest`), the version will be appended as a tag (e.g. `my-image:0.1.0`).
104        use_host_network: If set, along with docker_image, the connector will be executed using
105            the host network. This is useful for connectors that need to access resources on
106            the host machine, such as a local database. This parameter is ignored when
107            `docker_image` is not set.
108        source_manifest: If set, the connector will be executed based on a declarative YAML
109            source definition. This input can be `True` to attempt to auto-download a YAML spec,
110            `dict` to accept a Python dictionary as the manifest, `Path` to pull a manifest from
111            the local file system, or `str` to pull the definition from a web URL.
112        install_if_missing: Whether to install the connector if it is not available locally. This
113            parameter is ignored when `local_executable` or `source_manifest` are set.
114        install_root: (Optional.) The root directory where the virtual environment will be
115            created. If not provided, the current working directory will be used.
116        no_executor: If True, use NoOpExecutor which fetches specs from the registry without
117            local installation. This is useful for scenarios where you need to validate
118            configurations but don't need to run the connector locally (e.g., deploying to Cloud).
119    """
120    executor = get_connector_executor(
121        name=name,
122        version=version,
123        use_python=use_python,
124        pip_url=pip_url,
125        local_executable=local_executable,
126        docker_image=docker_image,
127        use_host_network=use_host_network,
128        source_manifest=source_manifest,
129        install_if_missing=install_if_missing,
130        install_root=install_root,
131        no_executor=no_executor,
132    )
133
134    return Source(
135        name=name,
136        config=config,
137        config_change_callback=config_change_callback,
138        streams=streams,
139        executor=executor,
140    )

Get a connector by name and version.

If an explicit install or execution method is requested (e.g. local_executable, docker_image, pip_url, source_manifest), the connector will be executed using this method.

Otherwise, an appropriate method will be selected based on the available connector metadata:

  1. If the connector is registered and has a YAML source manifest is available, the YAML manifest will be downloaded and used to to execute the connector.
  2. Else, if the connector is registered and has a PyPI package, it will be installed via pip.
  3. Else, if the connector is registered and has a Docker image, and if Docker is available, it will be executed using Docker.
Arguments:
  • name: connector name
  • config: connector config - if not provided, you need to set it later via the set_config method.
  • config_change_callback: callback function to be called when the connector config changes.
  • streams: list of stream names to select for reading. If set to "*", all streams will be selected. If not provided, you can set it later via the select_streams() or select_all_streams() method.
  • version: connector version - if not provided, the currently installed version will be used. If no version is installed, the latest available version will be used. The version can also be set to "latest" to force the use of the latest available version.
  • use_python: (Optional.) Python interpreter specification:
    • True: Use current Python interpreter. (Inferred if pip_url is set.)
    • False: Use Docker instead.
    • Path: Use interpreter at this path.
    • str: Use specific Python version. E.g. "3.11" or "3.11.10". If the version is not yet installed, it will be installed by uv. (This generally adds less than 3 seconds to install times.)
  • pip_url: connector pip URL - if not provided, the pip url will be inferred from the connector name.
  • local_executable: If set, the connector will be assumed to already be installed and will be executed using this path or executable name. Otherwise, the connector will be installed automatically in a virtual environment.
  • docker_image: If set, the connector will be executed using Docker. You can specify True to use the default image for the connector, or you can specify a custom image name. If version is specified and your image name does not already contain a tag (e.g. my-image:latest), the version will be appended as a tag (e.g. my-image:0.1.0).
  • use_host_network: If set, along with docker_image, the connector will be executed using the host network. This is useful for connectors that need to access resources on the host machine, such as a local database. This parameter is ignored when docker_image is not set.
  • source_manifest: If set, the connector will be executed based on a declarative YAML source definition. This input can be True to attempt to auto-download a YAML spec, dict to accept a Python dictionary as the manifest, Path to pull a manifest from the local file system, or str to pull the definition from a web URL.
  • install_if_missing: Whether to install the connector if it is not available locally. This parameter is ignored when local_executable or source_manifest are set.
  • install_root: (Optional.) The root directory where the virtual environment will be created. If not provided, the current working directory will be used.
  • no_executor: If True, use NoOpExecutor which fetches specs from the registry without local installation. This is useful for scenarios where you need to validate configurations but don't need to run the connector locally (e.g., deploying to Cloud).
def new_local_cache( cache_name: str | None = None, cache_dir: str | pathlib.Path | None = None, *, cleanup: bool = True) -> DuckDBCache:
44def new_local_cache(
45    cache_name: str | None = None,
46    cache_dir: str | Path | None = None,
47    *,
48    cleanup: bool = True,
49) -> DuckDBCache:
50    """Get a local cache for storing data, using a name string to seed the path.
51
52    Args:
53        cache_name: Name to use for the cache. Defaults to None.
54        cache_dir: Root directory to store the cache in. Defaults to None.
55        cleanup: Whether to clean up temporary files. Defaults to True.
56
57    Cache files are stored in the `.cache` directory, relative to the current
58    working directory.
59    """
60    if cache_name:
61        if " " in cache_name:
62            raise exc.PyAirbyteInputError(
63                message="Cache name cannot contain spaces.",
64                input_value=cache_name,
65            )
66
67        if not cache_name.replace("_", "").isalnum():
68            raise exc.PyAirbyteInputError(
69                message="Cache name can only contain alphanumeric characters and underscores.",
70                input_value=cache_name,
71            )
72
73    cache_name = cache_name or str(ulid.ULID())
74    cache_dir = cache_dir or (DEFAULT_CACHE_ROOT / cache_name)
75    if not isinstance(cache_dir, Path):
76        cache_dir = Path(cache_dir)
77
78    return DuckDBCache(
79        db_path=cache_dir / f"db_{cache_name}.duckdb",
80        cache_dir=cache_dir,
81        cleanup=cleanup,
82    )

Get a local cache for storing data, using a name string to seed the path.

Arguments:
  • cache_name: Name to use for the cache. Defaults to None.
  • cache_dir: Root directory to store the cache in. Defaults to None.
  • cleanup: Whether to clean up temporary files. Defaults to True.

Cache files are stored in the .cache directory, relative to the current working directory.

class BigQueryCache(airbyte._processors.sql.bigquery.BigQueryConfig, airbyte.caches.base.CacheBase):
39class BigQueryCache(BigQueryConfig, CacheBase):
40    """The BigQuery cache implementation."""
41
42    _sql_processor_class: ClassVar[type[SqlProcessorBase]] = BigQuerySqlProcessor
43
44    paired_destination_name: ClassVar[str | None] = "destination-bigquery"
45    paired_destination_config_class: ClassVar[type | None] = DestinationBigquery
46
47    @property
48    def paired_destination_config(self) -> DestinationBigquery:
49        """Return a dictionary of destination configuration values."""
50        return bigquery_cache_to_destination_configuration(cache=self)
51
52    def get_arrow_dataset(
53        self,
54        stream_name: str,
55        *,
56        max_chunk_size: int = DEFAULT_ARROW_MAX_CHUNK_SIZE,
57    ) -> NoReturn:
58        """Raises NotImplementedError; BigQuery doesn't support `pd.read_sql_table`.
59
60        See: https://github.com/airbytehq/PyAirbyte/issues/165
61        """
62        raise NotImplementedError(
63            "BigQuery doesn't currently support to_arrow"
64            "Please consider using a different cache implementation for these functionalities."
65        )

The BigQuery cache implementation.

paired_destination_name: ClassVar[str | None] = 'destination-bigquery'
paired_destination_config_class: ClassVar[type | None] = <class 'airbyte_api.models.destination_bigquery.DestinationBigquery'>
paired_destination_config: airbyte_api.models.destination_bigquery.DestinationBigquery
47    @property
48    def paired_destination_config(self) -> DestinationBigquery:
49        """Return a dictionary of destination configuration values."""
50        return bigquery_cache_to_destination_configuration(cache=self)

Return a dictionary of destination configuration values.

def get_arrow_dataset(self, stream_name: str, *, max_chunk_size: int = 100000) -> NoReturn:
52    def get_arrow_dataset(
53        self,
54        stream_name: str,
55        *,
56        max_chunk_size: int = DEFAULT_ARROW_MAX_CHUNK_SIZE,
57    ) -> NoReturn:
58        """Raises NotImplementedError; BigQuery doesn't support `pd.read_sql_table`.
59
60        See: https://github.com/airbytehq/PyAirbyte/issues/165
61        """
62        raise NotImplementedError(
63            "BigQuery doesn't currently support to_arrow"
64            "Please consider using a different cache implementation for these functionalities."
65        )

Raises NotImplementedError; BigQuery doesn't support pd.read_sql_table.

See: https://github.com/airbytehq/PyAirbyte/issues/165

class CachedDataset(airbyte.datasets._sql.SQLDataset):
150class CachedDataset(SQLDataset):
151    """A dataset backed by a SQL table cache.
152
153    Because this dataset includes all records from the underlying table, we also expose the
154    underlying table as a SQLAlchemy Table object.
155    """
156
157    def __init__(
158        self,
159        cache: CacheBase,
160        stream_name: str,
161        stream_configuration: ConfiguredAirbyteStream | Literal[False] | None = None,
162    ) -> None:
163        """We construct the query statement by selecting all columns from the table.
164
165        This prevents the need to scan the table schema to construct the query statement.
166
167        If stream_configuration is None, we attempt to retrieve the stream configuration from the
168        cache processor. This is useful when constructing a dataset from a CachedDataset object,
169        which already has the stream configuration.
170
171        If stream_configuration is set to False, we skip the stream configuration retrieval.
172        """
173        table_name = cache.processor.get_sql_table_name(stream_name)
174        schema_name = cache.schema_name
175        query = select("*").select_from(text(f"{schema_name}.{table_name}"))
176        super().__init__(
177            cache=cache,
178            stream_name=stream_name,
179            query_statement=query,
180            stream_configuration=stream_configuration,
181        )
182
183    @overrides
184    def to_pandas(self) -> DataFrame:
185        """Return the underlying dataset data as a pandas DataFrame."""
186        return self._cache.get_pandas_dataframe(self._stream_name)
187
188    @overrides
189    def to_arrow(
190        self,
191        *,
192        max_chunk_size: int = DEFAULT_ARROW_MAX_CHUNK_SIZE,
193    ) -> Dataset:
194        """Return an Arrow Dataset containing the data from the specified stream.
195
196        Args:
197            stream_name (str): Name of the stream to retrieve data from.
198            max_chunk_size (int): max number of records to include in each batch of pyarrow dataset.
199
200        Returns:
201            pa.dataset.Dataset: Arrow Dataset containing the stream's data.
202        """
203        return self._cache.get_arrow_dataset(
204            stream_name=self._stream_name,
205            max_chunk_size=max_chunk_size,
206        )
207
208    def to_sql_table(self) -> Table:
209        """Return the underlying SQL table as a SQLAlchemy Table object."""
210        return self._cache.processor.get_sql_table(self.stream_name)
211
212    def __eq__(self, value: object) -> bool:
213        """Return True if the value is a CachedDataset with the same cache and stream name.
214
215        In the case of CachedDataset objects, we can simply compare the cache and stream name.
216
217        Note that this equality check is only supported on CachedDataset objects and not for
218        the base SQLDataset implementation. This is because of the complexity and computational
219        cost of comparing two arbitrary SQL queries that could be bound to different variables,
220        as well as the chance that two queries can be syntactically equivalent without being
221        text-wise equivalent.
222        """
223        if not isinstance(value, SQLDataset):
224            return False
225
226        if self._cache is not value._cache:
227            return False
228
229        return not self._stream_name != value._stream_name
230
231    def __hash__(self) -> int:
232        return hash(self._stream_name)

A dataset backed by a SQL table cache.

Because this dataset includes all records from the underlying table, we also expose the underlying table as a SQLAlchemy Table object.

CachedDataset( cache: airbyte.caches.CacheBase, stream_name: str, stream_configuration: Union[airbyte_protocol.models.airbyte_protocol.ConfiguredAirbyteStream, Literal[False], NoneType] = None)
157    def __init__(
158        self,
159        cache: CacheBase,
160        stream_name: str,
161        stream_configuration: ConfiguredAirbyteStream | Literal[False] | None = None,
162    ) -> None:
163        """We construct the query statement by selecting all columns from the table.
164
165        This prevents the need to scan the table schema to construct the query statement.
166
167        If stream_configuration is None, we attempt to retrieve the stream configuration from the
168        cache processor. This is useful when constructing a dataset from a CachedDataset object,
169        which already has the stream configuration.
170
171        If stream_configuration is set to False, we skip the stream configuration retrieval.
172        """
173        table_name = cache.processor.get_sql_table_name(stream_name)
174        schema_name = cache.schema_name
175        query = select("*").select_from(text(f"{schema_name}.{table_name}"))
176        super().__init__(
177            cache=cache,
178            stream_name=stream_name,
179            query_statement=query,
180            stream_configuration=stream_configuration,
181        )

We construct the query statement by selecting all columns from the table.

This prevents the need to scan the table schema to construct the query statement.

If stream_configuration is None, we attempt to retrieve the stream configuration from the cache processor. This is useful when constructing a dataset from a CachedDataset object, which already has the stream configuration.

If stream_configuration is set to False, we skip the stream configuration retrieval.

@overrides
def to_pandas(self) -> pandas.core.frame.DataFrame:
183    @overrides
184    def to_pandas(self) -> DataFrame:
185        """Return the underlying dataset data as a pandas DataFrame."""
186        return self._cache.get_pandas_dataframe(self._stream_name)

Return the underlying dataset data as a pandas DataFrame.

@overrides
def to_arrow(self, *, max_chunk_size: int = 100000) -> pyarrow._dataset.Dataset:
188    @overrides
189    def to_arrow(
190        self,
191        *,
192        max_chunk_size: int = DEFAULT_ARROW_MAX_CHUNK_SIZE,
193    ) -> Dataset:
194        """Return an Arrow Dataset containing the data from the specified stream.
195
196        Args:
197            stream_name (str): Name of the stream to retrieve data from.
198            max_chunk_size (int): max number of records to include in each batch of pyarrow dataset.
199
200        Returns:
201            pa.dataset.Dataset: Arrow Dataset containing the stream's data.
202        """
203        return self._cache.get_arrow_dataset(
204            stream_name=self._stream_name,
205            max_chunk_size=max_chunk_size,
206        )

Return an Arrow Dataset containing the data from the specified stream.

Arguments:
  • stream_name (str): Name of the stream to retrieve data from.
  • max_chunk_size (int): max number of records to include in each batch of pyarrow dataset.
Returns:

pa.dataset.Dataset: Arrow Dataset containing the stream's data.

def to_sql_table(self) -> sqlalchemy.sql.schema.Table:
208    def to_sql_table(self) -> Table:
209        """Return the underlying SQL table as a SQLAlchemy Table object."""
210        return self._cache.processor.get_sql_table(self.stream_name)

Return the underlying SQL table as a SQLAlchemy Table object.

class Destination(airbyte._connector_base.ConnectorBase, airbyte._writers.base.AirbyteWriterInterface):
 46class Destination(ConnectorBase, AirbyteWriterInterface):
 47    """A class representing a destination that can be called."""
 48
 49    connector_type = "destination"
 50
 51    def __init__(
 52        self,
 53        executor: Executor,
 54        name: str,
 55        config: dict[str, Any] | None = None,
 56        *,
 57        config_change_callback: ConfigChangeCallback | None = None,
 58        validate: bool = False,
 59    ) -> None:
 60        """Initialize the source.
 61
 62        If config is provided, it will be validated against the spec if validate is True.
 63        """
 64        super().__init__(
 65            executor=executor,
 66            name=name,
 67            config=config,
 68            config_change_callback=config_change_callback,
 69            validate=validate,
 70        )
 71
 72    @staticmethod
 73    def _normalize_destination_name(name: str) -> str:
 74        """Normalize a destination name to canonical form (`destination-<type>`).
 75
 76        Accepts either the short form (e.g. `snowflake`) or the canonical
 77        form (e.g. `destination-snowflake`).
 78        """
 79        if not name.startswith(_CANONICAL_PREFIX):
 80            return f"{_CANONICAL_PREFIX}{name}"
 81        return name
 82
 83    @property
 84    def is_cache_supported(self) -> bool:
 85        """Whether this destination has a compatible cache implementation.
 86
 87        Returns `True` when `get_sql_cache()` is expected to succeed for
 88        the destination's connector type.
 89        """
 90        dest_type = self._normalize_destination_name(
 91            self.name,
 92        ).replace(_CANONICAL_PREFIX, "")
 93        return dest_type in get_supported_destination_types()
 94
 95    def get_sql_cache(
 96        self,
 97        *,
 98        schema_name: str | None = None,
 99    ) -> CacheBase:
100        """Return a SQL Cache for querying data written by this destination.
101
102        This follows the same pattern as
103        `SyncResult.get_sql_cache()` in `airbyte.cloud.sync_results`:
104        it builds a cache from the destination's configuration using
105        `destination_to_cache()`.
106
107        Args:
108            schema_name: Override the schema/namespace on the returned cache.
109                When `None` the cache uses the default schema from the
110                destination config.
111
112        Raises:
113            ValueError: If the destination type is not supported.
114        """
115        resolved_name = self._normalize_destination_name(self.name)
116        config = dict(self._hydrated_config)
117
118        # Ensure the config carries a destinationType key so that
119        # destination_to_cache() can dispatch correctly.
120        if "destinationType" not in config and "DESTINATION_TYPE" not in config:
121            dest_type = resolved_name.replace(_CANONICAL_PREFIX, "")
122            config["destinationType"] = dest_type
123
124        return destination_to_cache(config, schema_name=schema_name)
125
126    def write(  # noqa: PLR0912, PLR0915 # Too many arguments/statements
127        self,
128        source_data: Source | ReadResult,
129        *,
130        streams: list[str] | Literal["*"] | None = None,
131        cache: CacheBase | Literal[False] | None = None,
132        state_cache: CacheBase | Literal[False] | None = None,
133        write_strategy: WriteStrategy = WriteStrategy.AUTO,
134        force_full_refresh: bool = False,
135    ) -> WriteResult:
136        """Write data from source connector or already cached source data.
137
138        Caching is enabled by default, unless explicitly disabled.
139
140        Args:
141            source_data: The source data to write. Can be a `Source` or a `ReadResult` object.
142            streams: The streams to write to the destination. If omitted or if "*" is provided,
143                all streams will be written. If `source_data` is a source, then streams must be
144                selected here or on the source. If both are specified, this setting will override
145                the stream selection on the source.
146            cache: The cache to use for reading source_data. If `None`, no cache will be used. If
147                False, the cache will be disabled. This must be `None` if `source_data` is already
148                a `Cache` object.
149            state_cache: A cache to use for storing incremental state. You do not need to set this
150                if `cache` is specified or if `source_data` is a `Cache` object. Set to `False` to
151                disable state management.
152            write_strategy: The strategy to use for writing source_data. If `AUTO`, the connector
153                will decide the best strategy to use.
154            force_full_refresh: Whether to force a full refresh of the source_data. If `True`, any
155                existing state will be ignored and all source data will be reloaded.
156
157        For incremental syncs, `cache` or `state_cache` will be checked for matching state values.
158        If the cache has tracked state, this will be used for the sync. Otherwise, if there is
159        a known destination state, the destination-specific state will be used. If neither are
160        available, a full refresh will be performed.
161        """
162        if not isinstance(source_data, ReadResult | Source):
163            raise exc.PyAirbyteInputError(
164                message="Invalid source_data type for `source_data` arg.",
165                context={
166                    "source_data_type_provided": type(source_data).__name__,
167                },
168            )
169
170        # Resolve `source`, `read_result`, and `source_name`
171        source: Source | None = source_data if isinstance(source_data, Source) else None
172        read_result: ReadResult | None = (
173            source_data if isinstance(source_data, ReadResult) else None
174        )
175        source_name: str = source.name if source else cast("ReadResult", read_result).source_name
176
177        # State providers and writers default to no-op, unless overridden below.
178        cache_state_provider: StateProviderBase = StaticInputState([])
179        """Provides the state of the cache's data."""
180        cache_state_writer: StateWriterBase = NoOpStateWriter()
181        """Writes updates for the state of the cache's data."""
182        destination_state_provider: StateProviderBase = StaticInputState([])
183        """Provides the state of the destination's data, from `cache` or `state_cache`."""
184        destination_state_writer: StateWriterBase = NoOpStateWriter()
185        """Writes updates for the state of the destination's data, to `cache` or `state_cache`."""
186
187        # If caching not explicitly disabled
188        if cache is not False:
189            # Resolve `cache`, `cache_state_provider`, and `cache_state_writer`
190            if isinstance(source_data, ReadResult):
191                cache = source_data.cache
192
193            cache = cache or get_default_cache()
194            cache_state_provider = cache.get_state_provider(
195                source_name=source_name,
196                destination_name=None,  # This will just track the cache state
197            )
198            cache_state_writer = cache.get_state_writer(
199                source_name=source_name,
200                destination_name=None,  # This will just track the cache state
201            )
202
203        # Resolve `state_cache`
204        if state_cache is None:
205            state_cache = cache or get_default_cache()
206
207        # Resolve `destination_state_writer` and `destination_state_provider`
208        if state_cache:
209            destination_state_writer = state_cache.get_state_writer(
210                source_name=source_name,
211                destination_name=self.name,
212            )
213            if not force_full_refresh:
214                destination_state_provider = state_cache.get_state_provider(
215                    source_name=source_name,
216                    destination_name=self.name,
217                )
218        elif state_cache is not False:
219            warnings.warn(
220                "No state backend or cache provided. State will not be tracked."
221                "To track state, provide a cache or state backend."
222                "To silence this warning, set `state_cache=False` explicitly.",
223                category=exc.PyAirbyteWarning,
224                stacklevel=2,
225            )
226
227        # Resolve `catalog_provider`
228        if source:
229            catalog_provider = CatalogProvider(
230                configured_catalog=source.get_configured_catalog(
231                    streams=streams,
232                    force_full_refresh=force_full_refresh,
233                )
234            )
235        elif read_result:
236            catalog_provider = CatalogProvider.from_read_result(read_result)
237        else:
238            raise exc.PyAirbyteInternalError(
239                message="`source_data` must be a `Source` or `ReadResult` object.",
240            )
241
242        progress_tracker = ProgressTracker(
243            source=source if isinstance(source_data, Source) else None,
244            cache=cache or None,
245            destination=self,
246            expected_streams=catalog_provider.stream_names,
247        )
248
249        source_state_provider: StateProviderBase
250        source_state_provider = JoinedStateProvider(
251            primary=cache_state_provider,
252            secondary=destination_state_provider,
253        )
254
255        if source:
256            if cache is False:
257                # Get message iterator for source (caching disabled)
258                message_iterator: AirbyteMessageIterator = source._get_airbyte_message_iterator(  # noqa: SLF001 # Non-public API
259                    streams=streams,
260                    state_provider=source_state_provider,
261                    progress_tracker=progress_tracker,
262                    force_full_refresh=force_full_refresh,
263                )
264            else:
265                # Caching enabled and we are reading from a source.
266                # Read the data to cache if caching is enabled.
267                read_result = source._read_to_cache(  # noqa: SLF001  # Non-public API
268                    cache=cache,
269                    state_provider=source_state_provider,
270                    state_writer=cache_state_writer,
271                    catalog_provider=catalog_provider,
272                    stream_names=catalog_provider.stream_names,
273                    write_strategy=write_strategy,
274                    force_full_refresh=force_full_refresh,
275                    skip_validation=False,
276                    progress_tracker=progress_tracker,
277                )
278                message_iterator = AirbyteMessageIterator.from_read_result(
279                    read_result=read_result,
280                )
281        else:  # Else we are reading from a read result
282            assert read_result is not None
283            message_iterator = AirbyteMessageIterator.from_read_result(
284                read_result=read_result,
285            )
286
287        # Write the data to the destination
288        try:
289            self._write_airbyte_message_stream(
290                stdin=message_iterator,
291                catalog_provider=catalog_provider,
292                write_strategy=write_strategy,
293                state_writer=destination_state_writer,
294                progress_tracker=progress_tracker,
295            )
296        except Exception as ex:
297            progress_tracker.log_failure(exception=ex)
298            raise
299        else:
300            # No exceptions were raised, so log success
301            progress_tracker.log_success()
302
303        return WriteResult(
304            destination=self,
305            source_data=source_data,
306            catalog_provider=catalog_provider,
307            state_writer=destination_state_writer,
308            progress_tracker=progress_tracker,
309        )
310
311    def _write_airbyte_message_stream(
312        self,
313        stdin: IO[str] | AirbyteMessageIterator,
314        *,
315        catalog_provider: CatalogProvider,
316        write_strategy: WriteStrategy,
317        state_writer: StateWriterBase | None = None,
318        progress_tracker: ProgressTracker,
319    ) -> None:
320        """Read from the connector and write to the cache."""
321        # Run optional validation step
322        if state_writer is None:
323            state_writer = StdOutStateWriter()
324
325        # Apply the write strategy to the catalog provider before sending to the destination
326        catalog_provider = catalog_provider.with_write_strategy(write_strategy)
327
328        with as_temp_files(
329            files_contents=[
330                self._hydrated_config,
331                catalog_provider.configured_catalog.model_dump_json(exclude_none=True),
332            ]
333        ) as [
334            config_file,
335            catalog_file,
336        ]:
337            try:
338                # We call the connector to write the data, tallying the inputs and outputs
339                for destination_message in progress_tracker.tally_confirmed_writes(
340                    messages=self._execute(
341                        args=[
342                            "write",
343                            "--config",
344                            config_file,
345                            "--catalog",
346                            catalog_file,
347                        ],
348                        stdin=AirbyteMessageIterator(
349                            progress_tracker.tally_pending_writes(
350                                stdin,
351                            )
352                        ),
353                    )
354                ):
355                    if destination_message.state:
356                        state_writer.write_state(state_message=destination_message.state)
357
358            except exc.AirbyteConnectorFailedError as ex:
359                raise exc.AirbyteConnectorWriteError(
360                    connector_name=self.name,
361                    log_text=self._last_log_messages,
362                    original_exception=ex,
363                ) from None

A class representing a destination that can be called.

Destination( executor: airbyte._executors.base.Executor, name: str, config: dict[str, typing.Any] | None = None, *, config_change_callback: Callable[[dict[str, typing.Any]], None] | None = None, validate: bool = False)
51    def __init__(
52        self,
53        executor: Executor,
54        name: str,
55        config: dict[str, Any] | None = None,
56        *,
57        config_change_callback: ConfigChangeCallback | None = None,
58        validate: bool = False,
59    ) -> None:
60        """Initialize the source.
61
62        If config is provided, it will be validated against the spec if validate is True.
63        """
64        super().__init__(
65            executor=executor,
66            name=name,
67            config=config,
68            config_change_callback=config_change_callback,
69            validate=validate,
70        )

Initialize the source.

If config is provided, it will be validated against the spec if validate is True.

connector_type = 'destination'
is_cache_supported: bool
83    @property
84    def is_cache_supported(self) -> bool:
85        """Whether this destination has a compatible cache implementation.
86
87        Returns `True` when `get_sql_cache()` is expected to succeed for
88        the destination's connector type.
89        """
90        dest_type = self._normalize_destination_name(
91            self.name,
92        ).replace(_CANONICAL_PREFIX, "")
93        return dest_type in get_supported_destination_types()

Whether this destination has a compatible cache implementation.

Returns True when get_sql_cache() is expected to succeed for the destination's connector type.

def get_sql_cache(self, *, schema_name: str | None = None) -> airbyte.caches.CacheBase:
 95    def get_sql_cache(
 96        self,
 97        *,
 98        schema_name: str | None = None,
 99    ) -> CacheBase:
100        """Return a SQL Cache for querying data written by this destination.
101
102        This follows the same pattern as
103        `SyncResult.get_sql_cache()` in `airbyte.cloud.sync_results`:
104        it builds a cache from the destination's configuration using
105        `destination_to_cache()`.
106
107        Args:
108            schema_name: Override the schema/namespace on the returned cache.
109                When `None` the cache uses the default schema from the
110                destination config.
111
112        Raises:
113            ValueError: If the destination type is not supported.
114        """
115        resolved_name = self._normalize_destination_name(self.name)
116        config = dict(self._hydrated_config)
117
118        # Ensure the config carries a destinationType key so that
119        # destination_to_cache() can dispatch correctly.
120        if "destinationType" not in config and "DESTINATION_TYPE" not in config:
121            dest_type = resolved_name.replace(_CANONICAL_PREFIX, "")
122            config["destinationType"] = dest_type
123
124        return destination_to_cache(config, schema_name=schema_name)

Return a SQL Cache for querying data written by this destination.

This follows the same pattern as SyncResult.get_sql_cache() in airbyte.cloud.sync_results: it builds a cache from the destination's configuration using destination_to_cache().

Arguments:
  • schema_name: Override the schema/namespace on the returned cache. When None the cache uses the default schema from the destination config.
Raises:
  • ValueError: If the destination type is not supported.
def write( self, source_data: Source | ReadResult, *, streams: Union[list[str], Literal['*'], NoneType] = None, cache: Union[airbyte.caches.CacheBase, Literal[False], NoneType] = None, state_cache: Union[airbyte.caches.CacheBase, Literal[False], NoneType] = None, write_strategy: airbyte.strategies.WriteStrategy = <WriteStrategy.AUTO: 'auto'>, force_full_refresh: bool = False) -> WriteResult:
126    def write(  # noqa: PLR0912, PLR0915 # Too many arguments/statements
127        self,
128        source_data: Source | ReadResult,
129        *,
130        streams: list[str] | Literal["*"] | None = None,
131        cache: CacheBase | Literal[False] | None = None,
132        state_cache: CacheBase | Literal[False] | None = None,
133        write_strategy: WriteStrategy = WriteStrategy.AUTO,
134        force_full_refresh: bool = False,
135    ) -> WriteResult:
136        """Write data from source connector or already cached source data.
137
138        Caching is enabled by default, unless explicitly disabled.
139
140        Args:
141            source_data: The source data to write. Can be a `Source` or a `ReadResult` object.
142            streams: The streams to write to the destination. If omitted or if "*" is provided,
143                all streams will be written. If `source_data` is a source, then streams must be
144                selected here or on the source. If both are specified, this setting will override
145                the stream selection on the source.
146            cache: The cache to use for reading source_data. If `None`, no cache will be used. If
147                False, the cache will be disabled. This must be `None` if `source_data` is already
148                a `Cache` object.
149            state_cache: A cache to use for storing incremental state. You do not need to set this
150                if `cache` is specified or if `source_data` is a `Cache` object. Set to `False` to
151                disable state management.
152            write_strategy: The strategy to use for writing source_data. If `AUTO`, the connector
153                will decide the best strategy to use.
154            force_full_refresh: Whether to force a full refresh of the source_data. If `True`, any
155                existing state will be ignored and all source data will be reloaded.
156
157        For incremental syncs, `cache` or `state_cache` will be checked for matching state values.
158        If the cache has tracked state, this will be used for the sync. Otherwise, if there is
159        a known destination state, the destination-specific state will be used. If neither are
160        available, a full refresh will be performed.
161        """
162        if not isinstance(source_data, ReadResult | Source):
163            raise exc.PyAirbyteInputError(
164                message="Invalid source_data type for `source_data` arg.",
165                context={
166                    "source_data_type_provided": type(source_data).__name__,
167                },
168            )
169
170        # Resolve `source`, `read_result`, and `source_name`
171        source: Source | None = source_data if isinstance(source_data, Source) else None
172        read_result: ReadResult | None = (
173            source_data if isinstance(source_data, ReadResult) else None
174        )
175        source_name: str = source.name if source else cast("ReadResult", read_result).source_name
176
177        # State providers and writers default to no-op, unless overridden below.
178        cache_state_provider: StateProviderBase = StaticInputState([])
179        """Provides the state of the cache's data."""
180        cache_state_writer: StateWriterBase = NoOpStateWriter()
181        """Writes updates for the state of the cache's data."""
182        destination_state_provider: StateProviderBase = StaticInputState([])
183        """Provides the state of the destination's data, from `cache` or `state_cache`."""
184        destination_state_writer: StateWriterBase = NoOpStateWriter()
185        """Writes updates for the state of the destination's data, to `cache` or `state_cache`."""
186
187        # If caching not explicitly disabled
188        if cache is not False:
189            # Resolve `cache`, `cache_state_provider`, and `cache_state_writer`
190            if isinstance(source_data, ReadResult):
191                cache = source_data.cache
192
193            cache = cache or get_default_cache()
194            cache_state_provider = cache.get_state_provider(
195                source_name=source_name,
196                destination_name=None,  # This will just track the cache state
197            )
198            cache_state_writer = cache.get_state_writer(
199                source_name=source_name,
200                destination_name=None,  # This will just track the cache state
201            )
202
203        # Resolve `state_cache`
204        if state_cache is None:
205            state_cache = cache or get_default_cache()
206
207        # Resolve `destination_state_writer` and `destination_state_provider`
208        if state_cache:
209            destination_state_writer = state_cache.get_state_writer(
210                source_name=source_name,
211                destination_name=self.name,
212            )
213            if not force_full_refresh:
214                destination_state_provider = state_cache.get_state_provider(
215                    source_name=source_name,
216                    destination_name=self.name,
217                )
218        elif state_cache is not False:
219            warnings.warn(
220                "No state backend or cache provided. State will not be tracked."
221                "To track state, provide a cache or state backend."
222                "To silence this warning, set `state_cache=False` explicitly.",
223                category=exc.PyAirbyteWarning,
224                stacklevel=2,
225            )
226
227        # Resolve `catalog_provider`
228        if source:
229            catalog_provider = CatalogProvider(
230                configured_catalog=source.get_configured_catalog(
231                    streams=streams,
232                    force_full_refresh=force_full_refresh,
233                )
234            )
235        elif read_result:
236            catalog_provider = CatalogProvider.from_read_result(read_result)
237        else:
238            raise exc.PyAirbyteInternalError(
239                message="`source_data` must be a `Source` or `ReadResult` object.",
240            )
241
242        progress_tracker = ProgressTracker(
243            source=source if isinstance(source_data, Source) else None,
244            cache=cache or None,
245            destination=self,
246            expected_streams=catalog_provider.stream_names,
247        )
248
249        source_state_provider: StateProviderBase
250        source_state_provider = JoinedStateProvider(
251            primary=cache_state_provider,
252            secondary=destination_state_provider,
253        )
254
255        if source:
256            if cache is False:
257                # Get message iterator for source (caching disabled)
258                message_iterator: AirbyteMessageIterator = source._get_airbyte_message_iterator(  # noqa: SLF001 # Non-public API
259                    streams=streams,
260                    state_provider=source_state_provider,
261                    progress_tracker=progress_tracker,
262                    force_full_refresh=force_full_refresh,
263                )
264            else:
265                # Caching enabled and we are reading from a source.
266                # Read the data to cache if caching is enabled.
267                read_result = source._read_to_cache(  # noqa: SLF001  # Non-public API
268                    cache=cache,
269                    state_provider=source_state_provider,
270                    state_writer=cache_state_writer,
271                    catalog_provider=catalog_provider,
272                    stream_names=catalog_provider.stream_names,
273                    write_strategy=write_strategy,
274                    force_full_refresh=force_full_refresh,
275                    skip_validation=False,
276                    progress_tracker=progress_tracker,
277                )
278                message_iterator = AirbyteMessageIterator.from_read_result(
279                    read_result=read_result,
280                )
281        else:  # Else we are reading from a read result
282            assert read_result is not None
283            message_iterator = AirbyteMessageIterator.from_read_result(
284                read_result=read_result,
285            )
286
287        # Write the data to the destination
288        try:
289            self._write_airbyte_message_stream(
290                stdin=message_iterator,
291                catalog_provider=catalog_provider,
292                write_strategy=write_strategy,
293                state_writer=destination_state_writer,
294                progress_tracker=progress_tracker,
295            )
296        except Exception as ex:
297            progress_tracker.log_failure(exception=ex)
298            raise
299        else:
300            # No exceptions were raised, so log success
301            progress_tracker.log_success()
302
303        return WriteResult(
304            destination=self,
305            source_data=source_data,
306            catalog_provider=catalog_provider,
307            state_writer=destination_state_writer,
308            progress_tracker=progress_tracker,
309        )

Write data from source connector or already cached source data.

Caching is enabled by default, unless explicitly disabled.

Arguments:
  • source_data: The source data to write. Can be a Source or a ReadResult object.
  • streams: The streams to write to the destination. If omitted or if "*" is provided, all streams will be written. If source_data is a source, then streams must be selected here or on the source. If both are specified, this setting will override the stream selection on the source.
  • cache: The cache to use for reading source_data. If None, no cache will be used. If False, the cache will be disabled. This must be None if source_data is already a Cache object.
  • state_cache: A cache to use for storing incremental state. You do not need to set this if cache is specified or if source_data is a Cache object. Set to False to disable state management.
  • write_strategy: The strategy to use for writing source_data. If AUTO, the connector will decide the best strategy to use.
  • force_full_refresh: Whether to force a full refresh of the source_data. If True, any existing state will be ignored and all source data will be reloaded.

For incremental syncs, cache or state_cache will be checked for matching state values. If the cache has tracked state, this will be used for the sync. Otherwise, if there is a known destination state, the destination-specific state will be used. If neither are available, a full refresh will be performed.

class DuckDBCache(airbyte._processors.sql.duckdb.DuckDBConfig, airbyte.caches.base.CacheBase):
44class DuckDBCache(DuckDBConfig, CacheBase):
45    """A DuckDB cache."""
46
47    _sql_processor_class: ClassVar[type[SqlProcessorBase]] = DuckDBSqlProcessor
48
49    paired_destination_name: ClassVar[str | None] = "destination-duckdb"
50    paired_destination_config_class: ClassVar[type | None] = DestinationDuckdb
51
52    @property
53    def paired_destination_config(self) -> DestinationDuckdb:
54        """Return a dictionary of destination configuration values."""
55        return duckdb_cache_to_destination_configuration(cache=self)

A DuckDB cache.

paired_destination_name: ClassVar[str | None] = 'destination-duckdb'
paired_destination_config_class: ClassVar[type | None] = <class 'airbyte_api.models.destination_duckdb.DestinationDuckdb'>
paired_destination_config: airbyte_api.models.destination_duckdb.DestinationDuckdb
52    @property
53    def paired_destination_config(self) -> DestinationDuckdb:
54        """Return a dictionary of destination configuration values."""
55        return duckdb_cache_to_destination_configuration(cache=self)

Return a dictionary of destination configuration values.

class ReadResult(collections.abc.Mapping[str, airbyte.datasets._sql.CachedDataset]):
 33class ReadResult(Mapping[str, CachedDataset]):
 34    """The result of a read operation.
 35
 36    This class is used to return information about the read operation, such as the number of
 37    records read. It should not be created directly, but instead returned by the write method
 38    of a destination.
 39    """
 40
 41    def __init__(
 42        self,
 43        *,
 44        source_name: str,
 45        processed_streams: list[str],
 46        cache: CacheBase,
 47        progress_tracker: ProgressTracker,
 48    ) -> None:
 49        """Initialize a read result.
 50
 51        This class should not be created directly. Instead, it should be returned by the `read`
 52        method of the `Source` class.
 53        """
 54        self.source_name = source_name
 55        self._progress_tracker = progress_tracker
 56        self._cache = cache
 57        self._processed_streams = processed_streams
 58
 59    def __getitem__(self, stream: str) -> CachedDataset:
 60        """Return the cached dataset for a given stream name."""
 61        if stream not in self._processed_streams:
 62            raise KeyError(stream)
 63
 64        return CachedDataset(self._cache, stream)
 65
 66    def __contains__(self, stream: object) -> bool:
 67        """Return whether a given stream name was included in processing."""
 68        if not isinstance(stream, str):
 69            return False
 70
 71        return stream in self._processed_streams
 72
 73    def __iter__(self) -> Iterator[str]:
 74        """Return an iterator over the stream names that were processed."""
 75        return self._processed_streams.__iter__()
 76
 77    def __len__(self) -> int:
 78        """Return the number of streams that were processed."""
 79        return len(self._processed_streams)
 80
 81    def get_sql_engine(self) -> Engine:
 82        """Return the SQL engine used by the cache."""
 83        return self._cache.get_sql_engine()
 84
 85    @property
 86    def processed_records(self) -> int:
 87        """The total number of records read from the source."""
 88        return self._progress_tracker.total_records_read
 89
 90    @property
 91    def streams(self) -> Mapping[str, CachedDataset]:
 92        """Return a mapping of stream names to cached datasets."""
 93        return {
 94            stream_name: CachedDataset(self._cache, stream_name)
 95            for stream_name in self._processed_streams
 96        }
 97
 98    @property
 99    def cache(self) -> CacheBase:
100        """Return the cache object."""
101        return self._cache

The result of a read operation.

This class is used to return information about the read operation, such as the number of records read. It should not be created directly, but instead returned by the write method of a destination.

ReadResult( *, source_name: str, processed_streams: list[str], cache: airbyte.caches.CacheBase, progress_tracker: airbyte.progress.ProgressTracker)
41    def __init__(
42        self,
43        *,
44        source_name: str,
45        processed_streams: list[str],
46        cache: CacheBase,
47        progress_tracker: ProgressTracker,
48    ) -> None:
49        """Initialize a read result.
50
51        This class should not be created directly. Instead, it should be returned by the `read`
52        method of the `Source` class.
53        """
54        self.source_name = source_name
55        self._progress_tracker = progress_tracker
56        self._cache = cache
57        self._processed_streams = processed_streams

Initialize a read result.

This class should not be created directly. Instead, it should be returned by the read method of the Source class.

source_name
def get_sql_engine(self) -> sqlalchemy.engine.base.Engine:
81    def get_sql_engine(self) -> Engine:
82        """Return the SQL engine used by the cache."""
83        return self._cache.get_sql_engine()

Return the SQL engine used by the cache.

processed_records: int
85    @property
86    def processed_records(self) -> int:
87        """The total number of records read from the source."""
88        return self._progress_tracker.total_records_read

The total number of records read from the source.

streams: Mapping[str, CachedDataset]
90    @property
91    def streams(self) -> Mapping[str, CachedDataset]:
92        """Return a mapping of stream names to cached datasets."""
93        return {
94            stream_name: CachedDataset(self._cache, stream_name)
95            for stream_name in self._processed_streams
96        }

Return a mapping of stream names to cached datasets.

cache: airbyte.caches.CacheBase
 98    @property
 99    def cache(self) -> CacheBase:
100        """Return the cache object."""
101        return self._cache

Return the cache object.

class SecretSourceEnum(builtins.str, enum.Enum):
24class SecretSourceEnum(str, Enum):
25    """Enumeration of secret sources supported by PyAirbyte."""
26
27    ENV = "env"
28    DOTENV = "dotenv"
29    GOOGLE_COLAB = "google_colab"
30    GOOGLE_GSM = "google_gsm"  # Not enabled by default
31
32    PROMPT = "prompt"
33
34    def __str__(self) -> str:
35        """Return the string representation of the enum value."""
36        return self.value

Enumeration of secret sources supported by PyAirbyte.

ENV = <SecretSourceEnum.ENV: 'env'>
DOTENV = <SecretSourceEnum.DOTENV: 'dotenv'>
GOOGLE_COLAB = <SecretSourceEnum.GOOGLE_COLAB: 'google_colab'>
GOOGLE_GSM = <SecretSourceEnum.GOOGLE_GSM: 'google_gsm'>
PROMPT = <SecretSourceEnum.PROMPT: 'prompt'>
class Source(airbyte._connector_base.ConnectorBase):
  68class Source(ConnectorBase):  # noqa: PLR0904
  69    """A class representing a source that can be called."""
  70
  71    connector_type = "source"
  72
  73    def __init__(
  74        self,
  75        executor: Executor,
  76        name: str,
  77        config: dict[str, Any] | None = None,
  78        *,
  79        config_change_callback: ConfigChangeCallback | None = None,
  80        streams: str | list[str] | None = None,
  81        validate: bool = False,
  82        cursor_key_overrides: dict[str, str] | None = None,
  83        primary_key_overrides: dict[str, str | list[str]] | None = None,
  84    ) -> None:
  85        """Initialize the source.
  86
  87        If config is provided, it will be validated against the spec if validate is True.
  88        """
  89        self._to_be_selected_streams: list[str] | str = []
  90        """Used to hold selection criteria before catalog is known."""
  91
  92        super().__init__(
  93            executor=executor,
  94            name=name,
  95            config=config,
  96            config_change_callback=config_change_callback,
  97            validate=validate,
  98        )
  99        self._config_dict: dict[str, Any] | None = None
 100        self._last_log_messages: list[str] = []
 101        self._discovered_catalog: AirbyteCatalog | None = None
 102        self._selected_stream_names: list[str] = []
 103
 104        self._cursor_key_overrides: dict[str, str] = {}
 105        """A mapping of lower-cased stream names to cursor key overrides."""
 106
 107        self._primary_key_overrides: dict[str, list[str]] = {}
 108        """A mapping of lower-cased stream names to primary key overrides."""
 109
 110        if config is not None:
 111            self.set_config(config, validate=validate)
 112        if streams is not None:
 113            self.select_streams(streams)
 114        if cursor_key_overrides is not None:
 115            self.set_cursor_keys(**cursor_key_overrides)
 116        if primary_key_overrides is not None:
 117            self.set_primary_keys(**primary_key_overrides)
 118
 119    def set_streams(self, streams: list[str]) -> None:
 120        """Deprecated. See select_streams()."""
 121        warnings.warn(
 122            "The 'set_streams' method is deprecated and will be removed in a future version. "
 123            "Please use the 'select_streams' method instead.",
 124            DeprecationWarning,
 125            stacklevel=2,
 126        )
 127        self.select_streams(streams)
 128
 129    def set_cursor_key(
 130        self,
 131        stream_name: str,
 132        cursor_key: str,
 133    ) -> None:
 134        """Set the cursor for a single stream.
 135
 136        Note:
 137        - This does not unset previously set cursors.
 138        - The cursor key must be a single field name.
 139        - Not all streams support custom cursors. If a stream does not support custom cursors,
 140          the override may be ignored.
 141        - Stream names are case insensitive, while field names are case sensitive.
 142        - Stream names are not validated by PyAirbyte. If the stream name
 143          does not exist in the catalog, the override may be ignored.
 144        """
 145        self._cursor_key_overrides[stream_name.lower()] = cursor_key
 146
 147    def set_cursor_keys(
 148        self,
 149        **kwargs: str,
 150    ) -> None:
 151        """Override the cursor key for one or more streams.
 152
 153        Usage:
 154            ```python
 155            source.set_cursor_keys(
 156                stream1="cursor1",
 157                stream2="cursor2",
 158            )
 159            ```
 160
 161        Note:
 162        - This does not unset previously set cursors.
 163        - The cursor key must be a single field name.
 164        - Not all streams support custom cursors. If a stream does not support custom cursors,
 165          the override may be ignored.
 166        - Stream names are case insensitive, while field names are case sensitive.
 167        - Stream names are not validated by PyAirbyte. If the stream name
 168          does not exist in the catalog, the override may be ignored.
 169        """
 170        self._cursor_key_overrides.update({k.lower(): v for k, v in kwargs.items()})
 171
 172    def set_primary_key(
 173        self,
 174        stream_name: str,
 175        primary_key: str | list[str],
 176    ) -> None:
 177        """Set the primary key for a single stream.
 178
 179        Note:
 180        - This does not unset previously set primary keys.
 181        - The primary key must be a single field name or a list of field names.
 182        - Not all streams support overriding primary keys. If a stream does not support overriding
 183          primary keys, the override may be ignored.
 184        - Stream names are case insensitive, while field names are case sensitive.
 185        - Stream names are not validated by PyAirbyte. If the stream name
 186          does not exist in the catalog, the override may be ignored.
 187        """
 188        self._primary_key_overrides[stream_name.lower()] = (
 189            primary_key if isinstance(primary_key, list) else [primary_key]
 190        )
 191
 192    def set_primary_keys(
 193        self,
 194        **kwargs: str | list[str],
 195    ) -> None:
 196        """Override the primary keys for one or more streams.
 197
 198        This does not unset previously set primary keys.
 199
 200        Usage:
 201            ```python
 202            source.set_primary_keys(
 203                stream1="pk1",
 204                stream2=["pk1", "pk2"],
 205            )
 206            ```
 207
 208        Note:
 209        - This does not unset previously set primary keys.
 210        - The primary key must be a single field name or a list of field names.
 211        - Not all streams support overriding primary keys. If a stream does not support overriding
 212          primary keys, the override may be ignored.
 213        - Stream names are case insensitive, while field names are case sensitive.
 214        - Stream names are not validated by PyAirbyte. If the stream name
 215          does not exist in the catalog, the override may be ignored.
 216        """
 217        self._primary_key_overrides.update(
 218            {k.lower(): v if isinstance(v, list) else [v] for k, v in kwargs.items()}
 219        )
 220
 221    def _log_warning_preselected_stream(self, streams: str | list[str]) -> None:
 222        """Logs a warning message indicating stream selection which are not selected yet."""
 223        if streams == "*":
 224            print(
 225                "Warning: Config is not set yet. All streams will be selected after config is set.",
 226                file=sys.stderr,
 227            )
 228        else:
 229            print(
 230                "Warning: Config is not set yet. "
 231                f"Streams to be selected after config is set: {streams}",
 232                file=sys.stderr,
 233            )
 234
 235    def select_all_streams(self) -> None:
 236        """Select all streams.
 237
 238        This is a more streamlined equivalent to:
 239        > source.select_streams(source.get_available_streams()).
 240        """
 241        if self._config_dict is None:
 242            self._to_be_selected_streams = "*"
 243            self._log_warning_preselected_stream(self._to_be_selected_streams)
 244            return
 245
 246        self._selected_stream_names = self.get_available_streams()
 247
 248    def select_streams(self, streams: str | list[str]) -> None:
 249        """Select the stream names that should be read from the connector.
 250
 251        Args:
 252            streams: A list of stream names to select. If set to "*", all streams will be selected.
 253
 254        Currently, if this is not set, all streams will be read.
 255        """
 256        if self._config_dict is None:
 257            self._to_be_selected_streams = streams
 258            self._log_warning_preselected_stream(streams)
 259            return
 260
 261        if streams == "*":
 262            self.select_all_streams()
 263            return
 264
 265        if isinstance(streams, str):
 266            # If a single stream is provided, convert it to a one-item list
 267            streams = [streams]
 268
 269        available_streams = self.get_available_streams()
 270        for stream in streams:
 271            if stream not in available_streams:
 272                raise exc.AirbyteStreamNotFoundError(
 273                    stream_name=stream,
 274                    connector_name=self.name,
 275                    available_streams=available_streams,
 276                )
 277        self._selected_stream_names = streams
 278
 279    def get_selected_streams(self) -> list[str]:
 280        """Get the selected streams.
 281
 282        If no streams are selected, return an empty list.
 283        """
 284        return self._selected_stream_names
 285
 286    def set_config(
 287        self,
 288        config: dict[str, Any],
 289        *,
 290        validate: bool = True,
 291    ) -> None:
 292        """Set the config for the connector.
 293
 294        If validate is True, raise an exception if the config fails validation.
 295
 296        If validate is False, validation will be deferred until check() or validate_config()
 297        is called.
 298        """
 299        if validate:
 300            self.validate_config(config)
 301
 302        self._config_dict = config
 303
 304        if self._to_be_selected_streams:
 305            self.select_streams(self._to_be_selected_streams)
 306            self._to_be_selected_streams = []
 307
 308    def _discover(self) -> AirbyteCatalog:
 309        """Call discover on the connector.
 310
 311        This involves the following steps:
 312        - Write the config to a temporary file
 313        - execute the connector with discover --config <config_file>
 314        - Listen to the messages and return the first AirbyteCatalog that comes along.
 315        - Make sure the subprocess is killed when the function returns.
 316        """
 317        with as_temp_files([self._hydrated_config]) as [config_file]:
 318            for msg in self._execute(["discover", "--config", config_file]):
 319                if msg.type == Type.CATALOG and msg.catalog:
 320                    return msg.catalog
 321            raise exc.AirbyteConnectorMissingCatalogError(
 322                connector_name=self.name,
 323                log_text=self._last_log_messages,
 324            )
 325
 326    def get_available_streams(self) -> list[str]:
 327        """Get the available streams from the spec."""
 328        return [s.name for s in self.discovered_catalog.streams]
 329
 330    def _get_incremental_stream_names(self) -> list[str]:
 331        """Get the name of streams that support incremental sync."""
 332        return [
 333            stream.name
 334            for stream in self.discovered_catalog.streams
 335            if SyncMode.incremental in stream.supported_sync_modes
 336        ]
 337
 338    @override
 339    def _get_spec(self, *, force_refresh: bool = False) -> ConnectorSpecification:
 340        """Call spec on the connector.
 341
 342        This involves the following steps:
 343        * execute the connector with spec
 344        * Listen to the messages and return the first AirbyteCatalog that comes along.
 345        * Make sure the subprocess is killed when the function returns.
 346        """
 347        if force_refresh or self._spec is None:
 348            for msg in self._execute(["spec"]):
 349                if msg.type == Type.SPEC and msg.spec:
 350                    self._spec = msg.spec
 351                    break
 352
 353        if self._spec:
 354            return self._spec
 355
 356        raise exc.AirbyteConnectorMissingSpecError(
 357            connector_name=self.name,
 358            log_text=self._last_log_messages,
 359        )
 360
 361    @property
 362    def config_spec(self) -> dict[str, Any]:
 363        """Generate a configuration spec for this connector, as a JSON Schema definition.
 364
 365        This function generates a JSON Schema dictionary with configuration specs for the
 366        current connector, as a dictionary.
 367
 368        Returns:
 369            dict: The JSON Schema configuration spec as a dictionary.
 370        """
 371        return self._get_spec(force_refresh=True).connectionSpecification
 372
 373    @property
 374    def _yaml_spec(self) -> str:
 375        """Get the spec as a yaml string.
 376
 377        For now, the primary use case is for writing and debugging a valid config for a source.
 378
 379        This is private for now because we probably want better polish before exposing this
 380        as a stable interface. This will also get easier when we have docs links with this info
 381        for each connector.
 382        """
 383        spec_obj: ConnectorSpecification = self._get_spec()
 384        spec_dict: dict[str, Any] = spec_obj.model_dump(exclude_unset=True)
 385        # convert to a yaml string
 386        return yaml.dump(spec_dict)
 387
 388    @property
 389    def docs_url(self) -> str:
 390        """Get the URL to the connector's documentation."""
 391        return "https://docs.airbyte.com/integrations/sources/" + self.name.lower().replace(
 392            "source-", ""
 393        )
 394
 395    @property
 396    def discovered_catalog(self) -> AirbyteCatalog:
 397        """Get the raw catalog for the given streams.
 398
 399        If the catalog is not yet known, we call discover to get it.
 400        """
 401        if self._discovered_catalog is None:
 402            self._discovered_catalog = self._discover()
 403
 404        return self._discovered_catalog
 405
 406    @property
 407    def configured_catalog(self) -> ConfiguredAirbyteCatalog:
 408        """Get the configured catalog for the given streams.
 409
 410        If the raw catalog is not yet known, we call discover to get it.
 411
 412        If no specific streams are selected, we return a catalog that syncs all available streams.
 413
 414        TODO: We should consider disabling by default the streams that the connector would
 415        disable by default. (For instance, streams that require a premium license are sometimes
 416        disabled by default within the connector.)
 417        """
 418        # Ensure discovered catalog is cached before we start
 419        _ = self.discovered_catalog
 420
 421        # Filter for selected streams if set, otherwise use all available streams:
 422        streams_filter: list[str] = self._selected_stream_names or self.get_available_streams()
 423        return self.get_configured_catalog(streams=streams_filter)
 424
 425    def get_configured_catalog(
 426        self,
 427        streams: Literal["*"] | list[str] | None = None,
 428        *,
 429        force_full_refresh: bool = False,
 430    ) -> ConfiguredAirbyteCatalog:
 431        """Get a configured catalog for the given streams.
 432
 433        If no streams are provided, the selected streams will be used. If no streams are selected,
 434        all available streams will be used.
 435
 436        If '*' is provided, all available streams will be used.
 437
 438        If force_full_refresh is True, streams will be configured with full_refresh sync mode
 439        when supported by the stream. Otherwise, incremental sync mode is used when supported.
 440        """
 441        selected_streams: list[str] = []
 442        if streams is None:
 443            selected_streams = self._selected_stream_names or self.get_available_streams()
 444        elif streams == "*":
 445            selected_streams = self.get_available_streams()
 446        elif isinstance(streams, list):
 447            selected_streams = streams
 448        else:
 449            raise exc.PyAirbyteInputError(
 450                message="Invalid streams argument.",
 451                input_value=streams,
 452            )
 453
 454        def _get_sync_mode(stream: AirbyteStream) -> SyncMode:
 455            """Determine the sync mode for a stream based on force_full_refresh and support."""
 456            # Use getattr to handle mocks or streams without supported_sync_modes attribute
 457            supported_modes = getattr(stream, "supported_sync_modes", None)
 458
 459            if force_full_refresh:
 460                # When force_full_refresh is True, prefer full_refresh if supported
 461                if supported_modes and SyncMode.full_refresh in supported_modes:
 462                    return SyncMode.full_refresh
 463                # Fall back to incremental if full_refresh is not supported
 464                return SyncMode.incremental
 465
 466            # Default behavior: preserve previous semantics (always incremental)
 467            return SyncMode.incremental
 468
 469        return ConfiguredAirbyteCatalog(
 470            streams=[
 471                ConfiguredAirbyteStream(
 472                    stream=stream,
 473                    destination_sync_mode=DestinationSyncMode.overwrite,
 474                    sync_mode=_get_sync_mode(stream),
 475                    primary_key=(
 476                        [self._primary_key_overrides[stream.name.lower()]]
 477                        if stream.name.lower() in self._primary_key_overrides
 478                        else stream.source_defined_primary_key
 479                    ),
 480                    cursor_field=(
 481                        [self._cursor_key_overrides[stream.name.lower()]]
 482                        if stream.name.lower() in self._cursor_key_overrides
 483                        else stream.default_cursor_field
 484                    ),
 485                    # These are unused in the current implementation:
 486                    generation_id=None,
 487                    minimum_generation_id=None,
 488                    sync_id=None,
 489                )
 490                for stream in self.discovered_catalog.streams
 491                if stream.name in selected_streams
 492            ],
 493        )
 494
 495    def get_stream_json_schema(self, stream_name: str) -> dict[str, Any]:
 496        """Return the JSON Schema spec for the specified stream name."""
 497        catalog: AirbyteCatalog = self.discovered_catalog
 498        found: list[AirbyteStream] = [
 499            stream for stream in catalog.streams if stream.name == stream_name
 500        ]
 501
 502        if len(found) == 0:
 503            raise exc.PyAirbyteInputError(
 504                message="Stream name does not exist in catalog.",
 505                input_value=stream_name,
 506            )
 507
 508        if len(found) > 1:
 509            raise exc.PyAirbyteInternalError(
 510                message="Duplicate streams found with the same name.",
 511                context={
 512                    "found_streams": found,
 513                },
 514            )
 515
 516        return found[0].json_schema
 517
 518    def get_records(
 519        self,
 520        stream: str,
 521        *,
 522        limit: int | None = None,
 523        stop_event: threading.Event | None = None,
 524        normalize_field_names: bool = False,
 525        prune_undeclared_fields: bool = True,
 526    ) -> LazyDataset:
 527        """Read a stream from the connector.
 528
 529        Args:
 530            stream: The name of the stream to read.
 531            limit: The maximum number of records to read. If None, all records will be read.
 532            stop_event: If set, the event can be triggered by the caller to stop reading records
 533                and terminate the process.
 534            normalize_field_names: When `True`, field names will be normalized to lower case, with
 535                special characters removed. This matches the behavior of PyAirbyte caches and most
 536                Airbyte destinations.
 537            prune_undeclared_fields: When `True`, undeclared fields will be pruned from the records,
 538                which generally matches the behavior of PyAirbyte caches and most Airbyte
 539                destinations, specifically when you expect the catalog may be stale. You can disable
 540                this to keep all fields in the records.
 541
 542        This involves the following steps:
 543        * Call discover to get the catalog
 544        * Generate a configured catalog that syncs the given stream in full_refresh mode
 545        * Write the configured catalog and the config to a temporary file
 546        * execute the connector with read --config <config_file> --catalog <catalog_file>
 547        * Listen to the messages and return the first AirbyteRecordMessages that come along.
 548        * Make sure the subprocess is killed when the function returns.
 549        """
 550        stop_event = stop_event or threading.Event()
 551        configured_catalog = self.get_configured_catalog(streams=[stream])
 552        if len(configured_catalog.streams) == 0:
 553            raise exc.PyAirbyteInputError(
 554                message="Requested stream does not exist.",
 555                context={
 556                    "stream": stream,
 557                    "available_streams": self.get_available_streams(),
 558                    "connector_name": self.name,
 559                },
 560            ) from KeyError(stream)
 561
 562        configured_stream = configured_catalog.streams[0]
 563
 564        def _with_logging(records: Iterable[dict[str, Any]]) -> Iterator[dict[str, Any]]:
 565            yield from records
 566
 567        stream_record_handler = StreamRecordHandler(
 568            json_schema=self.get_stream_json_schema(stream),
 569            prune_extra_fields=prune_undeclared_fields,
 570            normalize_keys=normalize_field_names,
 571        )
 572
 573        # This method is non-blocking, so we use "PLAIN" to avoid a live progress display
 574        progress_tracker = ProgressTracker(
 575            ProgressStyle.PLAIN,
 576            source=self,
 577            cache=None,
 578            destination=None,
 579            expected_streams=[stream],
 580        )
 581
 582        iterator: Iterator[dict[str, Any]] = (
 583            StreamRecord.from_record_message(
 584                record_message=record.record,
 585                stream_record_handler=stream_record_handler,
 586            )
 587            for record in self._read_with_catalog(
 588                catalog=configured_catalog,
 589                progress_tracker=progress_tracker,
 590                stop_event=stop_event,
 591            )
 592            if record.record
 593        )
 594        if limit is not None:
 595            # Stop the iterator after the limit is reached
 596            iterator = islice(iterator, limit)
 597
 598        return LazyDataset(
 599            iterator,
 600            stream_metadata=configured_stream,
 601            stop_event=stop_event,
 602            progress_tracker=progress_tracker,
 603        )
 604
 605    def get_documents(
 606        self,
 607        stream: str,
 608        title_property: str | None = None,
 609        content_properties: list[str] | None = None,
 610        metadata_properties: list[str] | None = None,
 611        *,
 612        render_metadata: bool = False,
 613    ) -> Iterable[Document]:
 614        """Read a stream from the connector and return the records as documents.
 615
 616        If metadata_properties is not set, all properties that are not content will be added to
 617        the metadata.
 618
 619        If render_metadata is True, metadata will be rendered in the document, as well as the
 620        the main content.
 621        """
 622        return self.get_records(stream).to_documents(
 623            title_property=title_property,
 624            content_properties=content_properties,
 625            metadata_properties=metadata_properties,
 626            render_metadata=render_metadata,
 627        )
 628
 629    def get_samples(
 630        self,
 631        streams: list[str] | Literal["*"] | None = None,
 632        *,
 633        limit: int = 5,
 634        on_error: Literal["raise", "ignore", "log"] = "raise",
 635    ) -> dict[str, InMemoryDataset | None]:
 636        """Get a sample of records from the given streams."""
 637        if streams == "*":
 638            streams = self.get_available_streams()
 639        elif streams is None:
 640            streams = self.get_selected_streams()
 641
 642        results: dict[str, InMemoryDataset | None] = {}
 643        for stream in streams:
 644            stop_event = threading.Event()
 645            try:
 646                results[stream] = self.get_records(
 647                    stream,
 648                    limit=limit,
 649                    stop_event=stop_event,
 650                ).fetch_all()
 651                stop_event.set()
 652            except Exception as ex:
 653                results[stream] = None
 654                if on_error == "ignore":
 655                    continue
 656
 657                if on_error == "raise":
 658                    raise ex from None
 659
 660                if on_error == "log":
 661                    print(f"Error fetching sample for stream '{stream}': {ex}")
 662
 663        return results
 664
 665    def print_samples(
 666        self,
 667        streams: list[str] | Literal["*"] | None = None,
 668        *,
 669        limit: int = 5,
 670        on_error: Literal["raise", "ignore", "log"] = "log",
 671    ) -> None:
 672        """Print a sample of records from the given streams."""
 673        internal_cols: list[str] = [
 674            AB_EXTRACTED_AT_COLUMN,
 675            AB_META_COLUMN,
 676            AB_RAW_ID_COLUMN,
 677        ]
 678        col_limit = 10
 679        if streams == "*":
 680            streams = self.get_available_streams()
 681        elif streams is None:
 682            streams = self.get_selected_streams()
 683
 684        console = Console()
 685
 686        console.print(
 687            Markdown(
 688                f"# Sample Records from `{self.name}` ({len(streams)} selected streams)",
 689                justify="left",
 690            )
 691        )
 692
 693        for stream in streams:
 694            console.print(Markdown(f"## `{stream}` Stream Sample", justify="left"))
 695            samples = self.get_samples(
 696                streams=[stream],
 697                limit=limit,
 698                on_error=on_error,
 699            )
 700            dataset = samples[stream]
 701
 702            table = Table(
 703                show_header=True,
 704                show_lines=True,
 705            )
 706            if dataset is None:
 707                console.print(
 708                    Markdown("**⚠️ `Error fetching sample records.` ⚠️**"),
 709                )
 710                continue
 711
 712            if len(dataset.column_names) > col_limit:
 713                # We'll pivot the columns so each column is its own row
 714                table.add_column("Column Name")
 715                for _ in range(len(dataset)):
 716                    table.add_column(overflow="fold")
 717                for col in dataset.column_names:
 718                    table.add_row(
 719                        Markdown(f"**`{col}`**"),
 720                        *[escape(str(record[col])) for record in dataset],
 721                    )
 722            else:
 723                for col in dataset.column_names:
 724                    table.add_column(
 725                        Markdown(f"**`{col}`**"),
 726                        overflow="fold",
 727                    )
 728
 729                for record in dataset:
 730                    table.add_row(
 731                        *[
 732                            escape(str(val))
 733                            for key, val in record.items()
 734                            # Exclude internal Airbyte columns.
 735                            if key not in internal_cols
 736                        ]
 737                    )
 738
 739            console.print(table)
 740
 741        console.print(Markdown("--------------"))
 742
 743    def _get_airbyte_message_iterator(
 744        self,
 745        *,
 746        streams: Literal["*"] | list[str] | None = None,
 747        state_provider: StateProviderBase | None = None,
 748        progress_tracker: ProgressTracker,
 749        force_full_refresh: bool = False,
 750    ) -> AirbyteMessageIterator:
 751        """Get an AirbyteMessageIterator for this source."""
 752        return AirbyteMessageIterator(
 753            self._read_with_catalog(
 754                catalog=self.get_configured_catalog(
 755                    streams=streams,
 756                    force_full_refresh=force_full_refresh,
 757                ),
 758                state=state_provider if not force_full_refresh else None,
 759                progress_tracker=progress_tracker,
 760            )
 761        )
 762
 763    def _read_with_catalog(
 764        self,
 765        catalog: ConfiguredAirbyteCatalog,
 766        progress_tracker: ProgressTracker,
 767        *,
 768        state: StateProviderBase | None = None,
 769        stop_event: threading.Event | None = None,
 770    ) -> Generator[AirbyteMessage, None, None]:
 771        """Call read on the connector.
 772
 773        This involves the following steps:
 774        * Write the config to a temporary file
 775        * execute the connector with read --config <config_file> --catalog <catalog_file>
 776        * Listen to the messages and return the AirbyteRecordMessages that come along.
 777        * Send out telemetry on the performed sync (with information about which source was used and
 778          the type of the cache)
 779        """
 780        with as_temp_files(
 781            [
 782                self._hydrated_config,
 783                catalog.model_dump_json(exclude_none=True),
 784                state.to_state_input_file_text() if state else "[]",
 785            ]
 786        ) as [
 787            config_file,
 788            catalog_file,
 789            state_file,
 790        ]:
 791            message_generator = self._execute(
 792                [
 793                    "read",
 794                    "--config",
 795                    config_file,
 796                    "--catalog",
 797                    catalog_file,
 798                    "--state",
 799                    state_file,
 800                ],
 801                progress_tracker=progress_tracker,
 802            )
 803            for message in progress_tracker.tally_records_read(message_generator):
 804                if stop_event and stop_event.is_set():
 805                    progress_tracker._log_sync_cancel()  # noqa: SLF001
 806                    time.sleep(0.1)
 807                    return
 808
 809                yield message
 810
 811        progress_tracker.log_read_complete()
 812
 813    def _peek_airbyte_message(
 814        self,
 815        message: AirbyteMessage,
 816        *,
 817        raise_on_error: bool = True,
 818    ) -> None:
 819        """Process an Airbyte message.
 820
 821        This method handles reading Airbyte messages and taking action, if needed, based on the
 822        message type. For instance, log messages are logged, records are tallied, and errors are
 823        raised as exceptions if `raise_on_error` is True.
 824
 825        Raises:
 826            AirbyteConnectorFailedError: If a TRACE message of type ERROR is emitted.
 827        """
 828        super()._peek_airbyte_message(message, raise_on_error=raise_on_error)
 829
 830    def _log_incremental_streams(
 831        self,
 832        *,
 833        incremental_streams: set[str] | None = None,
 834    ) -> None:
 835        """Log the streams which are using incremental sync mode."""
 836        log_message = (
 837            "The following streams are currently using incremental sync:\n"
 838            f"{incremental_streams}\n"
 839            "To perform a full refresh, set 'force_full_refresh=True' in 'airbyte.read()' method."
 840        )
 841        print(log_message, file=sys.stderr)
 842
 843    def read(
 844        self,
 845        cache: CacheBase | None = None,
 846        *,
 847        streams: str | list[str] | None = None,
 848        write_strategy: str | WriteStrategy = WriteStrategy.AUTO,
 849        force_full_refresh: bool = False,
 850        skip_validation: bool = False,
 851    ) -> ReadResult:
 852        """Read from the connector and write to the cache.
 853
 854        Args:
 855            cache: The cache to write to. If not set, a default cache will be used.
 856            streams: Optional if already set. A list of stream names to select for reading. If set
 857                to "*", all streams will be selected.
 858            write_strategy: The strategy to use when writing to the cache. If a string, it must be
 859                one of "append", "merge", "replace", or "auto". If a WriteStrategy, it must be one
 860                of WriteStrategy.APPEND, WriteStrategy.MERGE, WriteStrategy.REPLACE, or
 861                WriteStrategy.AUTO.
 862            force_full_refresh: If True, the source will operate in full refresh mode. Otherwise,
 863                streams will be read in incremental mode if supported by the connector. This option
 864                must be True when using the "replace" strategy.
 865            skip_validation: If True, PyAirbyte will not pre-validate the input configuration before
 866                running the connector. This can be helpful in debugging, when you want to send
 867                configurations to the connector that otherwise might be rejected by JSON Schema
 868                validation rules.
 869        """
 870        cache = cache or get_default_cache()
 871        progress_tracker = ProgressTracker(
 872            source=self,
 873            cache=cache,
 874            destination=None,
 875            expected_streams=None,  # Will be set later
 876        )
 877
 878        # Set up state provider if not in full refresh mode
 879        if force_full_refresh:
 880            state_provider: StateProviderBase | None = None
 881        else:
 882            state_provider = cache.get_state_provider(
 883                source_name=self._name,
 884            )
 885        state_writer = cache.get_state_writer(source_name=self._name)
 886
 887        if streams:
 888            self.select_streams(streams)
 889
 890        if not self._selected_stream_names:
 891            raise exc.PyAirbyteNoStreamsSelectedError(
 892                connector_name=self.name,
 893                available_streams=self.get_available_streams(),
 894            )
 895
 896        try:
 897            result = self._read_to_cache(
 898                cache=cache,
 899                catalog_provider=CatalogProvider(
 900                    self.get_configured_catalog(force_full_refresh=force_full_refresh)
 901                ),
 902                stream_names=self._selected_stream_names,
 903                state_provider=state_provider,
 904                state_writer=state_writer,
 905                write_strategy=write_strategy,
 906                force_full_refresh=force_full_refresh,
 907                skip_validation=skip_validation,
 908                progress_tracker=progress_tracker,
 909            )
 910        except exc.PyAirbyteInternalError as ex:
 911            progress_tracker.log_failure(exception=ex)
 912            raise exc.AirbyteConnectorFailedError(
 913                connector_name=self.name,
 914                log_text=self._last_log_messages,
 915            ) from ex
 916        except Exception as ex:
 917            progress_tracker.log_failure(exception=ex)
 918            raise
 919
 920        progress_tracker.log_success()
 921        return result
 922
 923    def _read_to_cache(  # noqa: PLR0913  # Too many arguments
 924        self,
 925        cache: CacheBase,
 926        *,
 927        catalog_provider: CatalogProvider,
 928        stream_names: list[str],
 929        state_provider: StateProviderBase | None,
 930        state_writer: StateWriterBase | None,
 931        write_strategy: str | WriteStrategy = WriteStrategy.AUTO,
 932        force_full_refresh: bool = False,
 933        skip_validation: bool = False,
 934        progress_tracker: ProgressTracker,
 935    ) -> ReadResult:
 936        """Internal read method."""
 937        if write_strategy == WriteStrategy.REPLACE and not force_full_refresh:
 938            warnings.warn(
 939                message=(
 940                    "Using `REPLACE` strategy without also setting `force_full_refresh=True` "
 941                    "could result in data loss. "
 942                    "To silence this warning, use the following: "
 943                    'warnings.filterwarnings("ignore", '
 944                    'category="airbyte.warnings.PyAirbyteDataLossWarning")`'
 945                ),
 946                category=exc.PyAirbyteDataLossWarning,
 947                stacklevel=1,
 948            )
 949        if isinstance(write_strategy, str):
 950            try:
 951                write_strategy = WriteStrategy(write_strategy)
 952            except ValueError:
 953                raise exc.PyAirbyteInputError(
 954                    message="Invalid strategy",
 955                    context={
 956                        "write_strategy": write_strategy,
 957                        "available_strategies": [
 958                            s.value
 959                            for s in WriteStrategy  # pyrefly: ignore[not-iterable]
 960                        ],
 961                    },
 962                ) from None
 963
 964        # Run optional validation step
 965        if not skip_validation:
 966            self.validate_config()
 967
 968        # Log incremental stream if incremental streams are known
 969        if state_provider and state_provider.known_stream_names:
 970            # Retrieve set of the known streams support which support incremental sync
 971            incremental_streams = (
 972                set(self._get_incremental_stream_names())
 973                & state_provider.known_stream_names
 974                & set(self.get_selected_streams())
 975            )
 976            if incremental_streams:
 977                self._log_incremental_streams(incremental_streams=incremental_streams)
 978
 979        airbyte_message_iterator = AirbyteMessageIterator(
 980            self._read_with_catalog(
 981                catalog=catalog_provider.configured_catalog,
 982                state=state_provider,
 983                progress_tracker=progress_tracker,
 984            )
 985        )
 986        cache._write_airbyte_message_stream(  # noqa: SLF001  # Non-public API
 987            stdin=airbyte_message_iterator,
 988            catalog_provider=catalog_provider,
 989            write_strategy=write_strategy,
 990            state_writer=state_writer,
 991            progress_tracker=progress_tracker,
 992        )
 993
 994        # Flush the WAL, if applicable
 995        cache.processor._do_checkpoint()  # noqa: SLF001  # Non-public API
 996
 997        return ReadResult(
 998            source_name=self.name,
 999            progress_tracker=progress_tracker,
1000            processed_streams=stream_names,
1001            cache=cache,
1002        )

A class representing a source that can be called.

Source( executor: airbyte._executors.base.Executor, name: str, config: dict[str, typing.Any] | None = None, *, config_change_callback: Callable[[dict[str, typing.Any]], None] | None = None, streams: str | list[str] | None = None, validate: bool = False, cursor_key_overrides: dict[str, str] | None = None, primary_key_overrides: dict[str, str | list[str]] | None = None)
 73    def __init__(
 74        self,
 75        executor: Executor,
 76        name: str,
 77        config: dict[str, Any] | None = None,
 78        *,
 79        config_change_callback: ConfigChangeCallback | None = None,
 80        streams: str | list[str] | None = None,
 81        validate: bool = False,
 82        cursor_key_overrides: dict[str, str] | None = None,
 83        primary_key_overrides: dict[str, str | list[str]] | None = None,
 84    ) -> None:
 85        """Initialize the source.
 86
 87        If config is provided, it will be validated against the spec if validate is True.
 88        """
 89        self._to_be_selected_streams: list[str] | str = []
 90        """Used to hold selection criteria before catalog is known."""
 91
 92        super().__init__(
 93            executor=executor,
 94            name=name,
 95            config=config,
 96            config_change_callback=config_change_callback,
 97            validate=validate,
 98        )
 99        self._config_dict: dict[str, Any] | None = None
100        self._last_log_messages: list[str] = []
101        self._discovered_catalog: AirbyteCatalog | None = None
102        self._selected_stream_names: list[str] = []
103
104        self._cursor_key_overrides: dict[str, str] = {}
105        """A mapping of lower-cased stream names to cursor key overrides."""
106
107        self._primary_key_overrides: dict[str, list[str]] = {}
108        """A mapping of lower-cased stream names to primary key overrides."""
109
110        if config is not None:
111            self.set_config(config, validate=validate)
112        if streams is not None:
113            self.select_streams(streams)
114        if cursor_key_overrides is not None:
115            self.set_cursor_keys(**cursor_key_overrides)
116        if primary_key_overrides is not None:
117            self.set_primary_keys(**primary_key_overrides)

Initialize the source.

If config is provided, it will be validated against the spec if validate is True.

connector_type = 'source'
def set_streams(self, streams: list[str]) -> None:
119    def set_streams(self, streams: list[str]) -> None:
120        """Deprecated. See select_streams()."""
121        warnings.warn(
122            "The 'set_streams' method is deprecated and will be removed in a future version. "
123            "Please use the 'select_streams' method instead.",
124            DeprecationWarning,
125            stacklevel=2,
126        )
127        self.select_streams(streams)

Deprecated. See select_streams().

def set_cursor_key(self, stream_name: str, cursor_key: str) -> None:
129    def set_cursor_key(
130        self,
131        stream_name: str,
132        cursor_key: str,
133    ) -> None:
134        """Set the cursor for a single stream.
135
136        Note:
137        - This does not unset previously set cursors.
138        - The cursor key must be a single field name.
139        - Not all streams support custom cursors. If a stream does not support custom cursors,
140          the override may be ignored.
141        - Stream names are case insensitive, while field names are case sensitive.
142        - Stream names are not validated by PyAirbyte. If the stream name
143          does not exist in the catalog, the override may be ignored.
144        """
145        self._cursor_key_overrides[stream_name.lower()] = cursor_key

Set the cursor for a single stream.

Note:

  • This does not unset previously set cursors.
  • The cursor key must be a single field name.
  • Not all streams support custom cursors. If a stream does not support custom cursors, the override may be ignored.
  • Stream names are case insensitive, while field names are case sensitive.
  • Stream names are not validated by PyAirbyte. If the stream name does not exist in the catalog, the override may be ignored.
def set_cursor_keys(self, **kwargs: str) -> None:
147    def set_cursor_keys(
148        self,
149        **kwargs: str,
150    ) -> None:
151        """Override the cursor key for one or more streams.
152
153        Usage:
154            ```python
155            source.set_cursor_keys(
156                stream1="cursor1",
157                stream2="cursor2",
158            )
159            ```
160
161        Note:
162        - This does not unset previously set cursors.
163        - The cursor key must be a single field name.
164        - Not all streams support custom cursors. If a stream does not support custom cursors,
165          the override may be ignored.
166        - Stream names are case insensitive, while field names are case sensitive.
167        - Stream names are not validated by PyAirbyte. If the stream name
168          does not exist in the catalog, the override may be ignored.
169        """
170        self._cursor_key_overrides.update({k.lower(): v for k, v in kwargs.items()})

Override the cursor key for one or more streams.

Usage:
source.set_cursor_keys(
    stream1="cursor1",
    stream2="cursor2",
)

Note:

  • This does not unset previously set cursors.
  • The cursor key must be a single field name.
  • Not all streams support custom cursors. If a stream does not support custom cursors, the override may be ignored.
  • Stream names are case insensitive, while field names are case sensitive.
  • Stream names are not validated by PyAirbyte. If the stream name does not exist in the catalog, the override may be ignored.
def set_primary_key(self, stream_name: str, primary_key: str | list[str]) -> None:
172    def set_primary_key(
173        self,
174        stream_name: str,
175        primary_key: str | list[str],
176    ) -> None:
177        """Set the primary key for a single stream.
178
179        Note:
180        - This does not unset previously set primary keys.
181        - The primary key must be a single field name or a list of field names.
182        - Not all streams support overriding primary keys. If a stream does not support overriding
183          primary keys, the override may be ignored.
184        - Stream names are case insensitive, while field names are case sensitive.
185        - Stream names are not validated by PyAirbyte. If the stream name
186          does not exist in the catalog, the override may be ignored.
187        """
188        self._primary_key_overrides[stream_name.lower()] = (
189            primary_key if isinstance(primary_key, list) else [primary_key]
190        )

Set the primary key for a single stream.

Note:

  • This does not unset previously set primary keys.
  • The primary key must be a single field name or a list of field names.
  • Not all streams support overriding primary keys. If a stream does not support overriding primary keys, the override may be ignored.
  • Stream names are case insensitive, while field names are case sensitive.
  • Stream names are not validated by PyAirbyte. If the stream name does not exist in the catalog, the override may be ignored.
def set_primary_keys(self, **kwargs: str | list[str]) -> None:
192    def set_primary_keys(
193        self,
194        **kwargs: str | list[str],
195    ) -> None:
196        """Override the primary keys for one or more streams.
197
198        This does not unset previously set primary keys.
199
200        Usage:
201            ```python
202            source.set_primary_keys(
203                stream1="pk1",
204                stream2=["pk1", "pk2"],
205            )
206            ```
207
208        Note:
209        - This does not unset previously set primary keys.
210        - The primary key must be a single field name or a list of field names.
211        - Not all streams support overriding primary keys. If a stream does not support overriding
212          primary keys, the override may be ignored.
213        - Stream names are case insensitive, while field names are case sensitive.
214        - Stream names are not validated by PyAirbyte. If the stream name
215          does not exist in the catalog, the override may be ignored.
216        """
217        self._primary_key_overrides.update(
218            {k.lower(): v if isinstance(v, list) else [v] for k, v in kwargs.items()}
219        )

Override the primary keys for one or more streams.

This does not unset previously set primary keys.

Usage:
source.set_primary_keys(
    stream1="pk1",
    stream2=["pk1", "pk2"],
)

Note:

  • This does not unset previously set primary keys.
  • The primary key must be a single field name or a list of field names.
  • Not all streams support overriding primary keys. If a stream does not support overriding primary keys, the override may be ignored.
  • Stream names are case insensitive, while field names are case sensitive.
  • Stream names are not validated by PyAirbyte. If the stream name does not exist in the catalog, the override may be ignored.
def select_all_streams(self) -> None:
235    def select_all_streams(self) -> None:
236        """Select all streams.
237
238        This is a more streamlined equivalent to:
239        > source.select_streams(source.get_available_streams()).
240        """
241        if self._config_dict is None:
242            self._to_be_selected_streams = "*"
243            self._log_warning_preselected_stream(self._to_be_selected_streams)
244            return
245
246        self._selected_stream_names = self.get_available_streams()

Select all streams.

This is a more streamlined equivalent to:

source.select_streams(source.get_available_streams()).

def select_streams(self, streams: str | list[str]) -> None:
248    def select_streams(self, streams: str | list[str]) -> None:
249        """Select the stream names that should be read from the connector.
250
251        Args:
252            streams: A list of stream names to select. If set to "*", all streams will be selected.
253
254        Currently, if this is not set, all streams will be read.
255        """
256        if self._config_dict is None:
257            self._to_be_selected_streams = streams
258            self._log_warning_preselected_stream(streams)
259            return
260
261        if streams == "*":
262            self.select_all_streams()
263            return
264
265        if isinstance(streams, str):
266            # If a single stream is provided, convert it to a one-item list
267            streams = [streams]
268
269        available_streams = self.get_available_streams()
270        for stream in streams:
271            if stream not in available_streams:
272                raise exc.AirbyteStreamNotFoundError(
273                    stream_name=stream,
274                    connector_name=self.name,
275                    available_streams=available_streams,
276                )
277        self._selected_stream_names = streams

Select the stream names that should be read from the connector.

Arguments:
  • streams: A list of stream names to select. If set to "*", all streams will be selected.

Currently, if this is not set, all streams will be read.

def get_selected_streams(self) -> list[str]:
279    def get_selected_streams(self) -> list[str]:
280        """Get the selected streams.
281
282        If no streams are selected, return an empty list.
283        """
284        return self._selected_stream_names

Get the selected streams.

If no streams are selected, return an empty list.

def set_config(self, config: dict[str, typing.Any], *, validate: bool = True) -> None:
286    def set_config(
287        self,
288        config: dict[str, Any],
289        *,
290        validate: bool = True,
291    ) -> None:
292        """Set the config for the connector.
293
294        If validate is True, raise an exception if the config fails validation.
295
296        If validate is False, validation will be deferred until check() or validate_config()
297        is called.
298        """
299        if validate:
300            self.validate_config(config)
301
302        self._config_dict = config
303
304        if self._to_be_selected_streams:
305            self.select_streams(self._to_be_selected_streams)
306            self._to_be_selected_streams = []

Set the config for the connector.

If validate is True, raise an exception if the config fails validation.

If validate is False, validation will be deferred until check() or validate_config() is called.

def get_available_streams(self) -> list[str]:
326    def get_available_streams(self) -> list[str]:
327        """Get the available streams from the spec."""
328        return [s.name for s in self.discovered_catalog.streams]

Get the available streams from the spec.

config_spec: dict[str, typing.Any]
361    @property
362    def config_spec(self) -> dict[str, Any]:
363        """Generate a configuration spec for this connector, as a JSON Schema definition.
364
365        This function generates a JSON Schema dictionary with configuration specs for the
366        current connector, as a dictionary.
367
368        Returns:
369            dict: The JSON Schema configuration spec as a dictionary.
370        """
371        return self._get_spec(force_refresh=True).connectionSpecification

Generate a configuration spec for this connector, as a JSON Schema definition.

This function generates a JSON Schema dictionary with configuration specs for the current connector, as a dictionary.

Returns:

dict: The JSON Schema configuration spec as a dictionary.

docs_url: str
388    @property
389    def docs_url(self) -> str:
390        """Get the URL to the connector's documentation."""
391        return "https://docs.airbyte.com/integrations/sources/" + self.name.lower().replace(
392            "source-", ""
393        )

Get the URL to the connector's documentation.

discovered_catalog: airbyte_protocol.models.airbyte_protocol.AirbyteCatalog
395    @property
396    def discovered_catalog(self) -> AirbyteCatalog:
397        """Get the raw catalog for the given streams.
398
399        If the catalog is not yet known, we call discover to get it.
400        """
401        if self._discovered_catalog is None:
402            self._discovered_catalog = self._discover()
403
404        return self._discovered_catalog

Get the raw catalog for the given streams.

If the catalog is not yet known, we call discover to get it.

configured_catalog: airbyte_protocol.models.airbyte_protocol.ConfiguredAirbyteCatalog
406    @property
407    def configured_catalog(self) -> ConfiguredAirbyteCatalog:
408        """Get the configured catalog for the given streams.
409
410        If the raw catalog is not yet known, we call discover to get it.
411
412        If no specific streams are selected, we return a catalog that syncs all available streams.
413
414        TODO: We should consider disabling by default the streams that the connector would
415        disable by default. (For instance, streams that require a premium license are sometimes
416        disabled by default within the connector.)
417        """
418        # Ensure discovered catalog is cached before we start
419        _ = self.discovered_catalog
420
421        # Filter for selected streams if set, otherwise use all available streams:
422        streams_filter: list[str] = self._selected_stream_names or self.get_available_streams()
423        return self.get_configured_catalog(streams=streams_filter)

Get the configured catalog for the given streams.

If the raw catalog is not yet known, we call discover to get it.

If no specific streams are selected, we return a catalog that syncs all available streams.

TODO: We should consider disabling by default the streams that the connector would disable by default. (For instance, streams that require a premium license are sometimes disabled by default within the connector.)

def get_configured_catalog( self, streams: Union[list[str], Literal['*'], NoneType] = None, *, force_full_refresh: bool = False) -> airbyte_protocol.models.airbyte_protocol.ConfiguredAirbyteCatalog:
425    def get_configured_catalog(
426        self,
427        streams: Literal["*"] | list[str] | None = None,
428        *,
429        force_full_refresh: bool = False,
430    ) -> ConfiguredAirbyteCatalog:
431        """Get a configured catalog for the given streams.
432
433        If no streams are provided, the selected streams will be used. If no streams are selected,
434        all available streams will be used.
435
436        If '*' is provided, all available streams will be used.
437
438        If force_full_refresh is True, streams will be configured with full_refresh sync mode
439        when supported by the stream. Otherwise, incremental sync mode is used when supported.
440        """
441        selected_streams: list[str] = []
442        if streams is None:
443            selected_streams = self._selected_stream_names or self.get_available_streams()
444        elif streams == "*":
445            selected_streams = self.get_available_streams()
446        elif isinstance(streams, list):
447            selected_streams = streams
448        else:
449            raise exc.PyAirbyteInputError(
450                message="Invalid streams argument.",
451                input_value=streams,
452            )
453
454        def _get_sync_mode(stream: AirbyteStream) -> SyncMode:
455            """Determine the sync mode for a stream based on force_full_refresh and support."""
456            # Use getattr to handle mocks or streams without supported_sync_modes attribute
457            supported_modes = getattr(stream, "supported_sync_modes", None)
458
459            if force_full_refresh:
460                # When force_full_refresh is True, prefer full_refresh if supported
461                if supported_modes and SyncMode.full_refresh in supported_modes:
462                    return SyncMode.full_refresh
463                # Fall back to incremental if full_refresh is not supported
464                return SyncMode.incremental
465
466            # Default behavior: preserve previous semantics (always incremental)
467            return SyncMode.incremental
468
469        return ConfiguredAirbyteCatalog(
470            streams=[
471                ConfiguredAirbyteStream(
472                    stream=stream,
473                    destination_sync_mode=DestinationSyncMode.overwrite,
474                    sync_mode=_get_sync_mode(stream),
475                    primary_key=(
476                        [self._primary_key_overrides[stream.name.lower()]]
477                        if stream.name.lower() in self._primary_key_overrides
478                        else stream.source_defined_primary_key
479                    ),
480                    cursor_field=(
481                        [self._cursor_key_overrides[stream.name.lower()]]
482                        if stream.name.lower() in self._cursor_key_overrides
483                        else stream.default_cursor_field
484                    ),
485                    # These are unused in the current implementation:
486                    generation_id=None,
487                    minimum_generation_id=None,
488                    sync_id=None,
489                )
490                for stream in self.discovered_catalog.streams
491                if stream.name in selected_streams
492            ],
493        )

Get a configured catalog for the given streams.

If no streams are provided, the selected streams will be used. If no streams are selected, all available streams will be used.

If '*' is provided, all available streams will be used.

If force_full_refresh is True, streams will be configured with full_refresh sync mode when supported by the stream. Otherwise, incremental sync mode is used when supported.

def get_stream_json_schema(self, stream_name: str) -> dict[str, typing.Any]:
495    def get_stream_json_schema(self, stream_name: str) -> dict[str, Any]:
496        """Return the JSON Schema spec for the specified stream name."""
497        catalog: AirbyteCatalog = self.discovered_catalog
498        found: list[AirbyteStream] = [
499            stream for stream in catalog.streams if stream.name == stream_name
500        ]
501
502        if len(found) == 0:
503            raise exc.PyAirbyteInputError(
504                message="Stream name does not exist in catalog.",
505                input_value=stream_name,
506            )
507
508        if len(found) > 1:
509            raise exc.PyAirbyteInternalError(
510                message="Duplicate streams found with the same name.",
511                context={
512                    "found_streams": found,
513                },
514            )
515
516        return found[0].json_schema

Return the JSON Schema spec for the specified stream name.

def get_records( self, stream: str, *, limit: int | None = None, stop_event: threading.Event | None = None, normalize_field_names: bool = False, prune_undeclared_fields: bool = True) -> airbyte.datasets.LazyDataset:
518    def get_records(
519        self,
520        stream: str,
521        *,
522        limit: int | None = None,
523        stop_event: threading.Event | None = None,
524        normalize_field_names: bool = False,
525        prune_undeclared_fields: bool = True,
526    ) -> LazyDataset:
527        """Read a stream from the connector.
528
529        Args:
530            stream: The name of the stream to read.
531            limit: The maximum number of records to read. If None, all records will be read.
532            stop_event: If set, the event can be triggered by the caller to stop reading records
533                and terminate the process.
534            normalize_field_names: When `True`, field names will be normalized to lower case, with
535                special characters removed. This matches the behavior of PyAirbyte caches and most
536                Airbyte destinations.
537            prune_undeclared_fields: When `True`, undeclared fields will be pruned from the records,
538                which generally matches the behavior of PyAirbyte caches and most Airbyte
539                destinations, specifically when you expect the catalog may be stale. You can disable
540                this to keep all fields in the records.
541
542        This involves the following steps:
543        * Call discover to get the catalog
544        * Generate a configured catalog that syncs the given stream in full_refresh mode
545        * Write the configured catalog and the config to a temporary file
546        * execute the connector with read --config <config_file> --catalog <catalog_file>
547        * Listen to the messages and return the first AirbyteRecordMessages that come along.
548        * Make sure the subprocess is killed when the function returns.
549        """
550        stop_event = stop_event or threading.Event()
551        configured_catalog = self.get_configured_catalog(streams=[stream])
552        if len(configured_catalog.streams) == 0:
553            raise exc.PyAirbyteInputError(
554                message="Requested stream does not exist.",
555                context={
556                    "stream": stream,
557                    "available_streams": self.get_available_streams(),
558                    "connector_name": self.name,
559                },
560            ) from KeyError(stream)
561
562        configured_stream = configured_catalog.streams[0]
563
564        def _with_logging(records: Iterable[dict[str, Any]]) -> Iterator[dict[str, Any]]:
565            yield from records
566
567        stream_record_handler = StreamRecordHandler(
568            json_schema=self.get_stream_json_schema(stream),
569            prune_extra_fields=prune_undeclared_fields,
570            normalize_keys=normalize_field_names,
571        )
572
573        # This method is non-blocking, so we use "PLAIN" to avoid a live progress display
574        progress_tracker = ProgressTracker(
575            ProgressStyle.PLAIN,
576            source=self,
577            cache=None,
578            destination=None,
579            expected_streams=[stream],
580        )
581
582        iterator: Iterator[dict[str, Any]] = (
583            StreamRecord.from_record_message(
584                record_message=record.record,
585                stream_record_handler=stream_record_handler,
586            )
587            for record in self._read_with_catalog(
588                catalog=configured_catalog,
589                progress_tracker=progress_tracker,
590                stop_event=stop_event,
591            )
592            if record.record
593        )
594        if limit is not None:
595            # Stop the iterator after the limit is reached
596            iterator = islice(iterator, limit)
597
598        return LazyDataset(
599            iterator,
600            stream_metadata=configured_stream,
601            stop_event=stop_event,
602            progress_tracker=progress_tracker,
603        )

Read a stream from the connector.

Arguments:
  • stream: The name of the stream to read.
  • limit: The maximum number of records to read. If None, all records will be read.
  • stop_event: If set, the event can be triggered by the caller to stop reading records and terminate the process.
  • normalize_field_names: When True, field names will be normalized to lower case, with special characters removed. This matches the behavior of PyAirbyte caches and most Airbyte destinations.
  • prune_undeclared_fields: When True, undeclared fields will be pruned from the records, which generally matches the behavior of PyAirbyte caches and most Airbyte destinations, specifically when you expect the catalog may be stale. You can disable this to keep all fields in the records.

This involves the following steps:

  • Call discover to get the catalog
  • Generate a configured catalog that syncs the given stream in full_refresh mode
  • Write the configured catalog and the config to a temporary file
  • execute the connector with read --config --catalog
  • Listen to the messages and return the first AirbyteRecordMessages that come along.
  • Make sure the subprocess is killed when the function returns.
def get_documents( self, stream: str, title_property: str | None = None, content_properties: list[str] | None = None, metadata_properties: list[str] | None = None, *, render_metadata: bool = False) -> Iterable[airbyte.documents.Document]:
605    def get_documents(
606        self,
607        stream: str,
608        title_property: str | None = None,
609        content_properties: list[str] | None = None,
610        metadata_properties: list[str] | None = None,
611        *,
612        render_metadata: bool = False,
613    ) -> Iterable[Document]:
614        """Read a stream from the connector and return the records as documents.
615
616        If metadata_properties is not set, all properties that are not content will be added to
617        the metadata.
618
619        If render_metadata is True, metadata will be rendered in the document, as well as the
620        the main content.
621        """
622        return self.get_records(stream).to_documents(
623            title_property=title_property,
624            content_properties=content_properties,
625            metadata_properties=metadata_properties,
626            render_metadata=render_metadata,
627        )

Read a stream from the connector and return the records as documents.

If metadata_properties is not set, all properties that are not content will be added to the metadata.

If render_metadata is True, metadata will be rendered in the document, as well as the the main content.

def get_samples( self, streams: Union[list[str], Literal['*'], NoneType] = None, *, limit: int = 5, on_error: Literal['raise', 'ignore', 'log'] = 'raise') -> dict[str, airbyte.datasets._inmemory.InMemoryDataset | None]:
629    def get_samples(
630        self,
631        streams: list[str] | Literal["*"] | None = None,
632        *,
633        limit: int = 5,
634        on_error: Literal["raise", "ignore", "log"] = "raise",
635    ) -> dict[str, InMemoryDataset | None]:
636        """Get a sample of records from the given streams."""
637        if streams == "*":
638            streams = self.get_available_streams()
639        elif streams is None:
640            streams = self.get_selected_streams()
641
642        results: dict[str, InMemoryDataset | None] = {}
643        for stream in streams:
644            stop_event = threading.Event()
645            try:
646                results[stream] = self.get_records(
647                    stream,
648                    limit=limit,
649                    stop_event=stop_event,
650                ).fetch_all()
651                stop_event.set()
652            except Exception as ex:
653                results[stream] = None
654                if on_error == "ignore":
655                    continue
656
657                if on_error == "raise":
658                    raise ex from None
659
660                if on_error == "log":
661                    print(f"Error fetching sample for stream '{stream}': {ex}")
662
663        return results

Get a sample of records from the given streams.

def print_samples( self, streams: Union[list[str], Literal['*'], NoneType] = None, *, limit: int = 5, on_error: Literal['raise', 'ignore', 'log'] = 'log') -> None:
665    def print_samples(
666        self,
667        streams: list[str] | Literal["*"] | None = None,
668        *,
669        limit: int = 5,
670        on_error: Literal["raise", "ignore", "log"] = "log",
671    ) -> None:
672        """Print a sample of records from the given streams."""
673        internal_cols: list[str] = [
674            AB_EXTRACTED_AT_COLUMN,
675            AB_META_COLUMN,
676            AB_RAW_ID_COLUMN,
677        ]
678        col_limit = 10
679        if streams == "*":
680            streams = self.get_available_streams()
681        elif streams is None:
682            streams = self.get_selected_streams()
683
684        console = Console()
685
686        console.print(
687            Markdown(
688                f"# Sample Records from `{self.name}` ({len(streams)} selected streams)",
689                justify="left",
690            )
691        )
692
693        for stream in streams:
694            console.print(Markdown(f"## `{stream}` Stream Sample", justify="left"))
695            samples = self.get_samples(
696                streams=[stream],
697                limit=limit,
698                on_error=on_error,
699            )
700            dataset = samples[stream]
701
702            table = Table(
703                show_header=True,
704                show_lines=True,
705            )
706            if dataset is None:
707                console.print(
708                    Markdown("**⚠️ `Error fetching sample records.` ⚠️**"),
709                )
710                continue
711
712            if len(dataset.column_names) > col_limit:
713                # We'll pivot the columns so each column is its own row
714                table.add_column("Column Name")
715                for _ in range(len(dataset)):
716                    table.add_column(overflow="fold")
717                for col in dataset.column_names:
718                    table.add_row(
719                        Markdown(f"**`{col}`**"),
720                        *[escape(str(record[col])) for record in dataset],
721                    )
722            else:
723                for col in dataset.column_names:
724                    table.add_column(
725                        Markdown(f"**`{col}`**"),
726                        overflow="fold",
727                    )
728
729                for record in dataset:
730                    table.add_row(
731                        *[
732                            escape(str(val))
733                            for key, val in record.items()
734                            # Exclude internal Airbyte columns.
735                            if key not in internal_cols
736                        ]
737                    )
738
739            console.print(table)
740
741        console.print(Markdown("--------------"))

Print a sample of records from the given streams.

def read( self, cache: airbyte.caches.CacheBase | None = None, *, streams: str | list[str] | None = None, write_strategy: str | airbyte.strategies.WriteStrategy = <WriteStrategy.AUTO: 'auto'>, force_full_refresh: bool = False, skip_validation: bool = False) -> ReadResult:
843    def read(
844        self,
845        cache: CacheBase | None = None,
846        *,
847        streams: str | list[str] | None = None,
848        write_strategy: str | WriteStrategy = WriteStrategy.AUTO,
849        force_full_refresh: bool = False,
850        skip_validation: bool = False,
851    ) -> ReadResult:
852        """Read from the connector and write to the cache.
853
854        Args:
855            cache: The cache to write to. If not set, a default cache will be used.
856            streams: Optional if already set. A list of stream names to select for reading. If set
857                to "*", all streams will be selected.
858            write_strategy: The strategy to use when writing to the cache. If a string, it must be
859                one of "append", "merge", "replace", or "auto". If a WriteStrategy, it must be one
860                of WriteStrategy.APPEND, WriteStrategy.MERGE, WriteStrategy.REPLACE, or
861                WriteStrategy.AUTO.
862            force_full_refresh: If True, the source will operate in full refresh mode. Otherwise,
863                streams will be read in incremental mode if supported by the connector. This option
864                must be True when using the "replace" strategy.
865            skip_validation: If True, PyAirbyte will not pre-validate the input configuration before
866                running the connector. This can be helpful in debugging, when you want to send
867                configurations to the connector that otherwise might be rejected by JSON Schema
868                validation rules.
869        """
870        cache = cache or get_default_cache()
871        progress_tracker = ProgressTracker(
872            source=self,
873            cache=cache,
874            destination=None,
875            expected_streams=None,  # Will be set later
876        )
877
878        # Set up state provider if not in full refresh mode
879        if force_full_refresh:
880            state_provider: StateProviderBase | None = None
881        else:
882            state_provider = cache.get_state_provider(
883                source_name=self._name,
884            )
885        state_writer = cache.get_state_writer(source_name=self._name)
886
887        if streams:
888            self.select_streams(streams)
889
890        if not self._selected_stream_names:
891            raise exc.PyAirbyteNoStreamsSelectedError(
892                connector_name=self.name,
893                available_streams=self.get_available_streams(),
894            )
895
896        try:
897            result = self._read_to_cache(
898                cache=cache,
899                catalog_provider=CatalogProvider(
900                    self.get_configured_catalog(force_full_refresh=force_full_refresh)
901                ),
902                stream_names=self._selected_stream_names,
903                state_provider=state_provider,
904                state_writer=state_writer,
905                write_strategy=write_strategy,
906                force_full_refresh=force_full_refresh,
907                skip_validation=skip_validation,
908                progress_tracker=progress_tracker,
909            )
910        except exc.PyAirbyteInternalError as ex:
911            progress_tracker.log_failure(exception=ex)
912            raise exc.AirbyteConnectorFailedError(
913                connector_name=self.name,
914                log_text=self._last_log_messages,
915            ) from ex
916        except Exception as ex:
917            progress_tracker.log_failure(exception=ex)
918            raise
919
920        progress_tracker.log_success()
921        return result

Read from the connector and write to the cache.

Arguments:
  • cache: The cache to write to. If not set, a default cache will be used.
  • streams: Optional if already set. A list of stream names to select for reading. If set to "*", all streams will be selected.
  • write_strategy: The strategy to use when writing to the cache. If a string, it must be one of "append", "merge", "replace", or "auto". If a WriteStrategy, it must be one of WriteStrategy.APPEND, WriteStrategy.MERGE, WriteStrategy.REPLACE, or WriteStrategy.AUTO.
  • force_full_refresh: If True, the source will operate in full refresh mode. Otherwise, streams will be read in incremental mode if supported by the connector. This option must be True when using the "replace" strategy.
  • skip_validation: If True, PyAirbyte will not pre-validate the input configuration before running the connector. This can be helpful in debugging, when you want to send configurations to the connector that otherwise might be rejected by JSON Schema validation rules.
class StreamRecord(dict[str, typing.Any]):
175class StreamRecord(dict[str, Any]):
176    """The StreamRecord class is a case-aware, case-insensitive dictionary implementation.
177
178    It has these behaviors:
179    - When a key is retrieved, deleted, or checked for existence, it is always checked in a
180      case-insensitive manner.
181    - The original case is stored in a separate dictionary, so that the original case can be
182      retrieved when needed.
183    - Because it is subclassed from `dict`, the `StreamRecord` class can be passed as a normal
184      Python dictionary.
185    - In addition to the properties of the stream's records, the dictionary also stores the Airbyte
186      metadata columns: `_airbyte_raw_id`, `_airbyte_extracted_at`, and `_airbyte_meta`.
187
188    This behavior mirrors how a case-aware, case-insensitive SQL database would handle column
189    references.
190
191    There are two ways this class can store keys internally:
192    - If normalize_keys is True, the keys are normalized using the given normalizer.
193    - If normalize_keys is False, the original case of the keys is stored.
194
195    In regards to missing values, the dictionary accepts an 'expected_keys' input. When set, the
196    dictionary will be initialized with the given keys. If a key is not found in the input data, it
197    will be initialized with a value of None. When provided, the 'expected_keys' input will also
198    determine the original case of the keys.
199    """
200
201    def __init__(
202        self,
203        from_dict: dict,
204        *,
205        stream_record_handler: StreamRecordHandler,
206        with_internal_columns: bool = True,
207        extracted_at: datetime | None = None,
208    ) -> None:
209        """Initialize the dictionary with the given data.
210
211        Args:
212            from_dict: The dictionary to initialize the StreamRecord with.
213            stream_record_handler: The StreamRecordHandler to use for processing the record.
214            with_internal_columns: If `True`, the internal columns will be added to the record.
215            extracted_at: The time the record was extracted. If not provided, the current time will
216                be used.
217        """
218        self._stream_handler: StreamRecordHandler = stream_record_handler
219
220        # Start by initializing all values to None
221        self.update(dict.fromkeys(stream_record_handler.index_keys))
222
223        # Update the dictionary with the given data
224        if self._stream_handler.prune_extra_fields:
225            self.update(
226                {
227                    self._stream_handler.to_index_case(k): v
228                    for k, v in from_dict.items()
229                    if self._stream_handler.to_index_case(k) in self._stream_handler.index_keys
230                }
231            )
232        else:
233            self.update({self._stream_handler.to_index_case(k): v for k, v in from_dict.items()})
234
235        if with_internal_columns:
236            self.update(
237                {
238                    AB_RAW_ID_COLUMN: uuid7str(),
239                    AB_EXTRACTED_AT_COLUMN: extracted_at or datetime.now(timezone.utc),
240                    AB_META_COLUMN: {},
241                }
242            )
243
244    @classmethod
245    def from_record_message(
246        cls,
247        record_message: AirbyteRecordMessage,
248        *,
249        stream_record_handler: StreamRecordHandler,
250    ) -> StreamRecord:
251        """Return a StreamRecord from a RecordMessage."""
252        data_dict: dict[str, Any] = record_message.data.copy()
253        return cls(
254            from_dict=data_dict,
255            stream_record_handler=stream_record_handler,
256            with_internal_columns=True,
257            extracted_at=datetime.fromtimestamp(record_message.emitted_at / 1000, tz=timezone.utc),
258        )
259
260    def __getitem__(self, key: str) -> Any:  # noqa: ANN401
261        """Return the item with the given key."""
262        try:
263            return super().__getitem__(key)
264        except KeyError:
265            return super().__getitem__(self._stream_handler.to_index_case(key))
266
267    def __setitem__(self, key: str, value: Any) -> None:  # noqa: ANN401
268        """Set the item with the given key to the given value."""
269        index_case_key = self._stream_handler.to_index_case(key)
270        if (
271            self._stream_handler.prune_extra_fields
272            and index_case_key not in self._stream_handler.index_keys
273        ):
274            return
275
276        super().__setitem__(index_case_key, value)
277
278    def __delitem__(self, key: str) -> None:
279        """Delete the item with the given key."""
280        try:
281            super().__delitem__(key)
282        except KeyError:
283            index_case_key = self._stream_handler.to_index_case(key)
284            if super().__contains__(index_case_key):
285                super().__delitem__(index_case_key)
286                return
287        else:
288            # No failure. Key was deleted.
289            return
290
291        raise KeyError(key)
292
293    def __contains__(self, key: object) -> bool:
294        """Return whether the dictionary contains the given key."""
295        assert isinstance(key, str), "Key must be a string."
296        return super().__contains__(key) or super().__contains__(
297            self._stream_handler.to_index_case(key)
298        )
299
300    def __iter__(self) -> Iterator[str]:
301        """Return an iterator over the keys of the dictionary."""
302        return iter(super().__iter__())
303
304    def __len__(self) -> int:
305        """Return the number of items in the dictionary."""
306        return super().__len__()
307
308    def __eq__(self, other: object) -> bool:
309        """Return whether the StreamRecord is equal to the given dict or StreamRecord object."""
310        if isinstance(other, StreamRecord):
311            return dict(self) == dict(other)
312
313        if isinstance(other, dict):
314            return {k.lower(): v for k, v in self.items()} == {
315                k.lower(): v for k, v in other.items()
316            }
317        return False
318
319    def __hash__(self) -> int:  # type: ignore [override]  # Doesn't match superclass (dict)
320        """Return the hash of the dictionary with keys sorted."""
321        items = [(k, v) for k, v in self.items() if not isinstance(v, dict)]
322        return hash(tuple(sorted(items)))

The StreamRecord class is a case-aware, case-insensitive dictionary implementation.

It has these behaviors:

  • When a key is retrieved, deleted, or checked for existence, it is always checked in a case-insensitive manner.
  • The original case is stored in a separate dictionary, so that the original case can be retrieved when needed.
  • Because it is subclassed from dict, the StreamRecord class can be passed as a normal Python dictionary.
  • In addition to the properties of the stream's records, the dictionary also stores the Airbyte metadata columns: _airbyte_raw_id, _airbyte_extracted_at, and _airbyte_meta.

This behavior mirrors how a case-aware, case-insensitive SQL database would handle column references.

There are two ways this class can store keys internally:

  • If normalize_keys is True, the keys are normalized using the given normalizer.
  • If normalize_keys is False, the original case of the keys is stored.

In regards to missing values, the dictionary accepts an 'expected_keys' input. When set, the dictionary will be initialized with the given keys. If a key is not found in the input data, it will be initialized with a value of None. When provided, the 'expected_keys' input will also determine the original case of the keys.

@classmethod
def from_record_message( cls, record_message: airbyte_protocol.models.airbyte_protocol.AirbyteRecordMessage, *, stream_record_handler: airbyte.records.StreamRecordHandler) -> StreamRecord:
244    @classmethod
245    def from_record_message(
246        cls,
247        record_message: AirbyteRecordMessage,
248        *,
249        stream_record_handler: StreamRecordHandler,
250    ) -> StreamRecord:
251        """Return a StreamRecord from a RecordMessage."""
252        data_dict: dict[str, Any] = record_message.data.copy()
253        return cls(
254            from_dict=data_dict,
255            stream_record_handler=stream_record_handler,
256            with_internal_columns=True,
257            extracted_at=datetime.fromtimestamp(record_message.emitted_at / 1000, tz=timezone.utc),
258        )

Return a StreamRecord from a RecordMessage.

class WriteResult:
104class WriteResult:
105    """The result of a write operation.
106
107    This class is used to return information about the write operation, such as the number of
108    records written. It should not be created directly, but instead returned by the write method
109    of a destination.
110    """
111
112    def __init__(
113        self,
114        *,
115        destination: AirbyteWriterInterface | Destination,
116        source_data: Source | ReadResult,
117        catalog_provider: CatalogProvider,
118        state_writer: StateWriterBase,
119        progress_tracker: ProgressTracker,
120    ) -> None:
121        """Initialize a write result.
122
123        This class should not be created directly. Instead, it should be returned by the `write`
124        method of the `Destination` class.
125        """
126        self._destination: AirbyteWriterInterface | Destination = destination
127        self._source_data: Source | ReadResult = source_data
128        self._catalog_provider: CatalogProvider = catalog_provider
129        self._state_writer: StateWriterBase = state_writer
130        self._progress_tracker: ProgressTracker = progress_tracker
131
132    @property
133    def processed_records(self) -> int:
134        """The total number of records written to the destination."""
135        return self._progress_tracker.total_destination_records_delivered
136
137    def get_state_provider(self) -> StateProviderBase:
138        """Return the state writer as a state provider.
139
140        As a public interface, we only expose the state writer as a state provider. This is because
141        the state writer itself is only intended for internal use. As a state provider, the state
142        writer can be used to read the state artifacts that were written. This can be useful for
143        testing or debugging.
144        """
145        return self._state_writer

The result of a write operation.

This class is used to return information about the write operation, such as the number of records written. It should not be created directly, but instead returned by the write method of a destination.

WriteResult( *, destination: airbyte._writers.base.AirbyteWriterInterface | Destination, source_data: Source | ReadResult, catalog_provider: airbyte.shared.catalog_providers.CatalogProvider, state_writer: airbyte.shared.state_writers.StateWriterBase, progress_tracker: airbyte.progress.ProgressTracker)
112    def __init__(
113        self,
114        *,
115        destination: AirbyteWriterInterface | Destination,
116        source_data: Source | ReadResult,
117        catalog_provider: CatalogProvider,
118        state_writer: StateWriterBase,
119        progress_tracker: ProgressTracker,
120    ) -> None:
121        """Initialize a write result.
122
123        This class should not be created directly. Instead, it should be returned by the `write`
124        method of the `Destination` class.
125        """
126        self._destination: AirbyteWriterInterface | Destination = destination
127        self._source_data: Source | ReadResult = source_data
128        self._catalog_provider: CatalogProvider = catalog_provider
129        self._state_writer: StateWriterBase = state_writer
130        self._progress_tracker: ProgressTracker = progress_tracker

Initialize a write result.

This class should not be created directly. Instead, it should be returned by the write method of the Destination class.

processed_records: int
132    @property
133    def processed_records(self) -> int:
134        """The total number of records written to the destination."""
135        return self._progress_tracker.total_destination_records_delivered

The total number of records written to the destination.

def get_state_provider(self) -> airbyte.shared.state_providers.StateProviderBase:
137    def get_state_provider(self) -> StateProviderBase:
138        """Return the state writer as a state provider.
139
140        As a public interface, we only expose the state writer as a state provider. This is because
141        the state writer itself is only intended for internal use. As a state provider, the state
142        writer can be used to read the state artifacts that were written. This can be useful for
143        testing or debugging.
144        """
145        return self._state_writer

Return the state writer as a state provider.

As a public interface, we only expose the state writer as a state provider. This is because the state writer itself is only intended for internal use. As a state provider, the state writer can be used to read the state artifacts that were written. This can be useful for testing or debugging.