arccos-api 0.2.1__tar.gz → 0.3.0__tar.gz

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.
Files changed (37) hide show
  1. {arccos_api-0.2.1 → arccos_api-0.3.0}/PKG-INFO +28 -4
  2. {arccos_api-0.2.1 → arccos_api-0.3.0}/README.md +26 -3
  3. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/__init__.py +12 -1
  4. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/cli/__init__.py +2 -0
  5. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/cli/stats.py +132 -2
  6. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/PKG-INFO +28 -4
  7. {arccos_api-0.2.1 → arccos_api-0.3.0}/pyproject.toml +2 -1
  8. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_cli.py +115 -0
  9. {arccos_api-0.2.1 → arccos_api-0.3.0}/MANIFEST.in +0 -0
  10. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/__main__.py +0 -0
  11. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/_http.py +0 -0
  12. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/auth.py +0 -0
  13. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/cli/_helpers.py +0 -0
  14. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/cli/auth.py +0 -0
  15. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/cli/rounds.py +0 -0
  16. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/client.py +0 -0
  17. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/exceptions.py +0 -0
  18. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/__init__.py +0 -0
  19. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/clubs.py +0 -0
  20. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/courses.py +0 -0
  21. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/handicap.py +0 -0
  22. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/rounds.py +0 -0
  23. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/resources/stats.py +0 -0
  24. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos/types.py +0 -0
  25. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/SOURCES.txt +0 -0
  26. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/dependency_links.txt +0 -0
  27. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/entry_points.txt +0 -0
  28. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/requires.txt +0 -0
  29. {arccos_api-0.2.1 → arccos_api-0.3.0}/arccos_api.egg-info/top_level.txt +0 -0
  30. {arccos_api-0.2.1 → arccos_api-0.3.0}/setup.cfg +0 -0
  31. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_auth.py +0 -0
  32. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_client.py +0 -0
  33. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_exceptions.py +0 -0
  34. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_http.py +0 -0
  35. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_resources.py +0 -0
  36. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_rounds.py +0 -0
  37. {arccos_api-0.2.1 → arccos_api-0.3.0}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arccos-api
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Unofficial Python CLI and client library for the Arccos Golf API
5
5
  Author: Paul Frederiksen
6
6
  License: MIT
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
22
  Requires-Python: >=3.11
22
23
  Description-Content-Type: text/markdown
@@ -83,9 +84,11 @@ arccos clubs # smart club distances
83
84
  | `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
84
85
  | `arccos overview` | Performance summary — scoring avg + handicap breakdown |
85
86
  | `arccos scoring` | Scoring trend with visual bar chart |
86
- | `arccos courses` | List all courses you've played |
87
+ | `arccos courses` | List courses you've played (or `--search` any course) |
87
88
  | `arccos pace` | Pace of play analysis by course (color-coded) |
88
- | `arccos stats <id>` | Strokes gained analysis for a round |
89
+ | `arccos stats` | Strokes gained analysis (defaults to latest round) |
90
+ | `arccos club-shots <id>` | Shot history for a specific club |
91
+ | `arccos up-and-down` | Scramble / strokes-to-get-down stats |
89
92
  | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
90
93
  | `arccos logout` | Clear cached credentials |
91
94
 
@@ -116,9 +119,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
116
119
  arccos bests # all-time personal bests
117
120
  arccos overview # scoring avg + handicap breakdown
118
121
  arccos scoring # scoring trend with bar chart
122
+ arccos up-and-down # scramble stats
119
123
 
120
124
  # Courses
121
125
  arccos courses # all courses played
126
+ arccos courses --search "torrey" # search any course by name
127
+
128
+ # Club detail
129
+ arccos club-shots 1 # shot history for club ID 1
122
130
 
123
131
  # Pace of play
124
132
  arccos pace # all rounds, slowest courses first
@@ -208,6 +216,13 @@ export ARCCOS_PASSWORD="your_password"
208
216
  arccos rounds
209
217
  ```
210
218
 
219
+ Other environment variables:
220
+
221
+ ```bash
222
+ export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
223
+ export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
224
+ ```
225
+
211
226
  > **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
212
227
 
213
228
  ## Python Library
@@ -257,6 +272,13 @@ for c in pace["course_averages"][:5]:
257
272
 
258
273
  # Personal bests
259
274
  bests = client.stats.personal_bests()
