vantafin 0.1.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,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ node_modules/
9
+ target/
10
+ *.class
11
+ .idea/
12
+ .vscode/
13
+ .DS_Store
vantafin-0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Vantafin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: vantafin
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the Vantafin REST API (market data, fundamentals, SEC filings, news and more).
5
+ Project-URL: Homepage, https://vantafin.com
6
+ Project-URL: Documentation, https://vantafin.com/docs
7
+ Project-URL: Repository, https://github.com/vantafin/vantafin
8
+ Project-URL: Issues, https://github.com/vantafin/vantafin/issues
9
+ Author: Vantafin
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,finance,market-data,sec,stocks,vantafin
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Office/Business :: Financial
26
+ Requires-Python: >=3.8
27
+ Requires-Dist: requests>=2.25
28
+ Provides-Extra: websocket
29
+ Requires-Dist: websocket-client>=1.6; extra == 'websocket'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # vantafin
33
+
34
+ Official Python client for the [Vantafin](https://vantafin.com) REST API - quotes,
35
+ corporate actions, fundamentals, SEC filings, news, earnings transcripts, and
36
+ earnings materials (IR decks, press releases, supplements).
37
+
38
+ **Source:** [github.com/vantafin/vantafin](https://github.com/vantafin/vantafin)
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install vantafin
44
+ ```
45
+
46
+ Or install from source:
47
+
48
+ ```bash
49
+ pip install git+https://github.com/vantafin/vantafin.git
50
+ ```
51
+
52
+ ## Authentication
53
+
54
+ Grab your API key from **Settings → API Usage** at
55
+ [vantafin.com/app/settings](https://vantafin.com/app/settings). Keys look like
56
+ `vf-live-...`. You can reset a key at any time from the same page.
57
+
58
+ ## Quickstart
59
+
60
+ ```python
61
+ from vantafin import RESTClient
62
+
63
+ client = RESTClient("vf-live-your_api_key")
64
+
65
+ # Latest quote
66
+ print(client.get_quote("AAPL"))
67
+
68
+ # Company profile
69
+ print(client.get_ticker("AAPL"))
70
+
71
+ # All tickers (paginated)
72
+ page = client.list_tickers(type="stock", limit=100)
73
+ print(page["count"], page["next_cursor"])
74
+ ```
75
+
76
+ ## Available methods
77
+
78
+ | Method | Endpoint |
79
+ | --- | --- |
80
+ | `get_status()` | `GET /status` |
81
+ | `list_tickers(...)` | `GET /companydata/tickers` |
82
+ | `get_ticker(ticker)` | `GET /companydata/tickers/{ticker}` |
83
+ | `get_company_by_cik(cik)` | `GET /companydata/ciks/{cik}` |
84
+ | `get_company_by_isin(isin)` | `GET /companydata/isins/{isin}` |
85
+ | `get_company_by_cusip(cusip)` | `GET /companydata/cusips/{cusip}` |
86
+ | `search(query, ...)` | `GET /companydata/search` |
87
+ | `get_quote(ticker)` | `GET /quotes/{ticker}` |
88
+ | `get_quotes(tickers)` | `GET /quotes` |
89
+ | `screener(...)` | `GET /screener` |
90
+ | `screener_fields()` | `GET /screener/fields` |
91
+ | `list_earnings(...)` | `GET /earnings` |
92
+ | `get_earnings(ticker, ...)` | `GET /earnings/{ticker}` |
93
+ | `list_dividends(...)` | `GET /dividends` |
94
+ | `get_dividends(ticker, ...)` | `GET /dividends/{ticker}` |
95
+ | `list_splits(...)` | `GET /splits` |
96
+ | `get_splits(ticker, ...)` | `GET /splits/{ticker}` |
97
+ | `list_ipos(...)` | `GET /ipos` |
98
+ | `get_ipos(ticker, ...)` | `GET /ipos/{ticker}` |
99
+ | `get_etf_holdings(ticker)` | `GET /etf-holdings/{ticker}` |
100
+ | `get_institutional_ownership(ticker, ...)` | `GET /institutional-ownership/{ticker}` |
101
+ | `get_fund_holdings(ticker, ...)` | `GET /fund-holdings/{ticker}` |
102
+ | `get_short_interest(ticker, ...)` | `GET /short-interest/{ticker}` |
103
+ | `list_insider_transactions(...)` | `GET /insider-transactions` |
104
+ | `get_insider_transactions(ticker, ...)` | `GET /insider-transactions/{ticker}` |
105
+ | `get_financials(ticker, ...)` | `GET /financials/{ticker}` |
106
+ | `get_revenue_segmentation(ticker, ...)` | `GET /revenue-segmentation/{ticker}` |
107
+ | `get_allocation_breakdown(ticker)` | `GET /allocation-breakdown/{ticker}` |
108
+ | `get_macro(...)` | `GET /macro` |
109
+ | `get_filings(ticker, ...)` | `GET /filings/{ticker}` |
110
+ | `get_filing(filing_id)` | `GET /filings/by-id/{filing_id}` |
111
+ | `get_news(...)` | `GET /news` |
112
+ | `get_ticker_news(ticker, ...)` | `GET /news/{ticker}` |
113
+ | `list_transcript_tickers(...)` | `GET /transcripts/tickers` |
114
+ | `list_transcripts(ticker, ...)` | `GET /transcripts/{ticker}` |
115
+ | `get_transcript(ticker, transcript_id)` | `GET /transcripts/{ticker}/{transcript_id}` |
116
+ | `list_earnings_materials(ticker, ...)` | `GET /earnings-materials/{ticker}` |
117
+ | `get_earnings_material(ticker, material_id)` | `GET /earnings-materials/{ticker}/{material_id}` |
118
+
119
+ ## Rate limits
120
+
121
+ The API allows **1,000 requests per minute** per key. When you exceed it the
122
+ client raises `RateLimitError` (with `.retry_after` seconds). Every response
123
+ also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
124
+ headers.
125
+
126
+ ## Error handling
127
+
128
+ ```python
129
+ from vantafin import RESTClient, RateLimitError, NotFoundError
130
+
131
+ client = RESTClient("vf-live-your_api_key")
132
+ try:
133
+ client.get_quote("NOPE")
134
+ except NotFoundError:
135
+ print("Unknown ticker")
136
+ except RateLimitError as e:
137
+ print(f"Slow down, retry in {e.retry_after}s")
138
+ ```
139
+
140
+ ## WebSocket streaming
141
+
142
+ Stream real-time data over a single WebSocket connection to
143
+ `wss://socket.vantafin.com/v1/stocks`. Three channels are available:
144
+
145
+ | Channel | Subscribe with | Message `type` |
146
+ | --- | --- | --- |
147
+ | Live news / press releases | `N.<ticker>` or `N.*` | `news` |
148
+ | Live SEC filings | `F.<ticker>` or `F.*` | `filing` |
149
+ | Trading halts | `H.<ticker>` or `H.*` | `halt` |
150
+
151
+ The WebSocket client needs the optional `websocket-client` dependency:
152
+
153
+ ```bash
154
+ pip install vantafin[websocket]
155
+ ```
156
+
157
+ ```python
158
+ from vantafin import WebSocketClient
159
+
160
+ ws = WebSocketClient("vf-live-your_api_key")
161
+ ws.connect()
162
+ ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
163
+
164
+ for message in ws:
165
+ print(message)
166
+ ```
167
+
168
+ ## Other languages
169
+
170
+ Official clients for other languages live alongside this Python package:
171
+
172
+ | Language | Folder |
173
+ | --- | --- |
174
+ | JavaScript / Node | [`javascript/`](./javascript) |
175
+ | Go | [`go/`](./go) |
176
+ | Java | [`java/`](./java) |
177
+ | Ruby | [`ruby/`](./ruby) |
178
+
179
+ ## Full documentation
180
+
181
+ [https://vantafin.com/docs](https://vantafin.com/docs)
@@ -0,0 +1,150 @@
1
+ # vantafin
2
+
3
+ Official Python client for the [Vantafin](https://vantafin.com) REST API - quotes,
4
+ corporate actions, fundamentals, SEC filings, news, earnings transcripts, and
5
+ earnings materials (IR decks, press releases, supplements).
6
+
7
+ **Source:** [github.com/vantafin/vantafin](https://github.com/vantafin/vantafin)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install vantafin
13
+ ```
14
+
15
+ Or install from source:
16
+
17
+ ```bash
18
+ pip install git+https://github.com/vantafin/vantafin.git
19
+ ```
20
+
21
+ ## Authentication
22
+
23
+ Grab your API key from **Settings → API Usage** at
24
+ [vantafin.com/app/settings](https://vantafin.com/app/settings). Keys look like
25
+ `vf-live-...`. You can reset a key at any time from the same page.
26
+
27
+ ## Quickstart
28
+
29
+ ```python
30
+ from vantafin import RESTClient
31
+
32
+ client = RESTClient("vf-live-your_api_key")
33
+
34
+ # Latest quote
35
+ print(client.get_quote("AAPL"))
36
+
37
+ # Company profile
38
+ print(client.get_ticker("AAPL"))
39
+
40
+ # All tickers (paginated)
41
+ page = client.list_tickers(type="stock", limit=100)
42
+ print(page["count"], page["next_cursor"])
43
+ ```
44
+
45
+ ## Available methods
46
+
47
+ | Method | Endpoint |
48
+ | --- | --- |
49
+ | `get_status()` | `GET /status` |
50
+ | `list_tickers(...)` | `GET /companydata/tickers` |
51
+ | `get_ticker(ticker)` | `GET /companydata/tickers/{ticker}` |
52
+ | `get_company_by_cik(cik)` | `GET /companydata/ciks/{cik}` |
53
+ | `get_company_by_isin(isin)` | `GET /companydata/isins/{isin}` |
54
+ | `get_company_by_cusip(cusip)` | `GET /companydata/cusips/{cusip}` |
55
+ | `search(query, ...)` | `GET /companydata/search` |
56
+ | `get_quote(ticker)` | `GET /quotes/{ticker}` |
57
+ | `get_quotes(tickers)` | `GET /quotes` |
58
+ | `screener(...)` | `GET /screener` |
59
+ | `screener_fields()` | `GET /screener/fields` |
60
+ | `list_earnings(...)` | `GET /earnings` |
61
+ | `get_earnings(ticker, ...)` | `GET /earnings/{ticker}` |
62
+ | `list_dividends(...)` | `GET /dividends` |
63
+ | `get_dividends(ticker, ...)` | `GET /dividends/{ticker}` |
64
+ | `list_splits(...)` | `GET /splits` |
65
+ | `get_splits(ticker, ...)` | `GET /splits/{ticker}` |
66
+ | `list_ipos(...)` | `GET /ipos` |
67
+ | `get_ipos(ticker, ...)` | `GET /ipos/{ticker}` |
68
+ | `get_etf_holdings(ticker)` | `GET /etf-holdings/{ticker}` |
69
+ | `get_institutional_ownership(ticker, ...)` | `GET /institutional-ownership/{ticker}` |
70
+ | `get_fund_holdings(ticker, ...)` | `GET /fund-holdings/{ticker}` |
71
+ | `get_short_interest(ticker, ...)` | `GET /short-interest/{ticker}` |
72
+ | `list_insider_transactions(...)` | `GET /insider-transactions` |
73
+ | `get_insider_transactions(ticker, ...)` | `GET /insider-transactions/{ticker}` |
74
+ | `get_financials(ticker, ...)` | `GET /financials/{ticker}` |
75
+ | `get_revenue_segmentation(ticker, ...)` | `GET /revenue-segmentation/{ticker}` |
76
+ | `get_allocation_breakdown(ticker)` | `GET /allocation-breakdown/{ticker}` |
77
+ | `get_macro(...)` | `GET /macro` |
78
+ | `get_filings(ticker, ...)` | `GET /filings/{ticker}` |
79
+ | `get_filing(filing_id)` | `GET /filings/by-id/{filing_id}` |
80
+ | `get_news(...)` | `GET /news` |
81
+ | `get_ticker_news(ticker, ...)` | `GET /news/{ticker}` |
82
+ | `list_transcript_tickers(...)` | `GET /transcripts/tickers` |
83
+ | `list_transcripts(ticker, ...)` | `GET /transcripts/{ticker}` |
84
+ | `get_transcript(ticker, transcript_id)` | `GET /transcripts/{ticker}/{transcript_id}` |
85
+ | `list_earnings_materials(ticker, ...)` | `GET /earnings-materials/{ticker}` |
86
+ | `get_earnings_material(ticker, material_id)` | `GET /earnings-materials/{ticker}/{material_id}` |
87
+
88
+ ## Rate limits
89
+
90
+ The API allows **1,000 requests per minute** per key. When you exceed it the
91
+ client raises `RateLimitError` (with `.retry_after` seconds). Every response
92
+ also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
93
+ headers.
94
+
95
+ ## Error handling
96
+
97
+ ```python
98
+ from vantafin import RESTClient, RateLimitError, NotFoundError
99
+
100
+ client = RESTClient("vf-live-your_api_key")
101
+ try:
102
+ client.get_quote("NOPE")
103
+ except NotFoundError:
104
+ print("Unknown ticker")
105
+ except RateLimitError as e:
106
+ print(f"Slow down, retry in {e.retry_after}s")
107
+ ```
108
+
109
+ ## WebSocket streaming
110
+
111
+ Stream real-time data over a single WebSocket connection to
112
+ `wss://socket.vantafin.com/v1/stocks`. Three channels are available:
113
+
114
+ | Channel | Subscribe with | Message `type` |
115
+ | --- | --- | --- |
116
+ | Live news / press releases | `N.<ticker>` or `N.*` | `news` |
117
+ | Live SEC filings | `F.<ticker>` or `F.*` | `filing` |
118
+ | Trading halts | `H.<ticker>` or `H.*` | `halt` |
119
+
120
+ The WebSocket client needs the optional `websocket-client` dependency:
121
+
122
+ ```bash
123
+ pip install vantafin[websocket]
124
+ ```
125
+
126
+ ```python
127
+ from vantafin import WebSocketClient
128
+
129
+ ws = WebSocketClient("vf-live-your_api_key")
130
+ ws.connect()
131
+ ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
132
+
133
+ for message in ws:
134
+ print(message)
135
+ ```
136
+
137
+ ## Other languages
138
+
139
+ Official clients for other languages live alongside this Python package:
140
+
141
+ | Language | Folder |
142
+ | --- | --- |
143
+ | JavaScript / Node | [`javascript/`](./javascript) |
144
+ | Go | [`go/`](./go) |
145
+ | Java | [`java/`](./java) |
146
+ | Ruby | [`ruby/`](./ruby) |
147
+
148
+ ## Full documentation
149
+
150
+ [https://vantafin.com/docs](https://vantafin.com/docs)
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "vantafin"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Vantafin" },
10
+ ]
11
+ description = "Official Python client for the Vantafin REST API (market data, fundamentals, SEC filings, news and more)."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ keywords = ["vantafin", "stocks", "market-data", "finance", "sec", "api"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Intended Audience :: Financial and Insurance Industry",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.8",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3.14",
30
+ "Topic :: Office/Business :: Financial",
31
+ ]
32
+ dependencies = ["requests>=2.25"]
33
+
34
+ [project.optional-dependencies]
35
+ websocket = ["websocket-client>=1.6"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://vantafin.com"
39
+ Documentation = "https://vantafin.com/docs"
40
+ Repository = "https://github.com/vantafin/vantafin"
41
+ Issues = "https://github.com/vantafin/vantafin/issues"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/vantafin"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = [
48
+ "/src/vantafin",
49
+ "/LICENSE",
50
+ "/README.md",
51
+ "/pyproject.toml",
52
+ ]
@@ -0,0 +1,35 @@
1
+ """
2
+ Vantafin - official Python client for the Vantafin REST API.
3
+
4
+ from vantafin import RESTClient
5
+
6
+ client = RESTClient("vf-live-your_api_key")
7
+ print(client.get_quote("AAPL"))
8
+
9
+ Docs: https://vantafin.com/docs
10
+ """
11
+
12
+ from .client import RESTClient
13
+ from .ws import WebSocketClient, DEFAULT_WS_URL
14
+ from .exceptions import (
15
+ VantafinError,
16
+ AuthenticationError,
17
+ RateLimitError,
18
+ NotFoundError,
19
+ BadRequestError,
20
+ ServerError,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "RESTClient",
27
+ "WebSocketClient",
28
+ "DEFAULT_WS_URL",
29
+ "VantafinError",
30
+ "AuthenticationError",
31
+ "RateLimitError",
32
+ "NotFoundError",
33
+ "BadRequestError",
34
+ "ServerError",
35
+ ]
@@ -0,0 +1,376 @@
1
+ """Synchronous REST client for the Vantafin Data API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional, Sequence, Union
6
+
7
+ import requests
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ BadRequestError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ ServerError,
15
+ VantafinError,
16
+ )
17
+
18
+ DEFAULT_BASE_URL = "https://api.vantafin.com/v1"
19
+
20
+
21
+ class RESTClient:
22
+ """
23
+ Client for the Vantafin REST API.
24
+
25
+ Args:
26
+ api_key: Your Vantafin API key (``vf-live-...``). Find or reset it in
27
+ Settings → API Usage at https://vantafin.com/app/settings.
28
+ base_url: Override the API base URL (defaults to the production API).
29
+ timeout: Per-request timeout in seconds.
30
+ session: Optionally supply a pre-configured ``requests.Session``.
31
+
32
+ Example:
33
+ >>> from vantafin import RESTClient
34
+ >>> client = RESTClient("vf-live-...")
35
+ >>> client.get_quote("AAPL")
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: str,
41
+ *,
42
+ base_url: str = DEFAULT_BASE_URL,
43
+ timeout: float = 30.0,
44
+ session: Optional[requests.Session] = None,
45
+ ) -> None:
46
+ if not api_key or not isinstance(api_key, str):
47
+ raise ValueError("api_key is required (e.g. 'vf-live-...').")
48
+ self.api_key = api_key.strip()
49
+ self.base_url = base_url.rstrip("/")
50
+ self.timeout = timeout
51
+ self._session = session or requests.Session()
52
+ self._session.headers.update(
53
+ {
54
+ "Authorization": f"Bearer {self.api_key}",
55
+ "Accept": "application/json",
56
+ "User-Agent": "vantafin-python/0.1.0",
57
+ }
58
+ )
59
+
60
+ # ── core request ────────────────────────────────────────────────────
61
+ def _get(self, path: str, params: Optional[dict] = None) -> Any:
62
+ url = f"{self.base_url}{path}"
63
+ clean = {k: v for k, v in (params or {}).items() if v is not None}
64
+ resp = self._session.get(url, params=clean, timeout=self.timeout)
65
+ return self._handle(resp)
66
+
67
+ @staticmethod
68
+ def _handle(resp: requests.Response) -> Any:
69
+ if resp.status_code < 400:
70
+ try:
71
+ return resp.json()
72
+ except ValueError:
73
+ return resp.text
74
+
75
+ try:
76
+ payload = resp.json()
77
+ message = payload.get("error") or payload.get("detail") or resp.text
78
+ except ValueError:
79
+ payload = None
80
+ message = resp.text or resp.reason
81
+
82
+ code = resp.status_code
83
+ if code == 401:
84
+ raise AuthenticationError(message, status_code=code, response=resp)
85
+ if code == 400:
86
+ raise BadRequestError(message, status_code=code, response=resp)
87
+ if code == 404:
88
+ raise NotFoundError(message, status_code=code, response=resp)
89
+ if code == 429:
90
+ retry_after = None
91
+ if isinstance(payload, dict):
92
+ retry_after = payload.get("retry_after_seconds")
93
+ if retry_after is None:
94
+ hdr = resp.headers.get("Retry-After")
95
+ retry_after = int(hdr) if hdr and hdr.isdigit() else None
96
+ raise RateLimitError(
97
+ message, retry_after=retry_after, status_code=code, response=resp
98
+ )
99
+ if code >= 500:
100
+ raise ServerError(message, status_code=code, response=resp)
101
+ raise VantafinError(message, status_code=code, response=resp)
102
+
103
+ # ── status ──────────────────────────────────────────────────────────
104
+ def get_status(self) -> dict:
105
+ """Verify the API key and read your rate-limit ceiling."""
106
+ return self._get("/status")
107
+
108
+ # ── company data ───────────────────────────────────────────────────────
109
+ def list_tickers(
110
+ self,
111
+ *,
112
+ search: Optional[str] = None,
113
+ type: Optional[str] = None,
114
+ cursor: Optional[str] = None,
115
+ limit: Optional[int] = None,
116
+ ) -> dict:
117
+ """List the ticker universe. Paginate via the returned ``next_cursor``."""
118
+ return self._get(
119
+ "/companydata/tickers",
120
+ {"search": search, "type": type, "cursor": cursor, "limit": limit},
121
+ )
122
+
123
+ def get_ticker(self, ticker: str) -> dict:
124
+ """Full company/ETF profile for a single ticker."""
125
+ return self._get(f"/companydata/tickers/{ticker.upper()}")
126
+
127
+ def get_company_by_cik(self, cik: str) -> dict:
128
+ """Full company/ETF profile for a single SEC CIK."""
129
+ return self._get(f"/companydata/ciks/{cik.strip()}")
130
+
131
+ def get_company_by_isin(self, isin: str) -> dict:
132
+ """Full company/ETF profile for a single ISIN."""
133
+ return self._get(f"/companydata/isins/{isin.strip().upper()}")
134
+
135
+ def get_company_by_cusip(self, cusip: str) -> dict:
136
+ """Full company/ETF profile for a single CUSIP."""
137
+ return self._get(f"/companydata/cusips/{cusip.strip().upper()}")
138
+
139
+ def search(self, query: str, *, limit: Optional[int] = None) -> dict:
140
+ """Fuzzy search across tickers and company names."""
141
+ return self._get("/companydata/search", {"query": query, "limit": limit})
142
+
143
+ # ── market data ─────────────────────────────────────────────────────
144
+ def get_quote(self, ticker: str) -> dict:
145
+ """Latest stored quote for a ticker."""
146
+ return self._get(f"/quotes/{ticker.upper()}")
147
+
148
+ def get_quotes(self, tickers: Union[str, Sequence[str]]) -> dict:
149
+ """Latest quotes for up to 200 tickers at once."""
150
+ if not isinstance(tickers, str):
151
+ tickers = ",".join(s.upper() for s in tickers)
152
+ return self._get("/quotes", {"tickers": tickers})
153
+
154
+ # ── screener ────────────────────────────────────────────────────────
155
+ def screener(
156
+ self,
157
+ *,
158
+ filter: Optional[Union[str, Sequence[str]]] = None,
159
+ sort: Optional[str] = None,
160
+ order: Optional[str] = None,
161
+ columns: Optional[Union[str, Sequence[str]]] = None,
162
+ limit: Optional[int] = None,
163
+ offset: Optional[int] = None,
164
+ ) -> dict:
165
+ """Screen the equity universe across the full metric registry.
166
+
167
+ ``filter`` is one or more ``metric:operator:value`` clauses (a string or a list
168
+ of strings), combined with AND, e.g. ``["peRatio:below:20", "marketCap:above:1e9"]``.
169
+ Sort by any metric id, page with ``limit``/``offset`` and request extra metric
170
+ ``columns``. Call :meth:`screener_fields` for the available metrics.
171
+ """
172
+ cols = ",".join(columns) if isinstance(columns, (list, tuple)) else columns
173
+ flt = list(filter) if isinstance(filter, (list, tuple)) else filter
174
+ return self._get(
175
+ "/screener",
176
+ {
177
+ "filter": flt,
178
+ "sort": sort,
179
+ "order": order,
180
+ "columns": cols,
181
+ "limit": limit,
182
+ "offset": offset,
183
+ },
184
+ )
185
+
186
+ def screener_fields(self) -> dict:
187
+ """List every metric available to filter, sort or select in the screener."""
188
+ return self._get("/screener/fields")
189
+
190
+ # ── corporate actions ───────────────────────────────────────────────
191
+ def list_earnings(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
192
+ return self._get("/earnings", {"page": page, "limit": limit})
193
+
194
+ def get_earnings(self, ticker: str, *, limit: Optional[int] = None) -> dict:
195
+ return self._get(f"/earnings/{ticker.upper()}", {"limit": limit})
196
+
197
+ def list_dividends(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
198
+ return self._get("/dividends", {"page": page, "limit": limit})
199
+
200
+ def get_dividends(self, ticker: str, *, limit: Optional[int] = None) -> dict:
201
+ return self._get(f"/dividends/{ticker.upper()}", {"limit": limit})
202
+
203
+ def list_splits(self, *, page: Optional[int] = None, limit: Optional[int] = None) -> dict:
204
+ return self._get("/splits", {"page": page, "limit": limit})
205
+
206
+ def get_splits(self, ticker: str, *, limit: Optional[int] = None) -> dict:
207
+ return self._get(f"/splits/{ticker.upper()}", {"limit": limit})
208
+
209
+ def list_ipos(
210
+ self,
211
+ *,
212
+ from_: Optional[str] = None,
213
+ to: Optional[str] = None,
214
+ page: Optional[int] = None,
215
+ limit: Optional[int] = None,
216
+ ) -> dict:
217
+ """IPO calendar (past + upcoming) across all tickers, newest first."""
218
+ return self._get("/ipos", {"from": from_, "to": to, "page": page, "limit": limit})
219
+
220
+ def get_ipos(self, ticker: str, *, limit: Optional[int] = None) -> dict:
221
+ """Merged IPO record(s) for a single ticker."""
222
+ return self._get(f"/ipos/{ticker.upper()}", {"limit": limit})
223
+
224
+ # ── ownership ───────────────────────────────────────────────────────
225
+ def get_etf_holdings(self, ticker: str) -> dict:
226
+ """Constituent holdings for an ETF or mutual fund."""
227
+ return self._get(f"/etf-holdings/{ticker.upper()}")
228
+
229
+ def get_institutional_ownership(
230
+ self, ticker: str, *, limit: Optional[int] = None
231
+ ) -> dict:
232
+ return self._get(
233
+ f"/institutional-ownership/{ticker.upper()}", {"limit": limit}
234
+ )
235
+
236
+ def get_fund_holdings(self, ticker: str, *, limit: Optional[int] = None) -> dict:
237
+ return self._get(f"/fund-holdings/{ticker.upper()}", {"limit": limit})
238
+
239
+ def get_short_interest(self, ticker: str, *, limit: Optional[int] = None) -> dict:
240
+ return self._get(f"/short-interest/{ticker.upper()}", {"limit": limit})
241
+
242
+ def list_insider_transactions(
243
+ self, *, page: Optional[int] = None, limit: Optional[int] = None
244
+ ) -> dict:
245
+ return self._get("/insider-transactions", {"page": page, "limit": limit})
246
+
247
+ def get_insider_transactions(
248
+ self, ticker: str, *, page: Optional[int] = None, limit: Optional[int] = None
249
+ ) -> dict:
250
+ return self._get(
251
+ f"/insider-transactions/{ticker.upper()}", {"page": page, "limit": limit}
252
+ )
253
+
254
+ # ── fundamentals & macro ────────────────────────────────────────────
255
+ def get_financials(
256
+ self,
257
+ ticker: str,
258
+ *,
259
+ statement: str = "income",
260
+ period: str = "annual",
261
+ limit: Optional[int] = None,
262
+ ) -> dict:
263
+ """Financial statements: 'income', 'balance-sheet' or 'cash-flow'."""
264
+ return self._get(
265
+ f"/financials/{ticker.upper()}",
266
+ {"statement": statement, "period": period, "limit": limit},
267
+ )
268
+
269
+ def get_revenue_segmentation(
270
+ self,
271
+ ticker: str,
272
+ *,
273
+ dimension: str = "product",
274
+ period: str = "annual",
275
+ ) -> dict:
276
+ """Revenue by product line or geography for equity tickers."""
277
+ return self._get(
278
+ f"/revenue-segmentation/{ticker.upper()}",
279
+ {"dimension": dimension, "period": period},
280
+ )
281
+
282
+ def get_allocation_breakdown(self, ticker: str) -> dict:
283
+ """Sector and country weights for an ETF or mutual fund."""
284
+ return self._get(f"/allocation-breakdown/{ticker.upper()}")
285
+
286
+ def get_macro(self, *, category: Optional[str] = None, limit: Optional[int] = None) -> dict:
287
+ """Macro series. Omit ``category`` to list available categories."""
288
+ return self._get("/macro", {"category": category, "limit": limit})
289
+
290
+ # ── filings ─────────────────────────────────────────────────────────
291
+ def get_filings(
292
+ self,
293
+ ticker: str,
294
+ *,
295
+ form: Optional[str] = None,
296
+ page: Optional[int] = None,
297
+ limit: Optional[int] = None,
298
+ ) -> dict:
299
+ return self._get(
300
+ f"/filings/{ticker.upper()}", {"form": form, "page": page, "limit": limit}
301
+ )
302
+
303
+ def get_filing(self, filing_id: str) -> dict:
304
+ return self._get(f"/filings/by-id/{filing_id}")
305
+
306
+ # ── news ────────────────────────────────────────────────────────────
307
+ def get_news(
308
+ self,
309
+ *,
310
+ type: Optional[str] = None,
311
+ page: Optional[int] = None,
312
+ limit: Optional[int] = None,
313
+ ) -> dict:
314
+ return self._get("/news", {"type": type, "page": page, "limit": limit})
315
+
316
+ def get_ticker_news(
317
+ self,
318
+ ticker: str,
319
+ *,
320
+ type: Optional[str] = None,
321
+ page: Optional[int] = None,
322
+ limit: Optional[int] = None,
323
+ ) -> dict:
324
+ return self._get(
325
+ f"/news/{ticker.upper()}", {"type": type, "page": page, "limit": limit}
326
+ )
327
+
328
+ # ── transcripts ─────────────────────────────────────────────────────
329
+ def list_transcript_tickers(
330
+ self,
331
+ *,
332
+ search: Optional[str] = None,
333
+ cursor: Optional[str] = None,
334
+ limit: Optional[int] = None,
335
+ ) -> dict:
336
+ return self._get(
337
+ "/transcripts/tickers",
338
+ {"search": search, "cursor": cursor, "limit": limit},
339
+ )
340
+
341
+ def list_transcripts(
342
+ self, ticker: str, *, page: Optional[int] = None, limit: Optional[int] = None
343
+ ) -> dict:
344
+ return self._get(f"/transcripts/{ticker.upper()}", {"page": page, "limit": limit})
345
+
346
+ def get_transcript(self, ticker: str, transcript_id: str) -> dict:
347
+ return self._get(f"/transcripts/{ticker.upper()}/{transcript_id}")
348
+
349
+ # ── earnings materials ──────────────────────────────────────────────
350
+ def list_earnings_materials(
351
+ self,
352
+ ticker: str,
353
+ *,
354
+ period: Optional[str] = None,
355
+ fiscal_year: Optional[int] = None,
356
+ page: Optional[int] = None,
357
+ limit: Optional[int] = None,
358
+ ) -> dict:
359
+ """List IR decks, press releases, and supplements for a ticker."""
360
+ return self._get(
361
+ f"/earnings-materials/{ticker.upper()}",
362
+ {"period": period, "fiscal_year": fiscal_year, "page": page, "limit": limit},
363
+ )
364
+
365
+ def get_earnings_material(self, ticker: str, material_id: str) -> dict:
366
+ """Metadata for a single earnings material."""
367
+ return self._get(f"/earnings-materials/{ticker.upper()}/{material_id}")
368
+
369
+ def close(self) -> None:
370
+ self._session.close()
371
+
372
+ def __enter__(self) -> "RESTClient":
373
+ return self
374
+
375
+ def __exit__(self, *exc) -> None:
376
+ self.close()
@@ -0,0 +1,38 @@
1
+ """Exception hierarchy for the Vantafin client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+
8
+ class VantafinError(Exception):
9
+ """Base error for all Vantafin API failures."""
10
+
11
+ def __init__(self, message: str, *, status_code: Optional[int] = None, response=None):
12
+ super().__init__(message)
13
+ self.status_code = status_code
14
+ self.response = response
15
+
16
+
17
+ class AuthenticationError(VantafinError):
18
+ """Raised on 401 - missing, invalid or revoked API key."""
19
+
20
+
21
+ class BadRequestError(VantafinError):
22
+ """Raised on 400 - invalid parameters."""
23
+
24
+
25
+ class NotFoundError(VantafinError):
26
+ """Raised on 404 - resource does not exist."""
27
+
28
+
29
+ class RateLimitError(VantafinError):
30
+ """Raised on 429 - too many requests. ``retry_after`` is seconds to wait."""
31
+
32
+ def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs):
33
+ super().__init__(message, **kwargs)
34
+ self.retry_after = retry_after
35
+
36
+
37
+ class ServerError(VantafinError):
38
+ """Raised on 5xx - server-side failure."""
@@ -0,0 +1,105 @@
1
+ """WebSocket client for the Vantafin real-time Data API.
2
+
3
+ Streams live news / press releases (``N.<ticker>`` or ``N.*``),
4
+ live SEC filings (``F.<ticker>`` or ``F.*``), and trading halts
5
+ (``H.<ticker>`` or ``H.*``) over a single connection to
6
+ ``wss://socket.vantafin.com/v1/stocks``.
7
+
8
+ Requires the optional ``websocket-client`` dependency::
9
+
10
+ pip install vantafin[websocket]
11
+
12
+ Example::
13
+
14
+ from vantafin import WebSocketClient
15
+
16
+ ws = WebSocketClient("vf-live-your_api_key")
17
+ ws.connect()
18
+ ws.subscribe(["N.AAPL", "F.AAPL", "H.AAPL"])
19
+ for message in ws:
20
+ print(message)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ from typing import Iterable, Iterator, Optional, Union
27
+
28
+ DEFAULT_WS_URL = "wss://socket.vantafin.com/v1/stocks"
29
+
30
+
31
+ class WebSocketClient:
32
+ """Synchronous WebSocket client for Vantafin real-time streams.
33
+
34
+ Args:
35
+ api_key: Your Vantafin API key (``vf-live-...``).
36
+ url: Override the WebSocket URL (defaults to production).
37
+ """
38
+
39
+ def __init__(self, api_key: str, *, url: str = DEFAULT_WS_URL) -> None:
40
+ if not api_key or not isinstance(api_key, str):
41
+ raise ValueError("api_key is required (e.g. 'vf-live-...').")
42
+ self.api_key = api_key.strip()
43
+ self.url = url
44
+ self._ws = None
45
+
46
+ def connect(self, *, timeout: Optional[float] = None) -> "WebSocketClient":
47
+ """Open the connection, authenticating via the ``apiKey`` query parameter."""
48
+ try:
49
+ import websocket # type: ignore
50
+ except ImportError as exc: # pragma: no cover
51
+ raise ImportError(
52
+ "WebSocketClient requires the 'websocket-client' package. "
53
+ "Install it with: pip install vantafin[websocket]"
54
+ ) from exc
55
+
56
+ sep = "&" if "?" in self.url else "?"
57
+ full_url = f"{self.url}{sep}apiKey={self.api_key}"
58
+ self._ws = websocket.create_connection(full_url, timeout=timeout)
59
+ return self
60
+
61
+ def subscribe(self, channels: Union[str, Iterable[str]]) -> None:
62
+ """Subscribe to one or more channels, e.g. ``"N.AAPL"`` or ``["N.*", "F.AAPL", "H.*"]``."""
63
+ self._send("subscribe", channels)
64
+
65
+ def unsubscribe(self, channels: Union[str, Iterable[str]]) -> None:
66
+ """Unsubscribe from one or more channels."""
67
+ self._send("unsubscribe", channels)
68
+
69
+ def _send(self, action: str, channels: Union[str, Iterable[str]]) -> None:
70
+ if self._ws is None:
71
+ raise RuntimeError("WebSocket is not connected; call connect() first.")
72
+ if isinstance(channels, str):
73
+ channels = [channels]
74
+ params = ",".join(c.strip().upper() for c in channels)
75
+ self._ws.send(json.dumps({"action": action, "params": params}))
76
+
77
+ def recv(self) -> dict:
78
+ """Block for the next message and return it parsed as a dict."""
79
+ if self._ws is None:
80
+ raise RuntimeError("WebSocket is not connected; call connect() first.")
81
+ raw = self._ws.recv()
82
+ try:
83
+ return json.loads(raw)
84
+ except (json.JSONDecodeError, TypeError):
85
+ return {"type": "raw", "data": raw}
86
+
87
+ def __iter__(self) -> Iterator[dict]:
88
+ while True:
89
+ try:
90
+ yield self.recv()
91
+ except Exception:
92
+ return
93
+
94
+ def close(self) -> None:
95
+ if self._ws is not None:
96
+ try:
97
+ self._ws.close()
98
+ finally:
99
+ self._ws = None
100
+
101
+ def __enter__(self) -> "WebSocketClient":
102
+ return self.connect()
103
+
104
+ def __exit__(self, *exc) -> None:
105
+ self.close()