itelescope/api/main.py
Laurence a45f939f76 Add FastAPI wrapper + web console for the iTelescope portal
Read-only API over go.itelescope.net driven by username/password from a .env:
account status (balance/plan/renewal), plan discounts, telescope roster,
per-site weather, per-scope ACP reachability, and reservations (with a pointer
to the Cloudflare-protected live booking system). A dark dashboard GUI is served
on top at /. Booking is intentionally not exposed - it spends real points.
2026-07-24 22:47:04 +01:00

108 lines
2.8 KiB
Python

"""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")