From 30dcac1415784cb6d8d42c2e6df0310a980b6a9b Mon Sep 17 00:00:00 2001 From: cappy-dev Date: Sat, 29 Aug 2026 07:34:57 +0000 Subject: [PATCH 1/2] feat: add getTeamEvents for a team's full history (issue #9) Reuses the /team_events endpoint with a team filter instead of event, optionally narrowed by year. Returns every team-event row for the team, sorted by year then event key, and answers with an empty list on 404 (unknown team) like the other list endpoints. Adds tests covering parsing, the year/event sort, the optional year filter, and the empty-list-on-404 path. Bumps version to 0.3.2. --- CHANGELOG.md | 9 +++ README.md | 3 + lib/src/statbotics_client.dart | 40 ++++++++++++ pubspec.yaml | 2 +- test/statbotics_client_test.dart | 104 +++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9c284..96bbb7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.3.2 + +- Added `getTeamEvents(team, {year})` for a team's full Statbotics history + across events (issue #9). It reuses the `/team_events` endpoint with a `team` + filter instead of `event`, and optionally a `year`, returning every + team-event row for that team sorted by year then event key. List endpoints + return an empty list on 404, so an unknown team or a team with no recorded + events answers with an empty list rather than throwing. + ## 0.3.1 - `StatboticsTeamEvent.toJson` now serializes `team_name`, so the diff --git a/README.md b/README.md index 0e5545b..72d8fc4 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,13 @@ call `close()` when you are done so the underlying HTTP client is released. | `getEvent(eventKey)` | `GET /event/{eventKey}` | `StatboticsEvent?` | | `getEvents(year)` | `GET /events?year={year}` | `List` | | `getEventTeams(eventKey)` | `GET /team_events?event={eventKey}` | `List` | +| `getTeamEvents(team, {year})` | `GET /team_events?team={team}[&year={year}]` | `List` | | `getEventTeamsBasic(eventKey)` | `GET /teams?event={eventKey}` | `List` | | `getEventMatches(eventKey)` | `GET /matches?event={eventKey}` | `List` | - `getEventTeams` sorts results by rank ascending. +- `getTeamEvents` sorts results by year ascending, then event key, so a team's + history reads chronologically. Pass `year` to narrow to one season. - `getEvents` sorts results by week then name. - `getEventMatches` sorts results by comp level (`qm`, `ef`, `qf`, `sf`, `f`) then match number. diff --git a/lib/src/statbotics_client.dart b/lib/src/statbotics_client.dart index 66498ab..a85e9b3 100644 --- a/lib/src/statbotics_client.dart +++ b/lib/src/statbotics_client.dart @@ -63,6 +63,46 @@ class StatboticsClient { return results; } + /// `GET /team_events?team={team}[&year={year}]&limit=1000` returns every + /// team-event record for the given team: the full Statbotics history of that + /// team across all events. Pass [year] to narrow the history to one season. + /// + /// `/team_events` is the same endpoint `getEventTeams` uses; this overload + /// just drops the `event` filter and adds `team` (and optionally `year`), + /// answering with every row that mentions the team instead of every row for + /// one event. Results are sorted by year ascending, then event key, so the + /// history reads chronologically. Returns an empty list on 404 (an unknown + /// team number) or if the team has no recorded events. + Future> getTeamEvents( + int team, { + int? year, + int limit = 1000, + }) async { + final queryParameters = { + 'team': team.toString(), + 'limit': limit.toString(), + }; + if (year != null) queryParameters['year'] = year.toString(); + final body = await _get( + '/team_events', + queryParameters: queryParameters, + ); + if (body == null) return const []; + final list = jsonDecode(body) as List; + final results = list + .map( + (json) => StatboticsTeamEvent.fromJson( + (json as Map).cast(), + ), + ) + .toList(growable: true); + results.sort((a, b) { + if (a.year != b.year) return a.year.compareTo(b.year); + return a.event.compareTo(b.event); + }); + return results; + } + /// `GET /v3/events?year={year}` — returns all events for the given year, /// sorted by week then name. Future> getEvents(int year) async { diff --git a/pubspec.yaml b/pubspec.yaml index 5247b49..7a523be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: statbotics_client description: A typed Dart client for the Statbotics API v3 (FRC EPA statistics, events, matches). Pure Dart, no Flutter dependency. -version: 0.3.1 +version: 0.3.2 repository: https://github.com/Project516/statbotics_client topics: diff --git a/test/statbotics_client_test.dart b/test/statbotics_client_test.dart index c91cdd2..f6aa876 100644 --- a/test/statbotics_client_test.dart +++ b/test/statbotics_client_test.dart @@ -553,5 +553,109 @@ void main() { expect(team.team, 1234); expect(team.nickname, 'Example'); }); + + test('getTeamEvents parses the history and sorts by year then event', + () async { + final mockClient = MockClient((request) async { + expect( + request.url.toString(), + 'https://api.statbotics.io/v3/team_events?team=254&limit=1000', + ); + expect(request.url.queryParameters['team'], '254'); + expect(request.url.queryParameters['limit'], '1000'); + return http.Response( + jsonEncode(>[ + { + 'team': 254, + 'event': '2024cafr', + 'event_name': 'Cal Games', + 'team_name': 'The Cheesy Poofs', + 'year': 2024, + 'wins': 9, + 'losses': 1, + 'ties': 0, + 'rank': 1, + 'num_teams': 40, + 'epa': { + 'total_points': {'mean': 55.0, 'sd': 2.0}, + }, + }, + { + 'team': 254, + 'event': '2023cafr', + 'event_name': 'Cal Games', + 'team_name': 'The Cheesy Poofs', + 'year': 2023, + 'wins': 8, + 'losses': 2, + 'ties': 0, + 'rank': 2, + 'num_teams': 40, + 'epa': { + 'total_points': {'mean': 50.0, 'sd': 2.5}, + }, + }, + { + 'team': 254, + 'event': '2024txaus', + 'event_name': 'Austin', + 'team_name': 'The Cheesy Poofs', + 'year': 2024, + 'wins': 7, + 'losses': 3, + 'ties': 0, + 'rank': 3, + 'num_teams': 38, + 'epa': { + 'total_points': {'mean': 52.0, 'sd': 2.2}, + }, + }, + ]), + 200, + headers: {'content-type': 'application/json'}, + ); + }); + + final client = StatboticsClient(httpClient: mockClient); + final history = await client.getTeamEvents(254); + + expect(history.length, 3); + // Sorted by year ascending, then event key: 2023 first, then the two + // 2024 events in key order (2024cafr before 2024txaus). + expect(history[0].year, 2023); + expect(history[0].event, '2023cafr'); + expect(history[0].teamName, 'The Cheesy Poofs'); + expect(history[1].year, 2024); + expect(history[1].event, '2024cafr'); + expect(history[2].year, 2024); + expect(history[2].event, '2024txaus'); + }); + + test('getTeamEvents forwards the optional year filter', () async { + final mockClient = MockClient((request) async { + expect( + request.url.toString(), + 'https://api.statbotics.io/v3/team_events?team=254&limit=1000&year=2024', + ); + expect(request.url.queryParameters['year'], '2024'); + return http.Response( + jsonEncode(>>[]), + 200, + headers: {'content-type': 'application/json'}, + ); + }); + + final client = StatboticsClient(httpClient: mockClient); + final history = await client.getTeamEvents(254, year: 2024); + expect(history, isEmpty); + }); + + test('getTeamEvents returns an empty list on 404 (unknown team)', () async { + final client = StatboticsClient( + httpClient: MockClient((_) async => http.Response('', 404)), + ); + final history = await client.getTeamEvents(999999); + expect(history, isEmpty); + }); }); } From 9badc10bb97b930a9d6748d35d042e1372870eec Mon Sep 17 00:00:00 2001 From: Project516 <138796702+Project516@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:12:53 -0500 Subject: [PATCH 2/2] fix: drop the public limit on getTeamEvents and sort newest first The other list methods fix their page size internally (100 for getEventTeams, 500 for getEvents). Exposing limit on getTeamEvents let a caller ask for a truncated history from a method documented as returning the whole thing, so the cap is now a fixed 1000 like the rest. Issue #9 asks for newest first; the sort was year ascending. It is now year descending, then event key ascending as a stable tiebreak within a season. --- CHANGELOG.md | 10 +++++----- README.md | 4 ++-- lib/src/statbotics_client.dart | 30 ++++++++++++++++-------------- test/statbotics_client_test.dart | 15 +++++++-------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96bbb7f..b96d000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,11 +3,11 @@ ## 0.3.2 - Added `getTeamEvents(team, {year})` for a team's full Statbotics history - across events (issue #9). It reuses the `/team_events` endpoint with a `team` - filter instead of `event`, and optionally a `year`, returning every - team-event row for that team sorted by year then event key. List endpoints - return an empty list on 404, so an unknown team or a team with no recorded - events answers with an empty list rather than throwing. + across events (part of issue #9). It reuses the `/team_events` endpoint with + a `team` filter instead of `event`, and optionally a `year`, returning every + team-event row for that team sorted newest season first, then by event key. + List endpoints return an empty list on 404, so an unknown team or a team + with no recorded events answers with an empty list rather than throwing. ## 0.3.1 diff --git a/README.md b/README.md index 72d8fc4..5ae4c0d 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ call `close()` when you are done so the underlying HTTP client is released. | `getEventMatches(eventKey)` | `GET /matches?event={eventKey}` | `List` | - `getEventTeams` sorts results by rank ascending. -- `getTeamEvents` sorts results by year ascending, then event key, so a team's - history reads chronologically. Pass `year` to narrow to one season. +- `getTeamEvents` sorts results newest season first, then by event key, so a + team's most recent results lead. Pass `year` to narrow to one season. - `getEvents` sorts results by week then name. - `getEventMatches` sorts results by comp level (`qm`, `ef`, `qf`, `sf`, `f`) then match number. diff --git a/lib/src/statbotics_client.dart b/lib/src/statbotics_client.dart index a85e9b3..1a1739d 100644 --- a/lib/src/statbotics_client.dart +++ b/lib/src/statbotics_client.dart @@ -63,24 +63,26 @@ class StatboticsClient { return results; } - /// `GET /team_events?team={team}[&year={year}]&limit=1000` returns every - /// team-event record for the given team: the full Statbotics history of that - /// team across all events. Pass [year] to narrow the history to one season. + /// `GET /v3/team_events?team={team}[&year={year}]&limit=1000` — returns + /// every team-event record for the given team: the full Statbotics history + /// of that team across all events, newest season first. Pass [year] to + /// narrow the history to one season. /// - /// `/team_events` is the same endpoint `getEventTeams` uses; this overload + /// `/team_events` is the same endpoint `getEventTeams` uses; this method /// just drops the `event` filter and adds `team` (and optionally `year`), /// answering with every row that mentions the team instead of every row for - /// one event. Results are sorted by year ascending, then event key, so the - /// history reads chronologically. Returns an empty list on 404 (an unknown - /// team number) or if the team has no recorded events. - Future> getTeamEvents( - int team, { - int? year, - int limit = 1000, - }) async { + /// one event. Results are sorted by year descending, then event key + /// ascending. Returns an empty list on 404 (an unknown team number) or if + /// the team has no recorded events. + /// + /// The 1000-row cap is fixed rather than a parameter, the way the other + /// list methods fix theirs: a team plays a handful of events a season, so + /// no real team comes close to it and every call answers with the whole + /// history. + Future> getTeamEvents(int team, {int? year}) async { final queryParameters = { 'team': team.toString(), - 'limit': limit.toString(), + 'limit': '1000', }; if (year != null) queryParameters['year'] = year.toString(); final body = await _get( @@ -97,7 +99,7 @@ class StatboticsClient { ) .toList(growable: true); results.sort((a, b) { - if (a.year != b.year) return a.year.compareTo(b.year); + if (a.year != b.year) return b.year.compareTo(a.year); return a.event.compareTo(b.event); }); return results; diff --git a/test/statbotics_client_test.dart b/test/statbotics_client_test.dart index f6aa876..7e2cb66 100644 --- a/test/statbotics_client_test.dart +++ b/test/statbotics_client_test.dart @@ -554,7 +554,7 @@ void main() { expect(team.nickname, 'Example'); }); - test('getTeamEvents parses the history and sorts by year then event', + test('getTeamEvents parses the history and sorts newest season first', () async { final mockClient = MockClient((request) async { expect( @@ -620,15 +620,14 @@ void main() { final history = await client.getTeamEvents(254); expect(history.length, 3); - // Sorted by year ascending, then event key: 2023 first, then the two - // 2024 events in key order (2024cafr before 2024txaus). - expect(history[0].year, 2023); - expect(history[0].event, '2023cafr'); + // Newest season first, then event key ascending within a season. + expect(history[0].year, 2024); + expect(history[0].event, '2024cafr'); expect(history[0].teamName, 'The Cheesy Poofs'); expect(history[1].year, 2024); - expect(history[1].event, '2024cafr'); - expect(history[2].year, 2024); - expect(history[2].event, '2024txaus'); + expect(history[1].event, '2024txaus'); + expect(history[2].year, 2023); + expect(history[2].event, '2023cafr'); }); test('getTeamEvents forwards the optional year filter', () async {