sharpapi 0.1.0__py3-none-any.whl

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.
sharpapi/__init__.py ADDED
@@ -0,0 +1,93 @@
1
+ """SharpAPI Python SDK — Real-time sports betting odds, +EV, and arbitrage detection.
2
+
3
+ Example::
4
+
5
+ from sharpapi import SharpAPI
6
+
7
+ client = SharpAPI("sk_live_xxx")
8
+
9
+ # Arbitrage opportunities
10
+ arbs = client.arbitrage.get(min_profit=1.0)
11
+ for arb in arbs.data:
12
+ print(f"{arb.profit_percent}% — {arb.event_name}")
13
+
14
+ # +EV opportunities
15
+ evs = client.ev.get(min_ev=3.0, league="nba")
16
+ for opp in evs.data:
17
+ print(f"+{opp.ev_percent}% on {opp.selection} @ {opp.sportsbook}")
18
+ """
19
+
20
+ from .async_client import AsyncSharpAPI
21
+ from .client import SharpAPI
22
+ from .exceptions import (
23
+ AuthenticationError,
24
+ RateLimitedError,
25
+ SharpAPIError,
26
+ StreamError,
27
+ TierRestrictedError,
28
+ ValidationError,
29
+ )
30
+ from .models import (
31
+ APIResponse,
32
+ AccountInfo,
33
+ ArbitrageLeg,
34
+ ArbitrageOpportunity,
35
+ EVOpportunity,
36
+ Event,
37
+ GameState,
38
+ League,
39
+ LowHoldOpportunity,
40
+ LowHoldSide,
41
+ MiddleOpportunity,
42
+ MiddleSide,
43
+ OddsLine,
44
+ OddsValue,
45
+ Pagination,
46
+ RateLimitInfo,
47
+ ResponseMeta,
48
+ Sport,
49
+ Sportsbook,
50
+ )
51
+ from .streaming import EventStream
52
+ from ._utils import american_to_decimal, american_to_probability, decimal_to_american
53
+
54
+ __version__ = "0.1.0"
55
+
56
+ __all__ = [
57
+ # Clients
58
+ "SharpAPI",
59
+ "AsyncSharpAPI",
60
+ # Models
61
+ "APIResponse",
62
+ "AccountInfo",
63
+ "ArbitrageLeg",
64
+ "ArbitrageOpportunity",
65
+ "EVOpportunity",
66
+ "Event",
67
+ "GameState",
68
+ "League",
69
+ "LowHoldOpportunity",
70
+ "LowHoldSide",
71
+ "MiddleOpportunity",
72
+ "MiddleSide",
73
+ "OddsLine",
74
+ "OddsValue",
75
+ "Pagination",
76
+ "RateLimitInfo",
77
+ "ResponseMeta",
78
+ "Sport",
79
+ "Sportsbook",
80
+ # Streaming
81
+ "EventStream",
82
+ # Exceptions
83
+ "AuthenticationError",
84
+ "RateLimitedError",
85
+ "SharpAPIError",
86
+ "StreamError",
87
+ "TierRestrictedError",
88
+ "ValidationError",
89
+ # Utilities
90
+ "american_to_decimal",
91
+ "american_to_probability",
92
+ "decimal_to_american",
93
+ ]
sharpapi/_base.py ADDED
@@ -0,0 +1,121 @@
1
+ """Shared logic for sync and async clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ RateLimitedError,
12
+ SharpAPIError,
13
+ TierRestrictedError,
14
+ ValidationError,
15
+ )
16
+ from .models import APIResponse, RateLimitInfo, ResponseMeta
17
+
18
+ DEFAULT_BASE_URL = "https://api.sharpapi.io"
19
+ DEFAULT_TIMEOUT = 30.0
20
+ USER_AGENT = "sharpapi-python/0.1.0"
21
+
22
+
23
+ def parse_response(raw: dict, model_class: type) -> APIResponse:
24
+ """Parse raw API JSON into a typed APIResponse."""
25
+ data_raw = raw.get("data", [])
26
+ if isinstance(data_raw, list):
27
+ items = [model_class.model_validate(item) for item in data_raw]
28
+ else:
29
+ items = [model_class.model_validate(data_raw)]
30
+
31
+ meta = None
32
+ meta_raw = raw.get("meta")
33
+ if meta_raw:
34
+ meta = ResponseMeta.model_validate(meta_raw)
35
+
36
+ return APIResponse(
37
+ success=raw.get("success"),
38
+ data=items,
39
+ meta=meta,
40
+ timestamp=raw.get("timestamp"),
41
+ tier=raw.get("tier"),
42
+ )
43
+
44
+
45
+ def parse_rate_limit(response: httpx.Response) -> RateLimitInfo:
46
+ """Extract rate limit info from response headers."""
47
+ headers = response.headers
48
+ return RateLimitInfo(
49
+ limit=_int_or_none(headers.get("x-ratelimit-limit")),
50
+ remaining=_int_or_none(headers.get("x-ratelimit-remaining")),
51
+ reset=_float_or_none(headers.get("x-ratelimit-reset")),
52
+ tier=headers.get("x-tier"),
53
+ )
54
+
55
+
56
+ def handle_errors(response: httpx.Response) -> None:
57
+ """Raise typed exceptions for error responses."""
58
+ if response.is_success:
59
+ return
60
+
61
+ try:
62
+ body = response.json()
63
+ except Exception:
64
+ body = {}
65
+
66
+ error_obj = body.get("error", body)
67
+ if isinstance(error_obj, dict):
68
+ error_msg = error_obj.get("message", error_obj.get("error", f"HTTP {response.status_code}"))
69
+ code = error_obj.get("code", body.get("code", "unknown_error"))
70
+ else:
71
+ error_msg = str(error_obj) if error_obj else f"HTTP {response.status_code}"
72
+ code = body.get("code", "unknown_error")
73
+ status = response.status_code
74
+
75
+ if status == 401:
76
+ raise AuthenticationError(error_msg, code=code, status=status)
77
+ elif status == 403:
78
+ raise TierRestrictedError(
79
+ error_msg,
80
+ code=code,
81
+ status=status,
82
+ required_tier=body.get("required_tier"),
83
+ )
84
+ elif status == 429:
85
+ raise RateLimitedError(
86
+ error_msg,
87
+ code=code,
88
+ status=status,
89
+ retry_after=body.get("retry_after"),
90
+ )
91
+ elif status == 400:
92
+ raise ValidationError(error_msg, code=code, status=status)
93
+ else:
94
+ raise SharpAPIError(error_msg, code=code, status=status)
95
+
96
+
97
+ def make_headers(api_key: str) -> dict[str, str]:
98
+ """Build default request headers."""
99
+ return {
100
+ "X-API-Key": api_key,
101
+ "Content-Type": "application/json",
102
+ "User-Agent": USER_AGENT,
103
+ }
104
+
105
+
106
+ def _int_or_none(value: str | None) -> int | None:
107
+ if value is None:
108
+ return None
109
+ try:
110
+ return int(value)
111
+ except (ValueError, TypeError):
112
+ return None
113
+
114
+
115
+ def _float_or_none(value: str | None) -> float | None:
116
+ if value is None:
117
+ return None
118
+ try:
119
+ return float(value)
120
+ except (ValueError, TypeError):
121
+ return None
sharpapi/_utils.py ADDED
@@ -0,0 +1,39 @@
1
+ """Internal utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def american_to_decimal(american: int | float) -> float:
7
+ """Convert American odds to decimal odds."""
8
+ if american > 0:
9
+ return american / 100 + 1
10
+ return 100 / abs(american) + 1
11
+
12
+
13
+ def decimal_to_american(decimal: float) -> int:
14
+ """Convert decimal odds to American odds."""
15
+ if decimal >= 2.0:
16
+ return round((decimal - 1) * 100)
17
+ return round(-100 / (decimal - 1))
18
+
19
+
20
+ def american_to_probability(american: int | float) -> float:
21
+ """Convert American odds to implied probability (0-1)."""
22
+ if american > 0:
23
+ return 100 / (american + 100)
24
+ return abs(american) / (abs(american) + 100)
25
+
26
+
27
+ def _clean_params(params: dict) -> dict:
28
+ """Remove None values and join lists with commas."""
29
+ cleaned = {}
30
+ for key, value in params.items():
31
+ if value is None:
32
+ continue
33
+ if isinstance(value, bool):
34
+ cleaned[key] = str(value).lower()
35
+ elif isinstance(value, list):
36
+ cleaned[key] = ",".join(str(v) for v in value)
37
+ else:
38
+ cleaned[key] = value
39
+ return cleaned