massive 2.7.0__tar.gz → 2.9.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.
Files changed (50) hide show
  1. {massive-2.7.0 → massive-2.9.0}/PKG-INFO +3 -3
  2. massive-2.9.0/massive/certs.py +27 -0
  3. {massive-2.7.0 → massive-2.9.0}/massive/rest/__init__.py +26 -1
  4. {massive-2.7.0 → massive-2.9.0}/massive/rest/base.py +33 -10
  5. {massive-2.7.0 → massive-2.9.0}/massive/rest/futures.py +0 -1
  6. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/aggs.py +4 -0
  7. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/conditions.py +5 -0
  8. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/contracts.py +2 -0
  9. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/dividends.py +1 -0
  10. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/exchanges.py +1 -0
  11. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/financials.py +57 -0
  12. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/futures.py +15 -9
  13. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/indicators.py +5 -0
  14. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/markets.py +5 -0
  15. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/quotes.py +5 -0
  16. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/snapshot.py +15 -0
  17. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/splits.py +1 -0
  18. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/summaries.py +3 -0
  19. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/tickers.py +8 -0
  20. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/trades.py +2 -0
  21. {massive-2.7.0 → massive-2.9.0}/massive/rest/reference.py +84 -0
  22. {massive-2.7.0 → massive-2.9.0}/massive/rest/snapshot.py +55 -0
  23. {massive-2.7.0 → massive-2.9.0}/massive/websocket/__init__.py +55 -7
  24. {massive-2.7.0 → massive-2.9.0}/massive/websocket/models/models.py +9 -0
  25. {massive-2.7.0 → massive-2.9.0}/pyproject.toml +4 -4
  26. {massive-2.7.0 → massive-2.9.0}/LICENSE +0 -0
  27. {massive-2.7.0 → massive-2.9.0}/massive/__init__.py +0 -0
  28. {massive-2.7.0 → massive-2.9.0}/massive/exceptions.py +0 -0
  29. {massive-2.7.0 → massive-2.9.0}/massive/logging.py +0 -0
  30. {massive-2.7.0 → massive-2.9.0}/massive/modelclass.py +0 -0
  31. {massive-2.7.0 → massive-2.9.0}/massive/rest/aggs.py +0 -0
  32. {massive-2.7.0 → massive-2.9.0}/massive/rest/benzinga.py +0 -0
  33. {massive-2.7.0 → massive-2.9.0}/massive/rest/economy.py +0 -0
  34. {massive-2.7.0 → massive-2.9.0}/massive/rest/etf_global.py +0 -0
  35. {massive-2.7.0 → massive-2.9.0}/massive/rest/financials.py +0 -0
  36. {massive-2.7.0 → massive-2.9.0}/massive/rest/indicators.py +0 -0
  37. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/__init__.py +0 -0
  38. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/benzinga.py +0 -0
  39. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/common.py +0 -0
  40. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/economy.py +0 -0
  41. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/etf_global.py +0 -0
  42. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/request.py +0 -0
  43. {massive-2.7.0 → massive-2.9.0}/massive/rest/models/tmx.py +0 -0
  44. {massive-2.7.0 → massive-2.9.0}/massive/rest/quotes.py +0 -0
  45. {massive-2.7.0 → massive-2.9.0}/massive/rest/summaries.py +0 -0
  46. {massive-2.7.0 → massive-2.9.0}/massive/rest/tmx.py +0 -0
  47. {massive-2.7.0 → massive-2.9.0}/massive/rest/trades.py +0 -0
  48. {massive-2.7.0 → massive-2.9.0}/massive/rest/vX.py +0 -0
  49. {massive-2.7.0 → massive-2.9.0}/massive/websocket/models/__init__.py +0 -0
  50. {massive-2.7.0 → massive-2.9.0}/massive/websocket/models/common.py +0 -0
@@ -1,20 +1,20 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: massive
3
- Version: 2.7.0
3
+ Version: 2.9.0
4
4
  Summary: Official Massive (formerly Polygon.io) REST and Websocket client.
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: massive,free,rest,stock,market,data,api,massive.com,websocket,client
8
8
  Author: massive.com
9
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
18
  Requires-Dist: certifi (>=2022.5.18,<2027.0.0)
19
19
  Requires-Dist: urllib3 (>=1.26.9)
20
20
  Requires-Dist: websockets (>=14.0)
@@ -0,0 +1,27 @@
1
+ import logging
2
+ import os
3
+ from typing import Optional
4
+
5
+ import certifi
6
+
7
+
8
+ def ca_bundle_path(logger: Optional[logging.Logger] = None) -> str:
9
+ """
10
+ Path of the CA bundle used to verify TLS connections.
11
+
12
+ Honors the standard ``SSL_CERT_FILE`` and ``REQUESTS_CA_BUNDLE`` environment
13
+ variables (used for example behind corporate TLS inspection), falling back to
14
+ the certifi bundle. A variable that doesn't point to an existing file is
15
+ skipped with a warning, so a stale value doesn't break connections.
16
+ """
17
+ for env_var in ("SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"):
18
+ path = os.environ.get(env_var)
19
+ if not path:
20
+ continue
21
+ if os.path.isfile(path):
22
+ return path
23
+ if logger:
24
+ logger.warning(
25
+ "%s is set to %r, which isn't a file; ignoring it", env_var, path
26
+ )
27
+ return certifi.where()
@@ -20,7 +20,9 @@ from .reference import (
20
20
  ContractsClient,
21
21
  )
22
22
  from .vX import VXClient
23
- from typing import Optional, Any
23
+ from .base import logger
24
+ from ..certs import ca_bundle_path
25
+ from typing import Optional, Any, Dict
24
26
  import os
25
27
 
26
28
  BASE = "https://api.massive.com"
@@ -60,7 +62,22 @@ class RESTClient(
60
62
  verbose: bool = False,
61
63
  trace: bool = False,
62
64
  custom_json: Optional[Any] = None,
65
+ maxsize: int = 10,
66
+ ca_certs: Optional[str] = None,
67
+ proxy: Optional[str] = None,
68
+ connection_pool_kw: Optional[Dict[str, Any]] = None,
63
69
  ):
