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.
This commit is contained in:
parent
67f8455e41
commit
a45f939f76
7 changed files with 492 additions and 0 deletions
108
api/main.py
Normal file
108
api/main.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue