"""FastAPI wrapper over the iTelescope member portal. Run: uvicorn main:app --reload (from this directory, with .env present) GUI: http://127.0.0.1:8000/ (dashboard) Docs: http://127.0.0.1:8000/docs (OpenAPI) Credentials come from the environment / a .env file - never hard-coded. Every endpoint here is READ-ONLY. Booking spends real points and is intentionally not exposed; add it deliberately behind its own confirmation if ever needed. """ from __future__ import annotations import os from functools import lru_cache from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from itelescope_client import ITelescopeClient, ITelescopeError, NotConfigured, SITES load_dotenv() HERE = Path(__file__).parent app = FastAPI(title="iTelescope API", version="1.0", description="Read-only wrapper over the iTelescope.net member portal.") @lru_cache(maxsize=1) def client() -> ITelescopeClient: return ITelescopeClient() def _guard(fn): try: return fn() except NotConfigured as e: raise HTTPException(503, str(e)) except ITelescopeError as e: raise HTTPException(502, str(e)) @app.get("/api/health") def health(): c = client() return {"configured": bool(c.username and c.password), "sites": SITES} @app.get("/api/account") def account(): return _guard(lambda: client().account_status()) @app.get("/api/plans") def plans(): return _guard(lambda: client().available_plans()) @app.get("/api/telescopes") def telescopes(): """The telescope roster from the repo's snapshotted spec sheet.""" csv = HERE.parent / "data" / "itelescope-telescopes.csv" if not csv.exists(): raise HTTPException(404, "telescope csv not found") import csv as _csv with open(csv, encoding="utf-8-sig", newline="") as fh: return list(_csv.DictReader(fh)) @app.get("/api/weather/{site}") def weather(site: str): return _guard(lambda: client().weather(site)) @app.get("/api/weather") def weather_all(): c = client() out = {} for s in SITES: try: out[s] = c.weather(s) except ITelescopeError as e: out[s] = {"site": s, "error": str(e)} return out @app.get("/api/scope/{tid}/status") def scope_status(tid: str): return _guard(lambda: client().scope_status(tid)) @app.get("/api/reservations") def reservations(): return _guard(lambda: client().reservations()) @app.get("/api/raw") def raw(path: str): return _guard(lambda: client().raw(path)) # --- GUI ------------------------------------------------------------------ app.mount("/static", StaticFiles(directory=HERE / "static"), name="static") @app.get("/") def index(): return FileResponse(HERE / "static" / "index.html")