70
+ """
71
+ :param maxsize: Connections kept open per host, for reuse across threads.
72
+ :param ca_certs: Path to a CA bundle to verify TLS with. Defaults to the
73
+ SSL_CERT_FILE or REQUESTS_CA_BUNDLE environment variable, then certifi.
74
+ :param proxy: Proxy URL to send requests through, e.g. "http://user:pass@proxy:8080".
75
+ Environment proxy variables (HTTPS_PROXY etc.) are not used.
76
+ :param connection_pool_kw: Other urllib3 PoolManager options. They override
77
+ the client's defaults.
78
+ """
79
+ # resolve once, so the main and vx clients share it and warn only once
80
+ ca_certs = ca_certs or ca_bundle_path(logger)
64
81
  super().__init__(
65
82
  api_key=api_key,
66
83
  connect_timeout=connect_timeout,
@@ -72,6 +89,10 @@ class RESTClient(
72
89
  verbose=verbose,
73
90
  trace=trace,
74
91
  custom_json=custom_json,
92
+ maxsize=maxsize,
93
+ ca_certs=ca_certs,
94
+ proxy=proxy,
95
+ connection_pool_kw=connection_pool_kw,
75
96
  )
76
97
  self.vx = VXClient(
77
98
  api_key=api_key,
@@ -84,4 +105,8 @@ class RESTClient(
84
105
  verbose=verbose,
85
106
  trace=trace,
86
107
  custom_json=custom_json,
108
+ maxsize=maxsize,
109
+ ca_certs=ca_certs,
110
+ proxy=proxy,
111
+ connection_pool_kw=connection_pool_kw,
87
112
  )
@@ -1,4 +1,3 @@
1
- import certifi
2
1
  import json
3
2
  import urllib3
4
3
  import inspect
@@ -8,6 +7,7 @@ from typing import Optional, Any, Dict
8
7
  from datetime import datetime
9
8
  from importlib.metadata import version, PackageNotFoundError
10
9
  from .models.request import RequestOptionBuilder
10
+ from ..certs import ca_bundle_path
11
11
  from ..logging import get_logger
12
12
  import logging
13
13
  from urllib.parse import urlencode, urlparse
@@ -34,6 +34,10 @@ class BaseClient:
34
34
  verbose: bool,
35
35
  trace: bool,
36
36
  custom_json: Optional[Any] = None,
37
+ maxsize: int = 10,
38
+ ca_certs: Optional[str] = None,
39
+ proxy: Optional[str] = None,
40
+ connection_pool_kw: Optional[Dict[str, Any]] = None,
37
41
  ):
38
42
  if api_key is None:
39
43
  raise AuthError(
@@ -73,14 +77,30 @@ class BaseClient:
73
77
 
74
78
  # https://urllib3.readthedocs.io/en/stable/reference/urllib3.poolmanager.html
75
79
  # https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html#urllib3.HTTPConnectionPool
76
- self.client = urllib3.PoolManager(
77
- num_pools=num_pools,
78
- headers=self.headers, # default headers sent with each request.
79
- ca_certs=certifi.where(),
80
- cert_reqs="CERT_REQUIRED",
81
- retries=retry_strategy, # use the customized Retry instance
82
- timeout=self.timeout, # set timeout for each request
83
- )
80
+ pool_kw: Dict[str, Any] = {
81
+ "num_pools": num_pools,
82
+ # connections kept per host; urllib3 defaults to 1, which makes threaded
83
+ # clients reconnect (and redo the TLS handshake) for most requests
84
+ "maxsize": maxsize,
85
+ "headers": self.headers, # default headers sent with each request.
86
+ "ca_certs": ca_certs or ca_bundle_path(logger),
87
+ "cert_reqs": "CERT_REQUIRED",
88
+ "retries": retry_strategy, # use the customized Retry instance
89
+ "timeout": self.timeout, # set timeout for each request
90
+ }
91
+ # any other urllib3 pool options, which take precedence over the defaults
92
+ pool_kw.update(connection_pool_kw or {})
93
+ self.client: urllib3.PoolManager
94
+ if proxy:
95
+ # https://urllib3.readthedocs.io/en/stable/advanced-usage.html#proxies
96
+ proxy_auth = urllib3.util.parse_url(proxy).auth
97
+ if proxy_auth:
98
+ pool_kw.setdefault(
99
+ "proxy_headers", urllib3.make_headers(proxy_basic_auth=proxy_auth)
100
+ )
101
+ self.client = urllib3.ProxyManager(proxy, **pool_kw)
102
+ else:
103
+ self.client = urllib3.PoolManager(**pool_kw)
84
104
 
85
105
  if verbose:
86
106
  logger.setLevel(logging.DEBUG)
@@ -194,7 +214,10 @@ class BaseClient:
194
214
  # using ., and not _ (removesuffix would work)
195
215
  # but that is python 3.9+
196
216
  argname = argname[: -len(f"_{ext}")] + f".{ext}"
197
- if argname.endswith("any_of"):
217
+ # any_of accepts a comma-separated str (per the spec) or a
218
+ # list of values; only join the latter, since joining a str
219
+ # would split it into single characters
220
+ if argname.endswith("any_of") and not isinstance(val, str):
198
221
  val = ",".join(val)
199
222
  params[argname] = val
200
223
 
@@ -239,7 +239,6 @@ class FuturesClient(BaseClient):
239
239
  product_code_lt: Optional[str] = None,
240
240
  product_code_lte: Optional[str] = None,
241
241
  session_end_date: Optional[str] = None,
242
- session_end_date_any_of: Optional[str] = None,
243
242
  session_end_date_gt: Optional[str] = None,
244
243
  session_end_date_gte: Optional[str] = None,
245
244
  session_end_date_lt: Optional[str] = None,
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Agg:
7
7
  "Contains aggregate data for a given ticker symbol over a given date range in a custom time window size."
8
+
8
9
  open: Optional[float] = None
9
10
  high: Optional[float] = None
10
11
  low: Optional[float] = None
@@ -33,6 +34,7 @@ class Agg:
33
34
  @modelclass
34
35
  class GroupedDailyAgg:
35
36
  "Contains daily open, high, low, and close (OHLC) data for a given date."
37
+
36
38
  ticker: Optional[str] = None
37
39
  open: Optional[float] = None
38
40
  high: Optional[float] = None
@@ -63,6 +65,7 @@ class GroupedDailyAgg:
63
65
  @modelclass
64
66
  class DailyOpenCloseAgg:
65
67
  "Contains data for open, close and afterhours prices of a ticker symbol on a specified date."
68
+
66
69
  after_hours: Optional[float] = None
67
70
  close: Optional[float] = None
68
71
  from_: Optional[str] = None
@@ -95,6 +98,7 @@ class DailyOpenCloseAgg:
95
98
  @modelclass
96
99
  class PreviousCloseAgg:
97
100
  "Contains data for the previous day's open, high, low, and close (OHLC) of the specified stock ticker."
101
+
98
102
  ticker: Optional[str] = None
99
103
  close: Optional[float] = None
100
104
  high: Optional[float] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class SipMapping:
7
7
  "Contains data for a mapping to a symbol for each SIP that has a given condition."
8
+
8
9
  CTA: Optional[str] = None
9
10
  OPRA: Optional[str] = None
10
11
  UTP: Optional[str] = None
@@ -17,6 +18,7 @@ class SipMapping:
17
18
  @modelclass
18
19
  class Consolidated:
19
20
  "Contains data for aggregation rules on a consolidated (all exchanges) basis."
21
+
20
22
  updates_high_low: Optional[bool] = None
21
23
  updates_open_close: Optional[bool] = None
22
24
  updates_volume: Optional[bool] = None
@@ -29,6 +31,7 @@ class Consolidated:
29
31
  @modelclass
30
32
  class MarketCenter:
31
33
  "Contains data for aggregation rules on a per-market-center basis."
34
+
32
35
  updates_high_low: Optional[bool] = None
33
36
  updates_open_close: Optional[bool] = None
34
37
  updates_volume: Optional[bool] = None
@@ -41,6 +44,7 @@ class MarketCenter:
41
44
  @modelclass
42
45
  class UpdateRules:
43
46
  "Contains data for a list of aggregation rules."
47
+
44
48
  consolidated: Optional[Consolidated] = None
45
49
  market_center: Optional[MarketCenter] = None
46
50
 
@@ -63,6 +67,7 @@ class UpdateRules:
63
67
  @modelclass
64
68
  class Condition:
65
69
  "Condition contains data for a condition that Massive.com uses."
70
+
66
71
  abbreviation: Optional[str] = None
67
72
  asset_class: Optional[str] = None
68
73
  data_types: Optional[List[str]] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Underlying:
7
7
  "Underlying contains data for an underlying or deliverable associated with an option contract."
8
+
8
9
  amount: Optional[float] = None
9
10
  type: Optional[str] = None
10
11
  underlying: Optional[str] = None
@@ -17,6 +18,7 @@ class Underlying:
17
18
  @modelclass
18
19
  class OptionsContract:
19
20
  "OptionsContract contains data for a specified ticker symbol."
21
+
20
22
  additional_underlyings: Optional[List[Underlying]] = None
21
23
  cfi: Optional[str] = None
22
24
  contract_type: Optional[str] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Dividend:
7
7
  "Dividend contains data for a historical cash dividend, including the ticker symbol, declaration date, ex-dividend date, record date, pay date, frequency, and amount."
8
+
8
9
  id: Optional[int] = None
9
10
  cash_amount: Optional[float] = None
10
11
  currency: Optional[str] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Exchange:
7
7
  "Exchange contains data for a condition that Massive.com uses."
8
+
8
9
  acronym: Optional[str] = None
9
10
  asset_class: Optional[str] = None
10
11
  id: Optional[int] = None
@@ -922,6 +922,63 @@ class RiskFactorTaxonomy:
922
922
  )
923
923
 
924
924
 
925
+ @modelclass
926
+ class Disclosure:
927
+ """
928
+ A single tagged disclosure within an SEC 8-K filing. A filing can produce
929
+ multiple disclosures, each classified into primary, secondary, and tertiary
930
+ categories with a supporting text excerpt.
931
+ """
932
+
933
+ accession_number: Optional[str] = None
934
+ cik: Optional[str] = None
935
+ filing_date: Optional[str] = None
936
+ filing_url: Optional[str] = None
937
+ primary_category: Optional[str] = None
938
+ secondary_category: Optional[str] = None
939
+ supporting_text: Optional[str] = None
940
+ tertiary_category: Optional[str] = None
941
+ tickers: Optional[List[str]] = None
942
+
943
+ @staticmethod
944
+ def from_dict(d):
945
+ return Disclosure(
946
+ accession_number=d.get("accession_number"),
947
+ cik=d.get("cik"),
948
+ filing_date=d.get("filing_date"),
949
+ filing_url=d.get("filing_url"),
950
+ primary_category=d.get("primary_category"),
951
+ secondary_category=d.get("secondary_category"),
952
+ supporting_text=d.get("supporting_text"),
953
+ tertiary_category=d.get("tertiary_category"),
954
+ tickers=d.get("tickers"),
955
+ )
956
+
957
+
958
+ @modelclass
959
+ class DisclosureTaxonomy:
960
+ """
961
+ A single 8-K disclosure classification, part of the complete list of
962
+ classifications used in the 8-K disclosures endpoint.
963
+ """
964
+
965
+ description: Optional[str] = None
966
+ primary_category: Optional[str] = None
967
+ secondary_category: Optional[str] = None
968
+ taxonomy: Optional[str] = None
969
+ tertiary_category: Optional[str] = None
970
+
971
+ @staticmethod
972
+ def from_dict(d):
973
+ return DisclosureTaxonomy(
974
+ description=d.get("description"),
975
+ primary_category=d.get("primary_category"),
976
+ secondary_category=d.get("secondary_category"),
977
+ taxonomy=d.get("taxonomy"),
978
+ tertiary_category=d.get("tertiary_category"),
979
+ )
980
+
981
+
925
982
  @modelclass
926
983
  @dataclass
927
984
  class Filing13F:
@@ -1,4 +1,4 @@
1
- from typing import Optional, List
1
+ from typing import Optional, List, Union
2
2
  from ...modelclass import modelclass
3
3
 
4
4
 
@@ -150,6 +150,7 @@ class FuturesQuote:
150
150
  bid_price: Optional[float] = None
151
151
  bid_size: Optional[float] = None
152
152
  bid_timestamp: Optional[int] = None
153
+ channel: Optional[int] = None
153
154
  report_sequence: Optional[int] = None
154
155
  sequence_number: Optional[int] = None
155
156
 
@@ -165,6 +166,7 @@ class FuturesQuote:
165
166
  bid_price=d.get("bid_price"),
166
167
  bid_size=d.get("bid_size"),
167
168
  bid_timestamp=d.get("bid_timestamp"),
169
+ channel=d.get("channel"),
168
170
  report_sequence=d.get("report_sequence"),
169
171
  sequence_number=d.get("sequence_number"),
170
172
  )
