Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run one consistent backup: PostgreSQL, blobs/parsed artifacts, Qdrant snapshots.
|
||||
|
||||
Implements ``runbooks/BACKUP.md``'s steps 1-8 (step 9, the scheduled
|
||||
empty-environment restore test, is a separate task). See
|
||||
``src/ragcore/application/backup/service.py`` for the actual pipeline;
|
||||
this script only wires real settings and infrastructure to it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from qdrant_client import AsyncQdrantClient
|
||||
|
||||
from ragcore.application.backup import BackupService
|
||||
from ragcore.config import get_settings
|
||||
from ragcore.infrastructure.backup import (
|
||||
LocalFileCopier,
|
||||
PgDumpPostgresDumper,
|
||||
QdrantHttpSnapshotter,
|
||||
)
|
||||
from ragcore.infrastructure.database import create_database_engine, create_session_factory
|
||||
from ragcore.infrastructure.database.models.generation.repository import (
|
||||
PostgresGenerationProfileStore,
|
||||
)
|
||||
from ragcore.infrastructure.models.persistence import JsonEmbeddingProfileStore
|
||||
|
||||
|
||||
async def active_profile_provenance(
|
||||
*, settings: object, session_factory: object
|
||||
) -> dict[str, str]:
|
||||
"""Non-secret ``profile:version`` identifiers of every currently active model profile.
|
||||
|
||||
An absent optional profile is simply omitted rather than failing the
|
||||
backup: a deployment without an active generation profile still deserves
|
||||
a complete backup, it just has less provenance to record.
|
||||
"""
|
||||
|
||||
provenance: dict[str, str] = {}
|
||||
embedding = await JsonEmbeddingProfileStore(
|
||||
settings.embedding_profiles_path # type: ignore[attr-defined]
|
||||
).get_active()
|
||||
if embedding is not None:
|
||||
provenance["embedding"] = f"{embedding.profile}:{embedding.version}"
|
||||
generation = await PostgresGenerationProfileStore(
|
||||
session_factory # type: ignore[arg-type]
|
||||
).get_active()
|
||||
if generation is not None:
|
||||
provenance["generation"] = f"{generation.profile}:{generation.version}"
|
||||
return provenance
|
||||
|
||||
|
||||
async def run(*, backup_root: Path, keep_complete: int) -> None:
|
||||
settings = get_settings()
|
||||
engine = create_database_engine(settings)
|
||||
session_factory = create_session_factory(engine)
|
||||
# Snapshot creation/download can legitimately take minutes for large
|
||||
# collections or slow disks; a short default timeout would abort a
|
||||
# otherwise-healthy backup.
|
||||
qdrant_client = AsyncQdrantClient(
|
||||
url=settings.qdrant_url, api_key=settings.qdrant_api_key.get_secret_value(), timeout=300
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient(follow_redirects=False, timeout=300) as http_client:
|
||||
collections = await qdrant_client.get_collections()
|
||||
collection_names = [collection.name for collection in collections.collections]
|
||||
|
||||
service = BackupService(
|
||||
postgres=PgDumpPostgresDumper(
|
||||
database_url=settings.database_url, session_factory=session_factory
|
||||
),
|
||||
qdrant=QdrantHttpSnapshotter(
|
||||
client=qdrant_client,
|
||||
http_client=http_client,
|
||||
base_url=settings.qdrant_url,
|
||||
api_key=settings.qdrant_api_key.get_secret_value(),
|
||||
),
|
||||
files=LocalFileCopier(),
|
||||
)
|
||||
manifest = await service.run(
|
||||
backup_root=backup_root,
|
||||
blob_root=settings.blob_root,
|
||||
parsed_root=settings.parsed_root,
|
||||
collection_names=collection_names,
|
||||
active_profiles=await active_profile_provenance(
|
||||
settings=settings, session_factory=session_factory
|
||||
),
|
||||
)
|
||||
removed = service.prune(backup_root=backup_root, keep_complete=keep_complete)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
await qdrant_client.close()
|
||||
|
||||
print(f"backup {manifest.backup_id} complete at {backup_root / manifest.backup_id}")
|
||||
if removed:
|
||||
print(f"pruned {len(removed)} old backup(s): {', '.join(removed)}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--backup-root",
|
||||
type=Path,
|
||||
default=Path(".state/backups"),
|
||||
help="Directory to store backups in",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep-complete", type=int, default=7, help="Number of complete backups to retain"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run(backup_root=args.backup_root, keep_complete=args.keep_complete))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user