From 2f258be4842bbefc99938dcca0b1faa234acfcbf Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Tue, 19 Jan 2021 18:38:40 -0800 Subject: [PATCH 1/3] Add games/stats endpoint --- .../Controllers/GameStatsController.cs | 86 +++++++++++++++++++ SIBR.Storage.API/Models/ApiGameStats.cs | 15 ++++ SIBR.Storage.Data/Models/Read/GameView.cs | 1 + SIBR.Storage.Data/Schema/r__games_view.sql | 1 + SIBR.Storage.Data/Stores/GameStore.cs | 2 + 5 files changed, 105 insertions(+) create mode 100644 SIBR.Storage.API/Controllers/GameStatsController.cs create mode 100644 SIBR.Storage.API/Models/ApiGameStats.cs diff --git a/SIBR.Storage.API/Controllers/GameStatsController.cs b/SIBR.Storage.API/Controllers/GameStatsController.cs new file mode 100644 index 0000000..2579298 --- /dev/null +++ b/SIBR.Storage.API/Controllers/GameStatsController.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CsvHelper; +using Microsoft.AspNetCore.Mvc; +using NodaTime; +using Serilog; +using SIBR.Storage.API.Models; +using SIBR.Storage.API.Utils; +using SIBR.Storage.Data; +using SIBR.Storage.Data.Models; +using SIBR.Storage.Data.Query; + +namespace SIBR.Storage.API.Controllers +{ + [ApiController] + [ApiVersion("1.0")] + [Route("v{version:apiVersion}")] + public class GameStatsController : ControllerBase + { + private readonly GameStore _gameStore; + private readonly UpdateStore _store; + + public GameStatsController(GameStore gameStore, UpdateStore store) + { + _gameStore = gameStore; + _store = store; + } + + [Route("games/stats")] + public async Task GetGameStats([FromQuery] GameStatsOptions opts) + { + var game = await _gameStore.GetGames(new GameStore.GameQueryOptions { GameId = opts.Game, Count = 1 }).FirstAsync(); + var cutoff = CutoffTime(game.EndTime); + + var gameStats = await GetVersion(UpdateType.GameStatsheet, new [] { game.Statsheet }, cutoff).FirstAsync(); + + var teamSheets = new [] { gameStats.Data.GetProperty("awayTeamStats").GetGuid(), gameStats.Data.GetProperty("homeTeamStats").GetGuid() }; + var teamStats = await GetVersion(UpdateType.TeamStatsheet, teamSheets, cutoff).ToListAsync(); + + var playerSheets = teamStats.SelectMany(sheet => sheet.Data.GetProperty("playerStats").EnumerateArray().Select(el => el.GetGuid())); + var playerStats = await GetVersion(UpdateType.PlayerStatsheet, playerSheets.ToArray(), cutoff).ToListAsync(); + + return Ok(new ApiResponse() { Data = new [] + { + new ApiGameStats + { + GameId = game.GameId, + GameStats = gameStats.Data, + TeamStats = teamStats.Select(v => v.Data).ToArray(), + PlayerStats = playerStats.Select(v => v.Data).ToArray(), + } + }}); + } + + private Instant? CutoffTime(Instant? time) + { + if (time is null) + return null; + + // We want the last recorded statsheet before the next game begins, so take the end time and round up to the next hour. + var dt = (time ?? Instant.MaxValue).InUtc(); + var cutoff = dt.Date + TimeAdjusters.TruncateToHour(dt.TimeOfDay); + return cutoff.InUtc().ToInstant() + Duration.FromHours(1); + } + + private IAsyncEnumerable GetVersion(UpdateType type, Guid[] ids, Instant? before) + { + return _store.ExportAllUpdatesRaw(type, new UpdateStore.EntityVersionQuery + { + Ids = ids, + Before = before, + Order = SortOrder.Desc, + Count = ids.Length, + }); + } + + public class GameStatsOptions + { + public Guid Game { get; set; } + } + } +} diff --git a/SIBR.Storage.API/Models/ApiGameStats.cs b/SIBR.Storage.API/Models/ApiGameStats.cs new file mode 100644 index 0000000..8b1e2e1 --- /dev/null +++ b/SIBR.Storage.API/Models/ApiGameStats.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json; +using NodaTime; +using SIBR.Storage.Data.Models; + +namespace SIBR.Storage.API.Models +{ + public class ApiGameStats + { + public Guid GameId { get; set; } + public JsonElement GameStats { get; set; } + public JsonElement[] TeamStats { get; set; } + public JsonElement[] PlayerStats { get; set; } + } +} diff --git a/SIBR.Storage.Data/Models/Read/GameView.cs b/SIBR.Storage.Data/Models/Read/GameView.cs index 8007a17..dba92bf 100644 --- a/SIBR.Storage.Data/Models/Read/GameView.cs +++ b/SIBR.Storage.Data/Models/Read/GameView.cs @@ -7,6 +7,7 @@ namespace SIBR.Storage.Data.Models public class GameView: IGameData { public Guid GameId { get; set; } + public Guid Statsheet { get; set; } public Instant? StartTime { get; set; } public Instant? EndTime { get; set; } public JsonElement Data { get; set; } diff --git a/SIBR.Storage.Data/Schema/r__games_view.sql b/SIBR.Storage.Data/Schema/r__games_view.sql index a2652c8..d436158 100644 --- a/SIBR.Storage.Data/Schema/r__games_view.sql +++ b/SIBR.Storage.Data/Schema/r__games_view.sql @@ -18,6 +18,7 @@ create view games_view as (jsonb_array_length(data->'outcomes') > 0) as has_outcomes, (data->>'gameStart')::bool as has_started, (data->>'gameComplete')::bool as has_finished, + (data->>'statsheet')::uuid as statsheet, (data->>'homeTeam')::uuid as home_team, (data->>'awayTeam')::uuid as away_team, (data->>'homePitcher')::uuid as home_pitcher, diff --git a/SIBR.Storage.Data/Stores/GameStore.cs b/SIBR.Storage.Data/Stores/GameStore.cs index 580b290..52eea7f 100644 --- a/SIBR.Storage.Data/Stores/GameStore.cs +++ b/SIBR.Storage.Data/Stores/GameStore.cs @@ -25,6 +25,7 @@ public IAsyncEnumerable GetGames(GameQueryOptions opts) else q.OrderBy("season", "tournament", "day"); + if (opts.GameId != null) q.Where("game_id", opts.GameId.Value); if (opts.Season != null) q.Where("season", opts.Season.Value); if (opts.Tournament != null) q.Where("tournament", opts.Tournament.Value); if (opts.Day != null) q.Where("day", opts.Day.Value); @@ -43,6 +44,7 @@ public IAsyncEnumerable GetGames(GameQueryOptions opts) public class GameQueryOptions { + public Guid? GameId; public int? Tournament; public int? Season; public int? Day; From a71c4319d6595f748210375837edbc2dc9bb53ca Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Tue, 19 Jan 2021 19:29:52 -0800 Subject: [PATCH 2/3] Fix exception if statsheets not available --- SIBR.Storage.API/Controllers/GameStatsController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SIBR.Storage.API/Controllers/GameStatsController.cs b/SIBR.Storage.API/Controllers/GameStatsController.cs index 2579298..84bcda3 100644 --- a/SIBR.Storage.API/Controllers/GameStatsController.cs +++ b/SIBR.Storage.API/Controllers/GameStatsController.cs @@ -36,7 +36,9 @@ public async Task GetGameStats([FromQuery] GameStatsOptions opts) var game = await _gameStore.GetGames(new GameStore.GameQueryOptions { GameId = opts.Game, Count = 1 }).FirstAsync(); var cutoff = CutoffTime(game.EndTime); - var gameStats = await GetVersion(UpdateType.GameStatsheet, new [] { game.Statsheet }, cutoff).FirstAsync(); + var gameStats = await GetVersion(UpdateType.GameStatsheet, new [] { game.Statsheet }, cutoff).FirstOrDefaultAsync(); + if (gameStats is null) + return Ok(new ApiResponse() { Data = new ApiGameStats[] {} }); var teamSheets = new [] { gameStats.Data.GetProperty("awayTeamStats").GetGuid(), gameStats.Data.GetProperty("homeTeamStats").GetGuid() }; var teamStats = await GetVersion(UpdateType.TeamStatsheet, teamSheets, cutoff).ToListAsync(); @@ -44,7 +46,7 @@ public async Task GetGameStats([FromQuery] GameStatsOptions opts) var playerSheets = teamStats.SelectMany(sheet => sheet.Data.GetProperty("playerStats").EnumerateArray().Select(el => el.GetGuid())); var playerStats = await GetVersion(UpdateType.PlayerStatsheet, playerSheets.ToArray(), cutoff).ToListAsync(); - return Ok(new ApiResponse() { Data = new [] + return Ok(new ApiResponse() { Data = new ApiGameStats[] { new ApiGameStats { From d89bb8126320041d533ffe457ab90a0c49014bea Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Tue, 19 Jan 2021 19:30:04 -0800 Subject: [PATCH 3/3] Add timestamp to games/stats output --- SIBR.Storage.API/Controllers/GameStatsController.cs | 1 + SIBR.Storage.API/Models/ApiGameStats.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/SIBR.Storage.API/Controllers/GameStatsController.cs b/SIBR.Storage.API/Controllers/GameStatsController.cs index 84bcda3..951ecbe 100644 --- a/SIBR.Storage.API/Controllers/GameStatsController.cs +++ b/SIBR.Storage.API/Controllers/GameStatsController.cs @@ -51,6 +51,7 @@ public async Task GetGameStats([FromQuery] GameStatsOptions opts) new ApiGameStats { GameId = game.GameId, + Timestamp = gameStats.Timestamp, GameStats = gameStats.Data, TeamStats = teamStats.Select(v => v.Data).ToArray(), PlayerStats = playerStats.Select(v => v.Data).ToArray(), diff --git a/SIBR.Storage.API/Models/ApiGameStats.cs b/SIBR.Storage.API/Models/ApiGameStats.cs index 8b1e2e1..e689a27 100644 --- a/SIBR.Storage.API/Models/ApiGameStats.cs +++ b/SIBR.Storage.API/Models/ApiGameStats.cs @@ -8,6 +8,7 @@ namespace SIBR.Storage.API.Models public class ApiGameStats { public Guid GameId { get; set; } + public Instant Timestamp { get; set; } public JsonElement GameStats { get; set; } public JsonElement[] TeamStats { get; set; } public JsonElement[] PlayerStats { get; set; }