@@ -180,6 +182,7 @@ class FuturesTrade:
180
182
  ticker: Optional[str] = None
181
183
  timestamp: Optional[int] = None
182
184
  session_end_date: Optional[str] = None
185
+ channel: Optional[int] = None
183
186
  price: Optional[float] = None
184
187
  size: Optional[float] = None
185
188
  report_sequence: Optional[int] = None
@@ -191,6 +194,7 @@ class FuturesTrade:
191
194
  ticker=d.get("ticker"),
192
195
  timestamp=d.get("timestamp"),
193
196
  session_end_date=d.get("session_end_date"),
197
+ channel=d.get("channel"),
194
198
  price=d.get("price"),
195
199
  size=d.get("size"),
196
200
  report_sequence=d.get("report_sequence"),
@@ -248,13 +252,17 @@ class FuturesMarketStatus:
248
252
  @modelclass
249
253
  class FuturesSnapshotDetails:
250
254
  open_interest: Optional[int] = None
251
- settlement_date: Optional[int] = None
255
+ settlement_date: Optional[Union[str, int]] = None
256
+ ticker: Optional[str] = None
257
+ product_code: Optional[str] = None
252
258
 
253
259
  @staticmethod
254
260
  def from_dict(d):
255
261
  return FuturesSnapshotDetails(
256
262
  open_interest=d.get("open_interest"),
257
263
  settlement_date=d.get("settlement_date"),
264
+ ticker=d.get("ticker"),
265
+ product_code=d.get("product_code"),
258
266
  )
259
267
 
260
268
 
@@ -354,6 +362,7 @@ class FuturesSnapshotSession:
354
362
  class FuturesSnapshot:
355
363
  ticker: Optional[str] = None
