Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 0.3.2

- Added `getTeamEvents(team, {year})` for a team's full Statbotics history
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

- `StatboticsTeamEvent.toJson` now serializes `team_name`, so the
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<StatboticsEvent>` |
| `getEventTeams(eventKey)` | `GET /team_events?event={eventKey}` | `List<StatboticsTeamEvent>` |
| `getTeamEvents(team, {year})` | `GET /team_events?team={team}[&year={year}]` | `List<StatboticsTeamEvent>` |
| `getEventTeamsBasic(eventKey)` | `GET /teams?event={eventKey}` | `List<StatboticsTeamBasic>` |
| `getEventMatches(eventKey)` | `GET /matches?event={eventKey}` | `List<StatboticsMatch>` |

- `getEventTeams` sorts results by rank ascending.
- `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.
Expand Down
42 changes: 42 additions & 0 deletions lib/src/statbotics_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,48 @@ class StatboticsClient {
return results;
}

/// `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 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 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<List<StatboticsTeamEvent>> getTeamEvents(int team, {int? year}) async {
final queryParameters = <String, String>{
'team': team.toString(),
'limit': '1000',
};
if (year != null) queryParameters['year'] = year.toString();
final body = await _get(
'/team_events',
queryParameters: queryParameters,
);
if (body == null) return const <StatboticsTeamEvent>[];
final list = jsonDecode(body) as List<dynamic>;
final results = list
.map(
(json) => StatboticsTeamEvent.fromJson(
(json as Map).cast<String, dynamic>(),
),
)
.toList(growable: true);
results.sort((a, b) {
if (a.year != b.year) return b.year.compareTo(a.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<List<StatboticsEvent>> getEvents(int year) async {
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
103 changes: 103 additions & 0 deletions test/statbotics_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -553,5 +553,108 @@ void main() {
expect(team.team, 1234);
expect(team.nickname, 'Example');
});

test('getTeamEvents parses the history and sorts newest season first',
() 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(<Map<String, dynamic>>[
<String, dynamic>{
'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': <String, dynamic>{
'total_points': <String, dynamic>{'mean': 55.0, 'sd': 2.0},
},
},
<String, dynamic>{
'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': <String, dynamic>{
'total_points': <String, dynamic>{'mean': 50.0, 'sd': 2.5},
},
},
<String, dynamic>{
'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': <String, dynamic>{
'total_points': <String, dynamic>{'mean': 52.0, 'sd': 2.2},
},
},
]),
200,
headers: <String, String>{'content-type': 'application/json'},
);
});

final client = StatboticsClient(httpClient: mockClient);
final history = await client.getTeamEvents(254);

expect(history.length, 3);
// 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, '2024txaus');
expect(history[2].year, 2023);
expect(history[2].event, '2023cafr');
});

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(<List<Map<String, dynamic>>>[]),
200,
headers: <String, String>{'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);
});
});
}