summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorkboz <kutayboz@gmail.com>2026-04-19 01:50:32 +0200
committerkboz <kutayboz@gmail.com>2026-04-19 01:50:32 +0200
commitacbf6d6bd6d63ab0e9f7c4e119827f76e0382aac (patch)
tree9d2f74608ae4b910a480648c7bd95fdf6c403389 /scripts
parent8cc70dfee2049772a2da9cfdeb15f64452bc390b (diff)
downloadhaul-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-xscripts/haul.sh217
-rw-r--r--scripts/setup_auth.py129
-rw-r--r--scripts/setup_eve_data.py55
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