356
364
  product_code: Optional[str] = None
365
+
357
366
  details: Optional[FuturesSnapshotDetails] = None
358
367
  last_minute: Optional[FuturesSnapshotMinute] = None
359
368
  last_quote: Optional[FuturesSnapshotQuote] = None
@@ -362,14 +371,11 @@ class FuturesSnapshot:
362
371
 
363
372
  @staticmethod
364
373
  def from_dict(d):
374
+ details_dict = d.get("details") or {}
365
375
  return FuturesSnapshot(
366
- ticker=d.get("ticker"),
367
- product_code=d.get("product_code"),
368
- details=(
369
- FuturesSnapshotDetails.from_dict(d.get("details", {}))
370
- if d.get("details")
371
- else None
372
- ),
376
+ ticker=d.get("ticker") or details_dict.get("ticker"),
377
+ product_code=d.get("product_code") or details_dict.get("product_code"),
378
+ details=FuturesSnapshotDetails.from_dict(details_dict),
373
379
  last_minute=(
374
380
  FuturesSnapshotMinute.from_dict(d.get("last_minute", {}))
375
381
  if d.get("last_minute")
@@ -6,6 +6,7 @@ from .aggs import Agg
6
6
  @modelclass
7
7
  class IndicatorValue:
8
8
  "Contains one datum for indicators with a single value."
9
+
9
10
  timestamp: Optional[int] = None
10
11
  value: Optional[float] = None
11
12
 
@@ -20,6 +21,7 @@ class IndicatorValue:
20
21
  @modelclass
21
22
  class MACDIndicatorValue:
22
23
  "Contains one datum for all MACD values."
24
+
23
25
  timestamp: Optional[int] = None
24
26
  value: Optional[float] = None
25
27
  signal: Optional[float] = None
@@ -38,6 +40,7 @@ class MACDIndicatorValue:
38
40
  @modelclass
39
41
  class IndicatorUnderlying:
40
42
  "Contains the URL to call to get the aggs used for building the indicator."
43
+
41
44
  url: Optional[str] = None
42
45
  aggregates: Optional[List[Agg]] = None
43
46
 
@@ -52,6 +55,7 @@ class IndicatorUnderlying:
52
55
  @modelclass
53
56
  class SingleIndicatorResults:
54
57
  "Contains indicator values and Underlying."
58
+
55
59
  values: Optional[List[IndicatorValue]] = None
56
60
  underlying: Optional[IndicatorUnderlying] = None
57
61
 
@@ -71,6 +75,7 @@ RSIIndicatorResults = SingleIndicatorResults
71
75
  @modelclass
72
76
  class MACDIndicatorResults:
73
77
  "Contains indicator values and Underlying."
78
+
74
79
  values: Optional[List[MACDIndicatorValue]] = None
75
80
  underlying: Optional[IndicatorUnderlying] = None
76
81
 
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class MarketCurrencies:
7
7
  "Contains currency market status data."
8
+
8
9
  crypto: Optional[str] = None
9
10
  fx: Optional[str] = None
10
11
 
@@ -16,6 +17,7 @@ class MarketCurrencies:
16
17
  @modelclass
17
18
  class MarketExchanges:
18
19
  "Contains exchange market status data."
20
+
19
21
  nasdaq: Optional[str] = None
20
22
  nyse: Optional[str] = None
21
23
  otc: Optional[str] = None
@@ -28,6 +30,7 @@ class MarketExchanges:
28
30
  @modelclass
29
31
  class MarketIndices:
30
32
  "Contains indices market status data."
33
+
31
34
  s_and_p: Optional[str] = None
32
35
  societe_generale: Optional[str] = None
33
36
  cgi: Optional[str] = None
@@ -47,6 +50,7 @@ class MarketIndices:
47
50
  @modelclass
48
51
  class MarketHoliday:
49
52
  "MarketHoliday contains data for upcoming market holidays and their open/close times."
53
+
50
54
  close: Optional[str] = None
51
55
  date: Optional[str] = None
52
56
  exchange: Optional[str] = None
@@ -62,6 +66,7 @@ class MarketHoliday:
62
66
  @modelclass
63
67
  class MarketStatus:
64
68
  "MarketStatus contains data for the current trading status of the exchanges and overall financial markets."
69
+
65
70
  after_hours: Optional[bool] = None
66
71
  currencies: Optional[MarketCurrencies] = None
67
72
  early_hours: Optional[bool] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Quote:
7
7
  "Quote contains quote data for a specified ticker symbol."
8
+
8
9
  ask_exchange: Optional[int] = None
9
10
  ask_price: Optional[float] = None
10
11
  ask_size: Optional[float] = None
@@ -27,6 +28,7 @@ class Quote:
27
28
  @modelclass
28
29
  class LastQuote:
29
30
  "LastQuote contains data for the most recent NBBO (Quote) tick for a given stock."
31
+
30
32
  ticker: Optional[str] = None
31
33
  trf_timestamp: Optional[int] = None
32
34
  sequence_number: Optional[int] = None
@@ -65,6 +67,7 @@ class LastQuote:
65
67
  @modelclass
66
68
  class ForexQuote:
67
69
  "Contains data for a forex quote."
70
+
68
71
  ask: Optional[float] = None
69
72
  bid: Optional[float] = None
70
73
  exchange: Optional[int] = None
@@ -78,6 +81,7 @@ class ForexQuote:
78
81
  @modelclass
79
82
  class LastForexQuote:
80
83
  "ForexLastQuote contains data for the last quote tick for a forex currency pair."
84
+
81
85
  last: Optional[ForexQuote] = None
82
86
  symbol: Optional[str] = None
83
87
 
@@ -92,6 +96,7 @@ class LastForexQuote:
92
96
  @modelclass
93
97
  class RealTimeCurrencyConversion:
94
98
  "RealTimeCurrencyConversion contains data for currency conversions using the latest market conversion rates."
99
+
95
100
  converted: Optional[float] = None
96
101
  from_: Optional[str] = None
97
102
  initial_amount: Optional[float] = None
@@ -43,6 +43,7 @@ class MinuteSnapshot:
43
43
  @modelclass
44
44
  class IndicesSession:
45
45
  "Contains data for the most recent daily bar in an options contract."
46
+
46
47
  change: Optional[float] = None
47
48
  change_percent: Optional[float] = None
48
49
  close: Optional[float] = None
@@ -66,6 +67,8 @@ class IndicesSnapshot:
66
67
  session: Optional[IndicesSession] = None
67
68
  error: Optional[str] = None
68
69
  message: Optional[str] = None
70
+ last_updated: Optional[int] = None
71
+ timeframe: Optional[str] = None
69
72
 
70
73
  @staticmethod
71
74
  def from_dict(d):
@@ -80,12 +83,15 @@ class IndicesSnapshot:
80
83
  ),
81
84
  error=d.get("error", None),
82
85
  message=d.get("message", None),
86
+ last_updated=d.get("last_updated", None),
87
+ timeframe=d.get("timeframe", None),
83
88
  )
