diff options
| author | kboz <kutayboz@gmail.com> | 2026-04-19 01:50:32 +0200 |
|---|---|---|
| committer | kboz <kutayboz@gmail.com> | 2026-04-19 01:50:32 +0200 |
| commit | acbf6d6bd6d63ab0e9f7c4e119827f76e0382aac (patch) | |
| tree | 9d2f74608ae4b910a480648c7bd95fdf6c403389 /scripts | |
| parent | 8cc70dfee2049772a2da9cfdeb15f64452bc390b (diff) | |
| download | haul-acbf6d6bd6d63ab0e9f7c4e119827f76e0382aac.tar.gz | |
feat(backend): unify boot process and refactor route properties
- Added scripts/haul.sh, setup_auth.py, and setup_eve_data.py for simplified SSO and CSV data bootstrapping.
- Refactored `Ship` and `RouteStep` properties across route planning modules to utilize dictionaries instead of strict dataclass attributes.
- Re-aligned route limits, reducing upfront capital risk values in `arbitrage.py` and updating ship metrics in `manager.py`.
- Updated `.gitignore`, `.env.example`, and created AI-assistant prompt configuration.
- Added comprehensive "Getting Started" guides to the README and initialized CHANGELOG.md.
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/haul.sh | 217 | ||||
| -rw-r--r-- | scripts/setup_auth.py | 129 | ||||
| -rw-r--r-- | scripts/setup_eve_data.py | 55 |
3 files changed, 401 insertions, 0 deletions
diff --git a/scripts/haul.sh b/scripts/haul.sh new file mode 100755 index 0000000..b677694 --- /dev/null +++ b/scripts/haul.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PYTHON_BIN="$ROOT_DIR/.venv/bin/python" +NODE_BIN_DIR="$ROOT_DIR/.tools/node/bin" +AUTH_FILE="$ROOT_DIR/backend/src/cache/auth.json" +DATA_DIR="$ROOT_DIR/backend/data" +ENV_FILE="$ROOT_DIR/.env" + +REQUIRED_DATA_FILES=( + mapDenormalize.csv + mapJumps.csv + invTypes.csv + mapRegions.csv +) + +print_usage() { + cat <<'EOF' +Usage: scripts/haul.sh <command> + +Commands: + check Show what is ready and what is still missing + setup-data Download required EVE CSV files into backend/data + setup-auth Create backend/src/cache/auth.json via EVE SSO + backend Start only the Python backend on http://127.0.0.1:8000 + frontend Start only the React frontend on http://127.0.0.1:3000 + up Start backend and frontend together +EOF +} + +load_env() { + if [[ -f "$ENV_FILE" ]]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a + fi + + if [[ -n "${CHARACTER_ID:-}" && -z "${character_id:-}" ]]; then + export character_id="$CHARACTER_ID" + fi +} + +require_python() { + if [[ ! -x "$PYTHON_BIN" ]]; then + echo "Missing Python virtualenv at $PYTHON_BIN" + echo "Create it first or rerun environment setup." + exit 1 + fi +} + +require_node() { + if [[ ! -x "$NODE_BIN_DIR/node" || ! -x "$NODE_BIN_DIR/npm" ]]; then + echo "Missing repo-local Node runtime under $NODE_BIN_DIR" + exit 1 + fi +} + +require_frontend_install() { + if [[ ! -d "$ROOT_DIR/frontend/node_modules" ]]; then + echo "Missing frontend/node_modules" + echo "Run: scripts/haul.sh frontend" + exit 1 + fi +} + +backend_runtime_ready() { + local missing=0 + + if [[ -z "${CLIENT_ID:-}" || -z "${SECRET_KEY:-}" || -z "${character_id:-}" ]]; then + echo "Missing required env vars. Run: scripts/haul.sh setup-auth" + missing=1 + fi + + if [[ ! -f "$AUTH_FILE" ]]; then + echo "Missing auth cache: $AUTH_FILE" + echo "Run: scripts/haul.sh setup-auth" + missing=1 + fi + + for file_name in "${REQUIRED_DATA_FILES[@]}"; do + if [[ ! -f "$DATA_DIR/$file_name" ]]; then + echo "Missing data file: $DATA_DIR/$file_name" + missing=1 + fi + done + + if [[ $missing -ne 0 ]]; then + echo "Run: scripts/haul.sh setup-data" + return 1 + fi + + return 0 +} + +print_status() { + load_env + + if [[ -x "$PYTHON_BIN" ]]; then + echo "python: ready ($PYTHON_BIN)" + else + echo "python: missing ($PYTHON_BIN)" + fi + + if [[ -x "$NODE_BIN_DIR/node" ]]; then + echo "node: ready ($NODE_BIN_DIR/node)" + else + echo "node: missing ($NODE_BIN_DIR/node)" + fi + + if [[ -d "$ROOT_DIR/frontend/node_modules" ]]; then + echo "frontend deps: ready" + else + echo "frontend deps: missing" + fi + + if [[ -f "$AUTH_FILE" ]]; then + echo "auth cache: ready" + else + echo "auth cache: missing" + fi + + for file_name in "${REQUIRED_DATA_FILES[@]}"; do + if [[ -f "$DATA_DIR/$file_name" ]]; then + echo "data: ready ($file_name)" + else + echo "data: missing ($file_name)" + fi + done + + if [[ -f "$ENV_FILE" ]]; then + echo ".env: present" + else + echo ".env: missing (setup-auth can create it)" + fi +} + +run_setup_data() { + require_python + exec "$PYTHON_BIN" "$ROOT_DIR/scripts/setup_eve_data.py" +} + +run_setup_auth() { + require_python + load_env + exec "$PYTHON_BIN" "$ROOT_DIR/scripts/setup_auth.py" +} + +run_backend() { + require_python + load_env + backend_runtime_ready + cd "$ROOT_DIR/backend/src" + exec "$PYTHON_BIN" -m uvicorn main:app --reload --host 127.0.0.1 --port 8000 +} + +run_frontend() { + require_node + require_frontend_install + export PATH="$NODE_BIN_DIR:$PATH" + cd "$ROOT_DIR/frontend" + exec npm start +} + +run_up() { + require_python + require_node + require_frontend_install + load_env + backend_runtime_ready + + export PATH="$NODE_BIN_DIR:$PATH" + + cd "$ROOT_DIR/backend/src" + "$PYTHON_BIN" -m uvicorn main:app --host 127.0.0.1 --port 8000 & + backend_pid=$! + + cleanup() { + if kill -0 "$backend_pid" >/dev/null 2>&1; then + kill "$backend_pid" + wait "$backend_pid" 2>/dev/null || true + fi + } + + trap cleanup EXIT INT TERM + + cd "$ROOT_DIR/frontend" + exec npm start +} + +command_name="${1:-}" + +case "$command_name" in + check) + print_status + ;; + setup-data) + run_setup_data + ;; + setup-auth) + run_setup_auth + ;; + backend) + run_backend + ;; + frontend) + run_frontend + ;; + up) + run_up + ;; + *) + print_usage + exit 1 + ;; +esac
\ No newline at end of file diff --git a/scripts/setup_auth.py b/scripts/setup_auth.py new file mode 100644 index 0000000..d675ddd --- /dev/null +++ b/scripts/setup_auth.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import base64 +import json +from datetime import datetime, UTC +from getpass import getpass +from pathlib import Path +import shlex +from urllib.parse import urlencode, quote + +import requests +from dotenv import load_dotenv +from jose import jwt +import os + + +ROOT_DIR = Path(__file__).resolve().parent.parent +AUTH_FILE = ROOT_DIR / "backend" / "src" / "cache" / "auth.json" +ENV_FILE = ROOT_DIR / ".env" +REDIRECT_URI = "https://localhost/callback/" +TOKEN_URL = "https://login.eveonline.com/v2/oauth/token" +AUTH_URL = "https://login.eveonline.com/v2/oauth/authorize/" +SCOPES = [ + "esi-assets.read_assets.v1", + "esi-location.read_location.v1", + "esi-location.read_online.v1", + "esi-ui.open_window.v1", + "esi-ui.write_waypoint.v1", +] + + +def build_auth_url(client_id: str) -> str: + params = { + "response_type": "code", + "redirect_uri": REDIRECT_URI, + "client_id": client_id, + "state": "haul-local", + "scope": " ".join(SCOPES), + } + return f"{AUTH_URL}?{urlencode(params, quote_via=quote)}" + + +def prompt_for_value(name: str, secret: bool = False) -> str: + while True: + value = getpass(f"{name}: ") if secret else input(f"{name}: ").strip() + if value: + return value + print(f"{name} is required.") + + +def append_missing_env_values(values: dict[str, str]) -> None: + existing_text = ENV_FILE.read_text(encoding="utf-8") if ENV_FILE.exists() else "" + existing_keys = set() + + for raw_line in existing_text.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _ = line.split("=", 1) + existing_keys.add(key.strip()) + + lines_to_add = [] + for key, value in values.items(): + if key not in existing_keys: + lines_to_add.append(f"{key}={shlex.quote(value)}") + + if not lines_to_add: + return + + if existing_text and not existing_text.endswith("\n"): + existing_text += "\n" + + ENV_FILE.write_text(existing_text + "\n".join(lines_to_add) + "\n", encoding="utf-8") + + +def main() -> int: + load_dotenv(ENV_FILE) + + client_id = os.getenv("CLIENT_ID") or prompt_for_value("CLIENT_ID") + secret_key = os.getenv("SECRET_KEY") or prompt_for_value("SECRET_KEY", secret=True) + + print("Open this URL in your browser and complete the EVE SSO flow:\n") + print(build_auth_url(client_id)) + print("\nAfter the redirect, copy the 'code' query parameter and paste it below.") + code = input("Authorization code: ").strip() + + if not code: + print("No authorization code provided.") + return 1 + + user_pass = f"{client_id}:{secret_key}".encode("utf-8") + headers = { + "Authorization": f"Basic {base64.urlsafe_b64encode(user_pass).decode()}", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "login.eveonline.com", + } + form_values = { + "grant_type": "authorization_code", + "code": code, + } + + response = requests.post(TOKEN_URL, data=form_values, headers=headers, timeout=30) + response.raise_for_status() + auth_data = response.json() + auth_data["expires_at"] = int(datetime.now(UTC).timestamp()) + int(auth_data.get("expires_in", 0)) + + AUTH_FILE.parent.mkdir(parents=True, exist_ok=True) + AUTH_FILE.write_text(json.dumps(auth_data, indent=2), encoding="utf-8") + + claims = jwt.get_unverified_claims(auth_data["access_token"]) + character_id = claims.get("sub", "").split(":")[-1] if claims.get("sub") else "unknown" + character_name = claims.get("name", "unknown") + + append_missing_env_values( + { + "CLIENT_ID": client_id, + "SECRET_KEY": secret_key, + "character_id": character_id, + } + ) + + print(f"Saved auth cache to {AUTH_FILE}") + print(f"Detected character: {character_name} ({character_id})") + print(f"Ensured runtime env values exist in {ENV_FILE}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())
\ No newline at end of file diff --git a/scripts/setup_eve_data.py b/scripts/setup_eve_data.py new file mode 100644 index 0000000..a7f7efd --- /dev/null +++ b/scripts/setup_eve_data.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import bz2 +import shutil +import sys +import urllib.request +from pathlib import Path + + +ROOT_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = ROOT_DIR / "backend" / "data" +BASE_URL = "https://www.fuzzwork.co.uk/dump/latest" + +REQUIRED_FILES = { + "mapDenormalize.csv": f"{BASE_URL}/mapDenormalize.csv.bz2", + "mapJumps.csv": f"{BASE_URL}/mapJumps.csv.bz2", + "invTypes.csv": f"{BASE_URL}/invTypes.csv.bz2", + "mapRegions.csv": f"{BASE_URL}/mapRegions.csv.bz2", +} + + +def download_and_extract(file_name: str, url: str) -> None: + destination = DATA_DIR / file_name + archive_path = destination.with_suffix(destination.suffix + ".bz2") + + if destination.exists(): + print(f"Skipping {file_name}; already exists.") + return + + print(f"Downloading {file_name} from {url}") + + try: + with urllib.request.urlopen(url, timeout=60) as response, archive_path.open("wb") as output_file: + shutil.copyfileobj(response, output_file) + + with bz2.open(archive_path, "rb") as compressed_file, destination.open("wb") as output_file: + shutil.copyfileobj(compressed_file, output_file) + finally: + archive_path.unlink(missing_ok=True) + + print(f"Wrote {destination}") + + +def main() -> int: + DATA_DIR.mkdir(parents=True, exist_ok=True) + + for file_name, url in REQUIRED_FILES.items(): + download_and_extract(file_name, url) + + print("EVE static data is ready.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())
\ No newline at end of file |
