مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
50 lines
1.4 KiB
Python
50 lines
1.4 KiB
Python
"""Shared domain primitives.
|
|
|
|
Pure Python — no framework, no I/O. The domain layer must stay importable
|
|
without FastAPI, SQLAlchemy or a database connection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
|
|
_SLUG_RE = re.compile(r"[^a-z0-9]+")
|
|
|
|
|
|
def new_id() -> str:
|
|
"""Opaque, URL-safe entity id (portable across SQLite and PostgreSQL)."""
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
def utcnow() -> datetime:
|
|
return datetime.now(UTC)
|
|
|
|
|
|
def slugify(value: str, *, max_length: int = 48) -> str:
|
|
"""ASCII slug. Non-ASCII input (e.g. Persian) collapses to a stable fallback.
|
|
|
|
Persian organization names do not transliterate, so we keep a short random
|
|
suffix appended by the caller when the slug would be empty.
|
|
"""
|
|
slug = _SLUG_RE.sub("-", value.strip().lower()).strip("-")
|
|
slug = slug[:max_length].strip("-")
|
|
if not slug:
|
|
slug = f"x{uuid.uuid4().hex[:8]}"
|
|
return slug
|
|
|
|
|
|
def ensure_unique_slug(base: str, taken: set[str], *, max_length: int = 48) -> str:
|
|
"""Append ``-2``, ``-3`` ... until the slug is free."""
|
|
candidate = base[:max_length]
|
|
if candidate not in taken:
|
|
return candidate
|
|
n = 2
|
|
while True:
|
|
suffix = f"-{n}"
|
|
candidate = f"{base[: max_length - len(suffix)]}{suffix}"
|
|
if candidate not in taken:
|
|
return candidate
|
|
n += 1
|