cricinfo-cli-go 0.1.2 → 0.1.4
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/internal/cli/matches.go +96 -0
- package/internal/cli/matches_test.go +71 -0
- package/internal/cli/search.go +156 -0
- package/internal/cricinfo/analysis.go +177 -47
- package/internal/cricinfo/client.go +23 -2
- package/internal/cricinfo/coverage_ledger_test.go +2 -22
- package/internal/cricinfo/entity_index.go +27 -0
- package/internal/cricinfo/matches.go +1036 -22
- package/internal/cricinfo/matches_phase7_test.go +11 -4
- package/internal/cricinfo/normalize_entities.go +67 -35
- package/internal/cricinfo/players.go +236 -2
- package/internal/cricinfo/render_contract.go +139 -37
- package/internal/cricinfo/renderer.go +422 -13
- package/internal/cricinfo/resolver.go +92 -15
- package/internal/cricinfo/teams.go +109 -6
- package/internal/cricinfo/testdata/coverage/cricinfo-field-path-catalog.txt +2536 -0
- package/internal/cricinfo/testdata/coverage/cricinfo-working-templates.tsv +56 -0
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ const (
|
|
|
19
19
|
analysisMetricSixesConceded = "sixes-conceded"
|
|
20
20
|
analysisMetricFours = "fours"
|
|
21
21
|
analysisMetricSixes = "sixes"
|
|
22
|
+
analysisMetricRuns = "runs"
|
|
22
23
|
analysisMetricStrikeRate = "strike-rate"
|
|
23
24
|
)
|
|
24
25
|
|
|
@@ -349,54 +350,62 @@ func (s *AnalysisService) Bowling(ctx context.Context, opts AnalysisMetricOption
|
|
|
349
350
|
|
|
350
351
|
agg := map[string]*analysisAggregate{}
|
|
351
352
|
warnings := append([]string{}, run.warnings...)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if statusErr := analysisTransportResult(EntityAnalysisBowl, match.ID, hydrateErr); statusErr != nil {
|
|
357
|
-
return *statusErr, nil
|
|
358
|
-
}
|
|
359
|
-
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
360
|
-
continue
|
|
353
|
+
if run.mode == analysisScopeMatch {
|
|
354
|
+
if fastAgg, fastWarnings, used := s.hydrateMatchScopeBowlingFromScorecard(ctx, run, filters, groupBy); used {
|
|
355
|
+
agg = fastAgg
|
|
356
|
+
warnings = append(warnings, fastWarnings...)
|
|
361
357
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for _,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
TeamID: strings.TrimSpace(player.TeamID),
|
|
373
|
-
TeamName: strings.TrimSpace(teamName),
|
|
374
|
-
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
375
|
-
PlayerName: strings.TrimSpace(playerName),
|
|
376
|
-
CountValue: 1,
|
|
377
|
-
Dots: totals.dots,
|
|
378
|
-
SixesConceded: totals.sixesConceded,
|
|
379
|
-
Balls: totals.balls,
|
|
380
|
-
RunsConceded: totals.conceded,
|
|
381
|
-
EconomySample: totals.economy,
|
|
382
|
-
}
|
|
383
|
-
if !filters.matches(row) {
|
|
358
|
+
}
|
|
359
|
+
if len(agg) == 0 {
|
|
360
|
+
for _, match := range run.session.ScopedMatches() {
|
|
361
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
362
|
+
players, playerWarnings, hydrateErr := run.session.HydratePlayerMatchSummaries(ctx, match.ID)
|
|
363
|
+
if hydrateErr != nil {
|
|
364
|
+
if statusErr := analysisTransportResult(EntityAnalysisBowl, match.ID, hydrateErr); statusErr != nil {
|
|
365
|
+
return *statusErr, nil
|
|
366
|
+
}
|
|
367
|
+
warnings = append(warnings, fmt.Sprintf("match %s player summary: %v", match.ID, hydrateErr))
|
|
384
368
|
continue
|
|
385
369
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
370
|
+
warnings = append(warnings, playerWarnings...)
|
|
371
|
+
|
|
372
|
+
for _, player := range players {
|
|
373
|
+
totals := extractBowlingTotals(player)
|
|
374
|
+
playerName := analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName)
|
|
375
|
+
teamName := analysisDisplayTeamName(s.resolver, player.TeamID, player.TeamName)
|
|
376
|
+
row := analysisSourceRow{
|
|
377
|
+
MatchID: strings.TrimSpace(player.MatchID),
|
|
378
|
+
LeagueID: strings.TrimSpace(player.LeagueID),
|
|
379
|
+
SeasonID: seasonID,
|
|
380
|
+
TeamID: strings.TrimSpace(player.TeamID),
|
|
381
|
+
TeamName: strings.TrimSpace(teamName),
|
|
382
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
383
|
+
PlayerName: strings.TrimSpace(playerName),
|
|
384
|
+
CountValue: 1,
|
|
385
|
+
Dots: totals.dots,
|
|
386
|
+
SixesConceded: totals.sixesConceded,
|
|
387
|
+
Balls: totals.balls,
|
|
388
|
+
RunsConceded: totals.conceded,
|
|
389
|
+
EconomySample: totals.economy,
|
|
390
|
+
}
|
|
391
|
+
if !filters.matches(row) {
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
395
|
+
entry := agg[key]
|
|
396
|
+
if entry == nil {
|
|
397
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
398
|
+
agg[key] = entry
|
|
399
|
+
}
|
|
400
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
401
|
+
entry.dots += row.Dots
|
|
402
|
+
entry.sixesConceded += row.SixesConceded
|
|
403
|
+
entry.balls += row.Balls
|
|
404
|
+
entry.runsConceded += row.RunsConceded
|
|
405
|
+
if row.EconomySample > 0 {
|
|
406
|
+
entry.economyTotal += row.EconomySample
|
|
407
|
+
entry.economyCount++
|
|
408
|
+
}
|
|
400
409
|
}
|
|
401
410
|
}
|
|
402
411
|
}
|
|
@@ -447,6 +456,103 @@ func (s *AnalysisService) Bowling(ctx context.Context, opts AnalysisMetricOption
|
|
|
447
456
|
return analysisResult(EntityAnalysisBowl, view, warnings), nil
|
|
448
457
|
}
|
|
449
458
|
|
|
459
|
+
func (s *AnalysisService) hydrateMatchScopeBowlingFromScorecard(
|
|
460
|
+
ctx context.Context,
|
|
461
|
+
run *analysisScopeRun,
|
|
462
|
+
filters analysisFilterSpec,
|
|
463
|
+
groupBy []string,
|
|
464
|
+
) (map[string]*analysisAggregate, []string, bool) {
|
|
465
|
+
if run == nil || run.session == nil {
|
|
466
|
+
return nil, nil, false
|
|
467
|
+
}
|
|
468
|
+
matches := run.session.ScopedMatches()
|
|
469
|
+
if len(matches) != 1 {
|
|
470
|
+
return nil, nil, false
|
|
471
|
+
}
|
|
472
|
+
match := matches[0]
|
|
473
|
+
scorecardRef := matchSubresourceRef(match, "matchcards", "matchcards")
|
|
474
|
+
if strings.TrimSpace(scorecardRef) == "" {
|
|
475
|
+
return nil, nil, false
|
|
476
|
+
}
|
|
477
|
+
resolved, err := s.client.ResolveRefChain(ctx, scorecardRef)
|
|
478
|
+
if err != nil {
|
|
479
|
+
return nil, nil, false
|
|
480
|
+
}
|
|
481
|
+
scorecard, err := NormalizeMatchScorecard(resolved.Body, match)
|
|
482
|
+
if err != nil {
|
|
483
|
+
return nil, nil, false
|
|
484
|
+
}
|
|
485
|
+
if len(scorecard.BowlingCards) == 0 {
|
|
486
|
+
return nil, nil, false
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
teamIDByAlias := map[string]string{}
|
|
490
|
+
for _, team := range match.Teams {
|
|
491
|
+
teamID := strings.TrimSpace(team.ID)
|
|
492
|
+
for _, raw := range []string{team.Name, team.ShortName, team.Abbreviation} {
|
|
493
|
+
alias := normalizeAlias(raw)
|
|
494
|
+
if alias == "" {
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
if _, exists := teamIDByAlias[alias]; !exists {
|
|
498
|
+
teamIDByAlias[alias] = teamID
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
seasonID := seasonForMatch(match, run.seasonHint)
|
|
504
|
+
agg := map[string]*analysisAggregate{}
|
|
505
|
+
for _, card := range scorecard.BowlingCards {
|
|
506
|
+
teamName := strings.TrimSpace(card.TeamName)
|
|
507
|
+
teamID := teamIDByAlias[normalizeAlias(teamName)]
|
|
508
|
+
teamName = analysisDisplayTeamName(s.resolver, teamID, teamName)
|
|
509
|
+
|
|
510
|
+
for _, player := range card.Players {
|
|
511
|
+
conceded := analysisNumericString(player.Conceded)
|
|
512
|
+
wickets := analysisNumericString(player.Wickets)
|
|
513
|
+
balls := oversToBalls(player.Overs)
|
|
514
|
+
econ := 0.0
|
|
515
|
+
if balls > 0 {
|
|
516
|
+
econ = float64(conceded) / (float64(balls) / 6.0)
|
|
517
|
+
}
|
|
518
|
+
row := analysisSourceRow{
|
|
519
|
+
MatchID: strings.TrimSpace(match.ID),
|
|
520
|
+
LeagueID: strings.TrimSpace(nonEmpty(match.LeagueID, run.scope.League.ID)),
|
|
521
|
+
SeasonID: seasonID,
|
|
522
|
+
TeamID: teamID,
|
|
523
|
+
TeamName: teamName,
|
|
524
|
+
PlayerID: strings.TrimSpace(player.PlayerID),
|
|
525
|
+
PlayerName: analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName),
|
|
526
|
+
CountValue: 1,
|
|
527
|
+
Balls: balls,
|
|
528
|
+
RunsConceded: conceded,
|
|
529
|
+
EconomySample: econ,
|
|
530
|
+
}
|
|
531
|
+
if wickets > 0 {
|
|
532
|
+
row.Dots = 0
|
|
533
|
+
}
|
|
534
|
+
if !filters.matches(row) {
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
key, dims := buildAnalysisGroup(row, groupBy)
|
|
539
|
+
entry := agg[key]
|
|
540
|
+
if entry == nil {
|
|
541
|
+
entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
|
|
542
|
+
agg[key] = entry
|
|
543
|
+
}
|
|
544
|
+
entry.matchIDs[row.MatchID] = struct{}{}
|
|
545
|
+
entry.balls += row.Balls
|
|
546
|
+
entry.runsConceded += row.RunsConceded
|
|
547
|
+
if row.EconomySample > 0 {
|
|
548
|
+
entry.economyTotal += row.EconomySample
|
|
549
|
+
entry.economyCount++
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return agg, nil, true
|
|
554
|
+
}
|
|
555
|
+
|
|
450
556
|
// Batting ranks batting metrics over match or season scope.
|
|
451
557
|
func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOptions) (NormalizedResult, error) {
|
|
452
558
|
metric, err := normalizeBattingMetric(opts.Metric)
|
|
@@ -541,6 +647,9 @@ func (s *AnalysisService) Batting(ctx context.Context, opts AnalysisMetricOption
|
|
|
541
647
|
case analysisMetricSixes:
|
|
542
648
|
row.Value = float64(entry.battingSixes)
|
|
543
649
|
row.Count = entry.battingSixes
|
|
650
|
+
case analysisMetricRuns:
|
|
651
|
+
row.Value = float64(entry.runsScored)
|
|
652
|
+
row.Count = entry.runsScored
|
|
544
653
|
case analysisMetricStrikeRate:
|
|
545
654
|
row.Value = strikeRateFromAggregate(entry)
|
|
546
655
|
}
|
|
@@ -993,10 +1102,10 @@ func normalizeBattingMetric(raw string) (string, error) {
|
|
|
993
1102
|
metric = strings.ReplaceAll(metric, "_", "-")
|
|
994
1103
|
metric = strings.ReplaceAll(metric, " ", "-")
|
|
995
1104
|
switch metric {
|
|
996
|
-
case analysisMetricFours, analysisMetricSixes, analysisMetricStrikeRate:
|
|
1105
|
+
case analysisMetricFours, analysisMetricSixes, analysisMetricRuns, analysisMetricStrikeRate:
|
|
997
1106
|
return metric, nil
|
|
998
1107
|
default:
|
|
999
|
-
return "", fmt.Errorf("--metric must be one of: fours, sixes, strike-rate")
|
|
1108
|
+
return "", fmt.Errorf("--metric must be one of: fours, sixes, runs, strike-rate")
|
|
1000
1109
|
}
|
|
1001
1110
|
}
|
|
1002
1111
|
|
|
@@ -1437,6 +1546,27 @@ func analysisNumericString(raw string) int {
|
|
|
1437
1546
|
return value
|
|
1438
1547
|
}
|
|
1439
1548
|
|
|
1549
|
+
func oversToBalls(raw string) int {
|
|
1550
|
+
raw = strings.TrimSpace(raw)
|
|
1551
|
+
if raw == "" {
|
|
1552
|
+
return 0
|
|
1553
|
+
}
|
|
1554
|
+
if strings.Contains(raw, ".") {
|
|
1555
|
+
parts := strings.SplitN(raw, ".", 2)
|
|
1556
|
+
whole, errWhole := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
1557
|
+
frac, errFrac := strconv.Atoi(strings.TrimSpace(parts[1]))
|
|
1558
|
+
if errWhole != nil || errFrac != nil || whole < 0 || frac < 0 || frac > 6 {
|
|
1559
|
+
return 0
|
|
1560
|
+
}
|
|
1561
|
+
return whole*6 + frac
|
|
1562
|
+
}
|
|
1563
|
+
value, err := strconv.Atoi(raw)
|
|
1564
|
+
if err != nil || value < 0 {
|
|
1565
|
+
return 0
|
|
1566
|
+
}
|
|
1567
|
+
return value * 6
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1440
1570
|
func analysisDisplayPlayerName(resolver *Resolver, playerID, fallback string) string {
|
|
1441
1571
|
name := strings.TrimSpace(fallback)
|
|
1442
1572
|
if name != "" {
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"io"
|
|
9
9
|
"math"
|
|
10
10
|
"math/rand"
|
|
11
|
+
"net"
|
|
11
12
|
"net/http"
|
|
12
13
|
"net/url"
|
|
13
14
|
"strings"
|
|
@@ -138,7 +139,18 @@ func NewClient(cfg Config) (*Client, error) {
|
|
|
138
139
|
|
|
139
140
|
httpClient := cfg.HTTPClient
|
|
140
141
|
if httpClient == nil {
|
|
141
|
-
httpClient = &http.Client{
|
|
142
|
+
httpClient = &http.Client{
|
|
143
|
+
Transport: &http.Transport{
|
|
144
|
+
Proxy: http.ProxyFromEnvironment,
|
|
145
|
+
DialContext: (&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
|
146
|
+
ForceAttemptHTTP2: true,
|
|
147
|
+
MaxIdleConns: 128,
|
|
148
|
+
MaxIdleConnsPerHost: 32,
|
|
149
|
+
IdleConnTimeout: 90 * time.Second,
|
|
150
|
+
TLSHandshakeTimeout: 5 * time.Second,
|
|
151
|
+
ExpectContinueTimeout: 1 * time.Second,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
sleep := cfg.Sleep
|
|
@@ -207,7 +219,7 @@ func (c *Client) Fetch(ctx context.Context, ref string) (*Document, error) {
|
|
|
207
219
|
return nil, fmt.Errorf("read response %q: %w", requestURL, readErr)
|
|
208
220
|
}
|
|
209
221
|
|
|
210
|
-
if resp.StatusCode
|
|
222
|
+
if c.shouldRetryStatus(resp.StatusCode) && attempt < c.maxRetries {
|
|
211
223
|
if sleepErr := c.sleep(ctx, c.retryDelay(attempt)); sleepErr != nil {
|
|
212
224
|
return nil, sleepErr
|
|
213
225
|
}
|
|
@@ -348,6 +360,15 @@ func (c *Client) shouldRetryError(err error, parentCtx context.Context) bool {
|
|
|
348
360
|
return parentCtx.Err() == nil
|
|
349
361
|
}
|
|
350
362
|
|
|
363
|
+
func (c *Client) shouldRetryStatus(statusCode int) bool {
|
|
364
|
+
switch statusCode {
|
|
365
|
+
case http.StatusRequestTimeout, http.StatusTooManyRequests:
|
|
366
|
+
return true
|
|
367
|
+
default:
|
|
368
|
+
return statusCode >= defaultRetryStatusCode
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
351
372
|
func (c *Client) retryDelay(attempt int) time.Duration {
|
|
352
373
|
delay := float64(c.retryBaseDelay)
|
|
353
374
|
delay *= math.Pow(2, float64(attempt))
|
|
@@ -137,7 +137,7 @@ func TestFieldPathFamilyCoverageLedgerKnownFamiliesMapped(t *testing.T) {
|
|
|
137
137
|
func researchedTemplatesFromTSV(t *testing.T) []string {
|
|
138
138
|
t.Helper()
|
|
139
139
|
|
|
140
|
-
path := filepath.Join(
|
|
140
|
+
path := filepath.Join("testdata", "coverage", "cricinfo-working-templates.tsv")
|
|
141
141
|
file, err := os.Open(path)
|
|
142
142
|
if err != nil {
|
|
143
143
|
t.Fatalf("open working templates TSV %q: %v", path, err)
|
|
@@ -177,7 +177,7 @@ func researchedTemplatesFromTSV(t *testing.T) []string {
|
|
|
177
177
|
func researchedFieldPathFamilies(t *testing.T) map[string]struct{} {
|
|
178
178
|
t.Helper()
|
|
179
179
|
|
|
180
|
-
path := filepath.Join(
|
|
180
|
+
path := filepath.Join("testdata", "coverage", "cricinfo-field-path-catalog.txt")
|
|
181
181
|
file, err := os.Open(path)
|
|
182
182
|
if err != nil {
|
|
183
183
|
t.Fatalf("open field-path catalog %q: %v", path, err)
|
|
@@ -231,23 +231,3 @@ func isNumericToken(value string) bool {
|
|
|
231
231
|
}
|
|
232
232
|
return true
|
|
233
233
|
}
|
|
234
|
-
|
|
235
|
-
func repoRoot(t *testing.T) string {
|
|
236
|
-
t.Helper()
|
|
237
|
-
|
|
238
|
-
dir, err := os.Getwd()
|
|
239
|
-
if err != nil {
|
|
240
|
-
t.Fatalf("getwd: %v", err)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for {
|
|
244
|
-
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
245
|
-
return dir
|
|
246
|
-
}
|
|
247
|
-
parent := filepath.Dir(dir)
|
|
248
|
-
if parent == dir {
|
|
249
|
-
t.Fatalf("unable to locate repository root from %q", dir)
|
|
250
|
-
}
|
|
251
|
-
dir = parent
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -472,12 +472,39 @@ func mergeAliasSlices(slices ...[]string) []string {
|
|
|
472
472
|
}
|
|
473
473
|
seen[normalized] = struct{}{}
|
|
474
474
|
out = append(out, value)
|
|
475
|
+
if acronym := aliasAcronym(normalized); acronym != "" {
|
|
476
|
+
if _, ok := seen[acronym]; !ok {
|
|
477
|
+
seen[acronym] = struct{}{}
|
|
478
|
+
out = append(out, acronym)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
475
481
|
}
|
|
476
482
|
}
|
|
477
483
|
sort.Strings(out)
|
|
478
484
|
return out
|
|
479
485
|
}
|
|
480
486
|
|
|
487
|
+
func aliasAcronym(normalized string) string {
|
|
488
|
+
tokens := strings.Fields(strings.TrimSpace(normalized))
|
|
489
|
+
if len(tokens) < 2 {
|
|
490
|
+
return ""
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
var builder strings.Builder
|
|
494
|
+
for _, token := range tokens {
|
|
495
|
+
if token == "" {
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
builder.WriteByte(token[0])
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
acronym := strings.TrimSpace(builder.String())
|
|
502
|
+
if len(acronym) < 2 {
|
|
503
|
+
return ""
|
|
504
|
+
}
|
|
505
|
+
return acronym
|
|
506
|
+
}
|
|
507
|
+
|
|
481
508
|
func aliasSet(aliases []string) map[string]struct{} {
|
|
482
509
|
set := map[string]struct{}{}
|
|
483
510
|
for _, alias := range aliases {
|