84
89
 
85
90
 
86
91
  @modelclass
87
92
  class TickerSnapshot:
88
93
  "Contains the most up-to-date market data for all traded ticker symbols."
94
+
89
95
  day: Optional[Agg] = None
90
96
  last_quote: Optional[LastQuote] = None
91
97
  last_trade: Optional[LastTrade] = None
@@ -120,6 +126,7 @@ class TickerSnapshot:
120
126
  @modelclass
121
127
  class DayOptionContractSnapshot:
122
128
  "Contains data for the most recent daily bar in an options contract."
129
+
123
130
  change: Optional[float] = None
124
131
  change_percent: Optional[float] = None
125
132
  close: Optional[float] = None
@@ -139,6 +146,7 @@ class DayOptionContractSnapshot:
139
146
  @modelclass
140
147
  class OptionDetails:
141
148
  "Contains details for an options contract."
149
+
142
150
  contract_type: Optional[str] = None
143
151
  exercise_style: Optional[str] = None
144
152
  expiration_date: Optional[str] = None
@@ -154,6 +162,7 @@ class OptionDetails:
154
162
  @modelclass
155
163
  class LastQuoteOptionContractSnapshot:
156
164
  "Contains data for the most recent quote in an options contract."
165
+
157
166
  ask: Optional[float] = None
158
167
  ask_size: Optional[float] = None
159
168
  bid: Optional[float] = None
@@ -170,6 +179,7 @@ class LastQuoteOptionContractSnapshot:
170
179
  @modelclass
171
180
  class LastTradeOptionContractSnapshot:
172
181
  "Contains data for the most recent trade for an options contract."
182
+
173
183
  price: Optional[float] = None
174
184
  sip_timestamp: Optional[int] = None
175
185
  size: Optional[int] = None
@@ -185,6 +195,7 @@ class LastTradeOptionContractSnapshot:
185
195
  @modelclass
186
196
  class Greeks:
187
197
  "Contains data for the greeks in an options contract."
198
+
188
199
  delta: Optional[float] = None
189
200
  gamma: Optional[float] = None
190
201
  theta: Optional[float] = None
@@ -198,6 +209,7 @@ class Greeks:
198
209
  @modelclass
199
210
  class UnderlyingAsset:
200
211
  "Contains data for the underlying stock in an options contract."
212
+
201
213
  change_to_break_even: Optional[float] = None
202
214
  last_updated: Optional[int] = None
203
215
  price: Optional[float] = None
@@ -213,6 +225,7 @@ class UnderlyingAsset:
213
225
  @modelclass
214
226
  class OptionContractSnapshot:
215
227
  "Contains data for the snapshot of an option contract of a stock equity."
228
+
216
229
  break_even_price: Optional[float] = None
217
230
  day: Optional[DayOptionContractSnapshot] = None
218
231
  details: Optional[OptionDetails] = None
@@ -261,6 +274,7 @@ class OptionContractSnapshot:
261
274
  @modelclass
262
275
  class OrderBookQuote:
263
276
  "Contains data for a book bid or ask."
277
+
264
278
  price: Optional[float] = None
265
279
  exchange_shares: Optional[Dict[str, float]] = None
266
280
 
@@ -275,6 +289,7 @@ class OrderBookQuote:
275
289
  @modelclass
276
290
  class SnapshotTickerFullBook:
277
291
  "Contains the current level 2 book of a single ticker. This is the combined book from all of the exchanges."
292
+
278
293
  ticker: Optional[str] = None
279
294
  bids: Optional[List[OrderBookQuote]] = None
280
295
  asks: Optional[List[OrderBookQuote]] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Split:
7
7
  "Split contains data for a historical stock split, including the ticker symbol, the execution date, and the factors of the split ratio."
8
+
8
9
  id: Optional[int] = None
9
10
  execution_date: Optional[str] = None
10
11
  split_from: Optional[int] = None
@@ -6,6 +6,7 @@ from .tickers import Branding
6
6
  @modelclass
7
7
  class Session:
8
8
  "Contains Session data for the summaries endpoint."
9
+
9
10
  change: Optional[float] = None
10
11
  change_percent: Optional[float] = None
11
12
  early_trading_change: Optional[float] = None
@@ -27,6 +28,7 @@ class Session:
27
28
  @modelclass
28
29
  class Options:
29
30
  "Contains options data for the summaries endpoint"
31
+
30
32
  contract_type: Optional[str] = None
31
33
  exercise_style: Optional[str] = None
32
34
  expiration_date: Optional[str] = None
@@ -42,6 +44,7 @@ class Options:
42
44
  @modelclass
43
45
  class SummaryResult:
44
46
  "Contains summary result data for a list of tickers"
47
+
45
48
  price: Optional[float] = None
46
49
  name: Optional[str] = None
47
50
  ticker: Optional[str] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class CompanyAddress:
7
7
  "Contains address data for a ticker detail."
8
+
8
9
  address1: Optional[str] = None
9
10
  address2: Optional[str] = None
10
11
  city: Optional[str] = None
@@ -20,6 +21,7 @@ class CompanyAddress:
20
21
  @modelclass
21
22
  class Branding:
22
23
  "Contains branding data for a ticker detail."
24
+
23
25
  icon_url: Optional[str] = None
24
26
  logo_url: Optional[str] = None
25
27
  accent_color: Optional[str] = None
@@ -34,6 +36,7 @@ class Branding:
34
36
  @modelclass
35
37
  class Insight:
36
38
  "Contains the insights related to the article."
39
+
37
40
  sentiment: Optional[str] = None
38
41
  sentiment_reasoning: Optional[str] = None
39
42
  ticker: Optional[str] = None
@@ -46,6 +49,7 @@ class Insight:
46
49
  @modelclass
47
50
  class Publisher:
48
51
  "Contains publisher data for ticker news."
52
+
49
53
  favicon_url: Optional[str] = None
50
54
  homepage_url: Optional[str] = None
51
55
  logo_url: Optional[str] = None
@@ -59,6 +63,7 @@ class Publisher:
59
63
  @modelclass
60
64
  class Ticker:
61
65
  "Ticker contains data for a specified ticker symbol."
66
+
62
67
  active: Optional[bool] = None
63
68
  cik: Optional[str] = None
64
69
  composite_figi: Optional[str] = None
@@ -85,6 +90,7 @@ class Ticker:
85
90
  @modelclass
86
91
  class TickerDetails:
87
92
  "TickerDetails contains data for a specified ticker symbol."
93
+
88
94
  active: Optional[bool] = None
89
95
  address: Optional[CompanyAddress] = None
90
96
  branding: Optional[Branding] = None
@@ -157,6 +163,7 @@ class TickerDetails:
157
163
  @modelclass
158
164
  class TickerNews:
159
165
  "TickerDetails contains data for news articles relating to a stock ticker symbol."
166
+
160
167
  amp_url: Optional[str] = None
161
168
  article_url: Optional[str] = None
162
169
  author: Optional[str] = None
