feat(v2): complete v2 reimplementation

This commit is contained in:
2026-07-31 13:33:39 +02:00
parent 396219e776
commit bd107d6a30
137 changed files with 20737 additions and 155 deletions
+74
View File
@@ -0,0 +1,74 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Protocol
class RetentionError(ValueError):
pass
class BackupLike(Protocol):
id: str
created_at: datetime
pinned: bool
tombstoned_at: datetime | None
@dataclass(frozen=True)
class RetentionPolicy:
keep_last: int = 1
keep_days: int = 0
keep_daily: int = 0
keep_weekly: int = 0
keep_monthly: int = 0
@classmethod
def from_dict(cls, value: dict[str, object]) -> RetentionPolicy:
allowed = {"keep_last", "keep_days", "keep_daily", "keep_weekly", "keep_monthly"}
if set(value) - allowed:
raise RetentionError("retention policy contains unknown keys")
kwargs: dict[str, int] = {}
for key in allowed:
raw = value.get(key, 0 if key != "keep_last" else 1)
if not isinstance(raw, int) or isinstance(raw, bool) or raw < 0:
raise RetentionError("retention values must be non-negative integers")
kwargs[key] = raw
return cls(**kwargs)
def retained_ids(
backups: Iterable[BackupLike], policy: RetentionPolicy, now: datetime | None = None
) -> set[str]:
"""Return union retention set; the newest non-tombstoned backup is always protected."""
reference = (now or datetime.now(UTC)).astimezone(UTC)
items = sorted(
(item for item in backups if item.tombstoned_at is None),
key=lambda item: item.created_at.astimezone(UTC),
reverse=True,
)
if not items:
return set()
kept = {items[0].id}
kept.update(item.id for item in items[: policy.keep_last])
kept.update(item.id for item in items if item.pinned)
if policy.keep_days:
cutoff = reference - timedelta(days=policy.keep_days)
kept.update(item.id for item in items if item.created_at.astimezone(UTC) >= cutoff)
for count, key in (
(policy.keep_daily, lambda stamp: stamp.date()),
(policy.keep_weekly, lambda stamp: stamp.isocalendar()[:2]),
(policy.keep_monthly, lambda stamp: (stamp.year, stamp.month)),
):
buckets: set[object] = set()
for item in items:
stamp = item.created_at.astimezone(UTC)
bucket = key(stamp)
if len(buckets) >= count and bucket not in buckets:
continue
buckets.add(bucket)
if bucket in buckets:
kept.add(item.id)
return kept