summaryrefslogtreecommitdiff
path: root/src/srht_contrib/utils
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-09 19:45:45 -0500
committerChristian Cleberg <[email protected]>2026-04-09 19:45:45 -0500
commitacbff854f2da96bddcaede1385e7fefeba0fb34b (patch)
tree48d4707cd3276d2825370f0793008a6ffcfff936 /src/srht_contrib/utils
downloadhutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.tar.gz
hutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.tar.bz2
hutch-stats-acbff854f2da96bddcaede1385e7fefeba0fb34b.zip
initial commit
Diffstat (limited to 'src/srht_contrib/utils')
-rw-r--r--src/srht_contrib/utils/__init__.py1
-rw-r--r--src/srht_contrib/utils/dates.py37
-rw-r--r--src/srht_contrib/utils/identity.py27
-rw-r--r--src/srht_contrib/utils/repositories.py31
4 files changed, 96 insertions, 0 deletions
diff --git a/src/srht_contrib/utils/__init__.py b/src/srht_contrib/utils/__init__.py
new file mode 100644
index 0000000..e9b9a78
--- /dev/null
+++ b/src/srht_contrib/utils/__init__.py
@@ -0,0 +1 @@
+"""Utility helpers."""
diff --git a/src/srht_contrib/utils/dates.py b/src/srht_contrib/utils/dates.py
new file mode 100644
index 0000000..ad72968
--- /dev/null
+++ b/src/srht_contrib/utils/dates.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+
+from datetime import UTC, date, datetime, time, timedelta
+
+
+def ensure_utc(dt: datetime) -> datetime:
+ if dt.tzinfo is None:
+ return dt.replace(tzinfo=UTC)
+ return dt.astimezone(UTC)
+
+
+def parse_date(value: str) -> date:
+ return date.fromisoformat(value)
+
+
+def parse_datetime(value: str) -> datetime:
+ normalized = value.replace("Z", "+00:00")
+ return ensure_utc(datetime.fromisoformat(normalized))
+
+
+def date_range(start: date, end: date) -> list[date]:
+ days: list[date] = []
+ current = start
+ while current <= end:
+ days.append(current)
+ current += timedelta(days=1)
+ return days
+
+
+def year_bounds(year: int) -> tuple[date, date]:
+ return date(year, 1, 1), date(year, 12, 31)
+
+
+def date_to_utc_bounds(value: date) -> tuple[datetime, datetime]:
+ start = datetime.combine(value, time.min, tzinfo=UTC)
+ end = datetime.combine(value, time.max, tzinfo=UTC)
+ return start, end
diff --git a/src/srht_contrib/utils/identity.py b/src/srht_contrib/utils/identity.py
new file mode 100644
index 0000000..adc9a9d
--- /dev/null
+++ b/src/srht_contrib/utils/identity.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from srht_contrib.models import ActorAlias
+
+
+class ActorIdentityResolver:
+ def __init__(self, configured_aliases: dict[str, list[str]] | None = None) -> None:
+ self.configured_aliases = configured_aliases or {}
+
+ def canonicalize(self, actor: str, db: Session | None = None) -> str:
+ normalized = actor.strip()
+ if not normalized:
+ return normalized
+
+ for canonical, aliases in self.configured_aliases.items():
+ if normalized == canonical or normalized in aliases:
+ return canonical
+
+ if db is not None:
+ alias = db.scalar(select(ActorAlias).where(ActorAlias.alias == normalized))
+ if alias is not None:
+ return alias.canonical_actor
+
+ return normalized
diff --git a/src/srht_contrib/utils/repositories.py b/src/srht_contrib/utils/repositories.py
new file mode 100644
index 0000000..f8e65b5
--- /dev/null
+++ b/src/srht_contrib/utils/repositories.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+from fastapi import HTTPException, status
+
+
+def canonicalize_repository_name(actor: str, repo_name: str) -> str:
+ normalized_actor = actor.strip()
+ normalized_repo_name = repo_name.strip()
+
+ if not normalized_actor:
+ raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Actor must not be blank.")
+ if not normalized_repo_name:
+ raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Repository name must not be blank.")
+
+ if "/" in normalized_repo_name:
+ owner, name = normalized_repo_name.split("/", 1)
+ owner = owner.strip()
+ name = name.strip()
+ if not owner or not name or "/" in name:
+ raise HTTPException(
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
+ detail="Repository name must be `name` or `~owner/name`.",
+ )
+ canonical_owner = owner if owner.startswith("~") else f"~{owner}"
+ return f"{canonical_owner}/{name}"
+
+ if "/" in normalized_actor:
+ raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Actor must be a canonical sr.ht user.")
+
+ canonical_actor = normalized_actor if normalized_actor.startswith("~") else f"~{normalized_actor}"
+ return f"{canonical_actor}/{normalized_repo_name}"