275
+
276
+ # Iterate ALL rounds (auto-pagination)
277
+ for r in client.rounds.iter_all():
278
+ print(r["roundId"], r["noOfShots"])
279
+
280
+ # Course search
281
+ results = client.courses.search("Torrey Pines")
260
282
  ```
261
283
 
262
284
  ## API Reference
@@ -290,7 +312,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
290
312
  | Courses played | GET | `/users/{userId}/coursesPlayed` |
291
313
  | Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
292
314
  | Course search | GET | `/v2/courses?search={query}` |
315
+ | Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
293
316
  | Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
317
+ | Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
294
318
 
295
319
  ### Key data structures
296
320
 
@@ -343,7 +367,7 @@ python3 -m venv .venv
343
367
  source .venv/bin/activate
344
368
  pip install -e ".[dev]"
345
369
 
346
- pytest # tests with coverage (88%+, 132 tests)
370
+ pytest # tests with coverage (87%+, 139 tests)
347
371
  ruff check arccos/ # lint
348
372
  mypy arccos/ # type check
349
373
  ```
@@ -49,9 +49,11 @@ arccos clubs # smart club distances
49
49
  | `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
50
50
  | `arccos overview` | Performance summary — scoring avg + handicap breakdown |
51
51
  | `arccos scoring` | Scoring trend with visual bar chart |
52
- | `arccos courses` | List all courses you've played |
52
+ | `arccos courses` | List courses you've played (or `--search` any course) |
53
53
  | `arccos pace` | Pace of play analysis by course (color-coded) |
54
- | `arccos stats <id>` | Strokes gained analysis for a round |
54
+ | `arccos stats` | Strokes gained analysis (defaults to latest round) |
55
+ | `arccos club-shots <id>` | Shot history for a specific club |
56
+ | `arccos up-and-down` | Scramble / strokes-to-get-down stats |
55
57
  | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
56
58
  | `arccos logout` | Clear cached credentials |
57
59
 
@@ -82,9 +84,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
82
84
  arccos bests # all-time personal bests
83
85
  arccos overview # scoring avg + handicap breakdown
84
86
  arccos scoring # scoring trend with bar chart
87
+ arccos up-and-down # scramble stats
85
88
 
86
89
  # Courses
87
90
  arccos courses # all courses played
91
+ arccos courses --search "torrey" # search any course by name
92
+
93
+ # Club detail
94
+ arccos club-shots 1 # shot history for club ID 1
88
95
 
89
96
  # Pace of play
90
97
  arccos pace # all rounds, slowest courses first
@@ -174,6 +181,13 @@ export ARCCOS_PASSWORD="your_password"
174
181
  arccos rounds
175
182
  ```
176
183
 
184
+ Other environment variables:
185
+
186
+ ```bash
187
+ export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
188
+ export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
189
+ ```
190
+
177
191
  > **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
178
192
 
179
193
  ## Python Library
@@ -223,6 +237,13 @@ for c in pace["course_averages"][:5]:
223
237
 
224
238
  # Personal bests
225
239
  bests = client.stats.personal_bests()
240
+
241
+ # Iterate ALL rounds (auto-pagination)
242
+ for r in client.rounds.iter_all():
243
+ print(r["roundId"], r["noOfShots"])
244
+
245
+ # Course search
246
+ results = client.courses.search("Torrey Pines")
226
247
  ```
227
248
 
228
249
  ## API Reference
@@ -256,7 +277,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
256
277
  | Courses played | GET | `/users/{userId}/coursesPlayed` |
257
278
  | Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
258
279
  | Course search | GET | `/v2/courses?search={query}` |
280
+ | Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
259
281
  | Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
282
+ | Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
260
283
 
261
284
  ### Key data structures
262
285
 
@@ -309,7 +332,7 @@ python3 -m venv .venv
309
332
  source .venv/bin/activate
310
333
  pip install -e ".[dev]"
311
334
 
312
- pytest # tests with coverage (88%+, 132 tests)
335
+ pytest # tests with coverage (87%+, 139 tests)
313
336
  ruff check arccos/ # lint
314
337
  mypy arccos/ # type check
315
338
  ```
