feat: foo

This commit is contained in:
didier committed 2026-07-13 21:07:08 -04:00
1 parent 9d741b8516
commit 3413c4f94a
5 files changed
+81 -87

No files matched your search

+29
View File
@@ -1,8 +1,15 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Phenix.Models;
using Phenix.Services; using Phenix.Services;
namespace Phenix.Controllers; namespace Phenix.Controllers;
public record CreateGameRequest(
string Date, string Time, string Home, string Away,
int? HomeScore = null, int? AwayScore = null,
bool Practice = false, bool Rain = false
);
[Route("/api")] [Route("/api")]
public class ApiController : Controller public class ApiController : Controller
{ {
@@ -42,4 +49,26 @@ public class ApiController : Controller
var years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y); var years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y);
return Json(years); return Json(years);
} }
[HttpPost("games")]
public async Task<IActionResult> CreateGame([FromBody] CreateGameRequest req)
{
var (game, error) = await _games.CreateNewGame(req.Date, req.Time, req.Home, req.Away);
if (game is null)
return error switch
{
"game already exists at this date/time" => Conflict(new { error }),
_ => BadRequest(new { error }),
};
await _games.UpdateGame(game.Id, req.HomeScore, req.AwayScore, req.Rain, req.Practice);
return Created($"/api/games/{game.Id}", game);
}
[HttpDelete("games")]
public async Task<IActionResult> ClearGames()
{
await _games.ClearAllGames();
return NoContent();
}
} }
+1
View File
@@ -11,4 +11,5 @@ public interface IGameService
Task<bool> UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice); Task<bool> UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice);
Task<bool> DeleteGameById(string id); Task<bool> DeleteGameById(string id);
Task<bool> SwitchVisitors(string id); Task<bool> SwitchVisitors(string id);
Task ClearAllGames();
} }
+9
View File
@@ -145,6 +145,15 @@ public class PostgresGameService : IGameService
return await cmd.ExecuteNonQueryAsync() > 0; return await cmd.ExecuteNonQueryAsync() > 0;
} }
public async Task ClearAllGames()
{
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM games";
await cmd.ExecuteNonQueryAsync();
}
public async Task<bool> SwitchVisitors(string id) public async Task<bool> SwitchVisitors(string id)
{ {
var game = await GetGameById(id); var game = await GetGameById(id);
+9
View File
@@ -145,6 +145,15 @@ public class SqliteGameService : IGameService
return await cmd.ExecuteNonQueryAsync() > 0; return await cmd.ExecuteNonQueryAsync() > 0;
} }
public async Task ClearAllGames()
{
await using var conn = new SqliteConnection(_connectionString);
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM games";
await cmd.ExecuteNonQueryAsync();
}
public async Task<bool> SwitchVisitors(string id) public async Task<bool> SwitchVisitors(string id)
{ {
var game = await GetGameById(id); var game = await GetGameById(id);
+33 -87
View File
@@ -1,33 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse import argparse
import os import os
import sqlite3
import sys import sys
from pathlib import Path
import requests import requests
def find_project_root():
direnv_file = os.environ.get("DIRENV_FILE")
if direnv_file:
return Path(direnv_file).parent.resolve()
direnv_dir = os.environ.get("DIRENV_DIR")
if direnv_dir:
return Path(direnv_dir).resolve()
cwd = Path.cwd().resolve()
for parent in [cwd] + list(cwd.parents):
if (parent / "flake.nix").exists():
return parent
raise RuntimeError(
"Cannot find project root. Set DIRENV_FILE or DIRENV_DIR, "
"or run from within the project tree."
)
def fetch_json(api_root, path): def fetch_json(api_root, path):
url = f"{api_root}{path}" url = f"{api_root}{path}"
resp = requests.get(url, timeout=30) resp = requests.get(url, timeout=30)
@@ -37,17 +15,17 @@ def fetch_json(api_root, path):
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Populate a local SQLite DB from production data" description="Populate a local instance from production data via REST"
) )
parser.add_argument( parser.add_argument(
"--url", "--source",
default=os.environ.get("PHENIX_URL", "https://ballephenix.com"), default=os.environ.get("PHENIX_SOURCE_URL", "https://ballephenix.com"),
help="Source API root (default: https://ballephenix.com)", help="Source API root (default: https://ballephenix.com)",
) )
parser.add_argument( parser.add_argument(
"--db", "--target",
default=None, default=os.environ.get("PHENIX_TARGET_URL", "http://localhost:5000"),
help="Path to SQLite DB (default: <project-root>/phenix.db)", help="Target API root (default: http://localhost:5000)",
) )
parser.add_argument( parser.add_argument(
"--force", "--force",
@@ -56,41 +34,23 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
api_root = args.url.rstrip("/") source_root = args.source.rstrip("/")
target_root = args.target.rstrip("/")
project_root = find_project_root() years = fetch_json(source_root, "/api/available-years.json")
db_path = Path(args.db) if args.db else project_root / "phenix.db"
years = fetch_json(api_root, "/api/available-years.json")
print(f"Years on source: {len(years)} ({min(years)}–{max(years)})") print(f"Years on source: {len(years)} ({min(years)}–{max(years)})")
all_games = [] all_games = []
for year in years: for year in years:
games = fetch_json(api_root, f"/api/results/{year}.json") games = fetch_json(source_root, f"/api/results/{year}.json")
all_games.extend(games) all_games.extend(games)
print(f"Total games fetched: {len(all_games)}") print(f"Total games fetched: {len(all_games)}")
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS games (
id TEXT PRIMARY KEY,
date TEXT NOT NULL,
time TEXT NOT NULL,
home TEXT NOT NULL,
away TEXT NOT NULL,
year INTEGER NOT NULL,
home_score INTEGER,
away_score INTEGER,
practice INTEGER NOT NULL DEFAULT 0,
rain INTEGER NOT NULL DEFAULT 0,
UNIQUE(date, time)
)
""")
if args.force: if args.force:
cursor.execute("DELETE FROM games") resp = requests.delete(f"{target_root}/api/games", timeout=30)
if resp.status_code not in (204, 200):
print(f"Failed to clear games: {resp.status_code} {resp.text}", file=sys.stderr)
sys.exit(1)
print("Cleared existing games") print("Cleared existing games")
inserted = 0 inserted = 0
@@ -98,46 +58,32 @@ def main():
errors = 0 errors = 0
for g in all_games: for g in all_games:
try: payload = {
home_score = int(g["homeScore"]) if g.get("homeScore") is not None else None "date": g.get("date", ""),
away_score = int(g["awayScore"]) if g.get("awayScore") is not None else None "time": g.get("time", ""),
except (ValueError, TypeError): "home": g.get("home", ""),
home_score = None "away": g.get("away", ""),
away_score = None "homeScore": g.get("homeScore"),
"awayScore": g.get("awayScore"),
"practice": bool(g.get("practice")),
"rain": bool(g.get("rain")),
}
try: try:
cursor.execute( resp = requests.post(f"{target_root}/api/games", json=payload, timeout=30)
""" if resp.status_code == 201:
INSERT OR IGNORE INTO games
(id, date, time, home, away, year, home_score, away_score, practice, rain)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
g.get("id", ""),
g["date"],
g.get("time", ""),
g.get("home", ""),
g.get("away", ""),
g.get("year", 0),
home_score,
away_score,
1 if g.get("practice") else 0,
1 if g.get("rain") else 0,
),
)
if cursor.rowcount > 0:
inserted += 1 inserted += 1
else: elif resp.status_code == 409:
skipped += 1 skipped += 1
except sqlite3.Error as e: else:
print(f"DB error on {g.get('date')} {g.get('time')}: {e}", file=sys.stderr) print(f"Error on {g.get('date')} {g.get('time')}: {resp.status_code} {resp.text}", file=sys.stderr)
errors += 1
except requests.RequestException as e:
print(f"Request error on {g.get('date')} {g.get('time')}: {e}", file=sys.stderr)
errors += 1 errors += 1
conn.commit()
conn.close()
print(f"Inserted: {inserted}, skipped: {skipped}, errors: {errors}") print(f"Inserted: {inserted}, skipped: {skipped}, errors: {errors}")
print(f"Database: {db_path}") print(f"Target: {target_root}")
if __name__ == "__main__": if __name__ == "__main__":