sharpapi 0.2.5__tar.gz → 0.3.1__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.
@@ -10,3 +10,9 @@ build/
10
10
  .ruff_cache/
11
11
  *.egg
12
12
  .mypy_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .claude/
16
+ .DS_Store
17
+ tests/
18
+ .benchmarks/
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `sharpapi` Python SDK are documented here.
4
+
5
+ ## 0.3.1 — 2026-05-06
6
+
7
+ ### Added — TeamRef metadata
8
+
9
+ `TeamRef` now exposes five additional optional fields:
10
+
11
+ - `logo` — full CDN URL. ~93% of teams are populated.
12
+ - `city` — e.g. `"Arizona"` for the Diamondbacks.
13
+ - `mascot` — e.g. `"Diamondbacks"`.
14
+ - `conference` — e.g. `"NL"`, `"AFC"`, `"Western"`.
15
+ - `division` — e.g. `"West Division"`, `"NL East"`, `"Pacific Division"`.
16
+
17
+ All five default to `None` and are additive — existing 0.3.0 code keeps
18
+ working unchanged.
19
+
20
+ ## 0.3.0 — 2026-05-06
21
+
22
+ ### Added — nested refs
23
+
24
+ Every odds row, opportunity row, and reference-list row may now carry
25
+ optional structured reference objects alongside the existing flat fields.
26
+ All new fields are **optional and additive** — clients on older API
27
+ versions (or talking to older API servers) see `None` and behave
28
+ identically.
29
+
30
+ New models:
31
+
32
+ - `TeamRef` — `id`, `numerical_id`, `name`, `abbreviation` (latter only on
33
+ team-sport competitors)
34
+ - `SportRef` — `id`, `name`, `numerical_id`
35
+ - `EntityRef` — `id`, `label`, `numerical_id` (used for league / market /
36
+ sportsbook refs)
37
+
38
+ New optional fields:
39
+
40
+ - `OddsLine`, `EVOpportunity`, `ArbitrageOpportunity`, `MiddleOpportunity`,
41
+ `LowHoldOpportunity` — all gain `home`, `away`, `sport_ref`, `league_ref`,
42
+ `market_ref`, `sportsbook_ref` (legs / opps without a single book skip
43
+ `sportsbook_ref`).
44
+ - `ArbitrageLeg` — gains `sportsbook_ref`.
45
+ - `ClosingOddsLine` — gains `market_ref`, `sportsbook_ref`.
46
+ - `ClosingSnapshot` — gains `home`, `away`, `sport_ref`, `league_ref`.
47
+ - `Sport`, `League`, `Sportsbook`, `Market` — gain `numerical_id`.
48
+ - `Event` — gains `home`, `away`, `sport_ref`, `league_ref`.
49
+
50
+ New reference model:
51
+
52
+ - `Team` — for the `/teams` reference endpoint, includes optional
53
+ `abbreviation` and `numerical_id`.
54
+
55
+ ### Backward compatibility
56
+
57
+ No existing field was renamed, retyped, or removed. Code that does not
58
+ reference the new attributes continues to work without changes.
sharpapi-0.3.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SharpAPI LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,13 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sharpapi
3
- Version: 0.2.5
3
+ Version: 0.3.1
4
4
  Summary: Official Python SDK for the SharpAPI real-time sports betting odds API
5
5
  Project-URL: Homepage, https://sharpapi.io
6
6
  Project-URL: Documentation, https://docs.sharpapi.io/sdks/python
7
7
  Project-URL: Repository, https://github.com/Sharp-API/sharpapi-python
8
8
  Project-URL: Changelog, https://github.com/Sharp-API/sharpapi-python/releases
9
- Author-email: SharpAPI <support@sharpapi.io>
9
+ Author-email: SharpAPI <hello@sharpapi.io>
10
10
  License-Expression: MIT
11
+ License-File: LICENSE
11
12
  Keywords: api,arbitrage,ev,odds,pinnacle,real-time,sports-betting
12
13
  Classifier: Development Status :: 4 - Beta
13
14
  Classifier: Intended Audience :: Developers
@@ -0,0 +1,25 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ If you believe you have found a security vulnerability in this SDK or in
6
+ the SharpAPI service, please report it privately to:
7
+
8
+ **hello@sharpapi.io** (subject line: `[SECURITY] <short summary>`)
9
+
10
+ Please do not open a public GitHub issue for security reports.
11
+
12
+ We will acknowledge receipt within 72 hours and aim to provide a status
13
+ update within 7 days. If the issue is confirmed, we will work with you on
14
+ disclosure timing.
15
+
16
+ ## Scope
17
+
18
+ In scope:
19
+ - This SDK package and its published artifact on PyPI
20
+ - The SharpAPI HTTP and WebSocket APIs (`api.sharpapi.io`, `ws.sharpapi.io`)
21
+
22
+ Out of scope:
23
+ - Findings in third-party dependencies (please report those upstream)
24
+ - Denial of service via brute-force or volumetric attacks against the API
25
+ - Issues that require physical access to a user's device
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sharpapi"
7
- version = "0.2.5"
7
+ version = "0.3.1"
8
8
  description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
