Spaces:
Sleeping
Sleeping
Lazy st.secrets, prefer env vars; switch to SQLAlchemy engine for pandas 2.2 compatibility
Browse files- tools/tools.py +37 -15
- utils/config.py +10 -5
tools/tools.py
CHANGED
|
@@ -10,9 +10,37 @@ Two layers:
|
|
| 10 |
import pandas as pd
|
| 11 |
import psycopg2
|
| 12 |
import re
|
|
|
|
|
|
|
| 13 |
from utils.config import CLEAN_DB_URI, METRIC_LIBRARY
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 17 |
# SECTION 1: DETERMINISTIC SQL BUILD ENGINE
|
| 18 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -418,9 +446,7 @@ def execute_metric_query(metric_name, filters=None, group_by=None):
|
|
| 418 |
return None, f"Could not build SQL for '{metric_name}'"
|
| 419 |
|
| 420 |
try:
|
| 421 |
-
|
| 422 |
-
df = pd.read_sql(sql, conn)
|
| 423 |
-
conn.close()
|
| 424 |
return df, sql
|
| 425 |
except Exception as e:
|
| 426 |
return None, f"SQL Error: {str(e)}\nSQL: {sql}"
|
|
@@ -468,9 +494,7 @@ def execute_custom_sql(sql_query):
|
|
| 468 |
return None, "BLOCKED: Both fact tables without CTEs. Use execute_metric_query('revpar', ...) instead."
|
| 469 |
|
| 470 |
try:
|
| 471 |
-
|
| 472 |
-
df = pd.read_sql(sql_query, conn)
|
| 473 |
-
conn.close()
|
| 474 |
return df, None
|
| 475 |
except Exception as e:
|
| 476 |
return None, f"SQL Error: {str(e)}"
|
|
@@ -479,18 +503,16 @@ def execute_custom_sql(sql_query):
|
|
| 479 |
def get_database_context():
|
| 480 |
"""Direct Python function to get DB context."""
|
| 481 |
try:
|
| 482 |
-
|
| 483 |
|
| 484 |
weeks = pd.read_sql(
|
| 485 |
-
"SELECT week_no, COUNT(*) as days FROM dim_date GROUP BY week_no ORDER BY week_no::int",
|
| 486 |
)
|
| 487 |
-
cities = pd.read_sql("SELECT DISTINCT city FROM dim_hotels ORDER BY city",
|
| 488 |
-
properties = pd.read_sql("SELECT property_id, property_name, category, city FROM dim_hotels ORDER BY property_name",
|
| 489 |
-
platforms = pd.read_sql("SELECT DISTINCT booking_platform FROM fact_bookings ORDER BY booking_platform",
|
| 490 |
-
rooms = pd.read_sql("SELECT room_id, room_class FROM dim_rooms ORDER BY room_id",
|
| 491 |
-
date_range = pd.read_sql("SELECT MIN(date) as start_date, MAX(date) as end_date FROM dim_date",
|
| 492 |
-
|
| 493 |
-
conn.close()
|
| 494 |
|
| 495 |
latest_week = weeks.iloc[-1]['week_no']
|
| 496 |
full_weeks = weeks[weeks['days'] >= 7]
|
|
|
|
| 10 |
import pandas as pd
|
| 11 |
import psycopg2
|
| 12 |
import re
|
| 13 |
+
from sqlalchemy import create_engine
|
| 14 |
+
from sqlalchemy.engine import Engine
|
| 15 |
from utils.config import CLEAN_DB_URI, METRIC_LIBRARY
|
| 16 |
|
| 17 |
|
| 18 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
# SHARED SQLAlchemy ENGINE (pandas 2.2+ requires this; raw psycopg2
|
| 20 |
+
# connections raise warnings/errors in pd.read_sql).
|
| 21 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
|
| 23 |
+
_engine: Engine | None = None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _get_engine() -> Engine:
|
| 27 |
+
"""Lazily build a single SQLAlchemy engine for the lifetime of the process."""
|
| 28 |
+
global _engine
|
| 29 |
+
if _engine is None:
|
| 30 |
+
if not CLEAN_DB_URI:
|
| 31 |
+
raise RuntimeError(
|
| 32 |
+
"CLEAN_SUPABASE_DB_URI is not set. "
|
| 33 |
+
"Add it as a secret in HF Space settings or in .env locally."
|
| 34 |
+
)
|
| 35 |
+
uri = CLEAN_DB_URI
|
| 36 |
+
if uri.startswith("postgres://"):
|
| 37 |
+
uri = uri.replace("postgres://", "postgresql+psycopg2://", 1)
|
| 38 |
+
elif uri.startswith("postgresql://"):
|
| 39 |
+
uri = uri.replace("postgresql://", "postgresql+psycopg2://", 1)
|
| 40 |
+
_engine = create_engine(uri, pool_pre_ping=True, pool_size=2, max_overflow=3)
|
| 41 |
+
return _engine
|
| 42 |
+
|
| 43 |
+
|
| 44 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 45 |
# SECTION 1: DETERMINISTIC SQL BUILD ENGINE
|
| 46 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 446 |
return None, f"Could not build SQL for '{metric_name}'"
|
| 447 |
|
| 448 |
try:
|
| 449 |
+
df = pd.read_sql(sql, _get_engine())
|
|
|
|
|
|
|
| 450 |
return df, sql
|
| 451 |
except Exception as e:
|
| 452 |
return None, f"SQL Error: {str(e)}\nSQL: {sql}"
|
|
|
|
| 494 |
return None, "BLOCKED: Both fact tables without CTEs. Use execute_metric_query('revpar', ...) instead."
|
| 495 |
|
| 496 |
try:
|
| 497 |
+
df = pd.read_sql(sql_query, _get_engine())
|
|
|
|
|
|
|
| 498 |
return df, None
|
| 499 |
except Exception as e:
|
| 500 |
return None, f"SQL Error: {str(e)}"
|
|
|
|
| 503 |
def get_database_context():
|
| 504 |
"""Direct Python function to get DB context."""
|
| 505 |
try:
|
| 506 |
+
engine = _get_engine()
|
| 507 |
|
| 508 |
weeks = pd.read_sql(
|
| 509 |
+
"SELECT week_no, COUNT(*) as days FROM dim_date GROUP BY week_no ORDER BY week_no::int", engine
|
| 510 |
)
|
| 511 |
+
cities = pd.read_sql("SELECT DISTINCT city FROM dim_hotels ORDER BY city", engine)['city'].tolist()
|
| 512 |
+
properties = pd.read_sql("SELECT property_id, property_name, category, city FROM dim_hotels ORDER BY property_name", engine)
|
| 513 |
+
platforms = pd.read_sql("SELECT DISTINCT booking_platform FROM fact_bookings ORDER BY booking_platform", engine)['booking_platform'].tolist()
|
| 514 |
+
rooms = pd.read_sql("SELECT room_id, room_class FROM dim_rooms ORDER BY room_id", engine)
|
| 515 |
+
date_range = pd.read_sql("SELECT MIN(date) as start_date, MAX(date) as end_date FROM dim_date", engine).iloc[0]
|
|
|
|
|
|
|
| 516 |
|
| 517 |
latest_week = weeks.iloc[-1]['week_no']
|
| 518 |
full_weeks = weeks[weeks['days'] >= 7]
|
utils/config.py
CHANGED
|
@@ -25,17 +25,22 @@ def _load_env():
|
|
| 25 |
|
| 26 |
_load_env()
|
| 27 |
|
| 28 |
-
# Try
|
| 29 |
-
# then fall back to
|
|
|
|
|
|
|
| 30 |
def _get_secret(key):
|
| 31 |
-
"""Get secret from
|
|
|
|
|
|
|
|
|
|
| 32 |
try:
|
| 33 |
-
import streamlit as st
|
| 34 |
if hasattr(st, "secrets") and key in st.secrets:
|
| 35 |
return st.secrets[key]
|
| 36 |
except Exception:
|
| 37 |
pass
|
| 38 |
-
return
|
| 39 |
|
| 40 |
# ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
# CONNECTION & LLM CONFIG
|
|
|
|
| 25 |
|
| 26 |
_load_env()
|
| 27 |
|
| 28 |
+
# Try environment variables FIRST (works on HF Spaces, local .env, and Codespaces),
|
| 29 |
+
# then fall back to Streamlit secrets (Streamlit Cloud only).
|
| 30 |
+
# Reading st.secrets at import time can interfere with set_page_config on multipage apps,
|
| 31 |
+
# so it is only consulted when env vars are unset.
|
| 32 |
def _get_secret(key):
|
| 33 |
+
"""Get secret from env vars first; fall back to Streamlit secrets if available."""
|
| 34 |
+
val = os.getenv(key)
|
| 35 |
+
if val:
|
| 36 |
+
return val
|
| 37 |
try:
|
| 38 |
+
import streamlit as st # noqa: WPS433
|
| 39 |
if hasattr(st, "secrets") and key in st.secrets:
|
| 40 |
return st.secrets[key]
|
| 41 |
except Exception:
|
| 42 |
pass
|
| 43 |
+
return None
|
| 44 |
|
| 45 |
# ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
# CONNECTION & LLM CONFIG
|