Files
dify/api/services/data_migration/package_service.py
Blackoutta 0c40e1c2a0 feat: add cross-environment app migration workflow (#36765)
Co-authored-by: XW <wei.xu1@wiz.ai>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-28 07:30:33 +00:00

72 lines
2.6 KiB
Python

"""JSON persistence for versioned cross-environment migration packages.
The package service validates file shape and serializes only structured package
entities. It does not perform CLI rendering or database access, keeping it safe
to reuse from Click adapters, tests, and future import/export services.
"""
from __future__ import annotations
import json
from dataclasses import asdict
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from services.data_migration.entities import (
ImportOptions,
MigrationDataError,
MigrationMetadata,
MigrationPackage,
SourceTenant,
TargetTenantSelector,
)
PACKAGE_VERSION = "1"
class MigrationPackageService:
def load_package(self, path: str | Path) -> MigrationPackage:
package_path = Path(path)
with package_path.open(encoding="utf-8") as file:
raw = json.load(file)
if not isinstance(raw, dict):
raise MigrationDataError("Migration package JSON must be an object.")
package = MigrationPackage.from_mapping(raw)
if package.metadata.version != PACKAGE_VERSION:
raise MigrationDataError(f"Unsupported migration package version: {package.metadata.version}")
return package
def save_package(self, package: MigrationPackage, path: str | Path, *, overwrite: bool) -> None:
package_path = Path(path)
if package_path.exists() and not overwrite:
raise MigrationDataError(f"Output file already exists: {package_path}")
package_path.parent.mkdir(parents=True, exist_ok=True)
with package_path.open("w", encoding="utf-8") as file:
json.dump(self.to_mapping(package), file, ensure_ascii=False, indent=2)
file.write("\n")
def build_empty_package(
self,
*,
source_tenant_id: str,
source_tenant_name: str,
include_secrets: bool,
import_options: ImportOptions | None = None,
target_tenant: TargetTenantSelector | None = None,
) -> MigrationPackage:
return MigrationPackage(
metadata=MigrationMetadata(
version=PACKAGE_VERSION,
source_scope="single",
source_tenants=[SourceTenant(id=source_tenant_id, name=source_tenant_name)],
target_tenant=target_tenant,
created_at=datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
include_secrets=include_secrets,
import_options=import_options or ImportOptions(),
)
)
def to_mapping(self, package: MigrationPackage) -> dict[str, Any]:
return asdict(package)