@@ -197,6 +204,7 @@ class TickerNews:
197
204
  @modelclass
198
205
  class TickerTypes:
199
206
  "TickerTypes contains data for ticker types."
207
+
200
208
  asset_class: Optional[str] = None
201
209
  code: Optional[str] = None
202
210
  description: Optional[str] = None
@@ -5,6 +5,7 @@ from ...modelclass import modelclass
5
5
  @modelclass
6
6
  class Trade:
7
7
  "Trade contains trade data for a specified ticker symbol."
8
+
8
9
  conditions: Optional[List[int]] = None
9
10
  correction: Optional[int] = None
10
11
  exchange: Optional[int] = None
@@ -66,6 +67,7 @@ class LastTrade:
66
67
  @modelclass
67
68
  class CryptoTrade:
68
69
  "Contains data for a crypto trade."
70
+
69
71
  conditions: Optional[List[int]] = None
70
72
  exchange: Optional[int] = None
71
73
  price: Optional[float] = None
@@ -28,6 +28,8 @@ from .models import (
28
28
  ShortVolume,
29
29
  RiskFactor,
30
30
  RiskFactorTaxonomy,
31
+ Disclosure,
32
+ DisclosureTaxonomy,
31
33
  FilingSection,
32
34
  Filing8K,
33
35
  Filing13F,
@@ -862,6 +864,88 @@ class ContractsClient(BaseClient):
862
864
  options=options,
863
865
  )
864
866
 
867
+ def list_stocks_filings_8k_disclosures(
868
+ self,
869
+ cik: Optional[str] = None,
870
+ cik_any_of: Optional[str] = None,
871
+ tickers: Optional[str] = None,
872
+ tickers_all_of: Optional[str] = None,
873
+ tickers_any_of: Optional[str] = None,
874
+ filing_date: Optional[Union[str, date]] = None,
875
+ filing_date_any_of: Optional[str] = None,
876
+ filing_date_gt: Optional[Union[str, date]] = None,
877
+ filing_date_gte: Optional[Union[str, date]] = None,
878
+ filing_date_lt: Optional[Union[str, date]] = None,
879
+ filing_date_lte: Optional[Union[str, date]] = None,
880
+ tertiary_category: Optional[str] = None,
881
+ limit: Optional[int] = 100,
882
+ sort: Optional[Union[str, Sort]] = "filing_date.desc",
883
+ params: Optional[Dict[str, Any]] = None,
884
+ raw: bool = False,
885
+ options: Optional[RequestOptionBuilder] = None,
886
+ ) -> Union[Iterator[Disclosure], HTTPResponse]:
887
+ """
888
+ SEC 8-K filing disclosure categorization. A single 8-K filing can produce
889
+ multiple rows when it covers multiple disclosure types, each classified into
890
+ primary, secondary, and tertiary categories with a supporting text excerpt.
891
+ The full classification list is available at /stocks/taxonomies/vX/disclosures.
892
+ """
893
+ url = "/stocks/filings/8-K/vX/disclosures"
894
+ return self._paginate(
895
+ path=url,
896
+ params=self._get_params(self.list_stocks_filings_8k_disclosures, locals()),
897
+ result_key="results",
898
+ deserializer=Disclosure.from_dict,
899
+ raw=raw,
900
+ options=options,
901
+ )
902
+
903
+ def list_stocks_taxonomies_disclosures(
904
+ self,
905
+ taxonomy: Optional[str] = None,
906
+ taxonomy_any_of: Optional[str] = None,
907
+ taxonomy_gt: Optional[str] = None,
908
+ taxonomy_gte: Optional[str] = None,
909
+ taxonomy_lt: Optional[str] = None,
910
+ taxonomy_lte: Optional[str] = None,
911
+ primary_category: Optional[str] = None,
912
+ primary_category_any_of: Optional[str] = None,
913
+ primary_category_gt: Optional[str] = None,
914
+ primary_category_gte: Optional[str] = None,
915
+ primary_category_lt: Optional[str] = None,
916
+ primary_category_lte: Optional[str] = None,
917
+ secondary_category: Optional[str] = None,
918
+ secondary_category_any_of: Optional[str] = None,
919
+ secondary_category_gt: Optional[str] = None,
920
+ secondary_category_gte: Optional[str] = None,
921
+ secondary_category_lt: Optional[str] = None,
922
+ secondary_category_lte: Optional[str] = None,
923
+ tertiary_category: Optional[str] = None,
924
+ tertiary_category_any_of: Optional[str] = None,
925
+ tertiary_category_gt: Optional[str] = None,
926
+ tertiary_category_gte: Optional[str] = None,
927
+ tertiary_category_lt: Optional[str] = None,
928
+ tertiary_category_lte: Optional[str] = None,
929
+ limit: Optional[int] = 200,
930
+ sort: Optional[Union[str, Sort]] = "taxonomy.desc",
931
+ params: Optional[Dict[str, Any]] = None,
932
+ raw: bool = False,
933
+ options: Optional[RequestOptionBuilder] = None,
934
+ ) -> Union[Iterator[DisclosureTaxonomy], HTTPResponse]:
935
+ """
936
+ The complete list of 8-K disclosure classifications used in the 8-K
937
+ disclosures endpoint.
938
+ """
939
+ url = "/stocks/taxonomies/vX/disclosures"
940
+ return self._paginate(
941
+ path=url,
942
+ params=self._get_params(self.list_stocks_taxonomies_disclosures, locals()),
943
+ result_key="results",
944
+ deserializer=DisclosureTaxonomy.from_dict,
945
+ raw=raw,
946
+ options=options,
947
+ )
948
+
865
949
  def list_stocks_filings_10k_sections(
866
950
  self,
867
951
  cik: Optional[str] = None,
@@ -244,6 +244,17 @@ class SnapshotClient(BaseClient):
244
244
  raw: bool = False,
245
245
  options: Optional[RequestOptionBuilder] = None,
246
246
  ) -> Union[List[IndicesSnapshot], HTTPResponse]:
247
+ """
248
+ Get snapshots for indices. Returns a single page of results; use
249
+ list_snapshot_indices to paginate through all of them.
250
+ - https://massive.com/docs/indices/get_v3_snapshot_indices
251
+
252
+ :param ticker_any_of: Comma-separated list of tickers, up to a maximum of 250.
253
+ :param params: Any additional query params
254
+ :param raw: returns the raw HTTP response if true, else the response is deserialized into a structured object
255
+ :param options: request options
256
+ :return: list of IndicesSnapshot
257
+ """
247
258
  url = f"/v3/snapshot/indices"
248
259
 
249
260
  return self._get(
@@ -254,3 +265,47 @@ class SnapshotClient(BaseClient):
254
265
  result_key="results",
255
266
  options=options,
256
267
  )