@@ -10,6 +10,7 @@ Quick start:
10
10
  """
11
11
 
12
12
  import logging as _logging
13
+ import os as _os
13
14
  import re as _re
14
15
  from importlib.metadata import version as _pkg_version
15
16
 
@@ -40,7 +41,17 @@ class _SensitiveFilter(_logging.Filter):
40
41
  return True
41
42
 
42
43
 
43
- _logging.getLogger("arccos").addFilter(_SensitiveFilter())
44
+ _logger = _logging.getLogger("arccos")
45
+ _logger.addFilter(_SensitiveFilter())
46
+
47
+ # Allow ARCCOS_LOG_LEVEL=DEBUG (or INFO, WARNING, etc.) to enable logging
48
+ _log_level = _os.environ.get("ARCCOS_LOG_LEVEL")
49
+ if _log_level:
50
+ _logger.setLevel(getattr(_logging, _log_level.upper(), _logging.WARNING))
51
+ if not _logger.handlers:
52
+ _handler = _logging.StreamHandler()
53
+ _handler.setFormatter(_logging.Formatter("%(name)s %(levelname)s: %(message)s"))
54
+ _logger.addHandler(_handler)
44
55
 
45
56
  __version__ = _pkg_version("arccos-api")
46
57
  __all__ = [
@@ -31,6 +31,8 @@ def cli():
31
31
  arccos bests Personal bests (all-time records)
32
32
  arccos overview Overall stats summary
33
33
  arccos scoring Scoring trend over recent rounds
34
+ arccos club-shots <id> Shot history for a specific club
35
+ arccos up-and-down Scramble / up-and-down stats
34
36
  arccos export Export rounds to JSON/CSV
35
37
  arccos logout Clear cached credentials
36
38
 
@@ -16,10 +16,12 @@ from ._helpers import _get_client, _output_json, console, err_console
16
16
  def register(cli: click.Group) -> None:
17
17
  cli.add_command(handicap)
18
18
  cli.add_command(clubs)
19
+ cli.add_command(club_shots)
19
20
  cli.add_command(stats)
20
21
  cli.add_command(bests)
21
22
  cli.add_command(overview)
22
23
  cli.add_command(courses)
24
+ cli.add_command(up_and_down)
23
25
 
24
26
 
25
27
  # ---------------------------------------------------------------------------
@@ -462,14 +464,23 @@ def overview(limit: int, as_json: bool):
462
464
  # ---------------------------------------------------------------------------
463
465
 
464
466
  @click.command()
467
+ @click.option("--search", "-s", default=None, help="Search for a course by name.")
465
468
  @click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
466
- def courses(as_json: bool):
467
- """List courses you have played."""
469
+ def courses(search: str, as_json: bool):
470
+ """List courses you have played, or search for any course."""
468
471
  client = _get_client()
469
472
 
470
473
  with console.status("Fetching courses\u2026"):
471
474
  data = client.courses.played()
472
475
 
476
+ if search:
477
+ needle = search.lower()
478
+ data = [
479
+ c for c in data
480
+ if needle in (c.get("name") or c.get("courseName") or "").lower()
481
+ or needle in (c.get("city") or "").lower()
482
+ ]
483
+
473
484
  if as_json:
474
485
  _output_json(data)
475
486
  return
@@ -502,3 +513,122 @@ def courses(as_json: bool):
502
513
  table.add_row(name, city, state, played, cid)
503
514
 
504
515
  console.print(table)
516
+
517
+
518
+ # ---------------------------------------------------------------------------
519
+ # club-shots
520
+ # ---------------------------------------------------------------------------
521
+
522
+ @click.command(name="club-shots")
523
+ @click.argument("club_id", type=int)
524
+ @click.option("--limit", "-n", default=20, show_default=True, help="Number of shots to show.")
525
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
526
+ def club_shots(club_id: int, limit: int, as_json: bool):
527
+ """Show individual shot history for a specific club.
528
+
529
+ CLUB_ID is the bag-specific club ID (shown in `arccos clubs --json`).
530
+ """
531
+ client = _get_client()
532
+
533
+ with console.status("Fetching shots\u2026"):
534
+ profile = client._http.get(f"/users/{client.user_id}")
535
+ bag_id = profile.get("bagId")
536
+ if not bag_id:
537
+ err_console.print("[red]Could not determine bag ID.[/red]")
538
+ sys.exit(1)
539
+ data = client.clubs.club_shots(str(bag_id), club_id, limit=limit)
540
+
541
+ if as_json:
542
+ _output_json(data)
543
+ return
544
+
545
+ if not data:
546
+ console.print("[dim]No shots found for this club.[/dim]")
547
+ return
548
+
549
+ from arccos.resources.clubs import CLUB_TYPE_NAMES
550
+
551
+ # Try to resolve club name
552
+ try:
553
+ bag = client.clubs.bag(str(bag_id))
554
+ bag_club = next(
555
+ (c for c in bag.get("clubs", []) if c["clubId"] == club_id), {}
556
+ )
557
+ club_type = bag_club.get("clubType", club_id)
558
+ club_name = CLUB_TYPE_NAMES.get(club_type, f"Club {club_id}")
559
+ except Exception:
560
+ club_name = f"Club {club_id}"
561
+
562
+ table = Table(
563
+ box=box.ROUNDED,
564
+ header_style="bold cyan",
565
+ title=f"[bold]Shot History — {club_name}[/bold] [dim]({len(data)} shots)[/dim]",
566
+ )
567
+ table.add_column("Date", style="white", no_wrap=True)
568
+ table.add_column("Distance", justify="right", style="green bold")
569
+ table.add_column("Terrain", style="dim")
570
+
571
+ for shot in data:
572
+ date = shot.get("shotTime", shot.get("startTime", ""))[:10]
573
+ dist = shot.get("distance")
574
+ dist_s = f"{dist:.0f}y" if dist else "\u2014"
575
+ terrain = str(shot.get("userStartTerrainOverride", ""))
576
+ table.add_row(date, dist_s, terrain)
577
+
578
+ console.print(table)
579
+
580
+
581
+ # ---------------------------------------------------------------------------
582
+ # up-and-down
583
+ # ---------------------------------------------------------------------------
584
+
585
+ @click.command(name="up-and-down")
586
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
587
+ def up_and_down(as_json: bool):
588
+ """Show strokes-to-get-down (up-and-down / scramble) stats."""
589
+ client = _get_client()
590
+
591
+ from arccos.exceptions import ArccosError
592
+
593
+ try:
594
+ with console.status("Fetching up-and-down stats\u2026"):
595
+ data = client.stats.strokes_to_get_down()
596
+ except ArccosError as e:
597
+ err_console.print(f"[red]Failed:[/red] {e}")
598
+ err_console.print(
599
+ "[dim]This endpoint may not be available for all accounts.[/dim]"
600
+ )
601
+ sys.exit(1)
602
+
603
+ if as_json:
604
+ _output_json(data)
605
+ return
606
+
607
+ if not data:
608
+ console.print("[dim]No up-and-down data available.[/dim]")
609
+ return
610
+
611
+ # Display whatever the API returns as a key-value table
612
+ table = Table(
613
+ box=box.ROUNDED,
614
+ header_style="bold cyan",
615
+ title="[bold]Strokes to Get Down[/bold]",
616
+ )
617
+ table.add_column("Stat", style="white bold")
618
+ table.add_column("Value", justify="right", style="bold")
619
+
620
+ for key, val in data.items():
621
+ if isinstance(val, (int, float)):
622
+ display = f"{val:.1f}" if isinstance(val, float) else str(val)
623
+ table.add_row(key, display)
624
+
625
+ if table.row_count == 0:
626
+ # Nested structure — try one level deeper
627
+ for section, vals in data.items():
628
+ if isinstance(vals, dict):
629
+ for k, v in vals.items():
630
+ if isinstance(v, (int, float)):
631
+ display = f"{v:.1f}" if isinstance(v, float) else str(v)
632
+ table.add_row(f"{section}.{k}", display)
633
+
634
+ console.print(table)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arccos-api
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Unofficial Python CLI and client library for the Arccos Golf API
5
5
  Author: Paul Frederiksen
6
6
  License: MIT
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
22
  Requires-Python: >=3.11
22
23
  Description-Content-Type: text/markdown
@@ -83,9 +84,11 @@ arccos clubs # smart club distances
83
84
  | `arccos bests` | All-time personal bests (lowest score, longest drive, etc.) |
84
85
  | `arccos overview` | Performance summary — scoring avg + handicap breakdown |
85
86
  | `arccos scoring` | Scoring trend with visual bar chart |
86
- | `arccos courses` | List all courses you've played |
87
+ | `arccos courses` | List courses you've played (or `--search` any course) |
87
88
  | `arccos pace` | Pace of play analysis by course (color-coded) |
88
- | `arccos stats <id>` | Strokes gained analysis for a round |
89
+ | `arccos stats` | Strokes gained analysis (defaults to latest round) |
90
+ | `arccos club-shots <id>` | Shot history for a specific club |
91
+ | `arccos up-and-down` | Scramble / strokes-to-get-down stats |
89
92
  | `arccos export` | Export rounds to JSON, CSV, or NDJSON (`--detail` for hole data) |
90
93
  | `arccos logout` | Clear cached credentials |
91
94
 
@@ -116,9 +119,14 @@ arccos clubs --after 2025-01-01 # distances from this year only
116
119
  arccos bests # all-time personal bests
117
120
  arccos overview # scoring avg + handicap breakdown
118
121
  arccos scoring # scoring trend with bar chart
122
+ arccos up-and-down # scramble stats
119
123
 
120
124
  # Courses
121
125
  arccos courses # all courses played
126
+ arccos courses --search "torrey" # search any course by name
127
+
128
+ # Club detail
129
+ arccos club-shots 1 # shot history for club ID 1
122
130
 
123
131
  # Pace of play
124
132
  arccos pace # all rounds, slowest courses first
@@ -208,6 +216,13 @@ export ARCCOS_PASSWORD="your_password"
208
216
  arccos rounds
209
217
  ```
210
218
 
219
+ Other environment variables:
220
+
221
+ ```bash
222
+ export ARCCOS_TIMEOUT=30 # request timeout in seconds (default: 15)
223
+ export ARCCOS_LOG_LEVEL=DEBUG # enable debug logging (DEBUG, INFO, WARNING)
224
+ ```
225
+
211
226
  > **Security note:** Environment variables may be visible in process listings (`ps`) and shell history. Prefer `arccos login` for interactive use.
212
227
 
213
228
  ## Python Library
@@ -257,6 +272,13 @@ for c in pace["course_averages"][:5]:
257
272
 
258
273
  # Personal bests
259
274
  bests = client.stats.personal_bests()
275
+
276
+ # Iterate ALL rounds (auto-pagination)
277
+ for r in client.rounds.iter_all():
278
+ print(r["roundId"], r["noOfShots"])
279
+
280
+ # Course search
281
+ results = client.courses.search("Torrey Pines")
260
282
  ```
261
283
 
262
284
  ## API Reference
@@ -290,7 +312,9 @@ All data calls go to `https://api.arccosgolf.com` with `Authorization: Bearer <t
290
312
  | Courses played | GET | `/users/{userId}/coursesPlayed` |
291
313
  | Course metadata | GET | `/courses/{courseId}?courseVersion=1` |
292
314
  | Course search | GET | `/v2/courses?search={query}` |
315
+ | Club shots | GET | `/users/{userId}/bags/{bagId}/clubs/{clubId}/shots` |
293
316
  | Personal bests | GET | `/users/{userId}/personalBests?tags=allTimeBest` |
317
+ | Strokes to get down | GET | `/v2/sga/strokes-to-get-down` |
294
318
 
295
319
  ### Key data structures
296
320
 
@@ -343,7 +367,7 @@ python3 -m venv .venv
343
367
  source .venv/bin/activate
344
368
  pip install -e ".[dev]"
345
369
 
346
- pytest # tests with coverage (88%+, 132 tests)
370
+ pytest # tests with coverage (87%+, 139 tests)
347
371
  ruff check arccos/ # lint
348
372
  mypy arccos/ # type check
349
373
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arccos-api"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Unofficial Python CLI and client library for the Arccos Golf API"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.11",
22
22
  "Programming Language :: Python :: 3.12",
23
23
  "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
24
25
  "Topic :: Software Development :: Libraries :: Python Modules",
25
26
  ]
26
27
  dependencies = [
@@ -726,3 +726,118 @@ class TestExportDetailFlag:
726
726
  data = json.loads(output[json_start:])
727
727
  assert "holes" in data[0]
728
728
  assert data[0]["holes"][0]["holeId"] == 1
729
+
730
+
731
+ # ---------------------------------------------------------------------------
732
+ # course search
733
+ # ---------------------------------------------------------------------------
734
+
735
+ class TestCourseSearchCommand:
736
+ @patch(_STATS_GET_CLIENT)
737
+ def test_courses_search(self, mock_get, runner):
738
+ client = _mock_client()
739
+ client.courses.played.return_value = [
740
+ {"courseId": 100, "name": "Torrey Pines South", "city": "San Diego",
741
+ "state": "CA"},
742
+ {"courseId": 101, "name": "Torrey Pines North", "city": "San Diego",
743
+ "state": "CA"},
744
+ {"courseId": 200, "name": "Augusta National", "city": "Augusta",
745
+ "state": "GA"},
746
+ ]
747
+ mock_get.return_value = client
748
+
749
+ result = runner.invoke(cli, ["courses", "--search", "torrey"])
750
+ assert result.exit_code == 0
751
+ assert "Torrey Pines South" in result.output
752
+ assert "Torrey Pines North" in result.output
753
+ assert "Augusta" not in result.output
754
+
755
+ @patch(_STATS_GET_CLIENT)
756
+ def test_courses_search_empty(self, mock_get, runner):
757
+ client = _mock_client()
758
+ client.courses.played.return_value = [
759
+ {"courseId": 100, "name": "Augusta National"},
760
+ ]
761
+ mock_get.return_value = client
762
+
763
+ result = runner.invoke(cli, ["courses", "--search", "xyznotfound"])
764
+ assert result.exit_code == 0
765
+ assert "No courses found" in result.output
766
+
767
+
768
+ # ---------------------------------------------------------------------------
769
+ # club-shots
770
+ # ---------------------------------------------------------------------------
771
+
772
+ class TestClubShotsCommand:
773
+ @patch(_STATS_GET_CLIENT)
774
+ def test_club_shots_table(self, mock_get, runner):
775
+ client = _mock_client()
776
+ client._http.get.return_value = {"bagId": "bag-1"}
777
+ client.clubs.club_shots.return_value = [
778
+ {"shotTime": "2025-06-01T12:00:00Z", "distance": 255.3,
779
+ "userStartTerrainOverride": 0},
780
+ {"shotTime": "2025-06-02T14:00:00Z", "distance": 248.1,
781
+ "userStartTerrainOverride": 0},
782
+ ]
783
+ client.clubs.bag.return_value = {
784
+ "clubs": [{"clubId": 1, "clubType": 1, "isDeleted": "F"}],
785
+ }
786
+ mock_get.return_value = client
787
+
788
+ result = runner.invoke(cli, ["club-shots", "1"])
789
+ assert result.exit_code == 0
790
+ assert "255y" in result.output
791
+ assert "248y" in result.output
792
+
793
+ @patch(_STATS_GET_CLIENT)
794
+ def test_club_shots_json(self, mock_get, runner):
795
+ client = _mock_client()
796
+ client._http.get.return_value = {"bagId": "bag-1"}
797
+ client.clubs.club_shots.return_value = [{"distance": 255.3}]
798
+ mock_get.return_value = client
799
+
800
+ result = runner.invoke(cli, ["club-shots", "--json", "1"])
801
+ data = json.loads(result.output)
802
+ assert data[0]["distance"] == 255.3
803
+
804
+ @patch(_STATS_GET_CLIENT)
805
+ def test_club_shots_empty(self, mock_get, runner):
806
+ client = _mock_client()
807
+ client._http.get.return_value = {"bagId": "bag-1"}
808
+ client.clubs.club_shots.return_value = []
809
+ mock_get.return_value = client
810
+
811
+ result = runner.invoke(cli, ["club-shots", "99"])
812
+ assert result.exit_code == 0
813
+ assert "No shots found" in result.output
814
+
815
+
816
+ # ---------------------------------------------------------------------------
817
+ # up-and-down
818
+ # ---------------------------------------------------------------------------
819
+
820
+ class TestUpAndDownCommand:
821
+ @patch(_STATS_GET_CLIENT)
822
+ def test_up_and_down_table(self, mock_get, runner):
823
+ client = _mock_client()
824
+ client.stats.strokes_to_get_down.return_value = {
825
+ "totalAttempts": 50,
826
+ "successRate": 0.42,
827
+ "avgProximity": 15.3,
828
+ }
829
+ mock_get.return_value = client
830
+
831
+ result = runner.invoke(cli, ["up-and-down"])
832
+ assert result.exit_code == 0
833
+ assert "50" in result.output
834
+
835
+ @patch(_STATS_GET_CLIENT)
836
+ def test_up_and_down_json(self, mock_get, runner):
837
+ client = _mock_client()
838
+ client.stats.strokes_to_get_down.return_value = {"totalAttempts": 50}
839
+ mock_get.return_value = client
840
+
841
+ result = runner.invoke(cli, ["up-and-down", "--json"])
842
+ data = json.loads(result.output)
843
+ assert data["totalAttempts"] == 50
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes