feat(v2): complete v2 reimplementation
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user