Skip to content

Commit d7ee4f1

Browse files
committed
Merge branch 'aj/feat/add-standard-tests-cli' into devin/1744841809-add-build-command
2 parents b25671b + 0cc217e commit d7ee4f1

File tree

31 files changed

+1296
-443
lines changed

31 files changed

+1296
-443
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ dist
1515
.idea
1616
.vscode
1717
**/__pycache__
18+
.tmp
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""CLI commands for `airbyte-cdk`.
3+
4+
This CLI interface allows you to interact with your connector, including
5+
testing and running commands.
6+
7+
**Basic Usage:**
8+
9+
```bash
10+
airbyte-cdk --help
11+
airbyte-cdk --version
12+
airbyte-cdk connector --help
13+
airbyte-cdk manifest --help
14+
```
15+
16+
**Running Statelessly:**
17+
18+
You can run the latest version of this CLI, from any machine, using `pipx` or `uvx`:
19+
20+
```bash
21+
# Run the latest version of the CLI:
22+
pipx run airbyte-cdk connector --help
23+
uvx airbyte-cdk connector --help
24+
25+
# Run from a specific CDK version:
26+
pipx run airbyte-cdk==6.5.1 connector --help
27+
uvx airbyte-cdk==6.5.1 connector --help
28+
```
29+
30+
**Running within your virtualenv:**
31+
32+
You can also run from your connector's virtualenv:
33+
34+
```bash
35+
poetry run airbyte-cdk connector --help
36+
```
37+
38+
"""
39+
40+
from typing import cast
41+
42+
import rich_click as click
43+
44+
from airbyte_cdk.cli.airbyte_cdk._connector import connector_cli_group
45+
from airbyte_cdk.cli.airbyte_cdk._image import image_cli_group
46+
from airbyte_cdk.cli.airbyte_cdk._manifest import manifest_cli_group
47+
from airbyte_cdk.cli.airbyte_cdk._version import print_version
48+
49+
50+
@click.group(
51+
help=__doc__.replace("\n", "\n\n"), # Workaround to format help text correctly
52+
invoke_without_command=True,
53+
)
54+
@click.option(
55+
"--version",
56+
is_flag=True,
57+
help="Show the version of the Airbyte CDK.",
58+
)
59+
@click.pass_context
60+
def cli(
61+
ctx: click.Context,
62+
version: bool,
63+
) -> None:
64+
"""Airbyte CDK CLI.
65+
66+
Help text is provided from the file-level docstring.
67+
"""
68+
if version:
69+
print_version(short=False)
70+
ctx.exit()
71+
72+
if ctx.invoked_subcommand is None:
73+
# If no subcommand is provided, show the help message.
74+
click.echo(ctx.get_help())
75+
ctx.exit()
76+
77+
78+
cli.add_command(connector_cli_group)
79+
cli.add_command(manifest_cli_group)
80+
cli.add_command(image_cli_group)
81+
82+
83+
if __name__ == "__main__":
84+
cli()
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""CLI command for `airbyte-cdk`."""
3+
4+
USAGE = """CLI command for `airbyte-cdk`.
5+
6+
This CLI interface allows you to interact with your connector, including
7+
testing and running commands.
8+
9+
**Basic Usage:**
10+
11+
```bash
12+
airbyte-cdk --help
13+
airbyte-cdk connector --help
14+
airbyte-cdk manifest --help
15+
```
16+
17+
**Running Statelessly:**
18+
19+
You can run the latest version of this CLI, from any machine, using `pipx` or `uvx`:
20+
21+
```bash
22+
# Run the latest version of the CLI:
23+
pipx run airbyte-cdk connector --help
24+
uvx airbyte-cdk connector --help
25+
26+
# Run from a specific CDK version:
27+
pipx run airbyte-cdk==6.5.1 connector --help
28+
uvx airbyte-cdk==6.5.1 connector --help
29+
```
30+
31+
**Running within your virtualenv:**
32+
33+
You can also run from your connector's virtualenv:
34+
35+
```bash
36+
poetry run airbyte-cdk connector --help
37+
```
38+
39+
"""
40+
41+
import os
42+
from pathlib import Path
43+
from types import ModuleType
44+
45+
import rich_click as click
46+
47+
# from airbyte_cdk.test.standard_tests import pytest_hooks
48+
from airbyte_cdk.test.standard_tests.test_resources import find_connector_root_from_name
49+
from airbyte_cdk.test.standard_tests.util import create_connector_test_suite
50+
51+
click.rich_click.TEXT_MARKUP = "markdown"
52+
53+
pytest: ModuleType | None
54+
try:
55+
import pytest
56+
except ImportError:
57+
pytest = None
58+
# Handle the case where pytest is not installed.
59+
# This prevents import errors when running the script without pytest installed.
60+
# We will raise an error later if pytest is required for a given command.
61+
62+
63+
TEST_FILE_TEMPLATE = '''
64+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
65+
"""FAST Airbyte Standard Tests for the source_pokeapi_w_components source."""
66+
67+
#from airbyte_cdk.test.standard_tests import {base_class_name}
68+
from airbyte_cdk.test.standard_tests.util import create_connector_test_suite
69+
from pathlib import Path
70+
71+
pytest_plugins = [
72+
"airbyte_cdk.test.standard_tests.pytest_hooks",
73+
]
74+
75+
TestSuite = create_connector_test_suite(
76+
connector_directory=Path(),
77+
)
78+
79+
# class TestSuite({base_class_name}):
80+
# """Test suite for the source_pokeapi_w_components source.
81+
82+
# This class inherits from SourceTestSuiteBase and implements all of the tests in the suite.
83+
84+
# As long as the class name starts with "Test", pytest will automatically discover and run the
85+
# tests in this class.
86+
# """
87+
'''
88+
89+
90+
@click.group(name="connector")
91+
def connector_cli_group() -> None:
92+
"""Connector related commands."""
93+
pass
94+
95+
96+
@connector_cli_group.command()
97+
@click.option(
98+
"--connector-name",
99+
type=str,
100+
# help="Name of the connector to test. Ignored if --connector-directory is provided.",
101+
)
102+
@click.option(
103+
"--connector-directory",
104+
type=click.Path(exists=True, file_okay=False, path_type=Path),
105+
# help="Path to the connector directory.",
106+
)
107+
@click.option(
108+
"--collect-only",
109+
is_flag=True,
110+
default=False,
111+
help="Only collect tests, do not run them.",
112+
)
113+
def test(
114+
connector_name: str | None = None,
115+
connector_directory: Path | None = None,
116+
*,
117+
collect_only: bool = False,
118+
) -> None:
119+
"""Run connector tests.
120+
121+
This command runs the standard connector tests for a specific connector.
122+
123+
If no connector name or directory is provided, we will look within the current working
124+
directory. If the current working directory is not a connector directory (e.g. starting
125+
with 'source-') and no connector name or path is provided, the process will fail.
126+
"""
127+
if pytest is None:
128+
raise ImportError(
129+
"pytest is not installed. Please install pytest to run the connector tests."
130+
)
131+
click.echo("Connector test command executed.")
132+
if not connector_name and not connector_directory:
133+
cwd = Path().resolve().absolute()
134+
if cwd.name.startswith("source-") or cwd.name.startswith("destination-"):
135+
connector_name = cwd.name
136+
connector_directory = cwd
137+
else:
138+
raise ValueError(
139+
"Either connector_name or connector_directory must be provided if not "
140+
"running from a connector directory."
141+
)
142+
143+
if connector_directory:
144+
connector_directory = connector_directory.resolve().absolute()
145+
elif connector_name:
146+
connector_directory = find_connector_root_from_name(connector_name)
147+
else:
148+
raise ValueError("Either connector_name or connector_directory must be provided.")
149+
150+
connector_test_suite = create_connector_test_suite(
151+
connector_name=connector_name if not connector_directory else None,
152+
connector_directory=connector_directory,
153+
)
154+
155+
pytest_args: list[str] = []
156+
if connector_directory:
157+
pytest_args.append(f"--rootdir={connector_directory}")
158+
os.chdir(str(connector_directory))
159+
else:
160+
print("No connector directory provided. Running tests in the current directory.")
161+
162+
file_text = TEST_FILE_TEMPLATE.format(
163+
base_class_name=connector_test_suite.__bases__[0].__name__,
164+
connector_directory=str(connector_directory),
165+
)
166+
test_file_path = Path() / ".tmp" / "integration_tests/test_airbyte_standards.py"
167+
test_file_path = test_file_path.resolve().absolute()
168+
test_file_path.parent.mkdir(parents=True, exist_ok=True)
169+
test_file_path.write_text(file_text)
170+
171+
if collect_only:
172+
pytest_args.append("--collect-only")
173+
174+
pytest_args.append(str(test_file_path))
175+
click.echo(f"Running tests from connector directory: {connector_directory}...")
176+
click.echo(f"Test file: {test_file_path}")
177+
click.echo(f"Collect only: {collect_only}")
178+
click.echo(f"Pytest args: {pytest_args}")
179+
click.echo("Invoking Pytest...")
180+
pytest.main(
181+
pytest_args,
182+
plugins=[],
183+
)
184+
185+
186+
__all__ = [
187+
"connector_cli_group",
188+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Docker image commands."""
3+
4+
import click
5+
6+
7+
@click.group(name="image")
8+
def image_cli_group() -> None:
9+
"""Docker image commands."""
10+
pass
11+
12+
13+
__all__ = [
14+
"image_cli_group",
15+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Manifest related commands."""
3+
4+
import click
5+
6+
7+
@click.group(name="manifest")
8+
def manifest_cli_group() -> None:
9+
"""Manifest related commands."""
10+
pass
11+
12+
13+
__all__ = [
14+
"manifest_cli_group",
15+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Version information for the Airbyte CDK CLI."""
3+
4+
from airbyte_cdk import __version__
5+
6+
7+
def print_version(short: bool = False) -> None:
8+
"""Print the version of the Airbyte CDK CLI."""
9+
10+
if short:
11+
print(__version__)
12+
else:
13+
print(f"Airbyte CDK version: {__version__}")

airbyte_cdk/sources/declarative/auth/oauth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ def get_token_expiry_date(self) -> AirbyteDateTime:
239239
def _has_access_token_been_initialized(self) -> bool:
240240
return self._access_token is not None
241241

242-
def set_token_expiry_date(self, value: Union[str, int]) -> None:
243-
self._token_expiry_date = self._parse_token_expiration_date(value)
242+
def set_token_expiry_date(self, value: AirbyteDateTime) -> None:
243+
self._token_expiry_date = value
244244

245245
def get_assertion_name(self) -> str:
246246
return self.assertion_name

airbyte_cdk/sources/declarative/concurrent_declarative_source.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
from airbyte_cdk.sources.declarative.extractors.record_filter import (
2020
ClientSideIncrementalRecordFilterDecorator,
2121
)
22-
from airbyte_cdk.sources.declarative.incremental import ConcurrentPerPartitionCursor
22+
from airbyte_cdk.sources.declarative.incremental import (
23+
ConcurrentPerPartitionCursor,
24+
GlobalSubstreamCursor,
25+
)
2326
from airbyte_cdk.sources.declarative.incremental.datetime_based_cursor import DatetimeBasedCursor
2427
from airbyte_cdk.sources.declarative.incremental.per_partition_with_global import (
2528
PerPartitionWithGlobalCursor,
@@ -361,7 +364,8 @@ def _group_streams(
361364
== DatetimeBasedCursorModel.__name__
362365
and hasattr(declarative_stream.retriever, "stream_slicer")
363366
and isinstance(
364-
declarative_stream.retriever.stream_slicer, PerPartitionWithGlobalCursor
367+
declarative_stream.retriever.stream_slicer,
368+
(GlobalSubstreamCursor, PerPartitionWithGlobalCursor),
365369
)
366370
):
367371
stream_state = self._connector_state_manager.get_stream_state(

airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1439,7 +1439,9 @@ def create_concurrent_cursor_from_perpartition_cursor(
14391439
stream_state = self.apply_stream_state_migrations(stream_state_migrations, stream_state)
14401440

14411441
# Per-partition state doesn't make sense for GroupingPartitionRouter, so force the global state
1442-
use_global_cursor = isinstance(partition_router, GroupingPartitionRouter)
1442+
use_global_cursor = isinstance(
1443+
partition_router, GroupingPartitionRouter
1444+
) or component_definition.get("global_substream_cursor", False)
14431445

14441446
# Return the concurrent cursor and state converter
14451447
return ConcurrentPerPartitionCursor(

airbyte_cdk/sources/declarative/requesters/query_properties/property_chunking.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ def get_request_property_chunks(
5252
chunk_size = 0
5353
for property_field in property_fields:
5454
# If property_limit_type is not defined, we default to property_count which is just an incrementing count
55+
# todo: Add ability to specify parameter delimiter representation and take into account in property_field_size
5556
property_field_size = (
5657
len(property_field)
58+
+ 1 # The +1 represents the extra character for the delimiter in between properties
5759
if self.property_limit_type == PropertyLimitType.characters
5860
else 1
5961
)

0 commit comments

Comments
 (0)