airbyte-cdk 6.48.0__py3-none-any.whl → 6.48.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,16 @@ def get_limits(config: Mapping[str, Any]) -> TestLimits:
56
56
  return TestLimits(max_records, max_pages_per_slice, max_slices, max_streams)
57
57
 
58
58
 
59
+ def should_migrate_manifest(config: Mapping[str, Any]) -> bool:
60
+ """
61
+ Determines whether the manifest should be migrated,
62
+ based on the presence of the "__should_migrate" key in the config.
63
+
64
+ This flag is set by the UI.
65
+ """
66
+ return config.get("__should_migrate", False)
67
+
68
+
59
69
  def should_normalize_manifest(config: Mapping[str, Any]) -> bool:
60
70
  """
61
71
  Check if the manifest should be normalized.
@@ -71,6 +81,7 @@ def create_source(config: Mapping[str, Any], limits: TestLimits) -> ManifestDecl
71
81
  config=config,
72
82
  emit_connector_builder_messages=True,
73
83
  source_config=manifest,
84
+ migrate_manifest=should_migrate_manifest(config),
74
85
  normalize_manifest=should_normalize_manifest(config),
75
86
  component_factory=ModelToComponentFactory(
76
87
  emit_connector_builder_messages=True,
@@ -0,0 +1,73 @@
1
+ # Manifest Migrations
2
+
3
+ This directory contains the logic and registry for manifest migrations in the Airbyte CDK. Migrations are used to update or transform manifest components to newer formats or schemas as the CDK evolves.
4
+
5
+ ## Adding a New Migration
6
+
7
+ 1. **Create a Migration File:**
8
+ - Add a new Python file in the `migrations/` subdirectory.
9
+ - Name the file using the pattern: `<description_of_the_migration>.py`.
10
+ - Example: `http_requester_url_base_to_url.py`
11
+ - The filename should be unique and descriptive.
12
+
13
+ 2. **Define the Migration Class:**
14
+ - The migration class must inherit from `ManifestMigration`.
15
+ - Name the class using a descriptive name (e.g., `HttpRequesterUrlBaseToUrl`).
16
+ - Implement the following methods:
17
+ - `should_migrate(self, manifest: ManifestType) -> bool`
18
+ - `migrate(self, manifest: ManifestType) -> None`
19
+ - `validate(self, manifest: ManifestType) -> bool`
20
+
21
+ 3. **Register the Migration:**
22
+ - Open `migrations/registry.yaml`.
23
+ - Add an entry under the appropriate version, or create a new version section if needed.
24
+ - Each migration entry should include:
25
+ - `name`: The filename (without `.py`)
26
+ - `order`: The order in which this migration should be applied for the version
27
+ - `description`: A short description of the migration
28
+
29
+ Example:
30
+ ```yaml
31
+ manifest_migrations:
32
+ - version: 6.45.2
33
+ migrations:
34
+ - name: http_requester_url_base_to_url
35
+ order: 1
36
+ description: |
37
+ This migration updates the `url_base` field in the `HttpRequester` component spec to `url`.
38
+ ```
39
+
40
+ 4. **Testing:**
41
+ - Ensure your migration is covered by unit tests.
42
+ - Tests should verify both `should_migrate`, `migrate`, and `validate` behaviors.
43
+
44
+ ## Migration Discovery
45
+
46
+ - Migrations are discovered and registered automatically based on the entries in `migrations/registry.yaml`.
47
+ - Do not modify the migration registry in code manually.
48
+ - If you need to skip certain component types, use the `NON_MIGRATABLE_TYPES` list in `manifest_migration.py`.
49
+
50
+ ## Example Migration Skeleton
51
+
52
+ ```python
53
+ from airbyte_cdk.manifest_migrations.manifest_migration import TYPE_TAG, ManifestMigration, ManifestType
54
+
55
+ class ExampleMigration(ManifestMigration):
56
+ component_type = "ExampleComponent"
57
+ original_key = "old_key"
58
+ replacement_key = "new_key"
59
+
60
+ def should_migrate(self, manifest: ManifestType) -> bool:
61
+ return manifest[TYPE_TAG] == self.component_type and self.original_key in manifest
62
+
63
+ def migrate(self, manifest: ManifestType) -> None:
64
+ manifest[self.replacement_key] = manifest[self.original_key]
65
+ manifest.pop(self.original_key, None)
66
+
67
+ def validate(self, manifest: ManifestType) -> bool:
68
+ return self.replacement_key in manifest and self.original_key not in manifest
69
+ ```
70
+
71
+ ---
72
+
73
+ For more details, see the docstrings in `manifest_migration.py` and the examples in the `migrations/` folder.
@@ -0,0 +1,3 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
@@ -0,0 +1,12 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ class ManifestMigrationException(Exception):
7
+ """
8
+ Raised when a migration error occurs in the manifest.
9
+ """
10
+
11
+ def __init__(self, message: str) -> None:
12
+ super().__init__(f"Failed to migrate the manifest: {message}")
@@ -0,0 +1,134 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import asdict, dataclass
8
+ from typing import Any, Dict
9
+
10
+ ManifestType = Dict[str, Any]
11
+
12
+
13
+ TYPE_TAG = "type"
14
+
15
+ NON_MIGRATABLE_TYPES = [
16
+ # more info here: https://github.com/airbytehq/airbyte-internal-issues/issues/12423
17
+ "DynamicDeclarativeStream",
18
+ ]
19
+
20
+
21
+ @dataclass
22
+ class MigrationTrace:
23
+ """
24
+ This class represents a migration that has been applied to the manifest.
25
+ It contains information about the migration, including the version it was applied from,
26
+ the version it was applied to, and the time it was applied.
27
+ """
28
+
29
+ from_version: str
30
+ to_version: str
31
+ migration: str
32
+ migrated_at: str
33
+
34
+ def as_dict(self) -> Dict[str, Any]:
35
+ return asdict(self)
36
+
37
+
38
+ class ManifestMigration(ABC):
39
+ """
40
+ Base class for manifest migrations.
41
+ This class provides a framework for migrating manifest components.
42
+ It defines the structure for migration classes, including methods for checking if a migration is needed,
43
+ performing the migration, and validating the migration.
44
+ """
45
+
46
+ def __init__(self) -> None:
47
+ self.is_migrated: bool = False
48
+
49
+ @abstractmethod
50
+ def should_migrate(self, manifest: ManifestType) -> bool:
51
+ """
52
+ Check if the manifest should be migrated.
53
+
54
+ :param manifest: The manifest to potentially migrate
55
+
56
+ :return: true if the manifest is of the expected format and should be migrated. False otherwise.
57
+ """
58
+
59
+ @abstractmethod
60
+ def migrate(self, manifest: ManifestType) -> None:
61
+ """
62
+ Migrate the manifest. Assumes should_migrate(manifest) returned True.
63
+
64
+ :param manifest: The manifest to migrate
65
+ """
66
+
67
+ @abstractmethod
68
+ def validate(self, manifest: ManifestType) -> bool:
69
+ """
70
+ Validate the manifest to ensure the migration was successfully applied.
71
+
72
+ :param manifest: The manifest to validate
73
+ """
74
+
75
+ def _is_component(self, obj: Dict[str, Any]) -> bool:
76
+ """
77
+ Check if the object is a component.
78
+
79
+ :param obj: The object to check
80
+ :return: True if the object is a component, False otherwise
81
+ """
82
+ return TYPE_TAG in obj.keys()
83
+
84
+ def _is_migratable_type(self, obj: Dict[str, Any]) -> bool:
85
+ """
86
+ Check if the object is a migratable component,
87
+ based on the Type of the component and the migration version.
88
+
89
+ :param obj: The object to check
90
+ :return: True if the object is a migratable component, False otherwise
91
+ """
92
+ return obj[TYPE_TAG] not in NON_MIGRATABLE_TYPES
93
+
94
+ def _process_manifest(self, obj: Any) -> None:
95
+ """
96
+ Recursively processes a manifest object, migrating components that match the migration criteria.
97
+
98
+ This method traverses the entire manifest structure (dictionaries and lists) and applies
99
+ migrations to components that:
100
+ 1. Have a type tag
101
+ 2. Are not in the list of non-migratable types
102
+ 3. Meet the conditions defined in the should_migrate method
103
+
104
+ Parameters:
105
+ obj (Any): The object to process, which can be a dictionary, list, or any other type.
106
+ Dictionary objects are checked for component type tags and potentially migrated.
107
+ List objects have each of their items processed recursively.
108
+ Other types are ignored.
109
+
110
+ Returns:
111
+ None, since we process the manifest in place.
112
+ """
113
+ if isinstance(obj, dict):
114
+ # Check if the object is a component
115
+ if self._is_component(obj):
116
+ # Check if the object is allowed to be migrated
117
+ if not self._is_migratable_type(obj):
118
+ return
119
+
120
+ # Check if the object should be migrated
121
+ if self.should_migrate(obj):
122
+ # Perform the migration, if needed
123
+ self.migrate(obj)
124
+ # validate the migration
125
+ self.is_migrated = self.validate(obj)
126
+
127
+ # Process all values in the dictionary
128
+ for value in list(obj.values()):
129
+ self._process_manifest(value)
130
+
131
+ elif isinstance(obj, list):
132
+ # Process all items in the list
133
+ for item in obj:
134
+ self._process_manifest(item)
@@ -0,0 +1,163 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ import copy
7
+ import logging
8
+ from datetime import datetime, timezone
9
+ from typing import Type
10
+
11
+ from packaging.version import Version
12
+
13
+ from airbyte_cdk.manifest_migrations.exceptions import (
14
+ ManifestMigrationException,
15
+ )
16
+ from airbyte_cdk.manifest_migrations.manifest_migration import (
17
+ ManifestMigration,
18
+ ManifestType,
19
+ MigrationTrace,
20
+ )
21
+ from airbyte_cdk.manifest_migrations.migrations_registry import (
22
+ MANIFEST_MIGRATIONS,
23
+ )
24
+
25
+ METADATA_TAG = "metadata"
26
+ MANIFEST_VERSION_TAG = "version"
27
+ APPLIED_MIGRATIONS_TAG = "applied_migrations"
28
+
29
+ LOGGER = logging.getLogger("airbyte.cdk.manifest_migrations")
30
+
31
+
32
+ class ManifestMigrationHandler:
33
+ """
34
+ This class is responsible for handling migrations in the manifest.
35
+ """
36
+
37
+ def __init__(self, manifest: ManifestType) -> None:
38
+ self._manifest = manifest
39
+ self._migrated_manifest: ManifestType = copy.deepcopy(self._manifest)
40
+
41
+ def apply_migrations(self) -> ManifestType:
42
+ """
43
+ Apply all registered migrations to the manifest.
44
+
45
+ This method iterates through all migrations in the migrations registry and applies
46
+ them sequentially to the current manifest. If any migration fails with a
47
+ ManifestMigrationException, the original unmodified manifest is returned instead.
48
+
49
+ Returns:
50
+ ManifestType: The migrated manifest if all migrations succeeded, or the original
51
+ manifest if any migration failed.
52
+ """
53
+ try:
54
+ manifest_version = self._get_manifest_version()
55
+ for migration_version, migrations in MANIFEST_MIGRATIONS.items():
56
+ for migration_cls in migrations:
57
+ self._handle_migration(migration_cls, manifest_version, migration_version)
58
+ return self._migrated_manifest
59
+ except ManifestMigrationException:
60
+ # if any errors occur we return the original resolved manifest
61
+ return self._manifest
62
+
63
+ def _handle_migration(
64
+ self,
65
+ migration_class: Type[ManifestMigration],
66
+ manifest_version: str,
67
+ migration_version: str,
68
+ ) -> None:
69
+ """
70
+ Handles a single manifest migration by instantiating the migration class and processing the manifest.
71
+
72
+ Args:
73
+ migration_class (Type[ManifestMigration]): The migration class to apply to the manifest.
74
+
75
+ Raises:
76
+ ManifestMigrationException: If the migration process encounters any errors.
77
+ """
78
+ try:
79
+ migration_instance = migration_class()
80
+ if self._version_is_valid_for_migration(manifest_version, migration_version):
81
+ migration_instance._process_manifest(self._migrated_manifest)
82
+ if migration_instance.is_migrated:
83
+ # set the updated manifest version, after migration has been applied
84
+ self._set_manifest_version(migration_version)
85
+ self._set_migration_trace(migration_class, manifest_version, migration_version)
86
+ else:
87
+ LOGGER.info(
88
+ f"Manifest migration: `{self._get_migration_name(migration_class)}` is not supported for the given manifest version `{manifest_version}`.",
89
+ )
90
+ except Exception as e:
91
+ raise ManifestMigrationException(str(e)) from e
92
+
93
+ def _get_migration_name(self, migration_class: Type[ManifestMigration]) -> str:
94
+ """
95
+ Get the name of the migration instance.
96
+
97
+ Returns:
98
+ str: The name of the migration.
99
+ """
100
+ return migration_class.__name__
101
+
102
+ def _get_manifest_version(self) -> str:
103
+ """
104
+ Get the manifest version from the manifest.
105
+
106
+ :param manifest: The manifest to get the version from
107
+ :return: The manifest version
108
+ """
109
+ return str(self._migrated_manifest.get(MANIFEST_VERSION_TAG, "0.0.0"))
110
+
111
+ def _version_is_valid_for_migration(
112
+ self,
113
+ manifest_version: str,
114
+ migration_version: str,
115
+ ) -> bool:
116
+ """
117
+ Checks if the given manifest version is less than or equal to the specified migration version.
118
+
119
+ Args:
120
+ manifest_version (str): The version of the manifest to check.
121
+ migration_version (str): The migration version to compare against.
122
+
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)
127
+
128
+ def _set_manifest_version(self, version: str) -> None:
129
+ """
130
+ Set the manifest version in the manifest.
131
+
132
+ :param version: The version to set
133
+ """
134
+ self._migrated_manifest[MANIFEST_VERSION_TAG] = version
135
+
136
+ def _set_migration_trace(
137
+ self,
138
+ migration_instance: Type[ManifestMigration],
139
+ manifest_version: str,
140
+ migration_version: str,
141
+ ) -> None:
142
+ """
143
+ Set the migration trace in the manifest, under the `metadata.applied_migrations` property object.
144
+
145
+ :param migration_instance: The migration instance to set
146
+ :param manifest_version: The manifest version before migration
147
+ :param migration_version: The manifest version after migration
148
+ """
149
+
150
+ if METADATA_TAG not in self._migrated_manifest:
151
+ self._migrated_manifest[METADATA_TAG] = {}
152
+ if APPLIED_MIGRATIONS_TAG not in self._migrated_manifest[METADATA_TAG]:
153
+ self._migrated_manifest[METADATA_TAG][APPLIED_MIGRATIONS_TAG] = []
154
+
155
+ migration_trace = MigrationTrace(
156
+ from_version=manifest_version,
157
+ to_version=migration_version,
158
+ migration=self._get_migration_name(migration_instance),
159
+ migrated_at=datetime.now(tz=timezone.utc).isoformat(),
160
+ ).as_dict()
161
+
162
+ if migration_version not in self._migrated_manifest[METADATA_TAG][APPLIED_MIGRATIONS_TAG]:
163
+ self._migrated_manifest[METADATA_TAG][APPLIED_MIGRATIONS_TAG].append(migration_trace)
@@ -0,0 +1,4 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
@@ -0,0 +1,57 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from urllib.parse import urljoin
7
+
8
+ from airbyte_cdk.manifest_migrations.manifest_migration import (
9
+ TYPE_TAG,
10
+ ManifestMigration,
11
+ ManifestType,
12
+ )
13
+ from airbyte_cdk.sources.types import EmptyString
14
+
15
+
16
+ class HttpRequesterPathToUrl(ManifestMigration):
17
+ """
18
+ This migration is responsible for migrating the `path` key to `url` in the HttpRequester component.
19
+ The `path` key is expected to be a relative path, and the `url` key is expected to be a full URL.
20
+ The migration will concatenate the `url_base` and `path` to form a full URL.
21
+ """
22
+
23
+ component_type = "HttpRequester"
24
+ original_key = "path"
25
+ replacement_key = "url"
26
+
27
+ def should_migrate(self, manifest: ManifestType) -> bool:
28
+ return manifest[TYPE_TAG] == self.component_type and self.original_key in list(
29
+ manifest.keys()
30
+ )
31
+
32
+ def migrate(self, manifest: ManifestType) -> None:
33
+ original_key_value = manifest.get(self.original_key, EmptyString).lstrip("/")
34
+ replacement_key_value = manifest[self.replacement_key]
35
+
36
+ # return a full-url if provided directly from interpolation context
37
+ if original_key_value == EmptyString or original_key_value is None:
38
+ manifest[self.replacement_key] = replacement_key_value
39
+ manifest.pop(self.original_key, None)
40
+ else:
41
+ # since we didn't provide a full-url, the url_base might not have a trailing slash
42
+ # so we join the url_base and path correctly
43
+ if not replacement_key_value.endswith("/"):
44
+ replacement_key_value += "/"
45
+
46
+ manifest[self.replacement_key] = urljoin(replacement_key_value, original_key_value)
47
+ manifest.pop(self.original_key, None)
48
+
49
+ def validate(self, manifest: ManifestType) -> bool:
50
+ """
51
+ Validate the migration by checking if the `url` key is present and the `path` key is not.
52
+ """
53
+ return (
54
+ self.replacement_key in manifest
55
+ and self.original_key not in manifest
56
+ and manifest[self.replacement_key] is not None
57
+ )
@@ -0,0 +1,51 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from airbyte_cdk.manifest_migrations.manifest_migration import (
7
+ TYPE_TAG,
8
+ ManifestMigration,
9
+ ManifestType,
10
+ )
11
+
12
+
13
+ class HttpRequesterRequestBodyJsonDataToRequestBody(ManifestMigration):
14
+ """
15
+ This migration is responsible for migrating the `request_body_json` and `request_body_data` keys
16
+ to a unified `request_body` key in the HttpRequester component.
17
+ The migration will copy the value of either original key to `request_body` and remove the original key.
18
+ """
19
+
20
+ component_type = "HttpRequester"
21
+
22
+ body_json_key = "request_body_json"
23
+ body_data_key = "request_body_data"
24
+ original_keys = (body_json_key, body_data_key)
25
+
26
+ replacement_key = "request_body"
27
+
28
+ def should_migrate(self, manifest: ManifestType) -> bool:
29
+ return manifest[TYPE_TAG] == self.component_type and any(
30
+ key in list(manifest.keys()) for key in self.original_keys
31
+ )
32
+
33
+ def migrate(self, manifest: ManifestType) -> None:
34
+ for key in self.original_keys:
35
+ if key == self.body_json_key and key in manifest:
36
+ manifest[self.replacement_key] = {
37
+ "type": "RequestBodyJson",
38
+ "value": manifest[key],
39
+ }
40
+ manifest.pop(key, None)
41
+ elif key == self.body_data_key and key in manifest:
42
+ manifest[self.replacement_key] = {
43
+ "type": "RequestBodyData",
44
+ "value": manifest[key],
45
+ }
46
+ manifest.pop(key, None)
47
+
48
+ def validate(self, manifest: ManifestType) -> bool:
49
+ return self.replacement_key in manifest and all(
50
+ key not in manifest for key in self.original_keys
51
+ )
@@ -0,0 +1,41 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ from airbyte_cdk.manifest_migrations.manifest_migration import (
7
+ TYPE_TAG,
8
+ ManifestMigration,
9
+ ManifestType,
10
+ )
11
+
12
+
13
+ class HttpRequesterUrlBaseToUrl(ManifestMigration):
14
+ """
15
+ This migration is responsible for migrating the `url_base` key to `url` in the HttpRequester component.
16
+ The `url_base` key is expected to be a base URL, and the `url` key is expected to be a full URL.
17
+ The migration will copy the value of `url_base` to `url`.
18
+ """
19
+
20
+ component_type = "HttpRequester"
21
+ original_key = "url_base"
22
+ replacement_key = "url"
23
+
24
+ def should_migrate(self, manifest: ManifestType) -> bool:
25
+ return manifest[TYPE_TAG] == self.component_type and self.original_key in list(
26
+ manifest.keys()
27
+ )
28
+
29
+ def migrate(self, manifest: ManifestType) -> None:
30
+ manifest[self.replacement_key] = manifest[self.original_key]
31
+ manifest.pop(self.original_key, None)
32
+
33
+ def validate(self, manifest: ManifestType) -> bool:
34
+ """
35
+ Validate the migration by checking if the `url` key is present and the `url_base` key is not.
36
+ """
37
+ return (
38
+ self.replacement_key in manifest
39
+ and self.original_key not in manifest
40
+ and manifest[self.replacement_key] is not None
41
+ )
@@ -0,0 +1,22 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+ manifest_migrations:
6
+ - version: 6.47.1
7
+ migrations:
8
+ - name: http_requester_url_base_to_url
9
+ order: 1
10
+ description: |
11
+ This migration updates the `url_base` field in the `http_requester` spec to `url`.
12
+ The `url_base` field is deprecated and will be removed in a future version.
13
+ - name: http_requester_path_to_url
14
+ order: 2
15
+ description: |
16
+ This migration updates the `path` field in the `http_requester` spec to `url`.
17
+ The `path` field is deprecated and will be removed in a future version.
18
+ - name: http_requester_request_body_json_data_to_request_body
19
+ order: 3
20
+ description: |
21
+ This migration updates the `request_body_json_data` field in the `http_requester` spec to `request_body`.
22
+ The `request_body_json_data` field is deprecated and will be removed in a future version.
@@ -0,0 +1,76 @@
1
+ #
2
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
3
+ #
4
+
5
+
6
+ import importlib
7
+ import inspect
8
+ import os
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Dict, List, Type
12
+
13
+ import yaml
14
+
15
+ from airbyte_cdk.manifest_migrations.manifest_migration import (
16
+ ManifestMigration,
17
+ )
18
+
19
+ DiscoveredMigrations = Dict[str, List[Type[ManifestMigration]]]
20
+
21
+ MIGRATIONS_PATH = Path(__file__).parent / "migrations"
22
+ REGISTRY_PATH = MIGRATIONS_PATH / "registry.yaml"
23
+
24
+
25
+ def _find_migration_module(name: str) -> str:
26
+ """
27
+ Finds the migration module by name in the migrations directory.
28
+ The name should match the file name of the migration module (without the .py extension).
29
+ Raises ImportError if the module is not found.
30
+ """
31
+
32
+ for migration_file in os.listdir(MIGRATIONS_PATH):
33
+ migration_name = name + ".py"
34
+ if migration_file == migration_name:
35
+ return migration_file.replace(".py", "")
36
+
37
+ raise ImportError(f"Migration module '{name}' not found in {MIGRATIONS_PATH}.")
38
+
39
+
40
+ def _get_migration_class(module: ModuleType) -> Type[ManifestMigration]:
41
+ """
42
+ Returns the ManifestMigration subclass defined in the module.
43
+ """
44
+ for _, obj in inspect.getmembers(module, inspect.isclass):
45
+ if issubclass(obj, ManifestMigration):
46
+ return obj
47
+
48
+ raise ImportError(f"No ManifestMigration subclass found in module {module.__name__}.")
49
+
50
+
51
+ def _discover_migrations() -> DiscoveredMigrations:
52
+ """
53
+ Discovers and returns a list of ManifestMigration subclasses in the order specified by registry.yaml.
54
+ """
55
+ with open(REGISTRY_PATH, "r") as f:
56
+ registry = yaml.safe_load(f)
57
+ migrations: DiscoveredMigrations = {}
58
+ # Iterate through the registry and import the migration classes
59
+ # based on the version and order specified in the registry.yaml
60
+ for version_entry in registry.get("manifest_migrations", []):
61
+ migration_version = version_entry.get("version", "0.0.0")
62
+ if not migration_version in migrations:
63
+ migrations[migration_version] = []
64
+
65
+ for migration in sorted(version_entry.get("migrations", []), key=lambda m: m["order"]):
66
+ module = importlib.import_module(
67
+ f"airbyte_cdk.manifest_migrations.migrations.{_find_migration_module(migration['name'])}"
68
+ )
69
+ migration_class = _get_migration_class(module)
70
+ migrations[migration_version].append(migration_class)
71
+
72
+ return migrations
73
+
74
+
75
+ # registered migrations
76
+ MANIFEST_MIGRATIONS: DiscoveredMigrations = _discover_migrations()
@@ -15,6 +15,9 @@ from jsonschema.exceptions import ValidationError
15
15
  from jsonschema.validators import validate
16
16
  from packaging.version import InvalidVersion, Version
17
17
 
18
+ from airbyte_cdk.manifest_migrations.migration_handler import (
19
+ ManifestMigrationHandler,
20
+ )
18
21
  from airbyte_cdk.models import (
19
22
  AirbyteConnectionStatus,
20
23
  AirbyteMessage,
@@ -91,6 +94,7 @@ class ManifestDeclarativeSource(DeclarativeSource):
91
94
  debug: bool = False,
92
95
  emit_connector_builder_messages: bool = False,
93
96
  component_factory: Optional[ModelToComponentFactory] = None,
97
+ migrate_manifest: Optional[bool] = False,
94
98
  normalize_manifest: Optional[bool] = False,
95
99
  ) -> None:
96
100
  """
@@ -104,12 +108,11 @@ class ManifestDeclarativeSource(DeclarativeSource):
104
108
  """
105
109
  self.logger = logging.getLogger(f"airbyte.{self.name}")
106
110
  self._should_normalize = normalize_manifest
111
+ self._should_migrate = migrate_manifest
107
112
  self._declarative_component_schema = _get_declarative_component_schema()
108
113
  # If custom components are needed, locate and/or register them.
109
114
  self.components_module: ModuleType | None = get_registered_components_module(config=config)