268
+
269
+ def list_snapshot_indices(
270
+ self,
271
+ ticker: Optional[str] = None,
272
+ ticker_any_of: Optional[Union[str, List[str]]] = None,
273
+ ticker_lt: Optional[str] = None,
274
+ ticker_lte: Optional[str] = None,
275
+ ticker_gt: Optional[str] = None,
276
+ ticker_gte: Optional[str] = None,
277
+ order: Optional[Union[str, Order]] = None,
278
+ limit: Optional[int] = None,
279
+ sort: Optional[str] = None,
280
+ params: Optional[Dict[str, Any]] = None,
281
+ raw: bool = False,
282
+ options: Optional[RequestOptionBuilder] = None,
283
+ ) -> Union[Iterator[IndicesSnapshot], HTTPResponse]:
284
+ """
285
+ Get snapshots for indices, following pagination.
286
+ - https://massive.com/docs/indices/get_v3_snapshot_indices
287
+
288
+ :param ticker: A single index ticker, e.g. I:SPX.
289
+ :param ticker_any_of: Comma-separated list of tickers, up to a maximum of 250. If no tickers are passed then all
290
+ results will be returned in a paginated manner.
291
+ :param ticker_lt: search for tickers less than
292
+ :param ticker_lte: search for tickers less than or equal to
293
+ :param ticker_gt: search for tickers greater than
294
+ :param ticker_gte: search for tickers greater than or equal to
295
+ :param order: Order results based on the sort field.
296
+ :param limit: Limit the size of the response per-page, max is 250.
297
+ :param sort: Sort field used for ordering. Default is ticker.
298
+ :param params: Any additional query params
299
+ :param raw: returns the raw HTTP response if true, else the response is deserialized into a structured object
300
+ :param options: request options
301
+ :return: iterator of IndicesSnapshot
302
+ """
303
+ url = f"/v3/snapshot/indices"
304
+ return self._paginate(
305
+ path=url,
306
+ params=self._get_params(self.list_snapshot_indices, locals()),
307
+ result_key="results",
308
+ deserializer=IndicesSnapshot.from_dict,
309
+ raw=raw,
310
+ options=options,
311
+ )
@@ -1,14 +1,16 @@
1
1
  import os
2
+ from collections import Counter
2
3
  from enum import Enum
3
4
  from typing import Optional, Union, List, Set, Callable, Awaitable, Any
4
5
  import logging
5
6
  import json
6
7
  import asyncio
7
8
  import ssl
8
- import certifi
9
+ import time
9
10
  from .models import *
10
11
  from websockets.asyncio.client import connect, ClientConnection
11
12
  from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
13
+ from ..certs import ca_bundle_path
12
14
  from ..logging import get_logger
13
15
  import logging
14
16
  from ..exceptions import AuthError
@@ -16,6 +18,10 @@ from ..exceptions import AuthError
16
18
  env_key = "MASSIVE_API_KEY"
17
19
  logger = get_logger("WebSocketClient")
18
20
 
21
+ # Status messages that mean things are working. Any other status (e.g. "error",
22
+ # "max_connections", "slow_consumer", "force_disconnect") is logged as a warning.
23
+ OK_STATUSES = {"connected", "auth_success", "success"}
24
+
19
25
 
20
26
  class WebSocketClient:
21
27
  def __init__(
@@ -29,6 +35,7 @@ class WebSocketClient:
29
35
  max_reconnects: Optional[int] = 5,
30
36
  secure: bool = True,
31
37
  custom_json: Optional[Any] = None,
38
+ ca_certs: Optional[str] = None,
32
39
  **kwargs,
33
40
  ):
34
41
  """
@@ -41,6 +48,7 @@ class WebSocketClient:
41
48
  :param subscriptions: List of subscription parameters.
42
49
  :param max_reconnects: How many times to reconnect on network outage before ending .connect event loop.
43
50
  :param custom_json: Optional module exposing loads/dumps functions (similar to Python's json module) to be used for JSON conversions.
51
+ :param ca_certs: Path to a CA bundle to verify TLS with. Defaults to the SSL_CERT_FILE or REQUESTS_CA_BUNDLE environment variable, then certifi.
44
52
  :return: A client.
45
53
  """
46
54
  if api_key is None:
@@ -70,6 +78,9 @@ class WebSocketClient:
70
78
  subscriptions = []
71
79
  self.scheduled_subs: Set[str] = set(subscriptions)
72
80
  self.schedule_resub = True
81
+ self._server_errors: Counter = Counter()
82
+ self._server_errors_since = 0.0
83
+ self.ca_certs = ca_certs
73
84
  if custom_json:
74
85
  self.json = custom_json
75
86
  else:
@@ -94,11 +105,7 @@ class WebSocketClient:
94
105
  """
95
106
  reconnects = 0
96
107
  logger.debug("connect: %s", self.url)
97
- # darwin needs some extra <3
98
- ssl_context = None
99
- if self.url.startswith("wss://"):
100
- ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
101
- ssl_context.load_verify_locations(certifi.where())
108
+ ssl_context = self._ssl_context()
102
109
 
103
110
  last_exc = None
104
111
  async for s in connect(
@@ -112,6 +119,7 @@ class WebSocketClient:
112
119
  try:
113
120
  msg = await s.recv()
114
121
  logger.debug("connected: %s", msg)
122
+ self._log_server_statuses(self.json.loads(msg))
115
123
  logger.debug("authing...")
116
124
  await s.send(
117
125
  self.json.dumps({"action": "auth", "params": self.api_key})
@@ -121,6 +129,9 @@ class WebSocketClient:
121
129
  logger.debug("authed: %s", auth_msg)
122
130
  if auth_msg_parsed[0]["status"] == "auth_failed":
123
131
  raise AuthError(auth_msg_parsed[0]["message"])
132
+ # e.g. max_connections, which the server sends before closing
133
+ self._log_server_statuses(auth_msg_parsed)
134
+ self._flush_server_errors(force=True)
124
135
  while True:
125
136
  if self.schedule_resub:
126
137
  logger.debug(
@@ -139,6 +150,7 @@ class WebSocketClient:
139
150
  await asyncio.wait_for(s.recv(), timeout=1)
140
151
  )
141
152
  except asyncio.TimeoutError:
153
+ self._flush_server_errors()
142
154
  continue
143
155
 
144
156
  if not self.raw:
@@ -147,17 +159,21 @@ class WebSocketClient:
147
159
  for m in msgJson:
148
160
  if m["ev"] == "status":
149
161
  logger.debug("status: %s", m["message"])
150
- continue
162
+ self._log_server_statuses(msgJson)
151
163
  cmsg = parse(msgJson, logger, self.market)
164
+ self._flush_server_errors()
152
165
 
153
166
  if len(cmsg) > 0:
154
167
  await processor(cmsg) # type: ignore
155
168
  except ConnectionClosedOK as e:
156
169
  logger.debug("connection closed (OK): %s", e)
170
+ # the server sends a status (e.g. slow_consumer) before closing
171
+ self._flush_server_errors(force=True)
157
172
  return
158
173
  except ConnectionClosedError as e:
159
174
  last_exc = e
160
175
  logger.debug("connection closed (ERR): %s", e)
176
+ self._flush_server_errors(force=True)
161
177
  reconnects += 1
162
178
  self.scheduled_subs = set(self.subs)
163
179
  self.subs = set()
@@ -173,6 +189,14 @@ class WebSocketClient:
173
189
  ):
174
190
  raise last_exc
175
191
 
192
+ def _ssl_context(self) -> Optional[ssl.SSLContext]:
193
+ # darwin needs some extra <3
194
+ if not self.url.startswith("wss://"):
195
+ return None
196
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
197
+ context.load_verify_locations(self.ca_certs or ca_bundle_path(logger))
198
+ return context
199
+
176
200
  def run(
177
201
  self,
178
202
  handle_msg: Union[
@@ -195,6 +219,29 @@ class WebSocketClient:
195
219
 
196
220
  asyncio.run(self.connect(handle_msg_wrapper, close_timeout, **kwargs))
197
221
 
222
+ def _log_server_statuses(self, msgs: List[Any]):
223
+ # status messages are dropped by parse(), so problems (e.g. subscriptions
224
+ # the server rejected, or why it disconnected) are logged here. The server
225
+ # can send one error per channel, so identical ones are counted and logged
226
+ # once a second.
227
+ for m in msgs:
228
+ if m.get("ev") != "status" or m.get("status") in OK_STATUSES:
229
+ continue
230
+ if not self._server_errors:
231
+ self._server_errors_since = time.monotonic()
232
+ self._server_errors[(m.get("status"), m.get("message"))] += 1
233
+
234
+ def _flush_server_errors(self, force: bool = False):
235
+ if not self._server_errors:
236
+ return
237
+ if not force and time.monotonic() - self._server_errors_since < 1:
238
+ return
239
+ for (status, message), count in self._server_errors.items():
240
+ logger.warning(
241
+ "server %s: %s%s", status, message, f" (x{count})" if count > 1 else ""
242
+ )
243
+ self._server_errors = Counter()
244
+
198
245
  async def _subscribe(self, topics: Union[List[str], Set[str]]):
199
246
  if self.websocket is None or len(topics) == 0:
200
247
  return
@@ -276,6 +323,7 @@ class WebSocketClient:
276
323
  Close the websocket connection.
277
324
  """
