From 3e3f84a00e6b427500eb2d87ca781ea75553ecba Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Wed, 8 Apr 2026 07:04:40 +0200 Subject: [PATCH] feat: dynamic login providers + profiler auto-trigger + refresh endpoint --- app/api/catalog.py | 24 ++++++++++++++++++++++++ app/api/sync.py | 30 ++++++++++++++++++++++++++++++ app/web/router.py | 17 ++++++++++++++--- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/app/api/catalog.py b/app/api/catalog.py index 5f4e482..0d8d582 100644 --- a/app/api/catalog.py +++ b/app/api/catalog.py @@ -96,3 +96,27 @@ async def get_metric( return content except Exception as e: raise HTTPException(status_code=500, detail=f"Error parsing metric: {e}") + + +@router.post("/profile/{table_name}/refresh") +async def refresh_profile( + table_name: str, + user: dict = Depends(get_current_user), + conn: duckdb.DuckDBPyConnection = Depends(_get_db), +): + """Re-generate profile for a table on demand.""" + from src.profiler import profile_table, TableInfo + + data_dir = _get_data_dir() + extracts_dir = data_dir / "extracts" + candidates = list(extracts_dir.rglob(f"data/{table_name}.parquet")) + if not candidates: + raise HTTPException(status_code=404, detail=f"No parquet for '{table_name}'") + + try: + table_info = TableInfo(name=table_name, table_id=table_name) + profile = profile_table(table_info, candidates[0], [], {}, {}) + ProfileRepository(conn).save(table_name, profile) + return {"status": "ok", "table": table_name, "columns": len(profile.get("columns", {}))} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Profile failed: {e}") diff --git a/app/api/sync.py b/app/api/sync.py index 7c55981..4fb26a8 100644 --- a/app/api/sync.py +++ b/app/api/sync.py @@ -122,6 +122,36 @@ print(json.dumps(result)) views = orch.rebuild() print(f"[SYNC] Orchestrator rebuild: {{{', '.join(f'{k}: {len(v)}' for k, v in views.items())}}}", file=_sys.stderr, flush=True) + # Auto-profile synced tables (best-effort, don't fail sync on profile error) + try: + from src.profiler import profile_table, TableInfo + from src.repositories.profiles import ProfileRepository + + data_dir = Path(os.environ.get("DATA_DIR", "./data")) + extracts_dir = data_dir / "extracts" + + sys_conn = get_system_db() + try: + profile_repo = ProfileRepository(sys_conn) + profiled = 0 + for source_name, table_names in views.items(): + for table_name in table_names[:10]: # Limit per sync + pq_path = extracts_dir / source_name / "data" / f"{table_name}.parquet" + if not pq_path.exists(): + continue + try: + table_info = TableInfo(name=table_name, table_id=table_name) + profile = profile_table(table_info, pq_path, [], {}, {}) + profile_repo.save(table_name, profile) + profiled += 1 + except Exception as pe: + print(f"[SYNC] Profile {table_name}: {pe}", file=_sys.stderr, flush=True) + print(f"[SYNC] Profiled {profiled} tables", file=_sys.stderr, flush=True) + finally: + sys_conn.close() + except Exception as e: + print(f"[SYNC] Profiler skipped: {e}", file=_sys.stderr, flush=True) + except subprocess.TimeoutExpired: print("[SYNC] Extractor timed out after 1800s", file=_sys.stderr, flush=True) except Exception as e: diff --git a/app/web/router.py b/app/web/router.py index c1d62cc..a51f19f 100644 --- a/app/web/router.py +++ b/app/web/router.py @@ -151,9 +151,20 @@ async def index(request: Request, user: Optional[dict] = Depends(get_optional_us @router.get("/login", response_class=HTMLResponse) async def login_page(request: Request): - providers = [ - {"name": "google", "display_name": "Google", "icon": "google"}, - ] + providers = [] + try: + from app.auth.providers.google import is_available as google_available + if google_available(): + providers.append({"name": "google", "display_name": "Google", "icon": "google"}) + except Exception: + pass + providers.append({"name": "password", "display_name": "Email & Password", "icon": "key"}) + try: + from app.auth.providers.email import is_available as email_available + if email_available(): + providers.append({"name": "email", "display_name": "Email Link", "icon": "mail"}) + except Exception: + pass ctx = _build_context(request, providers=providers) return templates.TemplateResponse(request, "login.html", ctx)