sharpapi 0.2.2__tar.gz → 0.2.5__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 (27) hide show
  1. sharpapi-0.2.5/.github/dependabot.yml +22 -0
  2. sharpapi-0.2.5/.github/workflows/publish.yml +69 -0
  3. sharpapi-0.2.5/.github/workflows/test.yml +40 -0
  4. {sharpapi-0.2.2 → sharpapi-0.2.5}/PKG-INFO +1 -1
  5. {sharpapi-0.2.2 → sharpapi-0.2.5}/pyproject.toml +1 -1
  6. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/__init__.py +17 -4
  7. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/_base.py +52 -5
  8. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/async_client.py +159 -82
  9. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/client.py +195 -101
  10. sharpapi-0.2.5/src/sharpapi/exceptions.py +182 -0
  11. sharpapi-0.2.5/src/sharpapi/models.py +502 -0
  12. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/streaming.py +45 -6
  13. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/conftest.py +0 -2
  14. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/test_async_client.py +17 -2
  15. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/test_client.py +33 -9
  16. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/test_dataframe.py +1 -1
  17. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/test_models.py +2 -0
  18. sharpapi-0.2.2/.github/workflows/publish.yml +0 -27
  19. sharpapi-0.2.2/.github/workflows/test.yml +0 -28
  20. sharpapi-0.2.2/src/sharpapi/exceptions.py +0 -52
  21. sharpapi-0.2.2/src/sharpapi/models.py +0 -441
  22. {sharpapi-0.2.2 → sharpapi-0.2.5}/.gitignore +0 -0
  23. {sharpapi-0.2.2 → sharpapi-0.2.5}/README.md +0 -0
  24. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/_utils.py +0 -0
  25. {sharpapi-0.2.2 → sharpapi-0.2.5}/src/sharpapi/py.typed +0 -0
  26. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/__init__.py +0 -0
  27. {sharpapi-0.2.2 → sharpapi-0.2.5}/tests/test_utils.py +0 -0
@@ -0,0 +1,22 @@
1
+ # Dependabot keeps SHA-pinned GitHub Actions fresh.
2
+ #
3
+ # We pin third-party actions by full commit SHA (e.g. orhun/git-cliff-action
4
+ # @c93ef52f... # v4) so tag moves can't silently propagate into our runners.
5
+ # But frozen SHAs also freeze security fixes. This config opens a weekly PR
6
+ # to bump any action whose upstream tag has advanced since our last pin,
7
+ # with the changelog inlined so review is short. All action updates are
8
+ # grouped into a single PR per week to avoid a Monday flood.
9
+ #
10
+ # Scope: github-actions only. Adding gomod/pip/npm ecosystems is a separate
11
+ # decision — those PRs are much higher volume.
12
+ version: 2
13
+ updates:
14
+ - package-ecosystem: "github-actions"
15
+ directory: "/"
16
+ schedule:
17
+ interval: "weekly"
18
+ groups:
19
+ actions:
20
+ patterns: ["*"]
21
+ commit-message:
22
+ prefix: "chore(deps)"
@@ -0,0 +1,69 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ # workflow_dispatch lets operators dry-run the test matrix + gate
7
+ # wiring without cutting a real release (the actual PyPI upload step
8
+ # is guarded against non-release triggers, see below). Useful for
9
+ # verifying the gate after edits to test.yml or publish.yml.
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ # Re-run the full test matrix on the tagged commit before publish.
14
+ # Mirrors test.yml so a tagged release gets the same coverage as
15
+ # a push/PR. Previously publish.yml ran zero tests — a broken SDK
16
+ # could reach PyPI, and PyPI versions are immutable (only super-
17
+ # ceded). needs: test below gates the publish job on every matrix
18
+ # entry passing.
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ strategy:
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v5
27
+
28
+ - uses: actions/setup-python@v6
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+
32
+ - name: Install dependencies
33
+ run: |
34
+ pip install -e ".[test,pandas]"
35
+ pip install ruff pyright
36
+
37
+ - name: Ruff check
38
+ run: ruff check
39
+
40
+ - name: Pyright
41
+ run: pyright
42
+
43
+ - name: Run tests
44
+ run: pytest -v
45
+
46
+ publish:
47
+ needs: test
48
+ runs-on: ubuntu-latest
49
+ permissions:
50
+ id-token: write
51
+
52
+ steps:
53
+ - uses: actions/checkout@v5
54
+
55
+ - uses: actions/setup-python@v6
56
+ with:
57
+ python-version: "3.12"
58
+
59
+ - name: Install build tools
60
+ run: pip install hatch
61
+
62
+ - name: Build
63
+ run: hatch build
64
+
65
+ - name: Publish to PyPI
66
+ # Only publish on an actual release event. workflow_dispatch
67
+ # runs through test+build as a dry run but must not upload.
68
+ if: github.event_name == 'release'
69
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
@@ -0,0 +1,40 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v5
18
+
19
+ - uses: actions/setup-python@v6
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: |
25
+ pip install -e ".[test,pandas]"
26
+ pip install ruff pyright
27
+
28
+ - name: Ruff check
29
+ # [tool.ruff] config is already in pyproject.toml — rules E, F,
30
+ # I, UP. This step enforces them; before today they were only
31
+ # suggestions.
32
+ run: ruff check
33
+
34
+ - name: Pyright
35
+ # Same story — [tool.pyright] was set to standard mode in
36
+ # pyproject.toml but never invoked in CI.
37
+ run: pyright
38
+
39
+ - name: Run tests
40
+ run: pytest -v
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sharpapi
3
- Version: 0.2.2
3
+ Version: 0.2.5
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sharpapi"
7
- version = "0.2.2"
7
+ version = "0.2.5"
8
8
  description = "Official Python SDK for the SharpAPI real-time sports betting odds API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -17,9 +17,12 @@ Example::