110
- # resolve all components in the manifest
111
- self._source_config = self._preprocess_manifest(dict(source_config))
112
-
115
+ # set additional attributes
113
116
  self._debug = debug
114
117
  self._emit_connector_builder_messages = emit_connector_builder_messages
115
118
  self._constructor = (
@@ -126,11 +129,12 @@ class ManifestDeclarativeSource(DeclarativeSource):
126
129
  )
127
130
  self._config = config or {}
128
131
 
132
+ # resolve all components in the manifest
133
+ self._source_config = self._pre_process_manifest(dict(source_config))
129
134
  # validate resolved manifest against the declarative component schema
130
135
  self._validate_source()
131
-
132
136
  # apply additional post-processing to the manifest
133
- self._postprocess_manifest()
137
+ self._post_process_manifest()
134
138
 
135
139
  @property
136
140
  def resolved_manifest(self) -> Mapping[str, Any]:
@@ -145,7 +149,7 @@ class ManifestDeclarativeSource(DeclarativeSource):
145
149
  """
146
150
  return self._source_config
147
151
 
148
- def _preprocess_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
152
+ def _pre_process_manifest(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
149
153
  """
150
154
  Preprocesses the provided manifest dictionary by resolving any manifest references.
151
155
 
@@ -169,12 +173,14 @@ class ManifestDeclarativeSource(DeclarativeSource):
169
173
 
