From e4fc31b78973e5790f742ef8d529a2d466a4e054 Mon Sep 17 00:00:00 2001 From: Sthornberry9 <46094434+Sthornberry9@users.noreply.github.com> Date: Sat, 8 Aug 2026 03:20:54 -0400 Subject: [PATCH] Add modular architecture foundation --- ARCHITECTURE.md | 21 ++++ DOCS_INDEX.md | 2 + MODULARIZATION_PLAN.md | 96 ++++++++++++++++ README.md | 1 + pyproject.toml | 4 + tests.py | 69 +++++++++++- unityscraper/__init__.py | 7 ++ unityscraper/app/__init__.py | 2 + unityscraper/app/api/__init__.py | 2 + unityscraper/app/api/entrypoint.py | 8 ++ unityscraper/app/cli/__init__.py | 2 + unityscraper/app/cli/entrypoint.py | 8 ++ unityscraper/app/desktop/__init__.py | 2 + unityscraper/app/desktop/entrypoint.py | 8 ++ unityscraper/core/__init__.py | 2 + unityscraper/core/db/__init__.py | 18 +++ unityscraper/core/db/migrations.py | 104 ++++++++++++++++++ unityscraper/core/jobs.py | 65 +++++++++++ unityscraper/core/paths.py | 64 +++++++++++ unityscraper/domains/__init__.py | 2 + unityscraper/domains/backups/__init__.py | 2 + unityscraper/domains/backups/service.py | 40 +++++++ unityscraper/domains/collections/__init__.py | 2 + unityscraper/domains/collections/service.py | 8 ++ unityscraper/domains/console_sync/__init__.py | 2 + unityscraper/domains/console_sync/service.py | 8 ++ unityscraper/domains/knowledge/__init__.py | 2 + unityscraper/domains/knowledge/service.py | 9 ++ unityscraper/domains/library/__init__.py | 2 + unityscraper/domains/library/service.py | 14 +++ unityscraper/domains/packages/__init__.py | 2 + unityscraper/domains/packages/service.py | 28 +++++ unityscraper/domains/plugins/__init__.py | 2 + unityscraper/domains/plugins/service.py | 8 ++ unityscraper/domains/profiles/__init__.py | 2 + unityscraper/domains/profiles/service.py | 30 +++++ unityscraper/domains/tools/__init__.py | 2 + unityscraper/domains/tools/service.py | 15 +++ 38 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 MODULARIZATION_PLAN.md create mode 100644 unityscraper/__init__.py create mode 100644 unityscraper/app/__init__.py create mode 100644 unityscraper/app/api/__init__.py create mode 100644 unityscraper/app/api/entrypoint.py create mode 100644 unityscraper/app/cli/__init__.py create mode 100644 unityscraper/app/cli/entrypoint.py create mode 100644 unityscraper/app/desktop/__init__.py create mode 100644 unityscraper/app/desktop/entrypoint.py create mode 100644 unityscraper/core/__init__.py create mode 100644 unityscraper/core/db/__init__.py create mode 100644 unityscraper/core/db/migrations.py create mode 100644 unityscraper/core/jobs.py create mode 100644 unityscraper/core/paths.py create mode 100644 unityscraper/domains/__init__.py create mode 100644 unityscraper/domains/backups/__init__.py create mode 100644 unityscraper/domains/backups/service.py create mode 100644 unityscraper/domains/collections/__init__.py create mode 100644 unityscraper/domains/collections/service.py create mode 100644 unityscraper/domains/console_sync/__init__.py create mode 100644 unityscraper/domains/console_sync/service.py create mode 100644 unityscraper/domains/knowledge/__init__.py create mode 100644 unityscraper/domains/knowledge/service.py create mode 100644 unityscraper/domains/library/__init__.py create mode 100644 unityscraper/domains/library/service.py create mode 100644 unityscraper/domains/packages/__init__.py create mode 100644 unityscraper/domains/packages/service.py create mode 100644 unityscraper/domains/plugins/__init__.py create mode 100644 unityscraper/domains/plugins/service.py create mode 100644 unityscraper/domains/profiles/__init__.py create mode 100644 unityscraper/domains/profiles/service.py create mode 100644 unityscraper/domains/tools/__init__.py create mode 100644 unityscraper/domains/tools/service.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fa2fe9d..5d338d3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -11,6 +11,27 @@ for CLI and optional REST automation. SQLite is the durable local store. | `main.py` | XboxUnity, knowledge, backup, export, and API CLI | | `GUI.py` | Legacy advanced scraper controls | | `api.py` | Optional local automation API | +| `unityscraper.app.desktop.entrypoint` | Package entry point for installed desktop launches | +| `unityscraper.app.cli.entrypoint` | Package entry point for installed CLI launches | + +The top-level entry points remain supported while the codebase moves into the +`unityscraper` package. See [Modularization plan](MODULARIZATION_PLAN.md). + +## Package Direction + +New code should prefer the package layout: + +```text +unityscraper/ + app/ desktop, CLI, and API adapters + core/ shared paths, jobs, database contracts, and infrastructure + domains/ feature-owned library, knowledge, backup, profile, package, + collection, console sync, tool, and plugin modules +``` + +Domain modules should own business workflows and schema changes. Desktop pages, +REST routes, and CLI commands should call domain use cases instead of owning +filesystem, FTP, package, or database behavior directly. ## Layers diff --git a/DOCS_INDEX.md b/DOCS_INDEX.md index 5ea8549..9aa4f1e 100644 --- a/DOCS_INDEX.md +++ b/DOCS_INDEX.md @@ -35,6 +35,8 @@ - [Architecture](ARCHITECTURE.md) - modules, layers, schemas, data flows, and packaging +- [Modularization Plan](MODULARIZATION_PLAN.md) - package boundaries, domain + ownership, and the incremental AIO refactor path - [Plugin API v1](PLUGIN_API.md) - manifests, opt-in loading, and compatibility - [Contributing](CONTRIBUTING.md) - environment, tests, PR expectations, and adapter rules diff --git a/MODULARIZATION_PLAN.md b/MODULARIZATION_PLAN.md new file mode 100644 index 0000000..40cad6c --- /dev/null +++ b/MODULARIZATION_PLAN.md @@ -0,0 +1,96 @@ +# Modularization Plan + +UnityScraper is moving toward a modular monolith: one local desktop-first +application, with feature domains that own their models, services, schema, +commands, tests, and UI adapters. + +The current top-level modules remain supported while code moves into the +`unityscraper` package incrementally. + +## Target Shape + +```text +unityscraper/ + app/ + desktop/ + cli/ + api/ + core/ + db/ + jobs.py + paths.py + domains/ + library/ + knowledge/ + backups/ + profiles/ + packages/ + collections/ + console_sync/ + tools/ + plugins/ +``` + +## Boundaries + +- `unityscraper.app` adapts desktop, CLI, and REST entry points. +- `unityscraper.core` holds shared infrastructure that has no Xbox-specific + product behavior. +- `unityscraper.domains` holds feature behavior and should avoid importing UI + modules. +- Top-level modules are compatibility shims or legacy implementations until + their behavior is moved behind domain packages. + +## Domain Rules + +Each domain should grow toward this internal layout: + +```text +domain/ + models.py + service.py + repository.py + commands.py + api.py + migrations.py + ui.py +``` + +- `models.py` contains dataclasses, enums, and validation types. +- `service.py` owns business workflows. +- `repository.py` owns SQLite access for that domain. +- `commands.py` exposes UI-neutral use cases for CLI, REST, and desktop jobs. +- `api.py` adapts use cases to HTTP only. +- `ui.py` adapts use cases to Tk only. +- `migrations.py` registers additive schema changes through + `unityscraper.core.db.MigrationRegistry`. + +## Migration Order + +1. Keep existing entry points working. +2. Add package-facing adapters for existing services. +3. Move pure models and parsing helpers first. +4. Move repositories and schema ownership next. +5. Move command handlers after services are UI-neutral. +6. Split large UI pages only after their services are stable. + +## Feature Ownership + +| Domain | Owns | +| --- | --- | +| `library` | XboxUnity titles, covers, title updates, local library search | +| `knowledge` | source-attributed facts, citations, conflicts, offline archive | +| `backups` | owned-content inventory, import, export, verification, FTP-safe flows | +| `profiles` | profile/save discovery, snapshots, restore, achievement inspection | +| `packages` | STFS, XEX, XBE inspection and read-only extraction | +| `collections` | preservation matching, DAT checks, health reports, repair previews | +| `console_sync` | console inventories, durable transfer plans, dashboard capabilities | +| `tools` | external executable catalog, argument templates, captured execution | +| `plugins` | opt-in collectors, trust state, isolated plugin runs | + +## Long-Term AIO Rule + +New Xbox 360 capabilities should enter as domain use cases first. Desktop +buttons, REST routes, and CLI flags should call those use cases rather than +owning file, database, FTP, or package logic themselves. + diff --git a/README.md b/README.md index 6e46d8a..cb9c269 100644 --- a/README.md +++ b/README.md @@ -454,6 +454,7 @@ Build the macOS application bundle on macOS: - [Documentation index](DOCS_INDEX.md) - [Architecture](ARCHITECTURE.md) +- [Modularization plan](MODULARIZATION_PLAN.md) - [Linux support](LINUX.md) - [macOS preview](MACOS.md) - [Community Hub](COMMUNITY_HUB.md) diff --git a/pyproject.toml b/pyproject.toml index 2fb8fe2..618f9f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,10 @@ Homepage = "https://github.com/TrapEmAll/UnityScraper" Issues = "https://github.com/TrapEmAll/UnityScraper/issues" Releases = "https://github.com/TrapEmAll/UnityScraper/releases" +[project.scripts] +unityscraper = "unityscraper.app.desktop.entrypoint:main" +unityscraper-cli = "unityscraper.app.cli.entrypoint:main" + [tool.pytest.ini_options] testpaths = ["tests.py"] addopts = "-ra" diff --git a/tests.py b/tests.py index 7fb78ec..3182004 100644 --- a/tests.py +++ b/tests.py @@ -9,6 +9,7 @@ import json import hashlib import os +import sqlite3 import sys import time import zipfile @@ -62,12 +63,16 @@ package_destination, scan_local_target, ) -from backup_service import BackupRepository +from backup_service import BackupRepository, BackupService from api import UnityScraperAPI from app_version import DISPLAY_VERSION from app_paths import resolve_storage_paths from platform_support import desktop_font_family, path_opener_command from profile_manager import ProfileSaveManager, find_content_root, mask_identifier +from unityscraper.core.db import MigrationRegistry +from unityscraper.core.jobs import JobProgress, JobResult +from unityscraper.domains.backups.service import BackupService as ModularBackupService +from unityscraper.domains.library.service import LibraryService as ModularLibraryService class TestPlatformSupport(unittest.TestCase): @@ -185,9 +190,64 @@ def test_linux_desktop_metadata_is_complete(self): root.findtext("id"), "io.github.trapemall.UnityScraper", ) - - -class TestConfig(unittest.TestCase): + + +class TestModularFoundation(unittest.TestCase): + """Test package-level adapters that support the modular architecture.""" + + def test_domain_service_exports_preserve_existing_implementations(self): + self.assertIs(ModularBackupService, BackupService) + self.assertIs(ModularLibraryService, LibraryService) + + def test_job_progress_percent_is_bounded(self): + self.assertEqual( + JobProgress(status="running", message="working", current=5, total=10).percent, + 50.0, + ) + self.assertEqual( + JobProgress(status="running", message="over", current=15, total=10).percent, + 100.0, + ) + self.assertIsNone(JobProgress(status="running", message="unknown").percent) + + def test_job_result_factories_set_terminal_state(self): + completed = JobResult.completed("done", count=2) + failed = JobResult.failed("failed", reason="example") + + self.assertEqual(completed.status, "completed") + self.assertEqual(completed.payload["count"], 2) + self.assertIsNotNone(completed.finished_at) + self.assertEqual(failed.status, "failed") + self.assertEqual(failed.payload["reason"], "example") + self.assertIsNotNone(failed.finished_at) + + def test_domain_migration_registry_applies_once(self): + calls = [] + + def migration(connection): + calls.append("applied") + connection.execute("CREATE TABLE example_domain_table (id INTEGER PRIMARY KEY)") + + registry = MigrationRegistry() + registry.register(domain="example", version=1, name="example schema", apply=migration) + + with sqlite3.connect(":memory:") as connection: + first = registry.apply(connection) + second = registry.apply(connection) + table = connection.execute( + """ + SELECT name FROM sqlite_master + WHERE type = 'table' AND name = 'example_domain_table' + """ + ).fetchone() + + self.assertEqual([item.key for item in first], ["example:1"]) + self.assertEqual(second, []) + self.assertEqual(calls, ["applied"]) + self.assertIsNotNone(table) + + +class TestConfig(unittest.TestCase): """Test configuration management""" def setUp(self): @@ -2432,6 +2492,7 @@ def run_tests(): # Add all test classes suite.addTests(loader.loadTestsFromTestCase(TestPlatformSupport)) + suite.addTests(loader.loadTestsFromTestCase(TestModularFoundation)) suite.addTests(loader.loadTestsFromTestCase(TestConfig)) suite.addTests(loader.loadTestsFromTestCase(TestRateLimiter)) suite.addTests(loader.loadTestsFromTestCase(TestUnityScraper)) diff --git a/unityscraper/__init__.py b/unityscraper/__init__.py new file mode 100644 index 0000000..c808990 --- /dev/null +++ b/unityscraper/__init__.py @@ -0,0 +1,7 @@ +"""UnityScraper application package. + +The package namespace is the long-term home for new code. Top-level modules +remain as compatibility entry points while existing features move behind +domain-oriented packages. +""" + diff --git a/unityscraper/app/__init__.py b/unityscraper/app/__init__.py new file mode 100644 index 0000000..7984bdb --- /dev/null +++ b/unityscraper/app/__init__.py @@ -0,0 +1,2 @@ +"""Application entry-point adapters for desktop, CLI, and API surfaces.""" + diff --git a/unityscraper/app/api/__init__.py b/unityscraper/app/api/__init__.py new file mode 100644 index 0000000..f776a09 --- /dev/null +++ b/unityscraper/app/api/__init__.py @@ -0,0 +1,2 @@ +"""Local REST API application adapters.""" + diff --git a/unityscraper/app/api/entrypoint.py b/unityscraper/app/api/entrypoint.py new file mode 100644 index 0000000..17e66cf --- /dev/null +++ b/unityscraper/app/api/entrypoint.py @@ -0,0 +1,8 @@ +"""API application factory adapter for the package layout.""" + +from __future__ import annotations + +from api import UnityScraperAPI + +__all__ = ["UnityScraperAPI"] + diff --git a/unityscraper/app/cli/__init__.py b/unityscraper/app/cli/__init__.py new file mode 100644 index 0000000..bbdaa73 --- /dev/null +++ b/unityscraper/app/cli/__init__.py @@ -0,0 +1,2 @@ +"""Command-line application adapters.""" + diff --git a/unityscraper/app/cli/entrypoint.py b/unityscraper/app/cli/entrypoint.py new file mode 100644 index 0000000..3201b00 --- /dev/null +++ b/unityscraper/app/cli/entrypoint.py @@ -0,0 +1,8 @@ +"""CLI entry point for the package layout.""" + +from __future__ import annotations + +from main import main + +__all__ = ["main"] + diff --git a/unityscraper/app/desktop/__init__.py b/unityscraper/app/desktop/__init__.py new file mode 100644 index 0000000..ee3321c --- /dev/null +++ b/unityscraper/app/desktop/__init__.py @@ -0,0 +1,2 @@ +"""Desktop application shell adapters.""" + diff --git a/unityscraper/app/desktop/entrypoint.py b/unityscraper/app/desktop/entrypoint.py new file mode 100644 index 0000000..d34fe4d --- /dev/null +++ b/unityscraper/app/desktop/entrypoint.py @@ -0,0 +1,8 @@ +"""Desktop entry point for the package layout.""" + +from __future__ import annotations + +from desktop_app import main + +__all__ = ["main"] + diff --git a/unityscraper/core/__init__.py b/unityscraper/core/__init__.py new file mode 100644 index 0000000..5c1333a --- /dev/null +++ b/unityscraper/core/__init__.py @@ -0,0 +1,2 @@ +"""Shared infrastructure used across UnityScraper domains.""" + diff --git a/unityscraper/core/db/__init__.py b/unityscraper/core/db/__init__.py new file mode 100644 index 0000000..f3a6685 --- /dev/null +++ b/unityscraper/core/db/__init__.py @@ -0,0 +1,18 @@ +"""Database contracts and migration helpers.""" + +from __future__ import annotations + +from .migrations import ( + DomainMigration, + MigrationRegistry, + RegisteredMigration, + apply_registered_migrations, +) + +__all__ = [ + "DomainMigration", + "MigrationRegistry", + "RegisteredMigration", + "apply_registered_migrations", +] + diff --git a/unityscraper/core/db/migrations.py b/unityscraper/core/db/migrations.py new file mode 100644 index 0000000..2ba3ba9 --- /dev/null +++ b/unityscraper/core/db/migrations.py @@ -0,0 +1,104 @@ +"""Small migration registry for domain-owned schema work. + +The legacy ``database_migrations.py`` module remains authoritative today. This +registry gives new or moved domains a common contract so schema ownership can +shift incrementally instead of through one large rewrite. +""" + +from __future__ import annotations + +import sqlite3 +from collections.abc import Callable, Iterable +from dataclasses import dataclass, field +from datetime import datetime, timezone + +DomainMigration = Callable[[sqlite3.Connection], None] + + +@dataclass(frozen=True) +class RegisteredMigration: + """One additive schema migration owned by a feature domain.""" + + domain: str + version: int + name: str + apply: DomainMigration + + @property + def key(self) -> str: + return f"{self.domain}:{self.version}" + + +@dataclass +class MigrationRegistry: + """Collect domain migrations and apply the unapplied set once.""" + + migrations: list[RegisteredMigration] = field(default_factory=list) + + def register( + self, + *, + domain: str, + version: int, + name: str, + apply: DomainMigration, + ) -> None: + migration = RegisteredMigration(domain=domain, version=version, name=name, apply=apply) + if any(existing.key == migration.key for existing in self.migrations): + raise ValueError(f"Duplicate migration key: {migration.key}") + self.migrations.append(migration) + + def extend(self, migrations: Iterable[RegisteredMigration]) -> None: + for migration in migrations: + self.register( + domain=migration.domain, + version=migration.version, + name=migration.name, + apply=migration.apply, + ) + + def apply(self, connection: sqlite3.Connection) -> list[RegisteredMigration]: + return apply_registered_migrations(connection, self.migrations) + + +def apply_registered_migrations( + connection: sqlite3.Connection, + migrations: Iterable[RegisteredMigration], +) -> list[RegisteredMigration]: + """Apply migrations not yet present in ``app_domain_migrations``.""" + + connection.execute( + """ + CREATE TABLE IF NOT EXISTS app_domain_migrations ( + domain TEXT NOT NULL, + version INTEGER NOT NULL, + name TEXT NOT NULL, + applied_at TEXT NOT NULL, + PRIMARY KEY(domain, version) + ) + """ + ) + applied = { + (str(row[0]), int(row[1])) + for row in connection.execute("SELECT domain, version FROM app_domain_migrations") + } + completed: list[RegisteredMigration] = [] + for migration in sorted(migrations, key=lambda item: (item.domain, item.version)): + if (migration.domain, migration.version) in applied: + continue + migration.apply(connection) + connection.execute( + """ + INSERT INTO app_domain_migrations(domain, version, name, applied_at) + VALUES (?, ?, ?, ?) + """, + ( + migration.domain, + migration.version, + migration.name, + datetime.now(timezone.utc).isoformat(), + ), + ) + completed.append(migration) + return completed + diff --git a/unityscraper/core/jobs.py b/unityscraper/core/jobs.py new file mode 100644 index 0000000..69cadd2 --- /dev/null +++ b/unityscraper/core/jobs.py @@ -0,0 +1,65 @@ +"""Shared job result types for long-running desktop, CLI, and API workflows.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any, Literal + +JobStatus = Literal["queued", "running", "completed", "failed", "cancelled"] + + +def utc_now() -> str: + return datetime.now(timezone.utc).isoformat() + + +@dataclass(frozen=True) +class JobProgress: + """A UI-neutral progress update from a long-running operation.""" + + status: JobStatus + message: str + current: int = 0 + total: int = 0 + details: dict[str, Any] = field(default_factory=dict) + updated_at: str = field(default_factory=utc_now) + + @property + def percent(self) -> float | None: + if self.total <= 0: + return None + return min(100.0, max(0.0, (self.current / self.total) * 100.0)) + + +@dataclass(frozen=True) +class JobResult: + """A UI-neutral operation result.""" + + status: JobStatus + message: str + payload: dict[str, Any] = field(default_factory=dict) + started_at: str = field(default_factory=utc_now) + finished_at: str | None = None + + @classmethod + def completed(cls, message: str, **payload: Any) -> "JobResult": + now = utc_now() + return cls( + status="completed", + message=message, + payload=payload, + started_at=now, + finished_at=now, + ) + + @classmethod + def failed(cls, message: str, **payload: Any) -> "JobResult": + now = utc_now() + return cls( + status="failed", + message=message, + payload=payload, + started_at=now, + finished_at=now, + ) + diff --git a/unityscraper/core/paths.py b/unityscraper/core/paths.py new file mode 100644 index 0000000..65d4dcc --- /dev/null +++ b/unityscraper/core/paths.py @@ -0,0 +1,64 @@ +"""Package-facing access to application storage and resource paths.""" + +from __future__ import annotations + +from app_paths import ( + BASE_DIR, + CACHE_DIR, + CLI_LOG_PATH, + CONFIG_DIR, + CONFIG_PATH, + DATABASE_PATH, + DATA_DIR, + DIAGNOSTICS_DIR, + DOWNLOADS_DIR, + EXPORTS_DIR, + FIRST_RUN_PATH, + GUI_LOG_PATH, + LANGUAGE_PACKS_DIR, + LOG_DIR, + OFFLINE_KNOWLEDGE_DIR, + PLUGINS_DIR, + PROFILE_BACKUPS_DIR, + TITLEIDS_PATH, + StoragePaths, + app_root, + describe_storage, + ensure_app_dirs, + ensure_user_titleids_file, + executable_root, + portable_mode_enabled, + resolve_storage_paths, + resource_path, +) + +__all__ = [ + "BASE_DIR", + "CACHE_DIR", + "CLI_LOG_PATH", + "CONFIG_DIR", + "CONFIG_PATH", + "DATABASE_PATH", + "DATA_DIR", + "DIAGNOSTICS_DIR", + "DOWNLOADS_DIR", + "EXPORTS_DIR", + "FIRST_RUN_PATH", + "GUI_LOG_PATH", + "LANGUAGE_PACKS_DIR", + "LOG_DIR", + "OFFLINE_KNOWLEDGE_DIR", + "PLUGINS_DIR", + "PROFILE_BACKUPS_DIR", + "TITLEIDS_PATH", + "StoragePaths", + "app_root", + "describe_storage", + "ensure_app_dirs", + "ensure_user_titleids_file", + "executable_root", + "portable_mode_enabled", + "resolve_storage_paths", + "resource_path", +] + diff --git a/unityscraper/domains/__init__.py b/unityscraper/domains/__init__.py new file mode 100644 index 0000000..56f1c96 --- /dev/null +++ b/unityscraper/domains/__init__.py @@ -0,0 +1,2 @@ +"""Feature domains for the UnityScraper modular monolith.""" + diff --git a/unityscraper/domains/backups/__init__.py b/unityscraper/domains/backups/__init__.py new file mode 100644 index 0000000..49c371f --- /dev/null +++ b/unityscraper/domains/backups/__init__.py @@ -0,0 +1,2 @@ +"""Owned-content backup, inventory, and transfer domain.""" + diff --git a/unityscraper/domains/backups/service.py b/unityscraper/domains/backups/service.py new file mode 100644 index 0000000..0978302 --- /dev/null +++ b/unityscraper/domains/backups/service.py @@ -0,0 +1,40 @@ +"""Package-facing backup service exports.""" + +from __future__ import annotations + +from backup_manager import ( + BackupError, + BackupItem, + ConflictError, + FtpBackupClient, + FtpTarget, + InvalidPackageError, + ScanResult, + StfsPackage, + TransferResult, + UnsafeArchiveError, + install_stfs_package, + scan_local_target, + verify_backup_item, +) +from backup_service import BackupRepository, BackupService, ensure_backup_schema + +__all__ = [ + "BackupError", + "BackupItem", + "BackupRepository", + "BackupService", + "ConflictError", + "FtpBackupClient", + "FtpTarget", + "InvalidPackageError", + "ScanResult", + "StfsPackage", + "TransferResult", + "UnsafeArchiveError", + "ensure_backup_schema", + "install_stfs_package", + "scan_local_target", + "verify_backup_item", +] + diff --git a/unityscraper/domains/collections/__init__.py b/unityscraper/domains/collections/__init__.py new file mode 100644 index 0000000..040f120 --- /dev/null +++ b/unityscraper/domains/collections/__init__.py @@ -0,0 +1,2 @@ +"""Collection health, preservation matching, and reports domain.""" + diff --git a/unityscraper/domains/collections/service.py b/unityscraper/domains/collections/service.py new file mode 100644 index 0000000..d4679a2 --- /dev/null +++ b/unityscraper/domains/collections/service.py @@ -0,0 +1,8 @@ +"""Package-facing collection intelligence exports.""" + +from __future__ import annotations + +from collection_intelligence import CollectionIntelligenceService + +__all__ = ["CollectionIntelligenceService"] + diff --git a/unityscraper/domains/console_sync/__init__.py b/unityscraper/domains/console_sync/__init__.py new file mode 100644 index 0000000..8ea6eae --- /dev/null +++ b/unityscraper/domains/console_sync/__init__.py @@ -0,0 +1,2 @@ +"""Console inventory and transfer synchronization domain.""" + diff --git a/unityscraper/domains/console_sync/service.py b/unityscraper/domains/console_sync/service.py new file mode 100644 index 0000000..c3783ff --- /dev/null +++ b/unityscraper/domains/console_sync/service.py @@ -0,0 +1,8 @@ +"""Package-facing console sync exports.""" + +from __future__ import annotations + +from console_sync import ConsoleSyncService + +__all__ = ["ConsoleSyncService"] + diff --git a/unityscraper/domains/knowledge/__init__.py b/unityscraper/domains/knowledge/__init__.py new file mode 100644 index 0000000..51c6c90 --- /dev/null +++ b/unityscraper/domains/knowledge/__init__.py @@ -0,0 +1,2 @@ +"""Source-attributed knowledge and offline reference domain.""" + diff --git a/unityscraper/domains/knowledge/service.py b/unityscraper/domains/knowledge/service.py new file mode 100644 index 0000000..a4b8212 --- /dev/null +++ b/unityscraper/domains/knowledge/service.py @@ -0,0 +1,9 @@ +"""Package-facing knowledge service exports.""" + +from __future__ import annotations + +from knowledge_base import KnowledgeRepository, is_unknown +from knowledge_service import KnowledgeService + +__all__ = ["KnowledgeRepository", "KnowledgeService", "is_unknown"] + diff --git a/unityscraper/domains/library/__init__.py b/unityscraper/domains/library/__init__.py new file mode 100644 index 0000000..2a549a0 --- /dev/null +++ b/unityscraper/domains/library/__init__.py @@ -0,0 +1,2 @@ +"""Library and XboxUnity title/update domain.""" + diff --git a/unityscraper/domains/library/service.py b/unityscraper/domains/library/service.py new file mode 100644 index 0000000..5f574a3 --- /dev/null +++ b/unityscraper/domains/library/service.py @@ -0,0 +1,14 @@ +"""Package-facing library service exports.""" + +from __future__ import annotations + +from library_service import GameSummary, LibraryService +from title_catalog import TitleSuggestion, XboxUnityTitleCatalog + +__all__ = [ + "GameSummary", + "LibraryService", + "TitleSuggestion", + "XboxUnityTitleCatalog", +] + diff --git a/unityscraper/domains/packages/__init__.py b/unityscraper/domains/packages/__init__.py new file mode 100644 index 0000000..cb4100d --- /dev/null +++ b/unityscraper/domains/packages/__init__.py @@ -0,0 +1,2 @@ +"""STFS, XEX, XBE, and package inspection domain.""" + diff --git a/unityscraper/domains/packages/service.py b/unityscraper/domains/packages/service.py new file mode 100644 index 0000000..fec20a3 --- /dev/null +++ b/unityscraper/domains/packages/service.py @@ -0,0 +1,28 @@ +"""Package-facing Xbox package inspection exports.""" + +from __future__ import annotations + +from backup_manager import ( + StfsEntry, + StfsPackage, + XbePackage, + XexPackage, + extract_stfs_files, + inspect_stfs, + inspect_xbe, + inspect_xex, + list_stfs_entries, +) + +__all__ = [ + "StfsEntry", + "StfsPackage", + "XbePackage", + "XexPackage", + "extract_stfs_files", + "inspect_stfs", + "inspect_xbe", + "inspect_xex", + "list_stfs_entries", +] + diff --git a/unityscraper/domains/plugins/__init__.py b/unityscraper/domains/plugins/__init__.py new file mode 100644 index 0000000..5a00957 --- /dev/null +++ b/unityscraper/domains/plugins/__init__.py @@ -0,0 +1,2 @@ +"""Managed plugin domain.""" + diff --git a/unityscraper/domains/plugins/service.py b/unityscraper/domains/plugins/service.py new file mode 100644 index 0000000..ca88402 --- /dev/null +++ b/unityscraper/domains/plugins/service.py @@ -0,0 +1,8 @@ +"""Package-facing plugin management exports.""" + +from __future__ import annotations + +from plugins import MetadataCollectorPlugin, PluginManager, PluginManifest + +__all__ = ["MetadataCollectorPlugin", "PluginManager", "PluginManifest"] + diff --git a/unityscraper/domains/profiles/__init__.py b/unityscraper/domains/profiles/__init__.py new file mode 100644 index 0000000..98a594c --- /dev/null +++ b/unityscraper/domains/profiles/__init__.py @@ -0,0 +1,2 @@ +"""Xbox 360 profile, save, and achievement inspection domain.""" + diff --git a/unityscraper/domains/profiles/service.py b/unityscraper/domains/profiles/service.py new file mode 100644 index 0000000..d6c7a1a --- /dev/null +++ b/unityscraper/domains/profiles/service.py @@ -0,0 +1,30 @@ +"""Package-facing profile and save service exports.""" + +from __future__ import annotations + +from profile_manager import ( + ProfileInfo, + ProfileSaveConflict, + ProfileSaveError, + ProfileSaveManager, + ProfileSaveScanner, + ProfileScanResult, + RestoreResult, + SaveInfo, + find_content_root, + mask_identifier, +) + +__all__ = [ + "ProfileInfo", + "ProfileSaveConflict", + "ProfileSaveError", + "ProfileSaveManager", + "ProfileSaveScanner", + "ProfileScanResult", + "RestoreResult", + "SaveInfo", + "find_content_root", + "mask_identifier", +] + diff --git a/unityscraper/domains/tools/__init__.py b/unityscraper/domains/tools/__init__.py new file mode 100644 index 0000000..4e207b0 --- /dev/null +++ b/unityscraper/domains/tools/__init__.py @@ -0,0 +1,2 @@ +"""External tool catalog and execution domain.""" + diff --git a/unityscraper/domains/tools/service.py b/unityscraper/domains/tools/service.py new file mode 100644 index 0000000..9af7b71 --- /dev/null +++ b/unityscraper/domains/tools/service.py @@ -0,0 +1,15 @@ +"""Package-facing external tool exports.""" + +from __future__ import annotations + +from external_tools import ExternalToolError, ExternalToolRunner, ToolLaunch, ToolResult +from tool_catalog import ToolCatalog + +__all__ = [ + "ExternalToolError", + "ExternalToolRunner", + "ToolCatalog", + "ToolLaunch", + "ToolResult", +] +