|
| 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 commit comments