12
- authors = [{ name = "SharpAPI", email = "support@sharpapi.io" }]
12
+ authors = [{ name = "SharpAPI", email = "hello@sharpapi.io" }]
13
13
  keywords = ["sports-betting", "odds", "arbitrage", "ev", "api", "real-time", "pinnacle"]
14
14
  classifiers = [
15
15
  "Development Status :: 4 - Beta",
@@ -41,6 +41,25 @@ Changelog = "https://github.com/Sharp-API/sharpapi-python/releases"
41
41
  [tool.hatch.build.targets.wheel]
42
42
  packages = ["src/sharpapi"]
43
43
 
44
+ # Explicit sdist whitelist. Hatchling's default sdist would otherwise
45
+ # include everything not gitignored (CI workflows, dependabot config,
46
+ # .gitignore itself), all of which would land on PyPI on every release.
47
+ # Keep this in sync with what we actually want users who do
48
+ # `pip install --no-binary :all: sharpapi` to receive.
49
+ [tool.hatch.build.targets.sdist]
50
+ include = [
51
+ "src/sharpapi",
52
+ "README.md",
53
+ "LICENSE",
54
+ "CHANGELOG.md",
55
+ "SECURITY.md",
56
+ "pyproject.toml",
57
+ ]
58
+ exclude = [
59
+ ".gitignore",
60
+ ".github",
61
+ ]
62
+
44
63
  [tool.ruff]
45
64
  target-version = "py310"
46
65
  line-length = 100
@@ -38,6 +38,7 @@ from .models import (
38
38
  ArbitrageOpportunity,
39
39
  ClosingOddsLine,
40
40
  ClosingSnapshot,
41
+ EntityRef,
41
42
  Event,
42
43
  EVOpportunity,
43
44
  GameState,
@@ -53,11 +54,14 @@ from .models import (
53
54
  RateLimitInfo,
54
55
  ResponseMeta,
55
56
  Sport,
57
+ SportRef,
56
58
  Sportsbook,
59
+ Team,
60
+ TeamRef,
57
61
  )
58
62
  from .streaming import EventStream
59
63
 
60
- __version__ = "0.2.5"
64
+ __version__ = "0.3.1"
61
65
 
62
66
  __all__ = [
63
67
  # Clients
@@ -71,6 +75,7 @@ __all__ = [
71
75
  "ArbitrageOpportunity",
72
76
  "ClosingOddsLine",
73
77
  "ClosingSnapshot",
78
+ "EntityRef",
74
79
  "EVOpportunity",
75
80
  "Event",
76
81
  "GameState",
@@ -86,7 +91,10 @@ __all__ = [
86
91
  "RateLimitInfo",
87
92
  "ResponseMeta",
88
93
  "Sport",
94
+ "SportRef",
89
95
  "Sportsbook",
96
+ "Team",
97
+ "TeamRef",
90
98
  # Streaming
91
99
  "EventStream",
92
100
  # Exceptions
@@ -29,6 +29,7 @@ from .models import (
29
29
  ClosingSnapshot,
30
30
  Event,
31
31
  EVOpportunity,
32
+ GameState,
32
33
  League,
33
34
  LowHoldOpportunity,
34
35
  Market,
@@ -103,6 +104,7 @@ class AsyncSharpAPI:
103
104
  self.arbitrage = _AsyncArbitrageResource(self)
104
105
  self.middles = _AsyncMiddlesResource(self)
105
106
  self.low_hold = _AsyncLowHoldResource(self)
107
+ self.gamestate = _AsyncGameStateResource(self)
106
108
  self.sports = _AsyncSportsResource(self)
107
109
  self.leagues = _AsyncLeaguesResource(self)
108
110
  self.sportsbooks = _AsyncSportsbooksResource(self)
@@ -426,6 +428,41 @@ class _AsyncLowHoldResource:
426
428
  return parse_response(data, LowHoldOpportunity)
427
429
 
428
430
 
431
+ class _AsyncGameStateResource:
432
+ """Async access to live game state — scores, period, clock —
433
+ merged across sportsbooks.
434
+
435
+ Requires the Game State add-on ($79/mo) or Enterprise tier.
436
+ """
437
+
438
+ def __init__(self, client: AsyncSharpAPI):
439
+ self._client = client
440
+
441
+ async def get(self, sport: str | None = None) -> dict[str, dict[str, GameState]]:
442
+ """Fetch the current game state.
443
+
444
+ Args:
445
+ sport: Limit to a single sport (e.g. ``"basketball"``).
446
+ Omit to fetch every sport at once.
447
+
448
+ Returns:
449
+ Nested mapping ``{sport: {event_id: GameState}}``.
450
+ """
451
+ path = f"/gamestate/{sport}" if sport else "/gamestate"
452
+ data = await self._client._get(path)
453
+ raw = data.get("data", {}) or {}
454
+ result: dict[str, dict[str, GameState]] = {}
455
+ for sport_key, events in raw.items():
456
+ if not isinstance(events, dict):
457
+ continue
458
+ result[sport_key] = {
459
+ eid: GameState.model_validate(state)
460
+ for eid, state in events.items()
461
+ if isinstance(state, dict)
462
+ }
463
+ return result
464
+
465
+
429
466
  class _AsyncSportsResource:
430
467
  def __init__(self, client: AsyncSharpAPI):
431
468
  self._client = client
@@ -29,6 +29,7 @@ from .models import (
29
29
  ClosingSnapshot,
30
30
  Event,
31
31
  EVOpportunity,
32
+ GameState,
32
33
  League,
33
34
  LowHoldOpportunity,
34
35
  Market,
@@ -111,6 +112,7 @@ class SharpAPI:
111
112
  self.arbitrage = _ArbitrageResource(self)
112
113
  self.middles = _MiddlesResource(self)
113
114
  self.low_hold = _LowHoldResource(self)
115
+ self.gamestate = _GameStateResource(self)
114
116
  self.sports = _SportsResource(self)
115
117
  self.leagues = _LeaguesResource(self)
116
118
  self.sportsbooks = _SportsbooksResource(self)
@@ -542,6 +544,44 @@ class _LowHoldResource:
542
544
  return _parse_response(data, LowHoldOpportunity)
543
545
 
544
546
 
547
+ class _GameStateResource:
548
+ """Live game state — scores, period, clock — merged across sportsbooks.
549
+
550
+ Requires the Game State add-on ($79/mo) or Enterprise tier.
551
+ Pair with EV / arb / low-hold rows: those endpoints no longer carry
552
+ ``game_state`` themselves — look up the row's ``event_id`` here.
553
+ """
554
+
555
+ def __init__(self, client: SharpAPI):
556
+ self._client = client
557
+
558
+ def get(self, sport: str | None = None) -> dict[str, dict[str, GameState]]:
559
+ """Fetch the current game state.
560
+
561
+ Args:
562
+ sport: Limit to a single sport (e.g. ``"basketball"``,
563
+ ``"football"``). Omit to fetch every sport at once.
564
+
565
+ Returns:
566
+ Nested mapping ``{sport: {event_id: GameState}}``. Look up an
567
+ opportunity's state with
568
+ ``result.get(opp.sport, {}).get(opp.event_id)``.
569
+ """
570
+ path = f"/gamestate/{sport}" if sport else "/gamestate"
571
+ data = self._client._get(path)
572
+ raw = data.get("data", {}) or {}
573
+ result: dict[str, dict[str, GameState]] = {}
574
+ for sport_key, events in raw.items():
575
+ if not isinstance(events, dict):
576
+ continue
577
+ result[sport_key] = {
578
+ eid: GameState.model_validate(state)
579
+ for eid, state in events.items()
580
+ if isinstance(state, dict)
581
+ }
582
+ return result
583
+
584
+
545
585
  class _SportsResource:
546
586
  def __init__(self, client: SharpAPI):
547
587
  self._client = client
@@ -781,6 +821,15 @@ class _StreamResource:
781
821
  "market": market,
782
822
  })
783
823
 
824
+ def gamestate(self) -> EventStream:
825
+ """Stream live game state updates (scores, period, clock).
826
+
827
+ Emits ``gamestate:snapshot`` (initial dump on connect) and
828
+ ``gamestate:update`` / ``gamestate:removed`` events. Requires the
829
+ Game State add-on or Enterprise tier.
830
+ """
831
+ return self._build_stream("/stream/gamestate")
832
+
784
833
 
785
834
  # =============================================================================
786
835
  # Helpers
@@ -1,8 +1,7 @@
1
1
  """SharpAPI exceptions and canonical error-code registry.
2
2
 
3
- The error codes here mirror ``pkg/errcodes/errcodes.go`` in sharp-api-go, which
4
- is the single source of truth for every code the API emits. Keep this file in
5
- sync when new codes are added upstream.
3
+ The codes here mirror the canonical set the SharpAPI server emits.
4
+ Keep this file in sync when new codes are added upstream.
6
5
  """
7
6
 
8
7
  from __future__ import annotations
@@ -65,9 +64,10 @@ class StreamError(SharpAPIError):
65
64
  # =============================================================================
66
65
  # Canonical error-code registry
67
66
  #
68
- # Mirrors sharp-api-go/pkg/errcodes/errcodes.go. When upstream adds a new code,
69
- # add it here too and update the matching description. Each code maps to the
70
- # Python exception class that ``handle_errors`` (in ``_base.py``) raises for it.
67
+ # Mirrors the canonical SharpAPI server error-code set. When upstream adds
68
+ # a new code, add it here too and update the matching description. Each
69
+ # code maps to the Python exception class that ``handle_errors`` (in
70
+ # ``_base.py``) raises for it.
71
71
  # =============================================================================
72
72
 
73
73
  # HTTP error codes — emitted via REST handlers (httputil.WriteJSONError).
@@ -82,6 +82,8 @@ INVALID_TOKEN = "invalid_token"
82
82
  METHOD_NOT_ALLOWED = "method_not_allowed"
83
83
  MISSING_API_KEY = "missing_api_key"
84
84
  NOT_FOUND = "not_found"
85
+ NOT_READY = "not_ready"
86
+ OFFSET_TOO_LARGE = "offset_too_large"
85
87
  RATE_LIMITED = "rate_limited"
86
88
  SERVICE_UNAVAILABLE = "service_unavailable"
87
89
  TIER_RESTRICTED = "tier_restricted"
@@ -113,6 +115,11 @@ ERROR_CODE_DESCRIPTIONS: dict[str, str] = {
113
115
  METHOD_NOT_ALLOWED: "HTTP method not allowed on this endpoint.",
114
116
  MISSING_API_KEY: "No API key provided.",
115
117
  NOT_FOUND: "Resource not found.",
118
+ NOT_READY: "A required backing store is not yet ready to serve this request; retry shortly.",
119
+ OFFSET_TOO_LARGE: (
120
+ "offset exceeds the per-endpoint maximum; "
121
+ "use cursor-based pagination or advance `since`."
122
+ ),
116
123
  RATE_LIMITED: "Rate limit exceeded; see Retry-After header.",
117
124
  SERVICE_UNAVAILABLE: "Service is temporarily unavailable.",
118
125
  TIER_RESTRICTED: "Current subscription tier does not include this feature.",
@@ -148,6 +155,7 @@ ERROR_CODE_TO_EXCEPTION: dict[str, type[SharpAPIError]] = {
148
155
  TOO_MANY_STREAMS: RateLimitedError,
149
156
  # Validation
150
157
  VALIDATION_ERROR: ValidationError,
158
+ OFFSET_TOO_LARGE: ValidationError,
151
159
  # Streaming frames
152
160
  WS_ALREADY_AUTHENTICATED: StreamError,
153
161
  WS_INVALID_MESSAGE: StreamError,
@@ -160,15 +168,16 @@ ERROR_CODE_TO_EXCEPTION: dict[str, type[SharpAPIError]] = {
160
168
  INTERNAL_ERROR: SharpAPIError,
161
169
  METHOD_NOT_ALLOWED: SharpAPIError,
162
170
  NOT_FOUND: SharpAPIError,
171
+ NOT_READY: SharpAPIError,
163
172
  SERVICE_UNAVAILABLE: SharpAPIError,
164
173
  UNKNOWN_ENDPOINT: SharpAPIError,
165
174
  UPSTREAM_ERROR: SharpAPIError,
166
175
  }
167
176
 
168
- # Deprecated aliases. ``bad_request`` and ``invalid_request`` were both collapsed
169
- # into ``validation_error`` in sharp-api-go. Kept here so that older API
170
- # responses (or user code still checking these strings) resolve correctly.
171
- # TODO: remove after 2026-10.
177
+ # Deprecated aliases. ``bad_request`` and ``invalid_request`` were both
178
+ # collapsed into ``validation_error`` server-side. Kept here so that older
179
+ # API responses (or user code still checking these strings) resolve
180
+ # correctly. Will be removed after 2026-10.
172
181
  DEPRECATED_CODE_ALIASES: dict[str, str] = {
173
182
  "bad_request": VALIDATION_ERROR,
174
183
  "invalid_request": VALIDATION_ERROR,