owls-insight 0.2.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.
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ env/
11
+ .mypy_cache/
12
+ .pytest_cache/
13
+ .ruff_cache/
14
+ .coverage
15
+ htmlcov/
16
+ *.so
17
+
18
+ # IDE / OS
19
+ .vscode/
20
+ .idea/
21
+ .DS_Store
22
+
23
+ # Env
24
+ .env
25
+ .env.*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Owls Insight
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.
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: owls-insight
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for the Owls Insight real-time sports betting odds API
5
+ Project-URL: Homepage, https://owlsinsight.com
6
+ Project-URL: Documentation, https://owlsinsight.com/docs
7
+ Project-URL: Repository, https://github.com/Davidgsilva/owls-insight-sdk-python
8
+ Author-email: Owls Insight <david@wisesportsai.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,betting,draftkings,fanduel,mlb,nba,nfl,nhl,odds,pinnacle,real-time,soccer,sports,tennis,websocket
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: aiohttp>=3.9
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: pydantic>=2.5
26
+ Requires-Dist: python-socketio>=5.11
27
+ Requires-Dist: websocket-client>=1.7
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.8; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: ruff>=0.4; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # Owls Insight Python SDK
36
+
37
+ Official Python SDK for the [Owls Insight](https://owlsinsight.com) real-time sports betting odds API. Sync **and** async clients, fully typed with Pydantic v2, plus a Socket.io WebSocket client for live streams.
38
+
39
+ ```bash
40
+ pip install owls-insight
41
+ ```
42
+
43
+ Requires Python 3.9+. Get an API key at [owlsinsight.com](https://owlsinsight.com).
44
+
45
+ ## Quick start (sync)
46
+
47
+ ```python
48
+ from owls_insight import OwlsInsight
49
+
50
+ client = OwlsInsight(api_key="owlsinsight_...")
51
+
52
+ # REST: current NBA odds. `data` is keyed by sportsbook -> list of events.
53
+ odds = client.rest.get_odds("nba", books=["pinnacle", "fanduel"])
54
+ for book, events in odds.data.items():
55
+ for event in events:
56
+ print(book, event.home_team, "vs", event.away_team)
57
+
58
+ # WebSocket: stream live updates
59
+ client.ws.connect()
60
+ client.ws.subscribe(sports=["nba"], books=["pinnacle"])
61
+ client.ws.on("odds-update", lambda data: print("live:", data))
62
+
63
+ # or block for the next event
64
+ update = client.ws.wait_for("odds-update", timeout=15)
65
+
66
+ client.destroy()
67
+ ```
68
+
69
+ ## Quick start (async)
70
+
71
+ ```python
72
+ import asyncio
73
+ from owls_insight import AsyncOwlsInsight
74
+
75
+ async def main():
76
+ async with AsyncOwlsInsight(api_key="owlsinsight_...") as client:
77
+ odds = await client.rest.get_odds("nba", books=["pinnacle"])
78
+ print(sum(len(events) for events in odds.data.values()), "games")
79
+
80
+ await client.ws.connect()
81
+ await client.ws.subscribe(sports=["nba"], books=["pinnacle"])
82
+ update = await client.ws.wait_for("pinnacle-realtime", timeout=30)
83
+ print("realtime:", update)
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ## Authentication
89
+
90
+ Pass your API key to the constructor. REST uses the `Authorization: Bearer` header; the WebSocket uses the `?apiKey=` query string. Both are handled for you.
91
+
92
+ ```python
93
+ import os
94
+ client = OwlsInsight(api_key=os.environ["OWLS_INSIGHT_API_KEY"])
95
+ ```
96
+
97
+ ## REST
98
+
99
+ Method names are `snake_case` (the JS SDK's `getOdds` is `get_odds` here). Every method returns a typed Pydantic model; unknown fields from the evolving API are preserved (`model_extra`), never dropped.
100
+
101
+ Full parity with the TypeScript SDK — every endpoint is available on both `client.rest` (sync) and the async client.
102
+
103
+ | Area | Methods |
104
+ |------|---------|
105
+ | Odds | `get_odds`, `get_moneyline`, `get_spreads`, `get_totals`, `get_realtime`, `get_ps3838_realtime`, `get_esports_realtime`, `get_ev`, `list_events`, `get_one_x_bet_soccer`, `get_prophetx_odds` |
106
+ | Props | `get_props`, `get_book_props`, `get_props_history`, `get_props_stats`, `get_book_props_stats` |
107
+ | Scores / schedule | `get_scores`, `get_schedule`, `get_results`, `get_splits`, `normalize`, `normalize_batch` |
108
+ | Stats | `get_stats`, `get_match_stats`, `get_h2h`, `get_player_averages` |
109
+ | Line history | `get_odds_history`, `get_moneyline_history`, `get_spread_history`, `get_totals_history` |
110
+ | Historical | `get_history_games`, `get_history_odds`, `get_history_props`, `get_history_stats`, `get_history_tennis_stats`, `get_game_stats_detail`, `get_closing_odds`, `get_historical_player_props`, `get_public_betting`, `get_cs2_matches`, `get_cs2_match`, `get_cs2_players` |
111
+ | v2 Source API | `get_stake_v2`, `get_hard_rock_events`/`_leagues`/`get_hard_rock_ladder`, `get_bet365_v2`/`_leagues`, `get_draftkings_v2`/`_leagues`, `get_fanduel_v2`/`_leagues`, `get_mybookie_v2`, `get_thunderpick_v2`, `get_underdog_v2`, `get_bookmaker_v2`/`_leagues`, `get_kalshi_v2`/`_leagues`, `get_polymarket_v2`/`_leagues`, `get_pinnacle_v2`/`_leagues` |
112
+
113
+ > Methods whose response shape isn't individually modelled yet return a permissive `ApiResponse` envelope (`success`/`data`/`meta`, all fields preserved). Deep per-endpoint Pydantic typing is the remaining follow-on; the method surface is complete.
114
+
115
+ ### Hard Rock odds decoding
116
+
117
+ Hard Rock v2 selections carry a `rootIdx`, not inline odds. Decode it client-side:
118
+
119
+ ```python
120
+ from owls_insight import root_idx_to_american_odds
121
+ root_idx_to_american_odds(42) # -325
122
+ root_idx_to_american_odds(72) # 100 (even)
123
+ ```
124
+
125
+ ## WebSocket events
126
+
127
+ `odds-update`, `props-update` (and per-book `{book}-props-update`), `pinnacle-realtime`, `ps3838-realtime`, `esports-update`, `prophetx-update`, and the v2 `{book}-v2-update` deltas.
128
+
129
+ ```python
130
+ client.ws.connect()
131
+ client.ws.subscribe(sports=["nba", "nhl"], books=["pinnacle", "fanduel"])
132
+ client.ws.subscribe_props(book="fanduel") # props stream
133
+ client.ws.update_subscription(prophetx=True) # merge, don't replace
134
+ client.ws.on("props-update", handle_props)
135
+ ```
136
+
137
+ ## Errors
138
+
139
+ ```python
140
+ from owls_insight import AuthenticationError, RateLimitError, OwlsInsightError
141
+
142
+ try:
143
+ client.rest.get_odds("nba")
144
+ except AuthenticationError:
145
+ ... # 401
146
+ except RateLimitError as e:
147
+ print("retry after", e.retry_after_ms, "ms")
148
+ except OwlsInsightError as e:
149
+ print(e.status, e.message)
150
+ ```
151
+
152
+ ## License
153
+
154
+ MIT
@@ -0,0 +1,120 @@
1
+ # Owls Insight Python SDK
2
+
3
+ Official Python SDK for the [Owls Insight](https://owlsinsight.com) real-time sports betting odds API. Sync **and** async clients, fully typed with Pydantic v2, plus a Socket.io WebSocket client for live streams.
4
+
5
+ ```bash
6
+ pip install owls-insight
7
+ ```
8
+
9
+ Requires Python 3.9+. Get an API key at [owlsinsight.com](https://owlsinsight.com).
10
+
11
+ ## Quick start (sync)
12
+
13
+ ```python
14
+ from owls_insight import OwlsInsight
15
+
16
+ client = OwlsInsight(api_key="owlsinsight_...")
17
+
18
+ # REST: current NBA odds. `data` is keyed by sportsbook -> list of events.
19
+ odds = client.rest.get_odds("nba", books=["pinnacle", "fanduel"])
20
+ for book, events in odds.data.items():
21
+ for event in events:
22
+ print(book, event.home_team, "vs", event.away_team)
23
+
24
+ # WebSocket: stream live updates
25
+ client.ws.connect()
26
+ client.ws.subscribe(sports=["nba"], books=["pinnacle"])
27
+ client.ws.on("odds-update", lambda data: print("live:", data))
28
+
29
+ # or block for the next event
30
+ update = client.ws.wait_for("odds-update", timeout=15)
31
+
32
+ client.destroy()
33
+ ```
34
+
35
+ ## Quick start (async)
36
+
37
+ ```python
38
+ import asyncio
39
+ from owls_insight import AsyncOwlsInsight
40
+
41
+ async def main():
42
+ async with AsyncOwlsInsight(api_key="owlsinsight_...") as client:
43
+ odds = await client.rest.get_odds("nba", books=["pinnacle"])
44
+ print(sum(len(events) for events in odds.data.values()), "games")
45
+
46
+ await client.ws.connect()
47
+ await client.ws.subscribe(sports=["nba"], books=["pinnacle"])
48
+ update = await client.ws.wait_for("pinnacle-realtime", timeout=30)
49
+ print("realtime:", update)
50
+
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ ## Authentication
55
+
56
+ Pass your API key to the constructor. REST uses the `Authorization: Bearer` header; the WebSocket uses the `?apiKey=` query string. Both are handled for you.
57
+
58
+ ```python
59
+ import os
60
+ client = OwlsInsight(api_key=os.environ["OWLS_INSIGHT_API_KEY"])
61
+ ```
62
+
63
+ ## REST
64
+
65
+ Method names are `snake_case` (the JS SDK's `getOdds` is `get_odds` here). Every method returns a typed Pydantic model; unknown fields from the evolving API are preserved (`model_extra`), never dropped.
66
+
67
+ Full parity with the TypeScript SDK — every endpoint is available on both `client.rest` (sync) and the async client.
68
+
69
+ | Area | Methods |
70
+ |------|---------|
71
+ | Odds | `get_odds`, `get_moneyline`, `get_spreads`, `get_totals`, `get_realtime`, `get_ps3838_realtime`, `get_esports_realtime`, `get_ev`, `list_events`, `get_one_x_bet_soccer`, `get_prophetx_odds` |
72
+ | Props | `get_props`, `get_book_props`, `get_props_history`, `get_props_stats`, `get_book_props_stats` |
73
+ | Scores / schedule | `get_scores`, `get_schedule`, `get_results`, `get_splits`, `normalize`, `normalize_batch` |
74
+ | Stats | `get_stats`, `get_match_stats`, `get_h2h`, `get_player_averages` |
75
+ | Line history | `get_odds_history`, `get_moneyline_history`, `get_spread_history`, `get_totals_history` |
76
+ | Historical | `get_history_games`, `get_history_odds`, `get_history_props`, `get_history_stats`, `get_history_tennis_stats`, `get_game_stats_detail`, `get_closing_odds`, `get_historical_player_props`, `get_public_betting`, `get_cs2_matches`, `get_cs2_match`, `get_cs2_players` |
77
+ | v2 Source API | `get_stake_v2`, `get_hard_rock_events`/`_leagues`/`get_hard_rock_ladder`, `get_bet365_v2`/`_leagues`, `get_draftkings_v2`/`_leagues`, `get_fanduel_v2`/`_leagues`, `get_mybookie_v2`, `get_thunderpick_v2`, `get_underdog_v2`, `get_bookmaker_v2`/`_leagues`, `get_kalshi_v2`/`_leagues`, `get_polymarket_v2`/`_leagues`, `get_pinnacle_v2`/`_leagues` |
78
+
79
+ > Methods whose response shape isn't individually modelled yet return a permissive `ApiResponse` envelope (`success`/`data`/`meta`, all fields preserved). Deep per-endpoint Pydantic typing is the remaining follow-on; the method surface is complete.
80
+
81
+ ### Hard Rock odds decoding
82
+
83
+ Hard Rock v2 selections carry a `rootIdx`, not inline odds. Decode it client-side:
84
+
85
+ ```python
86
+ from owls_insight import root_idx_to_american_odds
87
+ root_idx_to_american_odds(42) # -325
88
+ root_idx_to_american_odds(72) # 100 (even)
89
+ ```
90
+
91
+ ## WebSocket events
92
+
93
+ `odds-update`, `props-update` (and per-book `{book}-props-update`), `pinnacle-realtime`, `ps3838-realtime`, `esports-update`, `prophetx-update`, and the v2 `{book}-v2-update` deltas.
94
+
95
+ ```python
96
+ client.ws.connect()
97
+ client.ws.subscribe(sports=["nba", "nhl"], books=["pinnacle", "fanduel"])
98
+ client.ws.subscribe_props(book="fanduel") # props stream
99
+ client.ws.update_subscription(prophetx=True) # merge, don't replace
100
+ client.ws.on("props-update", handle_props)
101
+ ```
102
+
103
+ ## Errors
104
+
105
+ ```python
106
+ from owls_insight import AuthenticationError, RateLimitError, OwlsInsightError
107
+
108
+ try:
109
+ client.rest.get_odds("nba")
110
+ except AuthenticationError:
111
+ ... # 401
112
+ except RateLimitError as e:
113
+ print("retry after", e.retry_after_ms, "ms")
114
+ except OwlsInsightError as e:
115
+ print(e.status, e.message)
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "owls-insight"
7
+ version = "0.2.0"
8
+ description = "Official Python SDK for the Owls Insight real-time sports betting odds API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Owls Insight", email = "david@wisesportsai.com" }]
13
+ keywords = [
14
+ "sports", "betting", "odds", "nba", "nfl", "nhl", "mlb", "soccer",
15
+ "tennis", "pinnacle", "fanduel", "draftkings", "websocket", "real-time", "api",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = [
30
+ "httpx>=0.27",
31
+ "pydantic>=2.5",
32
+ "python-socketio>=5.11",
33
+ "websocket-client>=1.7", # sync Socket.io websocket transport
34
+ "aiohttp>=3.9", # async Socket.io websocket transport
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://owlsinsight.com"
39
+ Documentation = "https://owlsinsight.com/docs"
40
+ Repository = "https://github.com/Davidgsilva/owls-insight-sdk-python"
41
+
42
+ [project.optional-dependencies]
43
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.8", "ruff>=0.4"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/owls_insight"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "auto"
50
+ testpaths = ["tests"]
51
+
52
+ [tool.mypy]
53
+ python_version = "3.10"
54
+ plugins = ["pydantic.mypy"]
55
+ ignore_missing_imports = true
@@ -0,0 +1,46 @@
1
+ """Owls Insight — Official Python SDK for the real-time sports betting odds API.
2
+
3
+ >>> from owls_insight import OwlsInsight
4
+ >>> client = OwlsInsight(api_key="owlsinsight_...")
5
+ >>> odds = client.rest.get_odds("nba", books=["pinnacle", "fanduel"])
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from . import models
11
+ from .client import AsyncOwlsInsight, OwlsInsight
12
+ from .errors import (
13
+ AuthenticationError,
14
+ ForbiddenError,
15
+ NotFoundError,
16
+ OwlsInsightError,
17
+ RateLimitError,
18
+ )
19
+ from .hardrock import (
20
+ HARDROCK_LADDER_EVEN_ROOTIDX,
21
+ HARDROCK_PRICE_LADDER,
22
+ root_idx_to_american_odds,
23
+ )
24
+ from .rest import AsyncRestClient, RestClient
25
+ from .websocket import AsyncWebSocketClient, WebSocketClient
26
+
27
+ __version__ = "0.2.0"
28
+
29
+ __all__ = [
30
+ "OwlsInsight",
31
+ "AsyncOwlsInsight",
32
+ "RestClient",
33
+ "AsyncRestClient",
34
+ "WebSocketClient",
35
+ "AsyncWebSocketClient",
36
+ "OwlsInsightError",
37
+ "AuthenticationError",
38
+ "ForbiddenError",
39
+ "NotFoundError",
40
+ "RateLimitError",
41
+ "root_idx_to_american_odds",
42
+ "HARDROCK_PRICE_LADDER",
43
+ "HARDROCK_LADDER_EVEN_ROOTIDX",
44
+ "models",
45
+ "__version__",
46
+ ]
@@ -0,0 +1,33 @@
1
+ """Shared Pydantic base + common types for all model modules.
2
+
3
+ Lives in its own module so per-domain model files can import ``_Model``/``Meta``
4
+ without importing the public ``models`` facade (which re-exports everything,
5
+ which would create an import cycle).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Optional
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+ # Enums are string literals upstream; permissive ``str`` aliases here.
15
+ Sport = str
16
+ Book = str
17
+ MarketKey = str
18
+ GameStatus = str
19
+ SubscriptionTier = str
20
+
21
+
22
+ class _Model(BaseModel):
23
+ """Base for all response models: preserves unknown fields, allows aliases."""
24
+
25
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
26
+
27
+
28
+ class Meta(_Model):
29
+ source: Optional[str] = None
30
+ status: Optional[str] = None
31
+ cached: Optional[bool] = None
32
+ timestamp: Optional[str] = None
33
+ age_seconds: Optional[float] = None # camelCase ageSeconds tolerated via extra
@@ -0,0 +1,67 @@
1
+ """Shared HTTP plumbing for the sync and async REST clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional, Type, TypeVar
6
+
7
+ import httpx
8
+ from pydantic import BaseModel
9
+
10
+ from .errors import error_from_response
11
+
12
+ DEFAULT_BASE_URL = "https://api.owlsinsight.com"
13
+ DEFAULT_TIMEOUT = 30.0
14
+ USER_AGENT = "owls-insight-python/0.2.0"
15
+
16
+ T = TypeVar("T", bound=BaseModel)
17
+
18
+
19
+ def clean_params(params: Optional[Dict[str, Any]]) -> Dict[str, str]:
20
+ """Drop ``None`` values and stringify the rest (booleans -> 'true'/'false')."""
21
+ out: Dict[str, str] = {}
22
+ for key, value in (params or {}).items():
23
+ if value is None:
24
+ continue
25
+ out[key] = "true" if value is True else "false" if value is False else str(value)
26
+ return out
27
+
28
+
29
+ def auth_headers(api_key: str) -> Dict[str, str]:
30
+ return {
31
+ "Authorization": f"Bearer {api_key}",
32
+ "Accept": "application/json",
33
+ "User-Agent": USER_AGENT,
34
+ }
35
+
36
+
37
+ def parse_response(resp: httpx.Response, model: Optional[Type[T]]) -> Any:
38
+ """Raise the right error on non-2xx, otherwise validate JSON into ``model``."""
39
+ if resp.status_code >= 400:
40
+ try:
41
+ body: Any = resp.json()
42
+ except Exception:
43
+ body = resp.text
44
+ raise error_from_response(resp.status_code, body, resp.headers)
45
+ data = resp.json()
46
+ if model is None:
47
+ return data
48
+ return model.model_validate(data)
49
+
50
+
51
+ class _BaseRest:
52
+ """Holds config common to the sync and async REST clients."""
53
+
54
+ def __init__(
55
+ self,
56
+ api_key: str,
57
+ base_url: Optional[str] = None,
58
+ timeout: Optional[float] = None,
59
+ ) -> None:
60
+ if not api_key:
61
+ raise ValueError("api_key is required")
62
+ self._api_key = api_key
63
+ self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
64
+ self._timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
65
+
66
+ def _full_url(self, path: str) -> str:
67
+ return f"{self._base_url}{path}"
@@ -0,0 +1,70 @@
1
+ """Top-level Owls Insight clients bundling REST + WebSocket."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from .rest import AsyncRestClient, RestClient
8
+ from .websocket import AsyncWebSocketClient, WebSocketClient
9
+
10
+
11
+ class OwlsInsight:
12
+ """Synchronous Owls Insight client.
13
+
14
+ >>> from owls_insight import OwlsInsight
15
+ >>> client = OwlsInsight(api_key="...")
16
+ >>> odds = client.rest.get_odds("nba", books=["pinnacle", "fanduel"])
17
+ >>> client.ws.connect()
18
+ >>> client.ws.subscribe(sports=["nba"], books=["pinnacle"])
19
+ >>> client.ws.on("odds-update", lambda data: print(data))
20
+ """
21
+
22
+ def __init__(self, api_key: str, base_url: Optional[str] = None, ws_url: Optional[str] = None, timeout: Optional[float] = None) -> None:
23
+ if not api_key:
24
+ raise ValueError("api_key is required")
25
+ self.rest = RestClient(api_key, base_url=base_url, timeout=timeout)
26
+ self.ws = WebSocketClient(api_key, ws_url=ws_url, base_url=base_url)
27
+
28
+ def destroy(self) -> None:
29
+ """Disconnect the WebSocket and close the HTTP session."""
30
+ self.ws.disconnect()
31
+ self.rest.close()
32
+
33
+ def __enter__(self) -> "OwlsInsight":
34
+ return self
35
+
36
+ def __exit__(self, *exc: Any) -> None:
37
+ self.destroy()
38
+
39
+
40
+ class AsyncOwlsInsight:
41
+ """Asynchronous Owls Insight client.
42
+
43
+ >>> import asyncio
44
+ >>> from owls_insight import AsyncOwlsInsight
45
+ >>> async def main():
46
+ ... client = AsyncOwlsInsight(api_key="...")
47
+ ... odds = await client.rest.get_odds("nba", books=["pinnacle"])
48
+ ... await client.ws.connect()
49
+ ... await client.ws.subscribe(sports=["nba"], books=["pinnacle"])
50
+ ... update = await client.ws.wait_for("odds-update", timeout=15)
51
+ ... await client.destroy()
52
+ >>> asyncio.run(main())
53
+ """
54
+
55
+ def __init__(self, api_key: str, base_url: Optional[str] = None, ws_url: Optional[str] = None, timeout: Optional[float] = None) -> None:
56
+ if not api_key:
57
+ raise ValueError("api_key is required")
58
+ self.rest = AsyncRestClient(api_key, base_url=base_url, timeout=timeout)
59
+ self.ws = AsyncWebSocketClient(api_key, ws_url=ws_url, base_url=base_url)
60
+
61
+ async def destroy(self) -> None:
62
+ """Disconnect the WebSocket and close the HTTP session."""
63
+ await self.ws.disconnect()
64
+ await self.rest.close()
65
+
66
+ async def __aenter__(self) -> "AsyncOwlsInsight":
67
+ return self
68
+
69
+ async def __aexit__(self, *exc: Any) -> None:
70
+ await self.destroy()