From 93fcb35bb0cba1bfd312c91d3949683b916f3b44 Mon Sep 17 00:00:00 2001 From: Abdessamad ANSSEM Date: Thu, 12 Dec 2024 00:56:43 +0100 Subject: [PATCH] manager implementation --- client/client.go | 2 + endpoints/bootstrap.go | 27 +++++++ endpoints/fixtures.go | 5 -- endpoints/manager.go | 153 ++++++++++++++++++++++++++++++++++++++ endpoints/manager_test.go | 121 ++++++++++++++++++++++++++++++ endpoints/players.go | 8 -- endpoints/teams_test.go | 10 ++- models/manager.go | 35 +++++++++ models/manager_history.go | 40 ++++++++++ models/manager_team.go | 67 +++++++++++++++++ 10 files changed, 451 insertions(+), 17 deletions(-) create mode 100644 endpoints/manager.go create mode 100644 endpoints/manager_test.go create mode 100644 models/manager.go create mode 100644 models/manager_history.go create mode 100644 models/manager_team.go diff --git a/client/client.go b/client/client.go index dbe376d..2ff6688 100644 --- a/client/client.go +++ b/client/client.go @@ -25,6 +25,7 @@ type Client struct { Players *endpoints.PlayerService Fixtures *endpoints.FixtureService Teams *endpoints.TeamService + Managers *endpoints.ManagerService } func NewClient(opts ...Option) *Client { @@ -54,6 +55,7 @@ func NewClient(opts ...Option) *Client { // services dependant on bootstrap: c.Players = endpoints.NewPlayerService(c, c.Bootstrap) c.Teams = endpoints.NewTeamService(c, c.Bootstrap) + c.Managers = endpoints.NewManagerService(c, c.Bootstrap) // standalone services c.Fixtures = endpoints.NewFixtureService(c) diff --git a/endpoints/bootstrap.go b/endpoints/bootstrap.go index 83ab4be..ba5915f 100644 --- a/endpoints/bootstrap.go +++ b/endpoints/bootstrap.go @@ -21,8 +21,13 @@ var ( fixturesCacheTTL = 10 * time.Minute gameweeksCacheTTL = 3 * time.Minute // Gameweeks status might change more often settingsCacheTTL = 24 * time.Hour // Game settings rarely change + managerCacheTTL = 5 * time.Minute // Managers data updates frequently ) +func init() { + sharedCache.StartCleanupTask(5 * time.Minute) +} + type Response struct { Teams []models.Team `json:"teams"` Elements []models.Player `json:"elements"` @@ -91,6 +96,28 @@ func (bs *BootstrapService) GetGameWeeks() ([]models.GameWeek, error) { return data.Events, nil } +func (bs *BootstrapService) GetCurrentGameWeek() (int, error) { + const cacheKey = "current_gameweek" + if cached, found := sharedCache.Get(cacheKey); found { + if gw, ok := cached.(int); ok { + return gw, nil + } + } + gameweeks, err := bs.GetGameWeeks() + if err != nil { + return 0, fmt.Errorf("failed to get gameweeks: %w", err) + } + + for _, gw := range gameweeks { + if gw.IsCurrent { + sharedCache.Set(cacheKey, gw.ID, gameweeksCacheTTL) + return gw.ID, nil + } + } + + return 0, fmt.Errorf("failed to find current gameweek") +} + func (bs *BootstrapService) GetSettings() (*models.GameSettings, error) { const cacheKey = "settings" if cached, found := sharedCache.Get(cacheKey); found { diff --git a/endpoints/fixtures.go b/endpoints/fixtures.go index 4ac4499..039d413 100644 --- a/endpoints/fixtures.go +++ b/endpoints/fixtures.go @@ -3,7 +3,6 @@ package endpoints import ( "encoding/json" "fmt" - "time" "github.com/AbdoAnss/go-fantasy-pl/api" "github.com/AbdoAnss/go-fantasy-pl/models" @@ -31,10 +30,6 @@ func NewFixtureService(client api.Client) *FixtureService { } } -func init() { - sharedCache.StartCleanupTask(5 * time.Minute) -} - func (fs *FixtureService) GetAllFixtures() ([]models.Fixture, error) { if cached, found := sharedCache.Get("fixtures"); found { if fixtures, ok := cached.([]models.Fixture); ok { diff --git a/endpoints/manager.go b/endpoints/manager.go new file mode 100644 index 0000000..71ac829 --- /dev/null +++ b/endpoints/manager.go @@ -0,0 +1,153 @@ +// Package endpoints provides access to the Fantasy Premier League API +package endpoints + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/AbdoAnss/go-fantasy-pl/api" + "github.com/AbdoAnss/go-fantasy-pl/models" +) + +const ( + managerDetailsEndpoint = "/entry/%d/" + managerHistoryEndpoint = "/entry/%d/history" + managerGameWeekPicksEndpoint = "/entry/%d/event/%d/picks/" +) + +type ManagerService struct { + client api.Client + bootstrapService *BootstrapService +} + +func NewManagerService(client api.Client, bootstrap *BootstrapService) *ManagerService { + return &ManagerService{ + client: client, + bootstrapService: bootstrap, + } +} + +func (ms *ManagerService) validateManager(manager *models.Manager) error { + if manager == nil { + return fmt.Errorf("received nil manager data") + } + if manager.ID == nil { + return fmt.Errorf("manager ID is missing") + } + return nil +} + +func (ms *ManagerService) GetManager(id int) (*models.Manager, error) { + cacheKey := fmt.Sprintf("manager_%d", id) + if cached, found := sharedCache.Get(cacheKey); found { + if manager, ok := cached.(*models.Manager); ok { + return manager, nil + } + } + + endpoint := fmt.Sprintf(managerDetailsEndpoint, id) + resp, err := ms.client.Get(endpoint) + if err != nil { + return nil, fmt.Errorf("failed to get manager data: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotFound: + return nil, fmt.Errorf("manager with ID %d not found", id) + default: + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var manager models.Manager + if err := json.Unmarshal(body, &manager); err != nil { + return nil, fmt.Errorf("failed to decode manager data: %w", err) + } + + if err := ms.validateManager(&manager); err != nil { + return nil, err + } + + sharedCache.Set(cacheKey, &manager, managerCacheTTL) + + return &manager, nil +} + +func (ms *ManagerService) GetCurrentTeam(managerID int) (*models.ManagerTeam, error) { + cacheKey := fmt.Sprintf("manager_team_%d", managerID) + if cached, found := sharedCache.Get(cacheKey); found { + if team, ok := cached.(*models.ManagerTeam); ok { + return team, nil + } + } + + currentGameWeekID, err := ms.bootstrapService.GetCurrentGameWeek() + if err != nil { + return nil, fmt.Errorf("failed to get current game week: %w", err) + } + + endpoint := fmt.Sprintf(managerGameWeekPicksEndpoint, managerID, currentGameWeekID) + resp, err := ms.client.Get(endpoint) + if err != nil { + return nil, fmt.Errorf("failed to get manager team: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get manager team: status %d", resp.StatusCode) + } + + var team models.ManagerTeam + if err := json.NewDecoder(resp.Body).Decode(&team); err != nil { + return nil, fmt.Errorf("failed to decode manager team: %w", err) + } + + sharedCache.Set(cacheKey, &team, managerCacheTTL) + return &team, nil +} + +func (ms *ManagerService) GetManagerHistory(id int) (*models.ManagerHistory, error) { + cacheKey := fmt.Sprintf("manager_history_%d", id) + if cached, found := sharedCache.Get(cacheKey); found { + if ManagerHistory, ok := cached.(*models.ManagerHistory); ok { + return ManagerHistory, nil + } + } + + endpoint := fmt.Sprintf(managerHistoryEndpoint, id) + resp, err := ms.client.Get(endpoint) + if err != nil { + return nil, fmt.Errorf("failed to get manager history data: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotFound: + return nil, fmt.Errorf("manager with ID %d not found", id) + default: + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var managerHistory models.ManagerHistory + if err := json.Unmarshal(body, &managerHistory); err != nil { + return nil, fmt.Errorf("failed to decode manager data: %w", err) + } + + sharedCache.Set(cacheKey, &managerHistory, managerCacheTTL) + + return &managerHistory, nil +} diff --git a/endpoints/manager_test.go b/endpoints/manager_test.go new file mode 100644 index 0000000..cc8c1d4 --- /dev/null +++ b/endpoints/manager_test.go @@ -0,0 +1,121 @@ +package endpoints_test + +import ( + "testing" + + "github.com/AbdoAnss/go-fantasy-pl/client" + "github.com/stretchr/testify/assert" +) + +var testManagerClient *client.Client + +func init() { + testManagerClient = client.NewClient() +} + +func TestManagerEndpoints(t *testing.T) { + t.Run("GetManager", func(t *testing.T) { + // Using a known manager ID (you can replace with a valid one) + managerID := 1387812 + + manager, err := testManagerClient.Managers.GetManager(managerID) + assert.NoError(t, err, "expected no error when getting manager") + assert.NotNil(t, manager, "expected manager to be returned") + + // Log manager details + t.Logf("Manager Details:") + t.Logf("ID: %d", *manager.ID) + t.Logf("Name: %s", manager.GetFullName()) + t.Logf("Team Name: %s", manager.Name) + t.Logf("Overall Points: %d", manager.SummaryOverallPoints) + t.Logf("Overall Rank: %d", manager.SummaryOverallRank) + }) + + t.Run("GetNonExistentManager", func(t *testing.T) { + manager, err := testManagerClient.Managers.GetManager(99999999) + assert.Error(t, err, "expected error when getting non-existent manager") + assert.Nil(t, manager, "expected nil manager for non-existent ID") + assert.Contains(t, err.Error(), "not found", "expected 'not found' error message") + }) + + t.Run("GetCurrentTeam", func(t *testing.T) { + // Using same manager ID + managerID := 1387812 + + team, err := testManagerClient.Managers.GetCurrentTeam(managerID) + assert.NoError(t, err, "expected no error when getting current team") + assert.NotNil(t, team, "expected team to be returned") + + // Log team details + t.Logf("Current Team Details:") + t.Logf("Number of Picks: %d", len(team.Picks)) + + // Log starting XI + t.Log("Starting XI:") + for _, pick := range team.GetStartingXI() { + t.Logf("Position %d: Player ID %d (Captain: %v)", + pick.Position, + pick.Element, + pick.IsCaptain) + } + + // Log bench + t.Log("Bench:") + for _, pick := range team.GetBench() { + t.Logf("Position %d: Player ID %d", + pick.Position, + pick.Element) + } + + t.Logf("Team Value: £%.1fm", team.GetTeamValueInMillions()) + t.Logf("Bank: £%.1fm", team.GetBankValueInMillions()) + }) + + t.Run("GetManagerHistory", func(t *testing.T) { + // Using same manager ID + managerID := 1387812 + + history, err := testManagerClient.Managers.GetManagerHistory(managerID) + assert.NoError(t, err, "expected no error when getting manager history") + assert.NotNil(t, history, "expected history to be returned") + + // Log history details + t.Logf("Manager History Details:") + + if len(history.Current) > 0 { + t.Log("Current Season Performance:") + for _, gw := range history.Current[:3] { // Show first 3 gameweeks + t.Logf("GW%d: Points: %d, Overall Rank: %d", + gw.Event, + gw.Points, + gw.OverallRank) + } + } + + if len(history.Past) > 0 { + t.Log("Past Seasons:") + for _, season := range history.Past { + t.Logf("Season %s: Points: %d, Overall Rank: %d", + season.SeasonName, + season.TotalPoints, + season.Rank) + } + } + }) + + t.Run("CacheConsistency", func(t *testing.T) { + managerID := 1387812 + + // First call + manager1, err := testManagerClient.Managers.GetManager(managerID) + assert.NoError(t, err) + + // Second call (should be from cache) + manager2, err := testManagerClient.Managers.GetManager(managerID) + assert.NoError(t, err) + + // Compare results + assert.Equal(t, manager1.ID, manager2.ID, "cached manager should match original") + assert.Equal(t, manager1.Name, manager2.Name, "cached manager name should match original") + }) +} diff --git a/endpoints/players.go b/endpoints/players.go index bc87848..d4d23bd 100644 --- a/endpoints/players.go +++ b/endpoints/players.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "time" "github.com/AbdoAnss/go-fantasy-pl/api" "github.com/AbdoAnss/go-fantasy-pl/models" @@ -27,16 +26,10 @@ func NewPlayerService(client api.Client, bootstrap *BootstrapService) *PlayerSer } } -func init() { - sharedCache.StartCleanupTask(5 * time.Minute) -} - -// GetAllPlayers now uses the bootstrap service to fetch players func (ps *PlayerService) GetAllPlayers() ([]models.Player, error) { return ps.bootstrapService.GetPlayers() } -// GetPlayer finds a specific player by ID func (ps *PlayerService) GetPlayer(id int) (*models.Player, error) { players, err := ps.GetAllPlayers() if err != nil { @@ -51,7 +44,6 @@ func (ps *PlayerService) GetPlayer(id int) (*models.Player, error) { return nil, fmt.Errorf("player with ID %d not found", id) } -// GetPlayerHistory fetches detailed history for a specific player func (ps *PlayerService) GetPlayerHistory(id int) (*models.PlayerHistory, error) { cacheKey := fmt.Sprintf("player_history_%d", id) if cached, found := sharedCache.Get(cacheKey); found { diff --git a/endpoints/teams_test.go b/endpoints/teams_test.go index 17b26f4..9beae17 100644 --- a/endpoints/teams_test.go +++ b/endpoints/teams_test.go @@ -7,14 +7,16 @@ import ( "github.com/stretchr/testify/assert" ) -var teamID = 13 // Example team ID +var teamID = 13 // Example team ID: MAN CITY + +var testTeamClient *client.Client func init() { - testClient = client.NewClient() + testTeamClient = client.NewClient() } func TestGetAllTeams(t *testing.T) { - teams, err := testClient.Teams.GetAllTeams() + teams, err := testTeamClient.Teams.GetAllTeams() assert.NoError(t, err, "expected no error when getting all teams") assert.NotEmpty(t, teams, "expected teams to be returned from API") @@ -38,7 +40,7 @@ func TestGetAllTeams(t *testing.T) { } func TestGetTeam(t *testing.T) { - team, err := testClient.Teams.GetTeam(teamID) + team, err := testTeamClient.Teams.GetTeam(teamID) assert.NoError(t, err, "expected no error when getting team") assert.NotNil(t, team, "expected team to be returned, got nil") diff --git a/models/manager.go b/models/manager.go new file mode 100644 index 0000000..6f9cc60 --- /dev/null +++ b/models/manager.go @@ -0,0 +1,35 @@ +package models + +import "time" + +// TODO: Implement Manager leagues + +type Manager struct { + ID *int `json:"id"` + JoinedTime time.Time `json:"joined_time"` + StartedEvent int `json:"started_event"` + FavouriteTeam int `json:"favourite_team"` + PlayerFirstName string `json:"player_first_name"` + PlayerLastName string `json:"player_last_name"` + PlayerRegionID int `json:"player_region_id"` + PlayerRegionName string `json:"player_region_name"` + PlayerRegionISOCodesShort string `json:"player_region_iso_code_short"` + PlayerRegionISOCodesLong string `json:"player_region_iso_code_long"` + YearsActive int `json:"years_active"` + SummaryOverallPoints int `json:"summary_overall_points"` + SummaryOverallRank int `json:"summary_overall_rank"` + SummaryEventPoints int `json:"summary_event_points"` + SummaryEventRank int `json:"summary_event_rank"` + CurrentEvent *int `json:"current_event"` + Name string `json:"name"` + NameChangeBlocked bool `json:"name_change_blocked"` + EnteredEvents []int `json:"entered_events"` + Kit *string `json:"kit"` + LastDeadlineBank int `json:"last_deadline_bank"` + LastDeadlineValue int `json:"last_deadline_value"` + LastDeadlineTotalTransfers int `json:"last_deadline_total_transfers"` +} + +func (m *Manager) GetFullName() string { + return m.PlayerFirstName + " " + m.PlayerLastName +} diff --git a/models/manager_history.go b/models/manager_history.go new file mode 100644 index 0000000..12f8746 --- /dev/null +++ b/models/manager_history.go @@ -0,0 +1,40 @@ +package models + +import "time" + +// ManagerHistory represents the history of a manager's performance. +type ManagerHistory struct { + Current []CurrentEvents `json:"current"` // Current gameweek events + Past []PastSeason `json:"past"` // Past seasons + Chips []Chip `json:"chips"` // Chips used +} + +// CurrentEvent represents the details of this year's gameweek events. +type CurrentEvents struct { + Event int `json:"event"` // Gameweek ID + Points int `json:"points"` // Points scored in the gameweek + TotalPoints int `json:"total_points"` // Total points accumulated + Rank int `json:"rank"` // Current rank + RankSort int `json:"rank_sort"` // Rank sort + OverallRank int `json:"overall_rank"` // Overall rank + PercentileRank int `json:"percentile_rank"` // Percentile rank + Bank int `json:"bank"` // Money in bank + Value int `json:"value"` // Team value + EventTransfers int `json:"event_transfers"` // Transfers made in the gameweek + EventTransfersCost int `json:"event_transfers_cost"` // Cost of transfers + PointsOnBench int `json:"points_on_bench"` // Points scored by bench players +} + +// PastSeason represents the details of a past season. +type PastSeason struct { + SeasonName string `json:"season_name"` // Name of the season (e.g., "2020/21") + TotalPoints int `json:"total_points"` // Total points scored in the season + Rank int `json:"rank"` // Rank achieved in the season +} + +// Chip represents the details of a chip used by the manager. +type Chip struct { + Name string `json:"name"` // Name of the chip (e.g., "wildcard") + Time time.Time `json:"time"` // Time when the chip was used + Event int `json:"event"` // Gameweek ID when the chip was used +} diff --git a/models/manager_team.go b/models/manager_team.go new file mode 100644 index 0000000..321b9ff --- /dev/null +++ b/models/manager_team.go @@ -0,0 +1,67 @@ +package models + +type ManagerTeam struct { + ActiveChip *string `json:"active_chip"` // Active chip (e.g., "wildcard", "benchboost") + AutomaticSubs []AutomaticSub `json:"automatic_subs"` // List of automatic substitutions + EntryHistory EntryHistory `json:"entry_history"` // Entry history details + Picks []Pick `json:"picks"` // Picks in the team +} + +type AutomaticSub struct { + Entry int `json:"entry"` // Entry ID of the player + ElementIn int `json:"element_in"` // Player ID coming in + ElementOut int `json:"element_out"` // Player ID going out + Event int `json:"event"` // Gameweek ID +} + +type EntryHistory struct { + Event int `json:"event"` // Gameweek ID + Points int `json:"points"` // Points scored + TotalPoints int `json:"total_points"` // Total points + Rank int `json:"rank"` // Current rank + RankSort int `json:"rank_sort"` // Rank sort + OverallRank int `json:"overall_rank"` // Overall rank + PercentileRank int `json:"percentile_rank"` // Percentile rank + Bank int `json:"bank"` // Money in bank + Value int `json:"value"` // Team value + EventTransfers int `json:"event_transfers"` // Transfers made this week + EventTransfersCost int `json:"event_transfers_cost"` // Cost of transfers + PointsOnBench int `json:"points_on_bench"` // Points scored by bench players +} + +type Pick struct { + Element int `json:"element"` // Player ID + Position int `json:"position"` // Position in team (1-15) + Multiplier int `json:"multiplier"` // 2 for captain, 3 for triple captain, 0 for benched + IsCaptain bool `json:"is_captain"` // Is this player captain? + IsViceCaptain bool `json:"is_vice_captain"` // Is this player vice-captain? + ElementType int `json:"element_type"` // Type of the player (e.g., defender, midfielder) +} + +func (mt *ManagerTeam) GetStartingXI() []Pick { + starters := make([]Pick, 0, 11) + for _, pick := range mt.Picks { + if pick.Position <= 11 { + starters = append(starters, pick) + } + } + return starters +} + +func (mt *ManagerTeam) GetBench() []Pick { + bench := make([]Pick, 0, 4) + for _, pick := range mt.Picks { + if pick.Position > 11 { + bench = append(bench, pick) + } + } + return bench +} + +func (mt *ManagerTeam) GetTeamValueInMillions() float64 { + return float64(mt.EntryHistory.Value) / 10 +} + +func (mt *ManagerTeam) GetBankValueInMillions() float64 { + return float64(mt.EntryHistory.Bank) / 10 +}