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.
108 lines
2.8 KiB
Python
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")
|