117 lines
2.6 KiB
Python
117 lines
2.6 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
import logging
|
|
import re
|
|
from datetime import datetime, timezone
|
|
|
|
import dataset
|
|
import sqlalchemy
|
|
import uuid_utils
|
|
from sqlalchemy.pool import NullPool
|
|
|
|
from molodetz import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ON_CONNECT_STATEMENTS = [
|
|
"PRAGMA journal_mode=WAL",
|
|
"PRAGMA synchronous=NORMAL",
|
|
"PRAGMA busy_timeout=30000",
|
|
"PRAGMA cache_size=-8000",
|
|
"PRAGMA temp_store=MEMORY",
|
|
"PRAGMA mmap_size=268435456",
|
|
]
|
|
|
|
config.ensure_data_dirs()
|
|
|
|
db = dataset.connect(
|
|
config.DATABASE_URL,
|
|
engine_kwargs={
|
|
"connect_args": {"timeout": 30, "check_same_thread": False},
|
|
"poolclass": NullPool,
|
|
},
|
|
on_connect_statements=ON_CONNECT_STATEMENTS,
|
|
)
|
|
|
|
|
|
def generate_uid():
|
|
return str(uuid_utils.uuid7())
|
|
|
|
|
|
def now_iso():
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def parse_iso(value):
|
|
if not value:
|
|
return None
|
|
try:
|
|
parsed = datetime.fromisoformat(str(value))
|
|
except ValueError:
|
|
return None
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
return parsed
|
|
|
|
|
|
def refresh_snapshot():
|
|
try:
|
|
if getattr(db.local, "tx", None):
|
|
return
|
|
conn = db.executable
|
|
if conn.in_transaction():
|
|
conn.commit()
|
|
except Exception as exc:
|
|
logger.warning("refresh_snapshot failed: %s", exc)
|
|
|
|
|
|
def table_exists(name):
|
|
return name in db.tables
|
|
|
|
|
|
def in_clause(values, prefix="v"):
|
|
params = {f"{prefix}{index}": value for index, value in enumerate(values)}
|
|
placeholders = ", ".join(f":{key}" for key in params)
|
|
return placeholders, params
|
|
|
|
|
|
def atomic_update(sql, **params):
|
|
with db:
|
|
result = db.executable.execute(sqlalchemy.text(sql), params)
|
|
return result.rowcount
|
|
|
|
|
|
def run_write(sql, **params):
|
|
with db:
|
|
db.query(sql, **params)
|
|
|
|
|
|
def slugify(text):
|
|
lowered = (text or "").lower()
|
|
replaced = re.sub(r"[^a-z0-9-]", "-", lowered)
|
|
return re.sub(r"-+", "-", replaced).strip("-")
|
|
|
|
|
|
def make_combined_slug(text, uid):
|
|
tail = uid.replace("-", "")[-12:]
|
|
slug = slugify(text)
|
|
return f"{slug}-{tail}" if slug else tail
|
|
|
|
|
|
def shard_path(uid):
|
|
tail = uid.replace("-", "")
|
|
return f"{tail[-2:]}/{tail[-4:-2]}"
|
|
|
|
|
|
def resolve_by_slug(table_name, slug, include_deleted=False):
|
|
if table_name not in db.tables:
|
|
return None
|
|
table = db[table_name]
|
|
for key in ("slug", "uid"):
|
|
criteria = {key: slug}
|
|
if not include_deleted:
|
|
criteria["deleted_at"] = None
|
|
row = table.find_one(**criteria)
|
|
if row:
|
|
return row
|
|
return None
|