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.
@@ -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
- for _, match := range run.session.ScopedMatches() {
353
- seasonID := seasonForMatch(match, run.seasonHint)
354
- players, playerWarnings, hydrateErr := run.session.HydratePlayerMatchSummaries(ctx, match.ID)
355
- if hydrateErr != nil {
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
- warnings = append(warnings, playerWarnings...)
363
-
364
- for _, player := range players {
365
- totals := extractBowlingTotals(player)
366
- playerName := analysisDisplayPlayerName(s.resolver, player.PlayerID, player.PlayerName)
367
- teamName := analysisDisplayTeamName(s.resolver, player.TeamID, player.TeamName)
368
- row := analysisSourceRow{
369
- MatchID: strings.TrimSpace(player.MatchID),
370
- LeagueID: strings.TrimSpace(player.LeagueID),
371
- SeasonID: seasonID,
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
- key, dims := buildAnalysisGroup(row, groupBy)
387
- entry := agg[key]
388
- if entry == nil {
389
- entry = &analysisAggregate{row: dims, matchIDs: map[string]struct{}{}}
390
- agg[key] = entry
391
- }
392
- entry.matchIDs[row.MatchID] = struct{}{}
393
- entry.dots += row.Dots
394
- entry.sixesConceded += row.SixesConceded
395
- entry.balls += row.Balls
396
- entry.runsConceded += row.RunsConceded
397
- if row.EconomySample > 0 {
398
- entry.economyTotal += row.EconomySample
399
- entry.economyCount++
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 >= defaultRetryStatusCode && attempt < c.maxRetries {
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(repoRoot(t), "gg", "agent-outputs", "cricinfo-working-templates.tsv")
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(repoRoot(t), "gg", "agent-outputs", "cricinfo-field-path-catalog.txt")
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 {