Skip to content

Commit 2841384

Browse files
authored
feat(connector-builder): add version constraint and wildcard support to manifest migrations (#569)
1 parent 4343391 commit 2841384

File tree

6 files changed

+99
-29
lines changed

6 files changed

+99
-29
lines changed

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

unit_tests/manifest_migrations/conftest.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66

77
import pytest
88

9+
from airbyte_cdk.manifest_migrations.manifest_migration import ManifestMigration
10+
911

1012
@pytest.fixture
1113
def manifest_with_url_base_to_migrate_to_url() -> Dict[str, Any]:
1214
return {
13-
"version": "0.0.0",
15+
"version": "6.48.3",
1416
"type": "DeclarativeSource",
1517
"check": {
1618
"type": "CheckStream",
@@ -493,14 +495,14 @@ def expected_manifest_with_url_base_migrated_to_url() -> Dict[str, Any]:
493495
"metadata": {
494496
"applied_migrations": [
495497
{
496-
"from_version": "0.0.0",
497-
"to_version": "6.48.3",
498+
"from_version": "6.48.3",
499+
"to_version": ">=6.48.2,<6.50.0",
498500
"migration": "HttpRequesterUrlBaseToUrl",
499501
"migrated_at": "2025-04-01T00:00:00+00:00", # time freezed in the test
500502
},
501503
{
502-
"from_version": "0.0.0",
503-
"to_version": "6.48.3",
504+
"from_version": "6.48.3",
505+
"to_version": ">=6.48.2,<6.50.0",
504506
"migration": "HttpRequesterPathToUrl",
505507
"migrated_at": "2025-04-01T00:00:00+00:00", # time freezed in the test
506508
},
@@ -832,7 +834,7 @@ def manifest_with_request_body_json_and_data_to_migrate_to_request_body() -> Dic
832834
@pytest.fixture
833835
def expected_manifest_with_migrated_to_request_body() -> Dict[str, Any]:
834836
return {
835-
"version": "6.48.3",
837+
"version": "0.0.0",
836838
"type": "DeclarativeSource",
837839
"check": {"type": "CheckStream", "stream_names": ["A"]},
838840
"definitions": {
@@ -1195,22 +1197,36 @@ def expected_manifest_with_migrated_to_request_body() -> Dict[str, Any]:
11951197
"applied_migrations": [
11961198
{
11971199
"from_version": "0.0.0",
1198-
"to_version": "6.48.3",
1200+
"to_version": "*",
11991201
"migration": "HttpRequesterUrlBaseToUrl",
12001202
"migrated_at": "2025-04-01T00:00:00+00:00",
12011203
},
12021204
{
12031205
"from_version": "0.0.0",
1204-
"to_version": "6.48.3",
1206+
"to_version": "*",
12051207
"migration": "HttpRequesterPathToUrl",
12061208
"migrated_at": "2025-04-01T00:00:00+00:00",
12071209
},
12081210
{
12091211
"from_version": "0.0.0",
1210-
"to_version": "6.48.3",
1212+
"to_version": "*",
12111213
"migration": "HttpRequesterRequestBodyJsonDataToRequestBody",
12121214
"migrated_at": "2025-04-01T00:00:00+00:00",
12131215
},
12141216
]
12151217
},
12161218
}
1219+
1220+
1221+
class DummyMigration(ManifestMigration):
1222+
def _process_manifest(self, manifest):
1223+
self.is_migrated = False
1224+
1225+
def should_migrate(self, manifest):
1226+
return True
1227+
1228+
def validate(self, manifest):
1229+
return True
1230+
1231+
def migrate(self, manifest):
1232+
pass

unit_tests/manifest_migrations/test_manifest_migration.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,39 @@
11
#
2-
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
33
#
4+
from unittest.mock import patch
45

56
from freezegun import freeze_time
67

8+
from airbyte_cdk.manifest_migrations import migrations_registry
79
from airbyte_cdk.manifest_migrations.migration_handler import (
810
ManifestMigrationHandler,
911
)
10-
from airbyte_cdk.sources.declarative.manifest_declarative_source import ManifestDeclarativeSource
12+
from airbyte_cdk.manifest_migrations.migrations import (
13+
HttpRequesterPathToUrl,
14+
HttpRequesterRequestBodyJsonDataToRequestBody,
15+
HttpRequesterUrlBaseToUrl,
16+
)
1117
from airbyte_cdk.sources.declarative.parsers.manifest_reference_resolver import (
1218
ManifestReferenceResolver,
1319
)
20+
from unit_tests.manifest_migrations.conftest import DummyMigration
1421

1522
resolver = ManifestReferenceResolver()
1623

1724

1825
@freeze_time("2025-04-01")
26+
@patch.dict(
27+
migrations_registry.MANIFEST_MIGRATIONS,
28+
{
29+
">=6.48.2,<6.50.0": [
30+
HttpRequesterUrlBaseToUrl,
31+
HttpRequesterPathToUrl,
32+
HttpRequesterRequestBodyJsonDataToRequestBody,
33+
]
34+
},
35+
clear=True,
36+
)
1937
def test_manifest_resolve_migrate_url_base_and_path_to_url(
2038
manifest_with_url_base_to_migrate_to_url,
2139
expected_manifest_with_url_base_migrated_to_url,
@@ -25,7 +43,9 @@ def test_manifest_resolve_migrate_url_base_and_path_to_url(
2543
when the `url_base` is migrated to `url` and the `path` is joined to `url`.
2644
"""
2745

28-
resolved_manifest = resolver.preprocess_manifest(manifest_with_url_base_to_migrate_to_url)
46+
resolved_manifest = ManifestReferenceResolver().preprocess_manifest(
47+
manifest_with_url_base_to_migrate_to_url
48+
)
2949
migrated_manifest = ManifestMigrationHandler(dict(resolved_manifest)).apply_migrations()
3050

3151
assert migrated_manifest == expected_manifest_with_url_base_migrated_to_url
@@ -50,6 +70,7 @@ def test_manifest_resolve_migrate_request_body_json_and_data_to_request_body(
5070

5171

5272
@freeze_time("2025-04-01")
73+
@patch.dict(migrations_registry.MANIFEST_MIGRATIONS, {"0.0.0": [DummyMigration]}, clear=True)
5374
def test_manifest_resolve_do_not_migrate(
5475
manifest_with_migrated_url_base_and_path_is_joined_to_url,
5576
) -> None:
@@ -58,7 +79,7 @@ def test_manifest_resolve_do_not_migrate(
5879
after the `url_base` and `path` is joined to `url`.
5980
"""
6081

61-
resolved_manifest = resolver.preprocess_manifest(
82+
resolved_manifest = ManifestReferenceResolver().preprocess_manifest(
6283
manifest_with_migrated_url_base_and_path_is_joined_to_url
6384
)
6485
migrated_manifest = ManifestMigrationHandler(dict(resolved_manifest)).apply_migrations()

0 commit comments

Comments
 (0)