cricinfo-cli-go 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +63 -0
- package/CONTRIBUTORS.md +75 -0
- package/LICENSE +21 -0
- package/Makefile +131 -0
- package/README.md +130 -0
- package/bin/cricinfo.js +44 -0
- package/cmd/cricinfo/main.go +15 -0
- package/go.mod +10 -0
- package/go.sum +10 -0
- package/internal/app/app.go +11 -0
- package/internal/app/app_test.go +122 -0
- package/internal/buildinfo/buildinfo.go +16 -0
- package/internal/cli/analysis.go +262 -0
- package/internal/cli/analysis_test.go +175 -0
- package/internal/cli/competitions.go +154 -0
- package/internal/cli/competitions_test.go +165 -0
- package/internal/cli/leagues.go +297 -0
- package/internal/cli/leagues_test.go +194 -0
- package/internal/cli/matches.go +403 -0
- package/internal/cli/matches_test.go +413 -0
- package/internal/cli/players.go +263 -0
- package/internal/cli/players_test.go +384 -0
- package/internal/cli/root.go +141 -0
- package/internal/cli/search.go +119 -0
- package/internal/cli/teams.go +214 -0
- package/internal/cli/teams_test.go +192 -0
- package/internal/cricinfo/analysis.go +1401 -0
- package/internal/cricinfo/analysis_phase15_test.go +267 -0
- package/internal/cricinfo/client.go +471 -0
- package/internal/cricinfo/client_test.go +280 -0
- package/internal/cricinfo/cmd/fixture-refresh/main.go +145 -0
- package/internal/cricinfo/competitions.go +405 -0
- package/internal/cricinfo/competitions_phase13_test.go +234 -0
- package/internal/cricinfo/coverage_ledger.go +122 -0
- package/internal/cricinfo/coverage_ledger_test.go +253 -0
- package/internal/cricinfo/decode.go +115 -0
- package/internal/cricinfo/decode_test.go +100 -0
- package/internal/cricinfo/entity_index.go +618 -0
- package/internal/cricinfo/entity_index_test.go +175 -0
- package/internal/cricinfo/fixture_matrix.go +243 -0
- package/internal/cricinfo/fixture_matrix_test.go +49 -0
- package/internal/cricinfo/fixtures_test.go +264 -0
- package/internal/cricinfo/historical_hydration.go +1641 -0
- package/internal/cricinfo/historical_phase14_test.go +542 -0
- package/internal/cricinfo/leagues.go +1210 -0
- package/internal/cricinfo/leagues_phase12_test.go +324 -0
- package/internal/cricinfo/live_leagues_test.go +169 -0
- package/internal/cricinfo/live_matches_test.go +203 -0
- package/internal/cricinfo/live_matrix_test.go +118 -0
- package/internal/cricinfo/live_players_test.go +122 -0
- package/internal/cricinfo/live_search_test.go +86 -0
- package/internal/cricinfo/live_smoke_test.go +213 -0
- package/internal/cricinfo/live_teams_test.go +104 -0
- package/internal/cricinfo/matches.go +1508 -0
- package/internal/cricinfo/matches_phase7_test.go +207 -0
- package/internal/cricinfo/matches_phase9_test.go +253 -0
- package/internal/cricinfo/normalize_entities.go +1727 -0
- package/internal/cricinfo/normalize_leagues.go +346 -0
- package/internal/cricinfo/players.go +1332 -0
- package/internal/cricinfo/players_phase10_test.go +174 -0
- package/internal/cricinfo/players_phase11_test.go +373 -0
- package/internal/cricinfo/render_contract.go +1088 -0
- package/internal/cricinfo/render_phase4_test.go +633 -0
- package/internal/cricinfo/renderer.go +1689 -0
- package/internal/cricinfo/resolver.go +813 -0
- package/internal/cricinfo/resolver_test.go +244 -0
- package/internal/cricinfo/teams.go +603 -0
- package/internal/cricinfo/teams_phase8_test.go +231 -0
- package/internal/cricinfo/testdata/fixtures/README.md +43 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/broadcasts.json +11 -0
- package/internal/cricinfo/testdata/fixtures/aux-competition-metadata/officials.json +150 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-110.json +157 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52545007.json +145 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/detail-52559021.json +143 -0
- package/internal/cricinfo/testdata/fixtures/details-plays/plays.json +15 -0
- package/internal/cricinfo/testdata/fixtures/endpoint-matrix.tsv +19 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow-1.json +12 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/fow.json +42 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/innings-1-2.json +38 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnership-1.json +31 -0
- package/internal/cricinfo/testdata/fixtures/innings-fow-partnerships/partnerships.json +42 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-offdays.json +20 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar-ondays.json +21 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/calendar.json +14 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-2025.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-group-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-groups.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-type-1.json +13 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/season-types.json +11 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/seasons.json +30 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-item-1.json +72 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings-root.json +3 -0
- package/internal/cricinfo/testdata/fixtures/leagues-seasons-standings/standings.json +15 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/competition.json +460 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/event-1529474.json +86 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/matchcards-1527966.json +368 -0
- package/internal/cricinfo/testdata/fixtures/matches-competitions/situation-1529474.json +10 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257-statistics.json +126 -0
- package/internal/cricinfo/testdata/fixtures/players/athlete-1361257.json +113 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-1-statistics-0.json +208 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores-1-2-statistics-0.json +252 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-linescores.json +74 -0
- package/internal/cricinfo/testdata/fixtures/players/roster-1361257-statistics-0.json +1008 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/events.json +72 -0
- package/internal/cricinfo/testdata/fixtures/root-discovery/root.json +28 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/competitor-789643.json +40 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/leaders-789643.json +353 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/records-789643.json +91 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772-object.json +231 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-1147772.json +235 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/roster-789643.json +322 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/scores-789643.json +19 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/statistics-789643.json +629 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643-athletes.json +7 -0
- package/internal/cricinfo/testdata/fixtures/team-competitor/team-789643.json +67 -0
- package/internal/cricinfo/testdata/golden/match-empty.golden +1 -0
- package/internal/cricinfo/testdata/golden/match-list.golden +2 -0
- package/internal/cricinfo/testdata/golden/match-partial.golden +3 -0
- package/internal/cricinfo/types.go +54 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +153 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/amxv/cricinfo-cli/internal/cricinfo"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type analysisCommandService interface {
|
|
13
|
+
Close() error
|
|
14
|
+
Dismissals(ctx context.Context, opts cricinfo.AnalysisDismissalOptions) (cricinfo.NormalizedResult, error)
|
|
15
|
+
Bowling(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
|
|
16
|
+
Batting(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
|
|
17
|
+
Partnerships(ctx context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type analysisRuntimeOptions struct {
|
|
21
|
+
leagueID string
|
|
22
|
+
typeQuery string
|
|
23
|
+
groupQuery string
|
|
24
|
+
dateFrom string
|
|
25
|
+
dateTo string
|
|
26
|
+
matchLimit int
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var newAnalysisService = func() (analysisCommandService, error) {
|
|
30
|
+
return cricinfo.NewAnalysisService(cricinfo.AnalysisServiceConfig{})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func newAnalysisCommand(global *globalOptions) *cobra.Command {
|
|
34
|
+
base := &analysisRuntimeOptions{}
|
|
35
|
+
|
|
36
|
+
cmd := &cobra.Command{
|
|
37
|
+
Use: "analysis",
|
|
38
|
+
Short: "Derived cricket analysis over normalized real-time hydrated data.",
|
|
39
|
+
Long: strings.Join([]string{
|
|
40
|
+
"Run ranking and grouping analysis over live scoped Cricinfo data.",
|
|
41
|
+
"Analysis reuses in-process hydration for the active command execution only.",
|
|
42
|
+
"No persistent analysis cache is written.",
|
|
43
|
+
"",
|
|
44
|
+
"Next steps:",
|
|
45
|
+
" cricinfo analysis dismissals --league 19138 --seasons 2024-2025",
|
|
46
|
+
" cricinfo analysis bowling --metric economy --scope season:2025 --league 19138",
|
|
47
|
+
" cricinfo analysis batting --metric strike-rate --scope match:1529474",
|
|
48
|
+
" cricinfo analysis partnerships --scope season:2025 --league 19138",
|
|
49
|
+
}, "\n"),
|
|
50
|
+
Args: cobra.NoArgs,
|
|
51
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
52
|
+
return cmd.Help()
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cmd.PersistentFlags().StringVar(&base.leagueID, "league", "", "League ID/ref/alias (required for season scope and dismissals)")
|
|
57
|
+
cmd.PersistentFlags().StringVar(&base.typeQuery, "type", "", "Optional season type scope for season traversal")
|
|
58
|
+
cmd.PersistentFlags().StringVar(&base.groupQuery, "group", "", "Optional season group scope for season traversal")
|
|
59
|
+
cmd.PersistentFlags().StringVar(&base.dateFrom, "date-from", "", "Optional lower date bound (RFC3339 or YYYY-MM-DD)")
|
|
60
|
+
cmd.PersistentFlags().StringVar(&base.dateTo, "date-to", "", "Optional upper date bound (RFC3339 or YYYY-MM-DD)")
|
|
61
|
+
cmd.PersistentFlags().IntVar(&base.matchLimit, "match-limit", 0, "Optional cap on scoped matches before analysis")
|
|
62
|
+
|
|
63
|
+
type dismissRuntime struct {
|
|
64
|
+
seasons string
|
|
65
|
+
groupBy string
|
|
66
|
+
team string
|
|
67
|
+
player string
|
|
68
|
+
dismissalType string
|
|
69
|
+
innings int
|
|
70
|
+
period int
|
|
71
|
+
top int
|
|
72
|
+
}
|
|
73
|
+
dismiss := &dismissRuntime{}
|
|
74
|
+
dismissalsCmd := &cobra.Command{
|
|
75
|
+
Use: "dismissals",
|
|
76
|
+
Short: "Rank dismissal patterns across league seasons",
|
|
77
|
+
Args: cobra.NoArgs,
|
|
78
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
79
|
+
if strings.TrimSpace(base.leagueID) == "" {
|
|
80
|
+
return fmt.Errorf("--league is required")
|
|
81
|
+
}
|
|
82
|
+
if strings.TrimSpace(dismiss.seasons) == "" {
|
|
83
|
+
return fmt.Errorf("--seasons is required")
|
|
84
|
+
}
|
|
85
|
+
return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
|
|
86
|
+
return service.Dismissals(ctx, cricinfo.AnalysisDismissalOptions{
|
|
87
|
+
LeagueQuery: base.leagueID,
|
|
88
|
+
Seasons: dismiss.seasons,
|
|
89
|
+
TypeQuery: base.typeQuery,
|
|
90
|
+
GroupQuery: base.groupQuery,
|
|
91
|
+
DateFrom: base.dateFrom,
|
|
92
|
+
DateTo: base.dateTo,
|
|
93
|
+
MatchLimit: base.matchLimit,
|
|
94
|
+
GroupBy: dismiss.groupBy,
|
|
95
|
+
TeamQuery: dismiss.team,
|
|
96
|
+
PlayerQuery: dismiss.player,
|
|
97
|
+
DismissalType: dismiss.dismissalType,
|
|
98
|
+
Innings: dismiss.innings,
|
|
99
|
+
Period: dismiss.period,
|
|
100
|
+
Top: dismiss.top,
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
dismissalsCmd.Flags().StringVar(&dismiss.seasons, "seasons", "", "Required: season range (for example 2023-2025 or 2024,2025)")
|
|
106
|
+
dismissalsCmd.Flags().StringVar(&dismiss.groupBy, "group-by", "dismissal-type", "Grouping fields (comma-separated): player,team,league,season,dismissal-type,innings")
|
|
107
|
+
dismissalsCmd.Flags().StringVar(&dismiss.team, "team", "", "Optional team filter (id or alias)")
|
|
108
|
+
dismissalsCmd.Flags().StringVar(&dismiss.player, "player", "", "Optional player filter (id or alias)")
|
|
109
|
+
dismissalsCmd.Flags().StringVar(&dismiss.dismissalType, "dismissal-type", "", "Optional dismissal type filter (for example caught, bowled)")
|
|
110
|
+
dismissalsCmd.Flags().IntVar(&dismiss.innings, "innings", 0, "Optional innings filter")
|
|
111
|
+
dismissalsCmd.Flags().IntVar(&dismiss.period, "period", 0, "Optional period filter")
|
|
112
|
+
dismissalsCmd.Flags().IntVar(&dismiss.top, "top", 20, "Maximum ranked rows to return")
|
|
113
|
+
|
|
114
|
+
type metricRuntime struct {
|
|
115
|
+
scope string
|
|
116
|
+
metric string
|
|
117
|
+
groupBy string
|
|
118
|
+
team string
|
|
119
|
+
player string
|
|
120
|
+
top int
|
|
121
|
+
}
|
|
122
|
+
bowling := &metricRuntime{}
|
|
123
|
+
bowlingCmd := &cobra.Command{
|
|
124
|
+
Use: "bowling",
|
|
125
|
+
Short: "Rank bowling metrics by match or season scope",
|
|
126
|
+
Args: cobra.NoArgs,
|
|
127
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
128
|
+
if strings.TrimSpace(bowling.scope) == "" {
|
|
129
|
+
return fmt.Errorf("--scope is required")
|
|
130
|
+
}
|
|
131
|
+
if strings.TrimSpace(bowling.metric) == "" {
|
|
132
|
+
return fmt.Errorf("--metric is required")
|
|
133
|
+
}
|
|
134
|
+
return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
|
|
135
|
+
return service.Bowling(ctx, cricinfo.AnalysisMetricOptions{
|
|
136
|
+
Metric: bowling.metric,
|
|
137
|
+
Scope: bowling.scope,
|
|
138
|
+
LeagueQuery: base.leagueID,
|
|
139
|
+
TypeQuery: base.typeQuery,
|
|
140
|
+
GroupQuery: base.groupQuery,
|
|
141
|
+
DateFrom: base.dateFrom,
|
|
142
|
+
DateTo: base.dateTo,
|
|
143
|
+
MatchLimit: base.matchLimit,
|
|
144
|
+
GroupBy: bowling.groupBy,
|
|
145
|
+
TeamQuery: bowling.team,
|
|
146
|
+
PlayerQuery: bowling.player,
|
|
147
|
+
Top: bowling.top,
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
bowlingCmd.Flags().StringVar(&bowling.scope, "scope", "", "Required: match:<match> or season:<season>")
|
|
153
|
+
bowlingCmd.Flags().StringVar(&bowling.metric, "metric", "", "Required: economy, dots, or sixes-conceded")
|
|
154
|
+
bowlingCmd.Flags().StringVar(&bowling.groupBy, "group-by", "player", "Grouping fields (comma-separated): player,team,league,season")
|
|
155
|
+
bowlingCmd.Flags().StringVar(&bowling.team, "team", "", "Optional team filter")
|
|
156
|
+
bowlingCmd.Flags().StringVar(&bowling.player, "player", "", "Optional player filter")
|
|
157
|
+
bowlingCmd.Flags().IntVar(&bowling.top, "top", 20, "Maximum ranked rows to return")
|
|
158
|
+
|
|
159
|
+
batting := &metricRuntime{}
|
|
160
|
+
battingCmd := &cobra.Command{
|
|
161
|
+
Use: "batting",
|
|
162
|
+
Short: "Rank batting metrics by match or season scope",
|
|
163
|
+
Args: cobra.NoArgs,
|
|
164
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
165
|
+
if strings.TrimSpace(batting.scope) == "" {
|
|
166
|
+
return fmt.Errorf("--scope is required")
|
|
167
|
+
}
|
|
168
|
+
if strings.TrimSpace(batting.metric) == "" {
|
|
169
|
+
return fmt.Errorf("--metric is required")
|
|
170
|
+
}
|
|
171
|
+
return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
|
|
172
|
+
return service.Batting(ctx, cricinfo.AnalysisMetricOptions{
|
|
173
|
+
Metric: batting.metric,
|
|
174
|
+
Scope: batting.scope,
|
|
175
|
+
LeagueQuery: base.leagueID,
|
|
176
|
+
TypeQuery: base.typeQuery,
|
|
177
|
+
GroupQuery: base.groupQuery,
|
|
178
|
+
DateFrom: base.dateFrom,
|
|
179
|
+
DateTo: base.dateTo,
|
|
180
|
+
MatchLimit: base.matchLimit,
|
|
181
|
+
GroupBy: batting.groupBy,
|
|
182
|
+
TeamQuery: batting.team,
|
|
183
|
+
PlayerQuery: batting.player,
|
|
184
|
+
Top: batting.top,
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
battingCmd.Flags().StringVar(&batting.scope, "scope", "", "Required: match:<match> or season:<season>")
|
|
190
|
+
battingCmd.Flags().StringVar(&batting.metric, "metric", "", "Required: fours, sixes, or strike-rate")
|
|
191
|
+
battingCmd.Flags().StringVar(&batting.groupBy, "group-by", "player", "Grouping fields (comma-separated): player,team,league,season")
|
|
192
|
+
battingCmd.Flags().StringVar(&batting.team, "team", "", "Optional team filter")
|
|
193
|
+
battingCmd.Flags().StringVar(&batting.player, "player", "", "Optional player filter")
|
|
194
|
+
battingCmd.Flags().IntVar(&batting.top, "top", 20, "Maximum ranked rows to return")
|
|
195
|
+
|
|
196
|
+
type partnershipRuntime struct {
|
|
197
|
+
scope string
|
|
198
|
+
groupBy string
|
|
199
|
+
team string
|
|
200
|
+
innings int
|
|
201
|
+
top int
|
|
202
|
+
}
|
|
203
|
+
partners := &partnershipRuntime{}
|
|
204
|
+
partnershipsCmd := &cobra.Command{
|
|
205
|
+
Use: "partnerships",
|
|
206
|
+
Short: "Rank partnerships by match or season scope",
|
|
207
|
+
Args: cobra.NoArgs,
|
|
208
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
209
|
+
if strings.TrimSpace(partners.scope) == "" {
|
|
210
|
+
return fmt.Errorf("--scope is required")
|
|
211
|
+
}
|
|
212
|
+
return runAnalysisCommand(cmd, global, func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error) {
|
|
213
|
+
return service.Partnerships(ctx, cricinfo.AnalysisMetricOptions{
|
|
214
|
+
Scope: partners.scope,
|
|
215
|
+
LeagueQuery: base.leagueID,
|
|
216
|
+
TypeQuery: base.typeQuery,
|
|
217
|
+
GroupQuery: base.groupQuery,
|
|
218
|
+
DateFrom: base.dateFrom,
|
|
219
|
+
DateTo: base.dateTo,
|
|
220
|
+
MatchLimit: base.matchLimit,
|
|
221
|
+
GroupBy: partners.groupBy,
|
|
222
|
+
TeamQuery: partners.team,
|
|
223
|
+
Innings: partners.innings,
|
|
224
|
+
Top: partners.top,
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
partnershipsCmd.Flags().StringVar(&partners.scope, "scope", "", "Required: match:<match> or season:<season>")
|
|
230
|
+
partnershipsCmd.Flags().StringVar(&partners.groupBy, "group-by", "innings", "Grouping fields (comma-separated): innings,team,league,season")
|
|
231
|
+
partnershipsCmd.Flags().StringVar(&partners.team, "team", "", "Optional team filter")
|
|
232
|
+
partnershipsCmd.Flags().IntVar(&partners.innings, "innings", 0, "Optional innings filter")
|
|
233
|
+
partnershipsCmd.Flags().IntVar(&partners.top, "top", 20, "Maximum ranked rows to return")
|
|
234
|
+
|
|
235
|
+
cmd.AddCommand(dismissalsCmd, bowlingCmd, battingCmd, partnershipsCmd)
|
|
236
|
+
return cmd
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func runAnalysisCommand(
|
|
240
|
+
cmd *cobra.Command,
|
|
241
|
+
global *globalOptions,
|
|
242
|
+
fn func(ctx context.Context, service analysisCommandService) (cricinfo.NormalizedResult, error),
|
|
243
|
+
) error {
|
|
244
|
+
service, err := newAnalysisService()
|
|
245
|
+
if err != nil {
|
|
246
|
+
return err
|
|
247
|
+
}
|
|
248
|
+
defer func() {
|
|
249
|
+
_ = service.Close()
|
|
250
|
+
}()
|
|
251
|
+
|
|
252
|
+
result, err := fn(cmd.Context(), service)
|
|
253
|
+
if err != nil {
|
|
254
|
+
return err
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
|
|
258
|
+
Format: global.format,
|
|
259
|
+
Verbose: global.verbose,
|
|
260
|
+
AllFields: global.allFields,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/amxv/cricinfo-cli/internal/cricinfo"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type fakeAnalysisService struct {
|
|
13
|
+
dismissalsResult cricinfo.NormalizedResult
|
|
14
|
+
bowlingResult cricinfo.NormalizedResult
|
|
15
|
+
battingResult cricinfo.NormalizedResult
|
|
16
|
+
partnershipResult cricinfo.NormalizedResult
|
|
17
|
+
|
|
18
|
+
dismissOpts []cricinfo.AnalysisDismissalOptions
|
|
19
|
+
bowlingOpts []cricinfo.AnalysisMetricOptions
|
|
20
|
+
battingOpts []cricinfo.AnalysisMetricOptions
|
|
21
|
+
partnerOpts []cricinfo.AnalysisMetricOptions
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func (f *fakeAnalysisService) Close() error { return nil }
|
|
25
|
+
|
|
26
|
+
func (f *fakeAnalysisService) Dismissals(_ context.Context, opts cricinfo.AnalysisDismissalOptions) (cricinfo.NormalizedResult, error) {
|
|
27
|
+
f.dismissOpts = append(f.dismissOpts, opts)
|
|
28
|
+
return f.dismissalsResult, nil
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func (f *fakeAnalysisService) Bowling(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
|
|
32
|
+
f.bowlingOpts = append(f.bowlingOpts, opts)
|
|
33
|
+
return f.bowlingResult, nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func (f *fakeAnalysisService) Batting(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
|
|
37
|
+
f.battingOpts = append(f.battingOpts, opts)
|
|
38
|
+
return f.battingResult, nil
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func (f *fakeAnalysisService) Partnerships(_ context.Context, opts cricinfo.AnalysisMetricOptions) (cricinfo.NormalizedResult, error) {
|
|
42
|
+
f.partnerOpts = append(f.partnerOpts, opts)
|
|
43
|
+
return f.partnershipResult, nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestAnalysisCommandsCallServiceWithScopedOptions(t *testing.T) {
|
|
47
|
+
service := &fakeAnalysisService{
|
|
48
|
+
dismissalsResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisDismiss, cricinfo.AnalysisView{Command: "dismissals"}),
|
|
49
|
+
bowlingResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisBowl, cricinfo.AnalysisView{Command: "bowling", Metric: "economy"}),
|
|
50
|
+
battingResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisBat, cricinfo.AnalysisView{Command: "batting", Metric: "strike-rate"}),
|
|
51
|
+
partnershipResult: cricinfo.NewDataResult(cricinfo.EntityAnalysisPart, cricinfo.AnalysisView{Command: "partnerships"}),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
originalFactory := newAnalysisService
|
|
55
|
+
newAnalysisService = func() (analysisCommandService, error) { return service, nil }
|
|
56
|
+
defer func() {
|
|
57
|
+
newAnalysisService = originalFactory
|
|
58
|
+
}()
|
|
59
|
+
|
|
60
|
+
var dismissOut bytes.Buffer
|
|
61
|
+
var dismissErr bytes.Buffer
|
|
62
|
+
if err := Run([]string{
|
|
63
|
+
"analysis", "dismissals",
|
|
64
|
+
"--league", "19138",
|
|
65
|
+
"--seasons", "2024-2025",
|
|
66
|
+
"--group-by", "dismissal-type,team",
|
|
67
|
+
"--team", "BOOST",
|
|
68
|
+
"--dismissal-type", "caught",
|
|
69
|
+
"--innings", "1",
|
|
70
|
+
"--top", "12",
|
|
71
|
+
"--format", "json",
|
|
72
|
+
}, &dismissOut, &dismissErr); err != nil {
|
|
73
|
+
t.Fatalf("Run analysis dismissals error: %v", err)
|
|
74
|
+
}
|
|
75
|
+
dismissPayload := decodeCLIJSONMap(t, dismissOut.Bytes())
|
|
76
|
+
if dismissPayload["kind"] != string(cricinfo.EntityAnalysisDismiss) {
|
|
77
|
+
t.Fatalf("expected kind %q, got %#v", cricinfo.EntityAnalysisDismiss, dismissPayload["kind"])
|
|
78
|
+
}
|
|
79
|
+
if len(service.dismissOpts) != 1 {
|
|
80
|
+
t.Fatalf("expected one dismissals call, got %d", len(service.dismissOpts))
|
|
81
|
+
}
|
|
82
|
+
if service.dismissOpts[0].LeagueQuery != "19138" || service.dismissOpts[0].Seasons != "2024-2025" {
|
|
83
|
+
t.Fatalf("unexpected dismissals opts: %+v", service.dismissOpts[0])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var bowlingOut bytes.Buffer
|
|
87
|
+
var bowlingErr bytes.Buffer
|
|
88
|
+
if err := Run([]string{
|
|
89
|
+
"analysis", "bowling",
|
|
90
|
+
"--scope", "season:2025",
|
|
91
|
+
"--league", "19138",
|
|
92
|
+
"--metric", "economy",
|
|
93
|
+
"--group-by", "player,team",
|
|
94
|
+
"--player", "1361257",
|
|
95
|
+
"--top", "5",
|
|
96
|
+
"--format", "json",
|
|
97
|
+
}, &bowlingOut, &bowlingErr); err != nil {
|
|
98
|
+
t.Fatalf("Run analysis bowling error: %v", err)
|
|
99
|
+
}
|
|
100
|
+
if len(service.bowlingOpts) != 1 {
|
|
101
|
+
t.Fatalf("expected one bowling call, got %d", len(service.bowlingOpts))
|
|
102
|
+
}
|
|
103
|
+
if service.bowlingOpts[0].Metric != "economy" || service.bowlingOpts[0].Scope != "season:2025" {
|
|
104
|
+
t.Fatalf("unexpected bowling opts: %+v", service.bowlingOpts[0])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var battingOut bytes.Buffer
|
|
108
|
+
var battingErr bytes.Buffer
|
|
109
|
+
if err := Run([]string{
|
|
110
|
+
"analysis", "batting",
|
|
111
|
+
"--scope", "match:1529474",
|
|
112
|
+
"--metric", "strike-rate",
|
|
113
|
+
"--group-by", "player",
|
|
114
|
+
"--top", "7",
|
|
115
|
+
"--format", "json",
|
|
116
|
+
}, &battingOut, &battingErr); err != nil {
|
|
117
|
+
t.Fatalf("Run analysis batting error: %v", err)
|
|
118
|
+
}
|
|
119
|
+
if len(service.battingOpts) != 1 {
|
|
120
|
+
t.Fatalf("expected one batting call, got %d", len(service.battingOpts))
|
|
121
|
+
}
|
|
122
|
+
if service.battingOpts[0].Metric != "strike-rate" || service.battingOpts[0].Scope != "match:1529474" {
|
|
123
|
+
t.Fatalf("unexpected batting opts: %+v", service.battingOpts[0])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var partnershipsOut bytes.Buffer
|
|
127
|
+
var partnershipsErr bytes.Buffer
|
|
128
|
+
if err := Run([]string{
|
|
129
|
+
"analysis", "partnerships",
|
|
130
|
+
"--scope", "season:2025",
|
|
131
|
+
"--league", "19138",
|
|
132
|
+
"--group-by", "innings,team",
|
|
133
|
+
"--innings", "1",
|
|
134
|
+
"--top", "9",
|
|
135
|
+
"--format", "json",
|
|
136
|
+
}, &partnershipsOut, &partnershipsErr); err != nil {
|
|
137
|
+
t.Fatalf("Run analysis partnerships error: %v", err)
|
|
138
|
+
}
|
|
139
|
+
if len(service.partnerOpts) != 1 {
|
|
140
|
+
t.Fatalf("expected one partnerships call, got %d", len(service.partnerOpts))
|
|
141
|
+
}
|
|
142
|
+
if service.partnerOpts[0].Scope != "season:2025" || service.partnerOpts[0].Innings != 1 {
|
|
143
|
+
t.Fatalf("unexpected partnerships opts: %+v", service.partnerOpts[0])
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func TestAnalysisCommandsValidateRequiredFlags(t *testing.T) {
|
|
148
|
+
service := &fakeAnalysisService{}
|
|
149
|
+
|
|
150
|
+
originalFactory := newAnalysisService
|
|
151
|
+
newAnalysisService = func() (analysisCommandService, error) { return service, nil }
|
|
152
|
+
defer func() {
|
|
153
|
+
newAnalysisService = originalFactory
|
|
154
|
+
}()
|
|
155
|
+
|
|
156
|
+
assertErrContains := func(args []string, want string) {
|
|
157
|
+
t.Helper()
|
|
158
|
+
var out bytes.Buffer
|
|
159
|
+
var errBuf bytes.Buffer
|
|
160
|
+
err := Run(args, &out, &errBuf)
|
|
161
|
+
if err == nil {
|
|
162
|
+
t.Fatalf("expected error for %v", args)
|
|
163
|
+
}
|
|
164
|
+
if !strings.Contains(err.Error(), want) {
|
|
165
|
+
t.Fatalf("expected %q in error for %v, got %v", want, args, err)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
assertErrContains([]string{"analysis", "dismissals", "--seasons", "2025"}, "--league is required")
|
|
170
|
+
assertErrContains([]string{"analysis", "dismissals", "--league", "19138"}, "--seasons is required")
|
|
171
|
+
assertErrContains([]string{"analysis", "bowling", "--metric", "economy"}, "--scope is required")
|
|
172
|
+
assertErrContains([]string{"analysis", "bowling", "--scope", "match:1529474"}, "--metric is required")
|
|
173
|
+
assertErrContains([]string{"analysis", "batting", "--scope", "match:1529474"}, "--metric is required")
|
|
174
|
+
assertErrContains([]string{"analysis", "partnerships"}, "--scope is required")
|
|
175
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"strings"
|
|
6
|
+
|
|
7
|
+
"github.com/amxv/cricinfo-cli/internal/cricinfo"
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type competitionCommandService interface {
|
|
12
|
+
Close() error
|
|
13
|
+
Show(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
14
|
+
Officials(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
15
|
+
Broadcasts(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
16
|
+
Tickets(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
17
|
+
Odds(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
18
|
+
Metadata(ctx context.Context, query string, opts cricinfo.CompetitionLookupOptions) (cricinfo.NormalizedResult, error)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type competitionRuntimeOptions struct {
|
|
22
|
+
leagueID string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var newCompetitionService = func() (competitionCommandService, error) {
|
|
26
|
+
return cricinfo.NewCompetitionService(cricinfo.CompetitionServiceConfig{})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func newCompetitionsCommand(global *globalOptions) *cobra.Command {
|
|
30
|
+
opts := &competitionRuntimeOptions{}
|
|
31
|
+
|
|
32
|
+
cmd := &cobra.Command{
|
|
33
|
+
Use: "competitions",
|
|
34
|
+
Short: "Competition metadata including officials, broadcasts, tickets, odds, and aggregate views.",
|
|
35
|
+
Long: strings.Join([]string{
|
|
36
|
+
"Resolve a match/competition and drill into auxiliary competition metadata routes.",
|
|
37
|
+
"Empty-but-valid metadata collections are rendered as clean zero-result views.",
|
|
38
|
+
"",
|
|
39
|
+
"Next steps:",
|
|
40
|
+
" cricinfo competitions show <match>",
|
|
41
|
+
" cricinfo competitions officials <match>",
|
|
42
|
+
" cricinfo competitions broadcasts <match>",
|
|
43
|
+
" cricinfo competitions tickets <match>",
|
|
44
|
+
" cricinfo competitions odds <match>",
|
|
45
|
+
" cricinfo competitions metadata <match>",
|
|
46
|
+
}, "\n"),
|
|
47
|
+
Args: cobra.NoArgs,
|
|
48
|
+
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
49
|
+
return cmd.Help()
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cmd.PersistentFlags().StringVar(&opts.leagueID, "league", "", "Preferred league ID for match resolution context")
|
|
54
|
+
|
|
55
|
+
showCmd := &cobra.Command{
|
|
56
|
+
Use: "show <match>",
|
|
57
|
+
Short: "Show one competition summary",
|
|
58
|
+
Args: cobra.MinimumNArgs(1),
|
|
59
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
60
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
61
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
62
|
+
return service.Show(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
officialsCmd := &cobra.Command{
|
|
68
|
+
Use: "officials <match>",
|
|
69
|
+
Short: "Show competition officials entries",
|
|
70
|
+
Args: cobra.MinimumNArgs(1),
|
|
71
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
72
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
73
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
74
|
+
return service.Officials(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
broadcastsCmd := &cobra.Command{
|
|
80
|
+
Use: "broadcasts <match>",
|
|
81
|
+
Short: "Show competition broadcast entries",
|
|
82
|
+
Args: cobra.MinimumNArgs(1),
|
|
83
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
84
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
85
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
86
|
+
return service.Broadcasts(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
ticketsCmd := &cobra.Command{
|
|
92
|
+
Use: "tickets <match>",
|
|
93
|
+
Short: "Show competition ticket entries",
|
|
94
|
+
Args: cobra.MinimumNArgs(1),
|
|
95
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
96
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
97
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
98
|
+
return service.Tickets(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
99
|
+
})
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
oddsCmd := &cobra.Command{
|
|
104
|
+
Use: "odds <match>",
|
|
105
|
+
Short: "Show competition odds entries",
|
|
106
|
+
Args: cobra.MinimumNArgs(1),
|
|
107
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
108
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
109
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
110
|
+
return service.Odds(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
111
|
+
})
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
metadataCmd := &cobra.Command{
|
|
116
|
+
Use: "metadata <match>",
|
|
117
|
+
Short: "Show aggregated competition metadata across officials, broadcasts, tickets, and odds",
|
|
118
|
+
Args: cobra.MinimumNArgs(1),
|
|
119
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
120
|
+
query := strings.TrimSpace(strings.Join(args, " "))
|
|
121
|
+
return runCompetitionCommand(cmd, global, func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error) {
|
|
122
|
+
return service.Metadata(ctx, query, cricinfo.CompetitionLookupOptions{LeagueID: opts.leagueID})
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cmd.AddCommand(showCmd, officialsCmd, broadcastsCmd, ticketsCmd, oddsCmd, metadataCmd)
|
|
128
|
+
return cmd
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func runCompetitionCommand(
|
|
132
|
+
cmd *cobra.Command,
|
|
133
|
+
global *globalOptions,
|
|
134
|
+
fn func(ctx context.Context, service competitionCommandService) (cricinfo.NormalizedResult, error),
|
|
135
|
+
) error {
|
|
136
|
+
service, err := newCompetitionService()
|
|
137
|
+
if err != nil {
|
|
138
|
+
return err
|
|
139
|
+
}
|
|
140
|
+
defer func() {
|
|
141
|
+
_ = service.Close()
|
|
142
|
+
}()
|
|
143
|
+
|
|
144
|
+
result, err := fn(cmd.Context(), service)
|
|
145
|
+
if err != nil {
|
|
146
|
+
return err
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return cricinfo.Render(cmd.OutOrStdout(), result, cricinfo.RenderOptions{
|
|
150
|
+
Format: global.format,
|
|
151
|
+
Verbose: global.verbose,
|
|
152
|
+
AllFields: global.allFields,
|
|
153
|
+
})
|
|
154
|
+
}
|