Skip to content

Commit 91a0c47

Browse files
authored
Merge branch 'main' into devin/1748296708-junit-test-results
2 parents b63dd12 + c64ff62 commit 91a0c47

File tree

31 files changed

+1126
-260
lines changed

31 files changed

+1126
-260
lines changed

airbyte_cdk/cli/airbyte_cdk/_connector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ def test(
162162
pytest_args.append("--collect-only")
163163

164164
pytest_args.append(str(test_file_path))
165+
166+
test_results_dir = connector_directory / "build" / "test-results"
167+
test_results_dir.mkdir(parents=True, exist_ok=True)
168+
junit_xml_path = test_results_dir / "standard-tests-junit.xml"
169+
pytest_args.extend(["--junitxml", str(junit_xml_path)])
170+
165171
click.echo(f"Running tests from connector directory: {connector_directory}...")
166172
click.echo(f"Test file: {test_file_path}")
167173
click.echo(f"Collect only: {collect_only}")

airbyte_cdk/manifest_migrations/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ This directory contains the logic and registry for manifest migrations in the Ai
2020

2121
3. **Register the Migration:**
2222
- Open `migrations/registry.yaml`.
23-
- Add an entry under the appropriate version, or create a new version section if needed.
23+
- Add an entry under the appropriate version, or create a new version section if needed.
24+
- Version can be: "*", "==6.48.3", "~=1.2", ">=1.0.0,<2.0.0", "6.48.3"
2425
- Each migration entry should include:
2526
- `name`: The filename (without `.py`)
2627
- `order`: The order in which this migration should be applied for the version

airbyte_cdk/manifest_migrations/migration_handler.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import copy
77
import logging
8+
import re
89
from datetime import datetime, timezone
9-
from typing import Type
10+
from typing import Tuple, Type
1011

12+
from packaging.specifiers import SpecifierSet
1113
from packaging.version import Version
1214

1315
from airbyte_cdk.manifest_migrations.exceptions import (
@@ -25,7 +27,7 @@
2527
METADATA_TAG = "metadata"
2628
MANIFEST_VERSION_TAG = "version"
2729
APPLIED_MIGRATIONS_TAG = "applied_migrations"
28-
30+
WILDCARD_VERSION_PATTERN = ".*"
2931
LOGGER = logging.getLogger("airbyte.cdk.manifest_migrations")
3032

3133

@@ -77,11 +79,14 @@ def _handle_migration(
7779
"""
7880
try:
7981
migration_instance = migration_class()
80-
if self._version_is_valid_for_migration(manifest_version, migration_version):
82+
can_apply_migration, should_bump_version = self._version_is_valid_for_migration(
83+
manifest_version, migration_version
84+
)
85+
if can_apply_migration:
8186
migration_instance._process_manifest(self._migrated_manifest)
8287
if migration_instance.is_migrated:
83-
# set the updated manifest version, after migration has been applied
84-
self._set_manifest_version(migration_version)
88+
if should_bump_version:
89+
self._set_manifest_version(migration_version)
8590
self._set_migration_trace(migration_class, manifest_version, migration_version)
8691
else:
8792
LOGGER.info(
@@ -112,18 +117,30 @@ def _version_is_valid_for_migration(
112117
self,
113118
manifest_version: str,
114119
migration_version: str,
115-
) -> bool:
120+
) -> Tuple[bool, bool]:
121+
"""
122+
Decide whether *manifest_version* satisfies the *migration_version* rule.
123+
124+
Rules
125+
-----
126+
1. ``"*"``
127+
– Wildcard: anything matches.
128+
2. String starts with a PEP 440 operator (``==``, ``!=``, ``<=``, ``>=``,
129+
``<``, ``>``, ``~=``, etc.)
130+
– Treat *migration_version* as a SpecifierSet and test the manifest
131+
version against it.
132+
3. Plain version
133+
– Interpret both strings as concrete versions and return
134+
``manifest_version <= migration_version``.
116135
"""
117-
Checks if the given manifest version is less than or equal to the specified migration version.
136+
if re.match(WILDCARD_VERSION_PATTERN, migration_version):
137+
return True, False
118138

119-
Args:
120-
manifest_version (str): The version of the manifest to check.
121-
migration_version (str): The migration version to compare against.
139+
if migration_version.startswith(("=", "!", ">", "<", "~")):
140+
spec = SpecifierSet(migration_version)
141+
return spec.contains(Version(manifest_version)), False
122142

123-
Returns:
124-
bool: True if the manifest version is less than or equal to the migration version, False otherwise.
125-
"""
126-
return Version(manifest_version) <= Version(migration_version)
143+
return Version(manifest_version) <= Version(migration_version), True
127144

128145
def _set_manifest_version(self, version: str) -> None:
129146
"""

airbyte_cdk/manifest_migrations/migrations/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,18 @@
22
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
33
#
44

5+
from airbyte_cdk.manifest_migrations.migrations.http_requester_path_to_url import (
6+
HttpRequesterPathToUrl,
7+
)
8+
from airbyte_cdk.manifest_migrations.migrations.http_requester_request_body_json_data_to_request_body import (
9+
HttpRequesterRequestBodyJsonDataToRequestBody,
10+
)
11+
from airbyte_cdk.manifest_migrations.migrations.http_requester_url_base_to_url import (
12+
HttpRequesterUrlBaseToUrl,
13+
)
14+
15+
__all__ = [
16+
"HttpRequesterPathToUrl",
17+
"HttpRequesterRequestBodyJsonDataToRequestBody",
18+
"HttpRequesterUrlBaseToUrl",
19+
]

airbyte_cdk/manifest_migrations/migrations/registry.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44

55
manifest_migrations:
6-
- version: 6.48.3
6+
- version: "*"
77
migrations:
88
- name: http_requester_url_base_to_url
99
order: 1

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3630,6 +3630,9 @@ definitions:
36303630
delimiter:
36313631
type: string
36323632
default: ","
3633+
set_empty_cell_to_none:
3634+
type: boolean
3635+
default: false
36333636
AsyncJobStatusMap:
36343637
description: Matches the api job status to Async Job Status.
36353638
type: object
@@ -4055,6 +4058,11 @@ definitions:
40554058
title: Value Type
40564059
description: The expected data type of the value. If omitted, the type will be inferred from the value provided.
40574060
"$ref": "#/definitions/ValueType"
4061+
create_or_update:
4062+
title: Create or Update
4063+
description: Determines whether to create a new path if it doesn't exist (true) or only update existing paths (false). When set to true, the resolver will create new paths in the stream template if they don't exist. When false (default), it will only update existing paths.
4064+
type: boolean
4065+
default: false
40584066
$parameters:
40594067
type: object
40604068
additionalProperties: true
@@ -4106,6 +4114,12 @@ definitions:
41064114
- ["data"]
41074115
- ["data", "streams"]
41084116
- ["data", "{{ parameters.name }}"]
4117+
default_values:
4118+
title: Default Values
4119+
description: A list of default values, each matching the structure expected from the parsed component value.
4120+
type: array
4121+
items:
4122+
type: object
41094123
$parameters:
41104124
type: object
41114125
additionalProperties: true
@@ -4117,7 +4131,11 @@ definitions:
41174131
type: string
41184132
enum: [ConfigComponentsResolver]
41194133
stream_config:
4120-
"$ref": "#/definitions/StreamConfig"
4134+
anyOf:
4135+
- type: array
4136+
items:
4137+
"$ref": "#/definitions/StreamConfig"
4138+
- "$ref": "#/definitions/StreamConfig"
41214139
components_mapping:
41224140
type: array
41234141
items:

airbyte_cdk/sources/declarative/decoders/composite_raw_decoder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class CsvParser(Parser):
103103
# TODO: migrate implementation to re-use file-base classes
104104
encoding: Optional[str] = "utf-8"
105105
delimiter: Optional[str] = ","
106+
set_empty_cell_to_none: Optional[bool] = False
106107

107108
def _get_delimiter(self) -> Optional[str]:
108109
"""
@@ -121,6 +122,8 @@ def parse(self, data: BufferedIOBase) -> PARSER_OUTPUT_TYPE:
121122
text_data = TextIOWrapper(data, encoding=self.encoding) # type: ignore
122123
reader = csv.DictReader(text_data, delimiter=self._get_delimiter() or ",")
123124
for row in reader:
125+
if self.set_empty_cell_to_none:
126+
row = {k: (None if v == "" else v) for k, v in row.items()}
124127
yield row
125128

126129

airbyte_cdk/sources/declarative/interpolation/macros.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import datetime
77
import typing
88
from typing import Optional, Union
9+
from urllib.parse import quote_plus
910

1011
import isodate
1112
import pytz
@@ -182,6 +183,17 @@ def format_datetime(
182183
return DatetimeParser().format(dt=dt_datetime, format=format)
183184

184185

186+
def sanitize_url(value: str) -> str:
187+
"""
188+
Sanitizes a value by via urllib.parse.quote_plus
189+
190+
Usage:
191+
`"{{ sanitize_url('https://example.com/path?query=value') }}"`
192+
"""
193+
sanitization_strategy = quote_plus
194+
return sanitization_strategy(value)
195+
196+
185197
_macros_list = [
186198
now_utc,
187199
today_utc,
@@ -193,5 +205,6 @@ def format_datetime(
193205
format_datetime,
194206
today_with_timezone,
195207
str_to_datetime,
208+
sanitize_url,
196209
]
197210
macros = {f.__name__: f for f in _macros_list}

airbyte_cdk/sources/declarative/manifest_declarative_source.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __init__(
128128
component_factory
129129
if component_factory
130130
else ModelToComponentFactory(
131-
emit_connector_builder_messages,
131+
emit_connector_builder_messages=emit_connector_builder_messages,
132132
max_concurrent_async_job_count=source_config.get("max_concurrent_async_job_count"),
133133
)
134134
)
@@ -300,9 +300,7 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
300300
}
301301
)
302302

303-
stream_configs = self._stream_configs(self._source_config) + self._dynamic_stream_configs(
304-
self._source_config, config
305-
)
303+
stream_configs = self._stream_configs(self._source_config) + self.dynamic_streams
306304

307305
api_budget_model = self._source_config.get("api_budget")
308306
if api_budget_model:

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,7 @@ class CsvDecoder(BaseModel):
13831383
type: Literal["CsvDecoder"]
13841384
encoding: Optional[str] = "utf-8"
13851385
delimiter: Optional[str] = ","
1386+
set_empty_cell_to_none: Optional[bool] = False
13861387

13871388

13881389
class AsyncJobStatusMap(BaseModel):
@@ -1478,6 +1479,11 @@ class ComponentMappingDefinition(BaseModel):
14781479
description="The expected data type of the value. If omitted, the type will be inferred from the value provided.",
14791480
title="Value Type",
14801481
)
1482+
create_or_update: Optional[bool] = Field(
1483+
False,
1484+
description="Determines whether to create a new path if it doesn't exist (true) or only update existing paths (false). When set to true, the resolver will create new paths in the stream template if they don't exist. When false (default), it will only update existing paths.",
1485+
title="Create or Update",
1486+
)
14811487
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
14821488

14831489

@@ -1489,12 +1495,17 @@ class StreamConfig(BaseModel):
14891495
examples=[["data"], ["data", "streams"], ["data", "{{ parameters.name }}"]],
14901496
title="Configs Pointer",
14911497
)
1498+
default_values: Optional[List[Dict[str, Any]]] = Field(
1499+
None,
1500+
description="A list of default values, each matching the structure expected from the parsed component value.",
1501+
title="Default Values",
1502+
)
14921503
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
14931504

14941505

14951506
class ConfigComponentsResolver(BaseModel):
14961507
type: Literal["ConfigComponentsResolver"]
1497-
stream_config: StreamConfig
1508+
stream_config: Union[List[StreamConfig], StreamConfig]
14981509
components_mapping: List[ComponentMappingDefinition]
14991510
parameters: Optional[Dict[str, Any]] = Field(None, alias="$parameters")
15001511

0 commit comments

Comments
 (0)