From 3413c4f94a304d6b53862e4fb9179ffd987fe5f3 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Mon, 13 Jul 2026 21:07:08 -0400 Subject: [PATCH] feat: foo --- Controllers/ApiController.cs | 29 ++++++++ Services/IGameService.cs | 1 + Services/PostgresGameService.cs | 9 +++ Services/SqliteGameService.cs | 9 +++ dev/populate-db/populate.py | 120 +++++++++----------------------- 5 files changed, 81 insertions(+), 87 deletions(-) diff --git a/Controllers/ApiController.cs b/Controllers/ApiController.cs index 4264160..4dff2fc 100644 --- a/Controllers/ApiController.cs +++ b/Controllers/ApiController.cs @@ -1,8 +1,15 @@ using Microsoft.AspNetCore.Mvc; +using Phenix.Models; using Phenix.Services; 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")] public class ApiController : Controller { @@ -42,4 +49,26 @@ public class ApiController : Controller var years = games.Select(g => g.Year).Distinct().OrderByDescending(y => y); return Json(years); } + + [HttpPost("games")] + public async Task 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 ClearGames() + { + await _games.ClearAllGames(); + return NoContent(); + } } diff --git a/Services/IGameService.cs b/Services/IGameService.cs index 8461126..b1df892 100644 --- a/Services/IGameService.cs +++ b/Services/IGameService.cs @@ -11,4 +11,5 @@ public interface IGameService Task UpdateGame(string id, int? homeScore, int? awayScore, bool? rain, bool? practice); Task DeleteGameById(string id); Task SwitchVisitors(string id); + Task ClearAllGames(); } diff --git a/Services/PostgresGameService.cs b/Services/PostgresGameService.cs index ebf68df..d3f5ce5 100644 --- a/Services/PostgresGameService.cs +++ b/Services/PostgresGameService.cs @@ -145,6 +145,15 @@ public class PostgresGameService : IGameService 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 SwitchVisitors(string id) { var game = await GetGameById(id); diff --git a/Services/SqliteGameService.cs b/Services/SqliteGameService.cs index ba1d367..adba441 100644 --- a/Services/SqliteGameService.cs +++ b/Services/SqliteGameService.cs @@ -145,6 +145,15 @@ public class SqliteGameService : IGameService 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 SwitchVisitors(string id) { var game = await GetGameById(id); diff --git a/dev/populate-db/populate.py b/dev/populate-db/populate.py index 42ceb18..90ae18a 100755 --- a/dev/populate-db/populate.py +++ b/dev/populate-db/populate.py @@ -1,33 +1,11 @@ #!/usr/bin/env python3 import argparse import os -import sqlite3 import sys -from pathlib import Path 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): url = f"{api_root}{path}" resp = requests.get(url, timeout=30) @@ -37,17 +15,17 @@ def fetch_json(api_root, path): def main(): 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( - "--url", - default=os.environ.get("PHENIX_URL", "https://ballephenix.com"), + "--source", + default=os.environ.get("PHENIX_SOURCE_URL", "https://ballephenix.com"), help="Source API root (default: https://ballephenix.com)", ) parser.add_argument( - "--db", - default=None, - help="Path to SQLite DB (default: /phenix.db)", + "--target", + default=os.environ.get("PHENIX_TARGET_URL", "http://localhost:5000"), + help="Target API root (default: http://localhost:5000)", ) parser.add_argument( "--force", @@ -56,41 +34,23 @@ def main(): ) args = parser.parse_args() - api_root = args.url.rstrip("/") + source_root = args.source.rstrip("/") + target_root = args.target.rstrip("/") - project_root = find_project_root() - db_path = Path(args.db) if args.db else project_root / "phenix.db" - - years = fetch_json(api_root, "/api/available-years.json") + years = fetch_json(source_root, "/api/available-years.json") print(f"Years on source: {len(years)} ({min(years)}–{max(years)})") all_games = [] 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) 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: - 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") inserted = 0 @@ -98,46 +58,32 @@ def main(): errors = 0 for g in all_games: - try: - home_score = int(g["homeScore"]) if g.get("homeScore") is not None else None - away_score = int(g["awayScore"]) if g.get("awayScore") is not None else None - except (ValueError, TypeError): - home_score = None - away_score = None + payload = { + "date": g.get("date", ""), + "time": g.get("time", ""), + "home": g.get("home", ""), + "away": g.get("away", ""), + "homeScore": g.get("homeScore"), + "awayScore": g.get("awayScore"), + "practice": bool(g.get("practice")), + "rain": bool(g.get("rain")), + } try: - cursor.execute( - """ - 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: + resp = requests.post(f"{target_root}/api/games", json=payload, timeout=30) + if resp.status_code == 201: inserted += 1 - else: + elif resp.status_code == 409: skipped += 1 - except sqlite3.Error as e: - print(f"DB error on {g.get('date')} {g.get('time')}: {e}", file=sys.stderr) + else: + 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 - conn.commit() - conn.close() - print(f"Inserted: {inserted}, skipped: {skipped}, errors: {errors}") - print(f"Database: {db_path}") + print(f"Target: {target_root}") if __name__ == "__main__":