fleettickets/shared.py
david kiania 4631cc6382 feat: fleettickets — INC/CRQ ticket ingestion, geocoding + read-schema
Standalone module extracted from the tracksolid repo (was migrations 21-23 +
tools/import_tickets.py). Owns the `tickets` schema in the shared tracksolid_db.

- migrations/01_tickets_schema.sql: consolidated final-state schema (tickets.inc/
  crq raw-jsonb-first, geo_clusters + geo_locations gazetteers, geom trigger,
  reporting.fn_tickets_for_map)
- import_tickets.py: rustfs bucket ingest + cluster/location geocoding
  (LocationIQ/OpenCage, viewbox-bounded + cluster-distance guard)
- run_migrations.py, shared.py (self-contained), pyproject, .env.example, README

The DB stays in tracksolid_db; dashboard_api keeps serving /webhook/tickets; the
Tickets map stays a FleetOps tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:13:50 +03:00

54 lines
1.5 KiB
Python

"""
shared.py — fleettickets · minimal DB + helper utilities.
Self-contained replacement for the handful of helpers the loader used to import
from the tracksolid repo's `ts_shared_rev`, so fleettickets stands alone.
Connection comes from the DATABASE_URL env var (points at the shared
`tracksolid_db`, where the `tickets` schema lives).
"""
from __future__ import annotations
import logging
import os
from contextlib import contextmanager
from typing import Any, Optional
import psycopg2
def get_logger(name: str) -> logging.Logger:
logger = logging.getLogger(f"fleettickets.{name}")
if not logger.handlers:
h = logging.StreamHandler()
h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s%(message)s",
datefmt="%Y-%m-%d %H:%M:%S"))
logger.addHandler(h)
logger.setLevel(os.getenv("LOG_LEVEL", "INFO"))
return logger
@contextmanager
def get_conn():
"""DB connection context manager. Auto-commits on success, rolls back on error."""
dsn = os.environ.get("DATABASE_URL")
if not dsn:
raise RuntimeError("DATABASE_URL is not set")
conn = psycopg2.connect(dsn)
try:
conn.autocommit = False
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def clean(v: Any) -> Optional[str]:
"""Trimmed string, or None if empty/None."""
if v is None:
return None
s = str(v).strip()
return s if s != "" else None