17
17
  print(f"+{opp.ev_percentage}% on {opp.selection} @ {opp.sportsbook}")
18
18
  """
19
19
 
20
+ from ._utils import american_to_decimal, american_to_probability, decimal_to_american
20
21
  from .async_client import AsyncSharpAPI
21
22
  from .client import SharpAPI
22
23
  from .exceptions import (
24
+ ERROR_CODE_DESCRIPTIONS,
25
+ ERROR_CODE_TO_EXCEPTION,
23
26
  AuthenticationError,
24
27
  RateLimitedError,
25
28
  SharpAPIError,
@@ -28,16 +31,20 @@ from .exceptions import (
28
31
  ValidationError,
29
32
  )
30
33
  from .models import (
31
- APIResponse,
32
34
  AccountInfo,
35
+ APIKey,
36
+ APIResponse,
33
37
  ArbitrageLeg,
34
38
  ArbitrageOpportunity,
35
- EVOpportunity,
39
+ ClosingOddsLine,
40
+ ClosingSnapshot,
36
41
  Event,
42
+ EVOpportunity,
37
43
  GameState,
38
44
  League,
39
45
  LowHoldOpportunity,
40
46
  LowHoldSide,
47
+ Market,
41
48
  MiddleOpportunity,
42
49
  MiddleSide,
43
50
  OddsLine,
@@ -49,25 +56,28 @@ from .models import (
49
56
  Sportsbook,
50
57
  )
51
58
  from .streaming import EventStream
52
- from ._utils import american_to_decimal, american_to_probability, decimal_to_american
53
59
 
54
- __version__ = "0.2.1"
60
+ __version__ = "0.2.5"
55
61
 
56
62
  __all__ = [
57
63
  # Clients
58
64
  "SharpAPI",
59
65
  "AsyncSharpAPI",
60
66
  # Models
67
+ "APIKey",
61
68
  "APIResponse",
62
69
  "AccountInfo",
63
70
  "ArbitrageLeg",
64
71
  "ArbitrageOpportunity",
72
+ "ClosingOddsLine",
73
+ "ClosingSnapshot",
65
74
  "EVOpportunity",
66
75
  "Event",
67
76
  "GameState",
68
77
  "League",
69
78
  "LowHoldOpportunity",
70
79
  "LowHoldSide",
80
+ "Market",
71
81
  "MiddleOpportunity",
72
82
  "MiddleSide",
73
83
  "OddsLine",
@@ -86,6 +96,9 @@ __all__ = [
86
96
  "StreamError",
87
97
  "TierRestrictedError",
88
98
  "ValidationError",
99
+ # Error-code registry
100
+ "ERROR_CODE_DESCRIPTIONS",
101
+ "ERROR_CODE_TO_EXCEPTION",
89
102
  # Utilities
90
103
  "american_to_decimal",
91
104
  "american_to_probability",
@@ -3,21 +3,29 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import random
6
+ from typing import Literal
6
7
 
7
8
  import httpx
8
9
 
9
10
  from .exceptions import (
11
+ ERROR_CODE_TO_EXCEPTION,
10
12
  AuthenticationError,
11
13
  RateLimitedError,
12
14
  SharpAPIError,
13
15
  TierRestrictedError,
14
16
  ValidationError,
17
+ canonical_code,
15
18
  )
16
19
  from .models import APIResponse, RateLimitInfo, ResponseMeta
17
20
 
18
21
  DEFAULT_BASE_URL = "https://api.sharpapi.io"
19
22
  DEFAULT_TIMEOUT = 30.0
20
- USER_AGENT = "sharpapi-python/0.2.2"
23
+ USER_AGENT = "sharpapi-python/0.2.5"
24
+
25
+ # Supported REST authentication methods. SSE always uses ``?api_key=`` query
26
+ # regardless of this setting because EventSource cannot set custom headers.
27
+ AuthMethod = Literal["x-api-key", "bearer"]
28
+ DEFAULT_AUTH_METHOD: AuthMethod = "x-api-key"
21
29
 
22
30
  RETRY_STATUSES = frozenset({502, 503, 504})
23
31
  RETRY_MAX_ATTEMPTS = 3
@@ -90,6 +98,30 @@ def handle_errors(response: httpx.Response) -> None:
90
98
  code = body.get("code", "unknown_error")
91
99
  status = response.status_code
92
100
 
101
+ # Resolve deprecated code aliases (bad_request, invalid_request → validation_error).
102
+ code = canonical_code(code)
103
+
104
+ # Prefer the canonical code→exception mapping for well-known codes; fall back
105
+ # to HTTP-status-based routing for responses that omit an error code.
106
+ exc_class = ERROR_CODE_TO_EXCEPTION.get(code or "")
107
+ if exc_class is TierRestrictedError:
108
+ raise TierRestrictedError(
109
+ error_msg,
110
+ code=code,
111
+ status=status,
112
+ required_tier=body.get("required_tier"),
113
+ )
114
+ if exc_class is RateLimitedError:
115
+ raise RateLimitedError(
116
+ error_msg,
117
+ code=code,
118
+ status=status,
119
+ retry_after=body.get("retry_after"),
120
+ )
121
+ if exc_class is not None and exc_class is not SharpAPIError:
122
+ raise exc_class(error_msg, code=code, status=status)
123
+
124
+ # No canonical code match — route by HTTP status.
93
125
  if status == 401:
94
126
  raise AuthenticationError(error_msg, code=code, status=status)
95
127
  elif status == 403:
@@ -112,13 +144,28 @@ def handle_errors(response: httpx.Response) -> None:
112
144
  raise SharpAPIError(error_msg, code=code, status=status)
113
145
 
114
146
 
115
- def make_headers(api_key: str) -> dict[str, str]:
116
- """Build default request headers."""
117
- return {
118
- "X-API-Key": api_key,
147
+ def make_headers(
148
+ api_key: str,
149
+ auth_method: AuthMethod = DEFAULT_AUTH_METHOD,
150
+ ) -> dict[str, str]:
151
+ """Build default request headers.
152
+
153
+ Args:
154
+ api_key: The SharpAPI key (e.g. ``sk_live_...``).
155
+ auth_method: Either ``"x-api-key"`` (default — sends an
156
+ ``X-API-Key`` header) or ``"bearer"`` (sends
157
+ ``Authorization: Bearer <key>``). Useful when proxies, IAM
158
+ layers, or SSO gateways strip non-standard custom headers.
159
+ """
160
+ headers: dict[str, str] = {
119
161
  "Content-Type": "application/json",
120
162
  "User-Agent": USER_AGENT,
121
163
  }
164
+ if auth_method == "bearer":
165
+ headers["Authorization"] = f"Bearer {api_key}"
166
+ else:
167
+ headers["X-API-Key"] = api_key
168
+ return headers
122
169
 
123
170
 
124
171
  def _int_or_none(value: str | None) -> int | None: