Skip to content

Commit 26b74b1

Browse files
Merge branch 'main' into daryna/fix-substream-partition-router
2 parents 367f291 + 3eb9ce8 commit 26b74b1

8 files changed

Lines changed: 105 additions & 19 deletions

File tree

airbyte_cdk/cli/airbyte_cdk/_connector.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@
4545

4646
import rich_click as click
4747

48-
from airbyte_cdk.test.standard_tests.util import create_connector_test_suite
49-
5048
# from airbyte_cdk.test.standard_tests import pytest_hooks
5149
from airbyte_cdk.utils.connector_paths import (
5250
find_connector_root_from_name,
@@ -174,6 +172,9 @@ def run_connector_tests(
174172
"pytest is not installed. Please install pytest to run the connector tests."
175173
)
176174

175+
# Lazy import: this module requires pytest, which is only available via the [dev] extra.
176+
from airbyte_cdk.test.standard_tests.util import create_connector_test_suite
177+
177178
connector_test_suite = create_connector_test_suite(
178179
connector_name=connector_name if not connector_directory else None,
179180
connector_directory=connector_directory,

airbyte_cdk/cli/airbyte_cdk/_secrets.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,12 @@
6161
logger = logging.getLogger("airbyte-cdk.cli.secrets")
6262

6363
try:
64+
import google.auth.exceptions
6465
from google.cloud import secretmanager_v1 as secretmanager
6566
from google.cloud.secretmanager_v1 import Secret
6667
except ImportError:
6768
# If the package is not installed, we will raise an error in the CLI command.
69+
google = None # type: ignore
6870
secretmanager = None # type: ignore
6971
Secret = None # type: ignore
7072

@@ -235,7 +237,10 @@ def list_(
235237
"""
236238
click.echo("Scanning secrets...", err=True)
237239

238-
connector_name, _ = resolve_connector_name_and_directory(connector)
240+
if connector and isinstance(connector, str) and "/" not in connector and "\\" not in connector:
241+
connector_name = connector
242+
else:
243+
connector_name, _ = resolve_connector_name_and_directory(connector)
239244
secrets: list[Secret] = _fetch_secret_handles( # type: ignore
240245
connector_name=connector_name,
241246
gcp_project_id=gcp_project_id,
@@ -411,7 +416,14 @@ def _get_secret_filepath(
411416

412417

413418
def _get_gsm_secrets_client() -> "secretmanager.SecretManagerServiceClient": # type: ignore
414-
"""Get the Google Secret Manager client."""
419+
"""Get the Google Secret Manager client.
420+
421+
If the `GCP_GSM_CREDENTIALS` environment variable is set, the client will be
422+
created using service account credentials from that JSON string. Otherwise, the
423+
client will fall back to Application Default Credentials (ADC), which supports
424+
user credentials from `gcloud auth application-default login`, GCE metadata
425+
server credentials, and other standard GCP authentication methods.
426+
"""
415427
if not secretmanager:
416428
raise ImportError(
417429
"google-cloud-secret-manager package is required for Secret Manager integration. "
@@ -420,18 +432,28 @@ def _get_gsm_secrets_client() -> "secretmanager.SecretManagerServiceClient": #
420432
)
421433

422434
credentials_json = os.environ.get("GCP_GSM_CREDENTIALS")
423-
if not credentials_json:
424-
raise ValueError(
425-
"No Google Cloud credentials found. "
426-
"Please set the `GCP_GSM_CREDENTIALS` environment variable."
435+
if credentials_json:
436+
click.echo(
437+
"Using GCP service account credentials from GCP_GSM_CREDENTIALS env var.", err=True
438+
)
439+
return cast(
440+
"secretmanager.SecretManagerServiceClient",
441+
secretmanager.SecretManagerServiceClient.from_service_account_info(
442+
json.loads(credentials_json)
443+
),
427444
)
428445

429-
return cast(
430-
"secretmanager.SecretManagerServiceClient",
431-
secretmanager.SecretManagerServiceClient.from_service_account_info(
432-
json.loads(credentials_json)
433-
),
446+
click.echo(
447+
"GCP_GSM_CREDENTIALS not set. Using Application Default Credentials (ADC).", err=True
434448
)
449+
try:
450+
return secretmanager.SecretManagerServiceClient()
451+
except google.auth.exceptions.DefaultCredentialsError:
452+
raise ValueError(
453+
"No Google Cloud credentials found. "
454+
"Either set the `GCP_GSM_CREDENTIALS` environment variable with service account JSON, "
455+
"or run `gcloud auth application-default login` to authenticate with your user account."
456+
) from None
435457

436458

437459
def _print_ci_secrets_masks(

airbyte_cdk/sources/declarative/interpolation/filters.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ def regex_search(value: str, regex: str) -> str:
136136
return ""
137137

138138

139+
def regex_replace(value: str, regex: str, replacement: str) -> str:
140+
"""
141+
Replace all occurrences of a regular expression pattern in a string.
142+
"""
143+
return re.sub(regex, replacement, value)
144+
145+
139146
def hmac(value: Any, key: str, hash_type: str = "sha256") -> str:
140147
"""
141148
Implementation of a custom Jinja2 hmac filter with SHA-256 support.
@@ -181,6 +188,7 @@ def hmac(value: Any, key: str, hash_type: str = "sha256") -> str:
181188
base64binascii_decode,
182189
string,
183190
regex_search,
191+
regex_replace,
184192
hmac,
185193
]
186194
filters = {f.__name__: f for f in _filters_list}

airbyte_cdk/sources/declarative/resolvers/config_components_resolver.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,10 @@ def _parse_yaml_if_possible(value: Any) -> Any:
204204
return yaml.safe_load(value)
205205
except ParserError: # "{{ record[0] in ['cohortActiveUsers'] }}" # not valid YAML
206206
return value
207-
except ScannerError as e: # "%Y-%m-%d' # not valid yaml
207+
except ScannerError as e: # "%Y-%m-%d" or strings with tabs - not valid YAML
208208
if "expected alphabetic or numeric character, but found '%'" in str(e):
209209
return value
210+
if "found character '\\t' that cannot start any token" in str(e):
211+
return value
210212
raise e
211213
return value

airbyte_cdk/sources/file_based/stream/concurrent/adapters.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,7 @@ def get_json_schema(self) -> Mapping[str, Any]:
155155

156156
@property
157157
def primary_key(self) -> PrimaryKeyType:
158-
return (
159-
self._legacy_stream.config.primary_key
160-
or self.get_parser().get_parser_defined_primary_key(self._legacy_stream.config)
161-
)
158+
return self._legacy_stream.primary_key
162159

163160
def get_parser(self) -> FileTypeParser:
164161
return self._legacy_stream.get_parser()

airbyte_cdk/sources/file_based/stream/permissions_file_based_stream.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from airbyte_cdk.models import AirbyteLogMessage, AirbyteMessage, Level
99
from airbyte_cdk.models import Type as MessageType
10+
from airbyte_cdk.sources.file_based.config.file_based_stream_config import PrimaryKeyType
1011
from airbyte_cdk.sources.file_based.file_based_stream_permissions_reader import (
1112
AbstractFileBasedStreamPermissionsReader,
1213
)
@@ -35,6 +36,10 @@ def __init__(
3536
super().__init__(**kwargs)
3637
self.stream_permissions_reader = stream_permissions_reader
3738

39+
@property
40+
def primary_key(self) -> PrimaryKeyType:
41+
return self.config.primary_key
42+
3843
def _filter_schema_invalid_properties(
3944
self, configured_catalog_json_schema: Dict[str, Any]
4045
) -> Dict[str, Any]:

unit_tests/sources/declarative/interpolation/test_filters.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,36 @@ def test_regex_search_no_match() -> None:
108108
assert val is None
109109

110110

111+
@pytest.mark.parametrize(
112+
"expression, expected",
113+
[
114+
pytest.param(
115+
"{{ 'hello world' | regex_replace('world', 'there') }}",
116+
"hello there",
117+
id="basic_replacement",
118+
),
119+
pytest.param(
120+
"{{ 'abc123def456' | regex_replace('[0-9]+', '') }}",
121+
"abcdef",
122+
id="regex_pattern_strip_digits",
123+
),
124+
pytest.param(
125+
"{{ 'hello world' | regex_replace('xyz', 'replaced') }}",
126+
"hello world",
127+
id="no_match_returns_original",
128+
),
129+
pytest.param(
130+
"{{ 'aaa bbb aaa' | regex_replace('aaa', 'ccc') }}",
131+
"ccc bbb ccc",
132+
id="multiple_occurrences",
133+
),
134+
],
135+
)
136+
def test_regex_replace(expression: str, expected: str) -> None:
137+
val = interpolation.eval(expression, {})
138+
assert val == expected
139+
140+
111141
def test_hmac_sha256_default() -> None:
112142
message = "test_message"
113143
secret_key = "test_secret_key"

unit_tests/sources/declarative/resolvers/test_config_components_resolver.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,20 @@ def to_configured_catalog(
160160
}
161161
)
162162

163+
# Manifest with component definition with value containing tab characters
164+
# which would cause YAML ScannerError (tabs cannot start tokens in YAML)
165+
_MANIFEST_WITH_TAB_SCANNER_ERROR = deepcopy(_MANIFEST)
166+
_MANIFEST_WITH_TAB_SCANNER_ERROR["dynamic_streams"][0]["components_resolver"][
167+
"components_mapping"
168+
].append(
169+
{
170+
"type": "ComponentMappingDefinition",
171+
"create_or_update": True,
172+
"field_path": ["retriever", "requester", "$parameters", "custom_query"],
173+
"value": "SELECT\n\tcampaign.name,\n\tcampaign.id\nFROM campaign", # Contains tab characters
174+
}
175+
)
176+
163177

164178
@pytest.mark.parametrize(
165179
"manifest, config, expected_exception, expected_stream_names",
@@ -173,8 +187,15 @@ def to_configured_catalog(
173187
),
174188
(_MANIFEST_WITH_STREAM_CONFIGS_LIST, _CONFIG, None, ["item_1", "item_2", "default_item"]),
175189
(_MANIFEST_WITH_SCANNER_ERROR, _CONFIG, None, ["item_1", "item_2", "default_item"]),
190+
(_MANIFEST_WITH_TAB_SCANNER_ERROR, _CONFIG, None, ["item_1", "item_2", "default_item"]),
191+
],
192+
ids=[
193+
"no_duplicates",
194+
"duplicates",
195+
"stream_configs_list",
196+
"scanner_error",
197+
"tab_scanner_error",
176198
],
177-
ids=["no_duplicates", "duplicates", "stream_configs_list", "scanner_error"],
178199
)
179200
def test_dynamic_streams_read_with_config_components_resolver(
180201
manifest, config, expected_exception, expected_stream_names

0 commit comments

Comments
 (0)