Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
56 changes: 56 additions & 0 deletions .github/workflows/binary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Binary build

on:
- push

jobs:
binary:
runs-on: ubuntu-${{ matrix.ubuntu-version }}
strategy:
fail-fast: false
matrix:
ubuntu-version: ["22.04", "24.04"]
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Install dependencies
run: |
make install-dev
make install-frontend

- name: Run codegen
run: make gen

- name: Build binary
run: make build-binary

- name: Smoke test
run: |
set -e
./dist/hyperleda-uploader --no-browser --port 8765 &
pid=$!
for i in $(seq 1 120); do
if curl -fsS -o /dev/null http://127.0.0.1:8765/api/tasks 2>/dev/null; then
break
fi
sleep 1
done
curl -fsS http://127.0.0.1:8765/ | grep -q "<!doctype html"
curl -fsS http://127.0.0.1:8765/api/tasks | head -c 500 | grep -q "\["
kill "$pid"
wait "$pid" 2>/dev/null || true

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: hyperleda-uploader-ubuntu-${{ matrix.ubuntu-version }}
path: dist/hyperleda-uploader
24 changes: 23 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,28 @@ fix-frontend:
@output=$$(cd frontend && yarn run --silent prettier --write src 2>&1) || { echo "$$output"; exit 1; }
@output=$$(cd frontend && yarn run --silent eslint --fix src 2>&1) || { echo "$$output"; exit 1; }

build-frontend:
cd frontend && yarn build

build-binary: build-frontend
uv run pyinstaller \
--onedir \
--name hyperleda-uploader \
--clean \
--noconfirm \
--add-data "frontend/dist:frontend/dist" \
--exclude-module matplotlib \
--exclude-module PIL \
--hidden-import uvicorn.logging \
--hidden-import uvicorn.loops.auto \
--hidden-import uvicorn.protocols.http.auto \
--hidden-import uvicorn.protocols.websockets.auto \
--hidden-import uvicorn.lifespan.on \
--collect-data astroquery \
--collect-data astropy \
run.py
@rm -f hyperleda-uploader.spec

# only for mac as this is faster
build:
docker build . \
Expand Down Expand Up @@ -99,7 +121,7 @@ gen:
--config openapigen.yaml \
--url https://leda.sao.ru/admin/api/openapi.json

.PHONY: serve frontend dev check-frontend fix-frontend install-frontend install-dev-frontend
.PHONY: serve frontend dev check-frontend fix-frontend install-frontend install-dev-frontend build-frontend build-binary

serve:
uv run uvicorn server.main:app --reload --port 8000
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dev = [
"pandas-stubs>=2.2.3.250308",
"pytest~=9.0.2",
"basedpyright~=1.38.3",
"pyinstaller>=6.10.0",
"matplotlib>=3.8.0",
]

[tool.pytest.ini_options]
Expand Down
39 changes: 39 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import argparse
import threading
import time
import webbrowser

import uvicorn

from server.main import app


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=8000)
parser.add_argument("--no-browser", action="store_true")
args = parser.parse_args()

if args.host == "0.0.0.0":
open_url = f"http://127.0.0.1:{args.port}/"
else:
open_url = f"http://{args.host}:{args.port}/"

if not args.no_browser:

def open_later() -> None:
time.sleep(0.5)
webbrowser.open(open_url)

threading.Thread(target=open_later, daemon=True).start()

uvicorn.run(
app,
host=args.host,
port=args.port,
)


if __name__ == "__main__":
main()
55 changes: 47 additions & 8 deletions server/main.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
import asyncio
import json
import sys
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from pydantic import ValidationError

from server.history import load_history
from server.task_registry import register_all_tasks
from server.tasks import TASKS, get_run, start_task


def frontend_dist_dir() -> Path | None:
meipass = getattr(sys, "_MEIPASS", None)
if getattr(sys, "frozen", False) and meipass is not None:
base = Path(meipass)
else:
base = Path(__file__).resolve().parent.parent
dist = base / "frontend" / "dist"
if dist.is_dir():
return dist
return None


register_all_tasks()

app = FastAPI(title="HyperLEDA Uploader")

app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if frontend_dist_dir() is None:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


@app.get("/api/tasks")
Expand Down Expand Up @@ -86,3 +103,25 @@ async def event_gen():
await asyncio.sleep(0.15)

return StreamingResponse(event_gen(), media_type="text/event-stream")


_static_root = frontend_dist_dir()
if _static_root is not None:
assets_dir = _static_root / "assets"
if assets_dir.is_dir():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")

@app.get("/")
def spa_root() -> FileResponse:
return FileResponse(_static_root / "index.html")

@app.get("/{full_path:path}")
def spa_fallback(full_path: str) -> FileResponse:
candidate = (_static_root / full_path).resolve()
try:
candidate.relative_to(_static_root.resolve())
except ValueError:
return FileResponse(_static_root / "index.html")
if candidate.is_file():
return FileResponse(candidate)
return FileResponse(_static_root / "index.html")
Loading
Loading