massive-api-client 0.0.1a0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 caseyvu
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,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: massive-api-client
3
+ Version: 0.0.1a0
4
+ Summary: A Python async wrapper for massive.com API
5
+ Author: Casey Vu
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: massive>=2.4.0
12
+ Dynamic: license-file
13
+
14
+ # TODO
@@ -0,0 +1 @@
1
+ # TODO
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "massive-api-client"
7
+ version = "0.0.1-alpha"
8
+ description = "A Python async wrapper for massive.com API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Casey Vu" }
14
+ ]
15
+ dependencies = [
16
+ "httpx >= 0.28.1",
17
+ "massive >= 2.4.0",
18
+ ]
19
+
20
+ [tool.setuptools]
21
+ package-dir = {"" = "src"}
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ import importlib.metadata
2
+ from .rest import MassiveAPIClient
3
+ from .exceptions import *
4
+
5
+
6
+ __version__ = importlib.metadata.version("massive-api-client")
7
+
8
+ print(__version__)
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(slots=True)
5
+ class MassiveClientConfig:
6
+ massive_api_key: str
7
+ api_base_url: str = "https://api.massive.com"
8
+ timeout_secs: float = 30.0
9
+ rate_limit_sleep_secs: float = 60.0
10
+ rate_limit_max_retries: int = 3
@@ -0,0 +1,19 @@
1
+ from httpx import Response
2
+ from typing import Optional
3
+
4
+
5
+ class APIException(Exception):
6
+
7
+ def __init__(self, message: Optional[str] = None, response: Optional[Response] = None):
8
+ self.status = None if response is None else response.status_code
9
+ self.response_text = None if response is None else response.text
10
+ self.message = message
11
+
12
+
13
+ class RateLimitException(APIException):
14
+ pass
15
+
16
+
17
+ class BadResponseException(APIException):
18
+ pass
19
+
@@ -0,0 +1,4 @@
1
+ from .reference import TickersClient
2
+
3
+ class MassiveAPIClient(TickersClient):
4
+ pass
@@ -0,0 +1,141 @@
1
+ import asyncio
2
+ import httpx
3
+ import inspect
4
+ from collections.abc import AsyncIterator
5
+ from enum import Enum
6
+ from datetime import datetime
7
+ from typing import Any, Dict, Optional, Union
8
+ from urllib.parse import urlparse
9
+
10
+ from ..config import MassiveClientConfig
11
+ from ..exceptions import BadResponseException, RateLimitException
12
+
13
+
14
+ class BaseClient:
15
+
16
+ RATE_LIMIT_HTTP_CODE = 429
17
+ RESULTS_FIELD = "results"
18
+ NEXT_URL_FIELD = "next_url"
19
+ HTTP_METHOD_GET = "GET"
20
+ DEFAULT_MAX_NUM_PAGES = 10000
21
+
22
+
23
+ def __init__(self, config: MassiveClientConfig) -> None:
24
+ self._api_key = config.massive_api_key
25
+ self._base_url = config.api_base_url.rstrip("/")
26
+ self._rate_limit_sleep_secs = config.rate_limit_sleep_secs
27
+ self._rate_limit_max_retries = config.rate_limit_max_retries
28
+ self._client = httpx.AsyncClient(timeout=config.timeout_secs)
29
+ self._headers = {
30
+ "Authorization": "Bearer " + self._api_key,
31
+ "Accept-Encoding": "gzip",
32
+ }
33
+
34
+ async def close(self) -> None:
35
+ await self.client.aclose()
36
+
37
+ async def request(self, uri: str, params: Optional[Dict] = None, deserializer=None, result_key: Optional[str] = RESULTS_FIELD, method: str = HTTP_METHOD_GET) -> Any:
38
+ full_url = self.make_full_url(uri)
39
+ response = await self._request_with_retries(method, full_url, params, self._headers)
40
+ if result_key is not None and result_key not in response:
41
+ raise BadResponseException(message=f"API Response does not contain field: {result_key}", response=response)
42
+
43
+ resp_obj = response if result_key is None else response[result_key]
44
+ return self._apply_deserializer(deserializer, resp_obj)
45
+
46
+ async def request_with_pagination(
47
+ self, uri: str, params: Optional[Dict] = None, deserializer=None,
48
+ max_num_pages: Optional[int] = DEFAULT_MAX_NUM_PAGES, # Defensive check to avoid infinite pagination
49
+ result_key: str = RESULTS_FIELD, method: str = HTTP_METHOD_GET) -> AsyncIterator[Any]:
50
+ full_url = self.make_full_url(uri)
51
+
52
+ max_num_pages = self.DEFAULT_MAX_NUM_PAGES if max_num_pages is None else max_num_pages
53
+ page_i = 0
54
+ while page_i < max_num_pages:
55
+ response = await self._request_with_retries(method, full_url, params, self._headers)
56
+ if result_key not in response:
57
+ raise BadResponseException(message=f"API Response does not contain field: {result_key}", response=response)
58
+
59
+ for item in response[result_key]:
60
+ yield self._apply_deserializer(deserializer, item)
61
+
62
+ next_url = response.get(self.NEXT_URL_FIELD)
63
+ if next_url is None:
64
+ break
65
+
66
+ parsed = urlparse(next_url)
67
+ params = parsed.query
68
+ page_i += 1
69
+
70
+ def _apply_deserializer(self, deserializer, resp_obj) -> Any:
71
+ if deserializer:
72
+ if type(resp_obj) == list:
73
+ return [deserializer(o) for o in resp_obj]
74
+ return deserializer(resp_obj)
75
+ return resp_obj
76
+
77
+ def make_full_url(self, uri: str) -> str:
78
+ return f"{self._base_url}{uri}"
79
+
80
+ async def _request_with_retries(self, method: str, url: str, params: Optional[Union[Dict, str]] = None, headers: Optional[Dict] = None) -> Any:
81
+ response = await self._client.request(method, url, params=params, headers=headers)
82
+
83
+ retries_count = 0
84
+ while response.status_code == self.RATE_LIMIT_HTTP_CODE and retries_count < self._rate_limit_max_retries:
85
+ print(f"SLEEPING 1 min because status_code = 409...")
86
+ await asyncio.sleep(self._rate_limit_sleep_secs)
87
+ response = await self._client.request(method, url, params=params, headers=headers)
88
+ retries_count += 1
89
+
90
+ if response.status_code == self.RATE_LIMIT_HTTP_CODE:
91
+ raise RateLimitException(message=f"Receive status_code={response.status_code} even after {retries_count} retries", response=response)
92
+
93
+ response.raise_for_status()
94
+ return response.json()
95
+
96
+ def _get_params(
97
+ self, fn, caller_locals: Dict[str, Any], datetime_res: str = "nanos"
98
+ ):
99
+ params = {}
100
+ # https://docs.python.org/3.8/library/inspect.html#inspect.Signature
101
+ for argname, v in inspect.signature(fn).parameters.items():
102
+ if argname in ["max_num_pages"]: # ignore these params as they are ops params
103
+ continue
104
+
105
+ # Only params with default are considered part of "params"
106
+ if v.default is v.empty:
107
+ continue
108
+
109
+ val = caller_locals.get(argname, v.default)
110
+ if val is None: # If val is None, we can skip this
111
+ continue
112
+
113
+ if isinstance(val, Enum):
114
+ val = val.value
115
+ elif isinstance(val, bool):
116
+ val = str(val).lower()
117
+ elif isinstance(val, datetime):
118
+ val = int(val.timestamp() * self.time_mult(datetime_res))
119
+
120
+ param_name = argname
121
+ for ext in ["lt", "lte", "gt", "gte", "any_of"]:
122
+ if argname.endswith(f"_{ext}"):
123
+ param_name = argname[: -len(f"_{ext}")] + f".{ext}"
124
+ break
125
+ if param_name.endswith(".any_of"):
126
+ val = ",".join(val)
127
+ params[param_name] = val
128
+
129
+ return params
130
+
131
+ @staticmethod
132
+ def time_mult(timestamp_res: str) -> int:
133
+ if timestamp_res == "nanos":
134
+ return 1000000000
135
+ elif timestamp_res == "micros":
136
+ return 1000000
137
+ elif timestamp_res == "millis":
138
+ return 1000
139
+
140
+ return 1
141
+
@@ -0,0 +1,204 @@
1
+ from collections.abc import AsyncIterator
2
+ from massive.rest.models import (
3
+ AssetClass,
4
+ Locale,
5
+ Order,
6
+ Sort,
7
+ RelatedCompany,
8
+ Ticker,
9
+ TickerChange,
10
+ TickerChangeEvent,
11
+ TickerChangeResults,
12
+ TickerDetails,
13
+ TickerNews,
14
+ TickerTypes,
15
+ )
16
+ from typing import Any, Dict, List, Optional, Union
17
+
18
+ from .base import BaseClient
19
+
20
+
21
+ class TickersClient(BaseClient):
22
+ async def list_tickers(
23
+ self,
24
+ ticker: Optional[str] = None,
25
+ ticker_lt: Optional[str] = None,
26
+ ticker_lte: Optional[str] = None,
27
+ ticker_gt: Optional[str] = None,
28
+ ticker_gte: Optional[str] = None,
29
+ type: Optional[str] = None,
30
+ market: Optional[str] = None,
31
+ exchange: Optional[str] = None,
32
+ cusip: Optional[int] = None,
33
+ cik: Optional[int] = None,
34
+ date: Optional[str] = None,
35
+ active: Optional[bool] = None,
36
+ search: Optional[str] = None,
37
+ limit: Optional[int] = 10,
38
+ sort: Optional[Union[str, Sort]] = "ticker",
39
+ order: Optional[Union[str, Order]] = "asc",
40
+ max_num_pages: Optional[int] = None,
41
+ ) -> AsyncIterator[Ticker]:
42
+ """
43
+ Query all ticker symbols which are supported by Massive.com. This API currently includes Stocks/Equities, Indices, Forex, and Crypto.
44
+
45
+ :param ticker: Specify a ticker symbol. Defaults to empty string which queries all tickers.
46
+ :param ticker_lt: Ticker less than.
47
+ :param ticker_lte: Ticker less than or equal to.
48
+ :param ticker_gt: Ticker greater than.
49
+ :param ticker_gte: Ticker greater than or equal to.
50
+ :param type: Specify the type of the tickers. Find the types that we support via our Ticker Types API. Defaults to empty string which queries all types.
51
+ :param market: Filter by market type. By default all markets are included.
52
+ :param exchange: Specify the assets primary exchange Market Identifier Code (MIC) according to ISO 10383. Defaults to empty string which queries all exchanges.
53
+ :param cusip: Specify the CUSIP code of the asset you want to search for. Find more information about CUSIP codes at their website. Defaults to empty string which queries all CUSIPs.
54
+ :param cik: Specify the CIK of the asset you want to search for. Find more information about CIK codes at their website. Defaults to empty string which queries all CIKs.
55
+ :param date: Specify a point in time to retrieve tickers available on that date. Defaults to the most recent available date.
56
+ :param search: Search for terms within the ticker and/or company name.
57
+ :param active: Specify if the tickers returned should be actively traded on the queried date. Default is true.
58
+ :param limit: Limit the size of the response per-page, default is 100 and max is 1000.
59
+ :param sort: The field to sort the results on. Default is ticker. If the search query parameter is present, sort is ignored and results are ordered by relevance.
60
+ :param order: The order to sort the results on. Default is asc (ascending).
61
+ :return: Iterator of tickers.
62
+ """
63
+ uri = "/v3/reference/tickers"
64
+
65
+ async for r in self.request_with_pagination(
66
+ uri=uri,
67
+ params=self._get_params(self.list_tickers, locals()),
68
+ deserializer=Ticker.from_dict,
69
+ max_num_pages=max_num_pages,
70
+ ):
71
+ yield r
72
+
73
+ async def get_ticker_details(
74
+ self,
75
+ ticker: str,
76
+ date: Optional[str] = None,
77
+ ) -> TickerDetails:
78
+ """
79
+ Get a single ticker supported by Massive.com. This response will have detailed information about the ticker and the company behind it.
80
+
81
+ :param ticker: The ticker symbol of the asset.
82
+ :param date: Specify a point in time to get information about the ticker available on that date. When retrieving information from SEC filings, we compare this date with the period of report date on the SEC filing.
83
+ :return: Ticker Details V3
84
+ """
85
+ uri = f"/v3/reference/tickers/{ticker}"
86
+
87
+ return await self.request(
88
+ uri=uri,
89
+ params=self._get_params(self.get_ticker_details, locals()),
90
+ deserializer=TickerDetails.from_dict,
91
+ )
92
+
93
+ async def get_ticker_events(
94
+ self,
95
+ ticker: str,
96
+ types: Optional[List[str]] = None,
97
+ ) -> TickerChangeResults:
98
+ """
99
+ Get event history of ticker given particular point in time.
100
+ :param ticker: The ticker symbol of the asset.
101
+ :param types: A comma-separated list of the types of event to include. Currently ticker_change is the only supported event_type. Leave blank to return all supported event_types.
102
+ :return: Ticker Event VX (Experimental)
103
+ """
104
+ uri = f"/vX/reference/tickers/{ticker}/events"
105
+
106
+ params = {"types": ",".join(types) if types is not None else ""}
107
+
108
+ def deserialize_event(e) -> TickerChangeEvent:
109
+ return TickerChangeEvent(
110
+ type=e.get("type"),
111
+ date=e.get("date"),
112
+ ticker_change=None if "ticker_change" not in e else TickerChange.from_dict(e.get("ticker_change")),
113
+ )
114
+
115
+ def deserialize_result(d) -> TickerChangeResults:
116
+ return TickerChangeResults(
117
+ name=d.get("name"),
118
+ composite_figi=d.get("composite_figi"),
119
+ cik=d.get("cik"),
120
+ events=[deserialize_event(e) for e in d.get("events", [])]
121
+ )
122
+
123
+ return await self.request(
124
+ uri=uri,
125
+ params=params,
126
+ deserializer=deserialize_result,
127
+ )
128
+
129
+ async def list_ticker_news(
130
+ self,
131
+ ticker: Optional[str] = None,
132
+ ticker_lt: Optional[str] = None,
133
+ ticker_lte: Optional[str] = None,
134
+ ticker_gt: Optional[str] = None,
135
+ ticker_gte: Optional[str] = None,
136
+ published_utc: Optional[str] = None,
137
+ published_utc_lt: Optional[str] = None,
138
+ published_utc_lte: Optional[str] = None,
139
+ published_utc_gt: Optional[str] = None,
140
+ published_utc_gte: Optional[str] = None,
141
+ limit: Optional[int] = None,
142
+ sort: Optional[Union[str, Sort]] = None,
143
+ order: Optional[Union[str, Order]] = None,
144
+ max_num_pages: Optional[int] = None,
145
+ ) -> AsyncIterator[TickerNews]:
146
+ """
147
+ Get the most recent news articles relating to a stock ticker symbol, including a summary of the article and a link to the original source.
148
+
149
+ :param ticker: Return results that contain this ticker.
150
+ :param published_utc: Return results published on, before, or after this date.
151
+ :param limit: Limit the number of results returned per-page, default is 10 and max is 1000.
152
+ :param sort: Sort field used for ordering.
153
+ :param order: Order results based on the sort field.
154
+ :return: Iterator of Ticker News.
155
+ """
156
+ uri = "/v2/reference/news"
157
+
158
+ async for r in self.request_with_pagination(
159
+ uri=uri,
160
+ params=self._get_params(self.list_ticker_news, locals()),
161
+ deserializer=TickerNews.from_dict,
162
+ max_num_pages=max_num_pages,
163
+ ):
164
+ yield r
165
+
166
+ async def get_ticker_types(
167
+ self,
168
+ asset_class: Optional[Union[str, AssetClass]] = None,
169
+ locale: Optional[Union[str, Locale]] = None,
170
+ ) -> List[TickerTypes]:
171
+ """
172
+ List all ticker types that Massive.com has.
173
+
174
+ :param asset_class: Filter by asset class.
175
+ :param locale: Filter by locale.
176
+ :param params: Any additional query params.
177
+ :param raw: Return raw object instead of results object.
178
+ :return: Ticker Types.
179
+ """
180
+ uri = "/v3/reference/tickers/types"
181
+
182
+ return await self.request(
183
+ uri=uri,
184
+ params=self._get_params(self.get_ticker_types, locals()),
185
+ deserializer=TickerTypes.from_dict,
186
+ )
187
+
188
+ async def get_related_companies(
189
+ self,
190
+ ticker: str,
191
+ ) -> RelatedCompany:
192
+ """
193
+ Get a list of tickers related to the queried ticker based on News and Returns data.
194
+
195
+ :param ticker: The ticker symbol to search.
196
+ :return: Related Companies.
197
+ """
198
+ uri = f"/v1/related-companies/{ticker}"
199
+
200
+ return await self.request(
201
+ uri=uri,
202
+ params={},
203
+ deserializer=RelatedCompany.from_dict,
204
+ )
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: massive-api-client
3
+ Version: 0.0.1a0
4
+ Summary: A Python async wrapper for massive.com API
5
+ Author: Casey Vu
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: massive>=2.4.0
12
+ Dynamic: license-file
13
+
14
+ # TODO
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/massive_api_client/__init__.py
5
+ src/massive_api_client/config.py
6
+ src/massive_api_client/exceptions.py
7
+ src/massive_api_client.egg-info/PKG-INFO
8
+ src/massive_api_client.egg-info/SOURCES.txt
9
+ src/massive_api_client.egg-info/dependency_links.txt
10
+ src/massive_api_client.egg-info/requires.txt
11
+ src/massive_api_client.egg-info/top_level.txt
12
+ src/massive_api_client/rest/__init__.py
13
+ src/massive_api_client/rest/base.py
14
+ src/massive_api_client/rest/reference.py
@@ -0,0 +1,2 @@
1
+ httpx>=0.28.1
2
+ massive>=2.4.0