170
174
  return propagated_manifest
171
175
 
172
- def _postprocess_manifest(self) -> None:
176
+ def _post_process_manifest(self) -> None:
173
177
  """
174
178
  Post-processes the manifest after validation.
175
179
  This method is responsible for any additional modifications or transformations needed
176
180
  after the manifest has been validated and before it is used in the source.
177
181
  """
182
+ # apply manifest migration, if required
183
+ self._migrate_manifest()
178
184
  # apply manifest normalization, if required
179
185
  self._normalize_manifest()
180
186
 
@@ -190,6 +196,19 @@ class ManifestDeclarativeSource(DeclarativeSource):
190
196
  normalizer = ManifestNormalizer(self._source_config, self._declarative_component_schema)
191
197
  self._source_config = normalizer.normalize()
192
198
 
199
+ def _migrate_manifest(self) -> None:
200
+ """
201
+ This method is used to migrate the manifest. It should be called after the manifest has been validated.
202
+ The migration is done in place, so the original manifest is modified.
203
+
204
+ The original manifest is returned if any error occurs during migration.
205
+ """
206
+ if self._should_migrate:
207
+ manifest_migrator = ManifestMigrationHandler(self._source_config)
208
+ self._source_config = manifest_migrator.apply_migrations()
209
+ # validate migrated manifest against the declarative component schema
210
+ self._validate_source()
211
+
193
212
  def _fix_source_type(self, manifest: Dict[str, Any]) -> Dict[str, Any]:
194
213
  """
195
214
  Fix the source type in the manifest. This is necessary because the source type is not always set in the manifest.
@@ -2833,13 +2833,9 @@ class ModelToComponentFactory:
2833
2833
  else None
2834
2834
  )
2835
2835
 
2836
- if model.transform_before_filtering is None:
2837
- # default to False if not set
2838
- model.transform_before_filtering = False
2839
-
2840
- assert model.transform_before_filtering is not None # for mypy
2841
-
2842
- transform_before_filtering = model.transform_before_filtering
2836
+ transform_before_filtering = (
2837
+ False if model.transform_before_filtering is None else model.transform_before_filtering
2838
+ )
2843
2839
  if client_side_incremental_sync:
2844
2840
  record_filter = ClientSideIncrementalRecordFilterDecorator(
2845
2841
  config=config,
@@ -2849,7 +2845,11 @@ class ModelToComponentFactory:
2849
2845
  else None,
2850
2846
  **client_side_incremental_sync,
2851
2847
  )
2852
- transform_before_filtering = True
2848
+ transform_before_filtering = (
2849
+ True
2850
+ if model.transform_before_filtering is None
2851
+ else model.transform_before_filtering
2852
+ )
2853
2853
 
2854
2854
  if model.schema_normalization is None:
2855
2855
  # default to no schema normalization if not set
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 6.48.0
3
+ Version: 6.48.1
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -14,7 +14,7 @@ airbyte_cdk/config_observation.py,sha256=7SSPxtN0nXPkm4euGNcTTr1iLbwUL01jy-24V1H
14
14
  airbyte_cdk/connector.py,sha256=bO23kdGRkl8XKFytOgrrWFc_VagteTHVEF6IsbizVkM,4224
15
15
  airbyte_cdk/connector_builder/README.md,sha256=Hw3wvVewuHG9-QgsAq1jDiKuLlStDxKBz52ftyNRnBw,1665
16
16
  airbyte_cdk/connector_builder/__init__.py,sha256=4Hw-PX1-VgESLF16cDdvuYCzGJtHntThLF4qIiULWeo,61
17
- airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=bJ7ufXOWLXN4i8QQ6WhAsnQY2AqlZq0NVL5G-TAPa1g,6241
17
+ airbyte_cdk/connector_builder/connector_builder_handler.py,sha256=OFTzxyfAevI3Um8fXTOLTgoCc4Sx9NzF0boqYkAATfM,6590
18
18
  airbyte_cdk/connector_builder/main.py,sha256=j1pP5N8RsnvQZ4iYxhLdLEHsJ5Ui7IVFBUi6wYMGBkM,3839
19
19
  airbyte_cdk/connector_builder/models.py,sha256=9pIZ98LW_d6fRS39VdnUOf3cxGt4TkC5MJ0_OrzcCRk,1578
20
20
  airbyte_cdk/connector_builder/test_reader/__init__.py,sha256=iTwBMoI9vaJotEgpqZbFjlxRcbxXYypSVJ9YxeHk7wc,120
@@ -36,6 +36,17 @@ airbyte_cdk/destinations/vector_db_based/writer.py,sha256=nZ00xPiohElJmYktEZZIhr
36
36
  airbyte_cdk/entrypoint.py,sha256=NRJv5BNZRSUEVTmNBa9N7ih6fW5sg4DwL0nkB9kI99Y,18570
37
37
  airbyte_cdk/exception_handler.py,sha256=D_doVl3Dt60ASXlJsfviOCswxGyKF2q0RL6rif3fNks,2013
38
38
  airbyte_cdk/logger.py,sha256=1cURbvawbunCAV178q-XhTHcbAQZTSf07WhU7U9AXWU,3744
39
+ airbyte_cdk/manifest_migrations/README.md,sha256=PvnbrW1gyzhlkeucd0YAOXcXVxi0xBUUynzs4DMqjDo,2942
40
+ airbyte_cdk/manifest_migrations/__init__.py,sha256=0eq9ic_6GGXMwzE31eAOSA7PLtBauMfgM9XshjYHF84,61
41
+ airbyte_cdk/manifest_migrations/exceptions.py,sha256=mmMZaCVEkYSGykVL5jKA0xsDWWkybRdQwnh9pGb7VG0,300
42
+ airbyte_cdk/manifest_migrations/manifest_migration.py,sha256=4ohLfbj2PeuPSgCMVbCArb0d-YdaZIllX4ieXQNiRRw,4420
43
+ airbyte_cdk/manifest_migrations/migration_handler.py,sha256=CF8in-Eb45TGzFBxEJrXSzqVr8Lgv0vqvZlbuz1rbQk,6096
44
+ airbyte_cdk/manifest_migrations/migrations/__init__.py,sha256=SJ7imfOgCRYOVaFkW2bVEnSUxbYPlkryWwYT2semsF0,62
45
+ airbyte_cdk/manifest_migrations/migrations/http_requester_path_to_url.py,sha256=IIn2SjRh1v2yaSBFUCDyBHpX6mBhlckhvbsSg55mREI,2153
46
+ airbyte_cdk/manifest_migrations/migrations/http_requester_request_body_json_data_to_request_body.py,sha256=70md8yDu8SWl2JkkFcEs8kyXUbP0F_obIzyHsygyR9k,1777
47
+ airbyte_cdk/manifest_migrations/migrations/http_requester_url_base_to_url.py,sha256=EX1MVYVpoWypA28qoH48wA0SYZjGdlR8bcSixTDzfgo,1346
48
+ airbyte_cdk/manifest_migrations/migrations/registry.yaml,sha256=56B09vbBlGWKF5KSB-3rashZLRiJmL8si_lmTokXWsc,960
49
+ airbyte_cdk/manifest_migrations/migrations_registry.py,sha256=zly2fwaOxDukqC7eowzrDlvhA2v71FjW74kDzvRXhSY,2619
39
50
  airbyte_cdk/models/__init__.py,sha256=Et9wJWs5VOWynGbb-3aJRhsdAHAiLkNNLxdwqJAuqkw,2114
40
51
  airbyte_cdk/models/airbyte_protocol.py,sha256=oZdKsZ7yPjUt9hvxdWNpxCtgjSV2RWhf4R9Np03sqyY,3613
41
52
  airbyte_cdk/models/airbyte_protocol_serializers.py,sha256=s6SaFB2CMrG_7jTQGn_fhFbQ1FUxhCxf5kq2RWGHMVI,1749
@@ -116,7 +127,7 @@ airbyte_cdk/sources/declarative/interpolation/interpolated_string.py,sha256=CQkH
116
127
  airbyte_cdk/sources/declarative/interpolation/interpolation.py,sha256=9IoeuWam3L6GyN10L6U8xNWXmkt9cnahSDNkez1OmFY,982
117
128
  airbyte_cdk/sources/declarative/interpolation/jinja.py,sha256=UQeuS4Vpyp4hlOn-R3tRyeBX0e9IoV6jQ6gH-Jz8lY0,7182
118
129
  airbyte_cdk/sources/declarative/interpolation/macros.py,sha256=UYSJ5gW7TkHALYnNvUnRP3RlyGwGuRMObF3BHuNzjJM,5320
119
- airbyte_cdk/sources/declarative/manifest_declarative_source.py,sha256=trTSVPJVAs6RN2sMeaLSuQT3jzJk8ZbdkUP6DMU29Ow,22011
130
+ airbyte_cdk/sources/declarative/manifest_declarative_source.py,sha256=yPQ4Qsxt8rzgbj52TLVFJiSRJp4lbFF-H7dSsr88E58,22961
120
131
  airbyte_cdk/sources/declarative/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
132
  airbyte_cdk/sources/declarative/migrations/legacy_to_per_partition_state_migration.py,sha256=iemy3fKLczcU0-Aor7tx5jcT6DRedKMqyK7kCOp01hg,3924
122
133
  airbyte_cdk/sources/declarative/migrations/state_migration.py,sha256=KWPjealMLKSMtajXgkdGgKg7EmTLR-CqqD7UIh0-eDU,794
@@ -128,7 +139,7 @@ airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=wnRUP0Xeru9R
128
139
  airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py,sha256=RUyFZS0zslLb7UfQrvqMC--k5CVLNSp7zHw6kbosvKE,9688
129
140
  airbyte_cdk/sources/declarative/parsers/manifest_normalizer.py,sha256=laBy7ebjA-PiNwc-50U4FHvMqS_mmHvnabxgFs4CjGw,17069
130
141
  airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py,sha256=pJmg78vqE5VfUrF_KJnWjucQ4k9IWFULeAxHCowrHXE,6806
131
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=DGHB4nWwsm4YLuWHhuOjAdeyVDY2U3CdbAiGRdm1YF0,163771
142
+ airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=YXR_cKEL6U-B6eji-FxvHFw_6V-bBAVmC4-M5IawXtQ,163774
132
143
  airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=TBC9AkGaUqHm2IKHMPN6punBIcY5tWGULowcLoAVkfw,1109
133
144
  airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py,sha256=VelO7zKqKtzMJ35jyFeg0ypJLQC0plqqIBNXoBW1G2E,3001
134
145
  airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py,sha256=c5cuVFM6NFkuQqG8Z5IwkBuwDrvXZN1CunUOM_L0ezg,6892
@@ -396,9 +407,9 @@ airbyte_cdk/utils/slice_hasher.py,sha256=EDxgROHDbfG-QKQb59m7h_7crN1tRiawdf5uU7G
396
407
  airbyte_cdk/utils/spec_schema_transformations.py,sha256=-5HTuNsnDBAhj-oLeQXwpTGA0HdcjFOf2zTEMUTTg_Y,816
397
408
  airbyte_cdk/utils/stream_status_utils.py,sha256=ZmBoiy5HVbUEHAMrUONxZvxnvfV9CesmQJLDTAIWnWw,1171
398
409
  airbyte_cdk/utils/traced_exception.py,sha256=C8uIBuCL_E4WnBAOPSxBicD06JAldoN9fGsQDp463OY,6292
399
- airbyte_cdk-6.48.0.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
400
- airbyte_cdk-6.48.0.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
401
- airbyte_cdk-6.48.0.dist-info/METADATA,sha256=slRcZy2PXZpm5mLpefKQWolH6bgEoo1Wy285FcBhxvc,6323
402
- airbyte_cdk-6.48.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
403
- airbyte_cdk-6.48.0.dist-info/entry_points.txt,sha256=AKWbEkHfpzzk9nF9tqBUaw1MbvTM4mGtEzmZQm0ZWvM,139
404
- airbyte_cdk-6.48.0.dist-info/RECORD,,
410
+ airbyte_cdk-6.48.1.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
411
+ airbyte_cdk-6.48.1.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
412
+ airbyte_cdk-6.48.1.dist-info/METADATA,sha256=scl_OXlgL349P2PgmdntFjkKVIBv9d_dPTIF7S2iKmA,6323
413
+ airbyte_cdk-6.48.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
414
+ airbyte_cdk-6.48.1.dist-info/entry_points.txt,sha256=AKWbEkHfpzzk9nF9tqBUaw1MbvTM4mGtEzmZQm0ZWvM,139
415
+ airbyte_cdk-6.48.1.dist-info/RECORD,,