278
325
  logger.debug("closing")
326
+ self._flush_server_errors(force=True)
279
327
 
280
328
  if self.websocket:
281
329
  await self.websocket.close()
@@ -51,6 +51,7 @@ class EquityAgg:
51
51
  @modelclass
52
52
  class CurrencyAgg:
53
53
  "CurrencyAgg contains aggregate data for either forex currency pairs or crypto pairs."
54
+
54
55
  event_type: Optional[Union[str, EventType]] = None
55
56
  pair: Optional[str] = None
56
57
  open: Optional[float] = None
@@ -83,6 +84,7 @@ class CurrencyAgg:
83
84
  @modelclass
84
85
  class EquityTrade:
85
86
  "EquityTrade contains trade data for either stock tickers or option contracts."
87
+
86
88
  event_type: Optional[Union[str, EventType]] = None
87
89
  symbol: Optional[str] = None
88
90
  exchange: Optional[int] = None
@@ -119,6 +121,7 @@ class EquityTrade:
119
121
  @modelclass
120
122
  class CryptoTrade:
121
123
  "CryptoTrade contains trade data for a crypto pair."
124
+
122
125
  event_type: Optional[Union[str, EventType]] = None
123
126
  pair: Optional[str] = None
124
127
  exchange: Optional[int] = None
@@ -147,6 +150,7 @@ class CryptoTrade:
147
150
  @modelclass
148
151
  class EquityQuote:
149
152
  "EquityQuote contains quote data for either stock tickers or option contracts."
153
+
150
154
  event_type: Optional[Union[str, EventType]] = None
151
155
  symbol: Optional[str] = None
152
156
  bid_exchange_id: Optional[int] = None
@@ -187,6 +191,7 @@ class EquityQuote:
187
191
  @modelclass
188
192
  class ForexQuote:
189
193
  "ForexQuote contains quote data for a forex currency pair."
194
+
190
195
  event_type: Optional[Union[str, EventType]] = None
191
196
  pair: Optional[str] = None
192
197
  exchange_id: Optional[int] = None
@@ -209,6 +214,7 @@ class ForexQuote:
209
214
  @modelclass
210
215
  class CryptoQuote:
211
216
  "CryptoQuote contains quote data for a crypto pair."
217
+
212
218
  event_type: Optional[Union[str, EventType]] = None
213
219
  pair: Optional[str] = None
214
220
  bid_price: Optional[int] = None
@@ -237,6 +243,7 @@ class CryptoQuote:
237
243
  @modelclass
238
244
  class Imbalance:
239
245
  "Imbalance contains imbalance event data for a given stock ticker symbol."
246
+
240
247
  event_type: Optional[Union[str, EventType]] = None
241
248
  symbol: Optional[str] = None
242
249
  time_stamp: Optional[int] = None
@@ -267,6 +274,7 @@ class Imbalance:
267
274
  @modelclass
268
275
  class LimitUpLimitDown:
269
276
  "LimitUpLimitDown contains LULD event data for a given stock ticker symbol."
277
+
270
278
  event_type: Optional[Union[str, EventType]] = None
271
279
  symbol: Optional[str] = None
272
280
  high_price: Optional[float] = None
@@ -293,6 +301,7 @@ class LimitUpLimitDown:
293
301
  @modelclass
294
302
  class Level2Book:
295
303
  "Level2Book contains level 2 book data for a given crypto pair."
304
+
296
305
  event_type: Optional[Union[str, EventType]] = None
297
306
  pair: Optional[str] = None
298
307
  bid_prices: Optional[float] = None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "massive"
3
- version = "2.7.0"
3
+ version = "2.9.0"
4
4
  description = "Official Massive (formerly Polygon.io) REST and Websocket client."
5
5
  authors = ["massive.com"]
6
6
  license = "MIT"
@@ -24,13 +24,13 @@ packages = [
24
24
  ]
25
25
 
26
26
  [tool.poetry.dependencies]
27
- python = "^3.9"
27
+ python = "^3.10"
28
28
  urllib3 = ">=1.26.9"
29
29
  websockets = ">=14.0"
30
30
  certifi = ">=2022.5.18,<2027.0.0"
31
31
 
32
32
  [tool.poetry.dev-dependencies]
33
- black = "^24.8.0"
33
+ black = "^26.3.1"
34
34
  mypy = "^1.19"
35
35
  types-urllib3 = "^1.26.25"
36
36
  Sphinx = "^7.4.7"
@@ -40,7 +40,7 @@ sphinx-autodoc-typehints = "^2.3.0"
40
40
  types-certifi = "^2021.10.8"
41
41
  types-setuptools = "^81.0.0"
42
42
  pook = "^2.1.4"
43
- orjson = "^3.11.5"
43
+ orjson = "^3.11.6"
44
44
 
45
45
  [build-system]
46
46
  requires = ["poetry-core>=1.0.0"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes