Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,15 @@ max-line-length = 79
# E203: whitespace before :, flake8 disagrees with PEP-8
# W503: line break after binary operator, flake8 disagrees with PEP-8
ignore = E203, W503
per-file-ignores =
# E231, E225, E702: flake8 misreads operators/punctuation inside f-string literals
src/obsloctap/skymap.py: E231, E225, E702
# E231: flake8 misreads :// inside f-string URL literals
scripts/render_skymap.py: E231
# E231, E702: flake8 misreads punctuation inside f-string literals
src/obsloctap/db.py: E231, E702
src/obsloctap/consdbhelp.py: E231
# E231, E702: flake8 misreads punctuation inside f-string literals
tests/DBmock.py: E231, E702
exclude =
docs/conf.py
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
uv.lock

# Translations
*.mo
Expand Down Expand Up @@ -112,6 +113,7 @@ venv/
ENV/
env.bak/
venv.bak/
.idea

# Spyder project settings
.spyderproject
Expand All @@ -136,3 +138,6 @@ dmypy.json

# macOS
.DS_Store

# Test artifacts
tests/output/
2 changes: 1 addition & 1 deletion Dockerfile.consumekafka
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM python:3.13.6-slim-bullseye AS base-image
COPY scripts/install-base-packages.sh .
RUN ./install-base-packages.sh && rm ./install-base-packages.sh

RUN apt-get update && apt-get install -y --no-install-recommends -o APT::Immediate-Configure=false gcc g++
RUN apt-get update && apt-get install -y --no-install-recommends -o APT::Immediate-Configure=false gcc g++ libproj-dev proj-bin


FROM base-image AS dependencies-image
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ obsloctap:
docker build . -f Dockerfile.obsloctap -t obsloctap

runlocal:
@echo "Viewer: http://127.0.0.1:8000/obsloctap/static/viewer.html"
uvicorn obsloctap.main:app
775 changes: 421 additions & 354 deletions requirements/dev.txt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ scikit-learn
rubin_sim>=2.6.0
psycopg2-binary
types-requests
bokeh>=3.9.0
jinja2>=3.1.6

2,650 changes: 1,335 additions & 1,315 deletions requirements/main.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/install-dependency-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ apt-get update
# Install build-essential because sometimes Python dependencies need to build
# C modules, particularly when upgrading to newer Python versions. libffi-dev
# is sometimes needed to build cffi (a cryptography dependency).
apt-get -y install --no-install-recommends build-essential libffi-dev
apt-get -y install --no-install-recommends build-essential libffi-dev libproj-dev proj-bin

# Delete cached files we don't need anymore:
apt-get clean
Expand Down
155 changes: 155 additions & 0 deletions scripts/render_skymap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Render the obsloctap skymap to a local HTML file and open it in a browser.

Loads a local fixture by default; use --live or --refresh-live to hit the
deployed service.

Usage (from the repo root):
python3.13 scripts/render_skymap.py
python3.13 scripts/render_skymap.py --live [--time 48] [--start now]
python3.13 scripts/render_skymap.py --refresh-live
[--time 48] [--start now]
"""

from __future__ import annotations

import argparse
import json
import os
import pathlib
import sys
import tempfile
import webbrowser
from typing import Any

import requests

from obsloctap.models import Obsplan
from obsloctap.skymap import make_sky_html

_REPO_ROOT = pathlib.Path(__file__).parent.parent
sys.path.insert(0, str(_REPO_ROOT / "src"))

LIVE_URL = "https://usdf-rsp.slac.stanford.edu/obsloctap/schedule"
LIVE_PREFIX = "https://usdf-rsp.slac.stanford.edu/obsloctap"
_FIXTURE = _REPO_ROOT / "tests" / "fixtures" / "schedule_48h.json"


def _should_open_browser(no_open: bool) -> bool:
if no_open:
return False
return os.environ.get("CI", "").lower() not in {"1", "true", "yes"}


def _fetch_live_data(start: str, time: int) -> list[dict[str, Any]] | None:
params: dict[str, Any] = {"time": time}
if start.lower() != "now":
params["start"] = start

print(
f"Checking live service at {LIVE_URL} with params {params} …",
flush=True,
)
try:
resp = requests.get(LIVE_URL, params=params, timeout=30)
resp.raise_for_status()
raw = resp.json()
except requests.RequestException as exc:
print(f"Live service is down or not returning data: {exc}")
return None
except ValueError as exc:
print(f"Live service returned invalid JSON: {exc}")
return None

if not raw:
print("Live service is up but returned no schedule data.")
return None

print(f" → {len(raw)} observations received")
return raw


def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--time",
type=int,
default=48,
help="Lookahead window in hours (default: 48)",
)
parser.add_argument(
"--start",
default="now",
help="Start time: 'now', ISO, or MJD (default: now)",
)
parser.add_argument(
"--no-open",
action="store_true",
help="Write the HTML file but do not open a browser",
)
parser.add_argument(
"--live",
action="store_true",
help="Render using the live service, but do not refresh the fixture",
)
parser.add_argument(
"--refresh-live",
action="store_true",
help=(
f"Fetch the live service and overwrite "
f"{_FIXTURE.relative_to(_REPO_ROOT)}."
),
)
args = parser.parse_args()

if args.live and args.refresh_live:
print("Choose only one of --live or --refresh-live.")
sys.exit(2)

if args.live or args.refresh_live:
raw = _fetch_live_data(args.start, args.time)
if raw is None:
sys.exit(0)
if args.refresh_live:
_FIXTURE.parent.mkdir(parents=True, exist_ok=True)
_FIXTURE.write_text(json.dumps(raw))
print(f" → Fixture updated: {_FIXTURE.relative_to(_REPO_ROOT)}")
else:
if not _FIXTURE.exists():
print(f"Fixture not found: {_FIXTURE}")
print(
"Refresh it explicitly with "
"'python scripts/render_skymap.py --refresh-live'."
)
sys.exit(1)
print(f"Loading fixture {_FIXTURE} …", flush=True)
raw = json.loads(_FIXTURE.read_text())
print(f" → {len(raw)} observations loaded from fixture")

schedule = [Obsplan(**obs) for obs in raw]

html = make_sky_html(
schedule,
start_val=args.start,
time_val=args.time,
path_prefix=LIVE_PREFIX,
)

with tempfile.NamedTemporaryFile(
mode="w", suffix=".html", prefix="skymap_", delete=False
) as fh:
fh.write(html)
path = fh.name

print(f" -- Wrote {path}")
if _should_open_browser(args.no_open):
webbrowser.open(f"file://{path}")
print(" -- Opened in browser")
else:
print(f" → Open manually: file://{path}")


if __name__ == "__main__":
main()
6 changes: 3 additions & 3 deletions src/obsloctap/consdbhelp.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,15 @@ async def getHelper() -> ConsDbHelp:
config = Configuration()
driver = "postgresql+asyncpg"
full_url = (
f"{driver}://{config.consdb_username}:" # noqa: E231
f"{driver}://{config.consdb_username}:"
f"{config.consdb_password}@"
f"{config.consdb_url}/{config.consdb_database}"
)
if "memory" in config.consdb_url:
driver = "sqlite+aiosqlite"
full_url = ( # just using one db for sqllite
f"{driver}:///file:obloctabdb" # noqa: E231
"?mode=memory&cache=shared&uri=true" # noqa: E231
f"{driver}:///file:obloctabdb"
"?mode=memory&cache=shared&uri=true"
)
log.info(
f"Creating SQlAlchemy engine For CONSDB with "
Expand Down
14 changes: 8 additions & 6 deletions src/obsloctap/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ async def insert_exposure(
if not exp.band or exp.band not in spectral_ranges:
log.warning(
f"{exp.exposure_id} has no band - "
f"will insert 'other:pinhole'" # noqa: E231 em_max
f"will insert 'other:pinhole'"
)
exp.band = "other:pinhole"
"""put in an obsplan line based on an exposure
Expand All @@ -292,9 +292,9 @@ async def insert_exposure(
f"{exp.obs_end_mjd - exp.obs_start_mjd}, " # t_exptime
f"15, " # t_resolution
f"'{spectral_ranges[
exp.band or 'other:pinhole'][0]}', " # noqa: E231 em_min
exp.band or 'other:pinhole'][0]}', "
f"'{spectral_ranges[
exp.band or 'other:pinhole'][1]}', " # noqa: E231 em_max
exp.band or 'other:pinhole'][1]}', "
f"0, " # em_res_power
f"'phot.flux.density', " # o_ucd
f"'', " # pol_states
Expand Down Expand Up @@ -586,6 +586,8 @@ async def get_schedule(
obs.s_dec = -74.60384434722222
obs.rubin_rot_sky_pos = 18.33895879413964
obs.rubin_nexp = 3
obs.em_min = spectral_ranges["r"][0]
obs.em_max = spectral_ranges["r"][1]
observations.append(obs)
return observations

Expand Down Expand Up @@ -620,15 +622,15 @@ async def getHelper() -> DbHelp:
config = Configuration()
driver = "postgresql+asyncpg"
full_url = (
f"{driver}://{config.database_user}:" # noqa: E231
f"{driver}://{config.database_user}:"
f"{config.database_password}@"
f"{config.database_url}/{config.database}"
)
if "memory" in config.database_url:
driver = "sqlite+aiosqlite"
full_url = (
f"{driver}:///file:obloctabdb" # noqa: E231
"?mode=memory&cache=shared&uri=true" # noqa: E231
f"{driver}:///file:obloctabdb"
"?mode=memory&cache=shared&uri=true"
)
log.info(f"Creating SQlAlchemy engine with " f"{full_url}")
else:
Expand Down
42 changes: 42 additions & 0 deletions src/obsloctap/handlers/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
from astropy.time import Time, TimeDelta
from fastapi import APIRouter, Depends
from fastapi.params import Query
from fastapi.responses import HTMLResponse
from safir.dependencies.logger import logger_dependency
from safir.metadata import get_metadata
from structlog.stdlib import BoundLogger

from ..config import config
from ..db import DbHelpProvider
from ..models import Index, Obsplan
from ..skymap import make_sky_html

__all__ = ["get_index", "external_router"]

Expand Down Expand Up @@ -101,3 +103,43 @@ async def get_schedule(
else:
schedule = await dbhelp.get_schedule(time)
return schedule


@external_router.get(
"/skymap",
description="Interactive all-sky map of the observing schedule.",
response_class=HTMLResponse,
summary="Sky Map",
include_in_schema=False,
)
async def get_skymap(
start: str = Query("now", description="Start time ('now' or ISO/MJD)"),
time: int = Query(24, description="Hours of schedule lookahead"),
logger: BoundLogger = Depends(logger_dependency),
) -> HTMLResponse:
logger.info(f"Skymap requested for time: {time}, start {start}")
dbhelp = await DbHelpProvider.getHelper()
if start and start.lower() != "now":
t = Time.now() - TimeDelta("1440min")
success = False
try:
t = Time(start, format="iso", scale="utc")
success = True
except Exception:
pass
try:
t = Time(float(start), format="mjd")
success = True
except Exception:
pass
logger.info(f"Converted time: {success} - Using start time: {t}")
schedule = await dbhelp.get_schedule(time, start=t)
else:
schedule = await dbhelp.get_schedule(time)
html = make_sky_html(
schedule,
start_val=start,
time_val=time,
path_prefix=config.path_prefix,
)
return HTMLResponse(content=html)
Loading
Loading