Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5b5b45f
fix: stop sending the options argument as a query parameter
chipkent Oct 24, 2026
052a9ff
test: stop pook mocks leaking between REST test classes
chipkent Oct 24, 2026
a000902
ci: run unit tests on Python 3.12, 3.13 and 3.14
chipkent Oct 24, 2026
a06ef39
fix: log the topic for an invalid subscription
chipkent Oct 23, 2026
182dd9d
feat: let type checkers see model constructors
chipkent Oct 24, 2026
ce957c7
fix: correct type hints that disagree with the API
chipkent Oct 24, 2026
89df459
fix: include the HTTP status in BadResponse
chipkent Oct 24, 2026
a4601b6
feat: add raise_on_status to RESTClient
chipkent Oct 24, 2026
ab20477
ci: run unit tests on free-threaded Python 3.14t
chipkent Oct 24, 2026
0f6a9d7
feat: let type checkers see model constructors
chipkent Oct 24, 2026
81b08a3
fix: match WebSocket wildcard topics on the exact channel
chipkent Oct 24, 2026
8da79e1
feat: ship py.typed so the package is checked as typed
chipkent Oct 24, 2026
52c0ab5
feat: add list_funding_conditions for /fed/v1/funding-conditions
chipkent Oct 23, 2026
6ead873
feat: add get_daily_open_close_crypto for /v1/open-close/crypto/{from…
chipkent Oct 23, 2026
1b7e6ef
feat: overload REST methods on raw so callers get precise return types
chipkent Oct 24, 2026
5b3307f
test: type-check a strict user of the public API
chipkent Oct 24, 2026
4c8e194
fix: keep pending WebSocket subscriptions across a reconnect
chipkent Oct 24, 2026
6ebec59
feat: add opt-in WebSocket reconnect options
chipkent Oct 24, 2026
f47a026
feat: add WebSocketClient.request_close()
chipkent Oct 24, 2026
a444d2c
fix: make WebSocket subscribe and unsubscribe thread-safe
chipkent Oct 24, 2026
8247c09
feat: send WebSocket subscription changes without waiting for recv
chipkent Oct 24, 2026
d847d8f
feat: add WebSocketClient.set_subscriptions() and resubscribe()
chipkent Oct 24, 2026
fb5ea45
Merge #4 fix: log the topic for an invalid subscription
chipkent Oct 24, 2026
57e5acd
Merge #7 feat: add funding-conditions and crypto daily open/close end…
chipkent Oct 24, 2026
26df0d3
Merge #8 fix: stop sending the options argument as a query parameter
chipkent Oct 24, 2026
d96c5c7
Merge #9 test: stop pook mocks leaking between REST test classes
chipkent Oct 24, 2026
5680a72
Merge #10 ci: run unit tests on Python 3.12, 3.13 and 3.14
chipkent Oct 24, 2026
2da21d3
Merge #11 ci: run unit tests on free-threaded Python 3.14t
chipkent Oct 24, 2026
556f19e
Merge #12 fix: include the HTTP status in BadResponse
chipkent Oct 24, 2026
4125ecb
Merge #13 feat: add raise_on_status to RESTClient
chipkent Oct 24, 2026
779b97b
Merge #14 fix: correct type hints that disagree with the API
chipkent Oct 24, 2026
65e3eb8
Merge #15 feat: let type checkers see model constructors
chipkent Oct 24, 2026
e77b5d7
Merge #16 feat: ship py.typed so the package is checked as typed
chipkent Oct 24, 2026
279edde
Merge #17 feat: overload REST methods on raw so callers get precise r…
chipkent Oct 24, 2026
9cbf734
Merge #18 fix: match WebSocket wildcard topics on the exact channel
chipkent Oct 24, 2026
595dec8
Merge #19 fix: keep pending WebSocket subscriptions across a reconnect
chipkent Oct 24, 2026
09da25e
Merge #20 fix: make WebSocket subscribe and unsubscribe thread-safe
chipkent Oct 24, 2026
247a6a5
Merge #21 feat: send WebSocket subscription changes without waiting f…
chipkent Oct 24, 2026
896991c
Merge #22 feat: add WebSocketClient.set_subscriptions() and resubscri…
chipkent Oct 24, 2026
2720e3e
Merge #23 feat: add opt-in WebSocket reconnect options
chipkent Oct 24, 2026
152b5e3
Merge #24 feat: add WebSocketClient.request_close()
chipkent Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.10', '3.11']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Unit test ${{ matrix.python-version }}
steps:
Expand All @@ -32,3 +32,23 @@ jobs:
run: poetry install
- name: Unit tests
run: make test
test-free-threaded:
runs-on: ubuntu-latest
name: ubuntu-latest Unit test 3.14t (PYTHON_GIL=0)
env:
PYTHON_GIL: '0'
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.14t'
- name: Install pypi deps
# the tests need only the package and pook, not the dev tools
run: pip install -e . pook
- name: Check that the GIL is off
run: python -c "import sys; assert not sys._is_gil_enabled(), sys.version"
- name: Unit tests
run: |
python -m unittest discover -s test_rest
python -m unittest discover -s test_websocket
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ The OpenAPI spec at https://api.massive.com/openapi is the source of truth for p
- Single-object endpoints are named `get_<...>` and call `self._get(...)`.
- Every query param is a keyword argument defaulting to `None`. Write range filters as `x_gt`, `x_gte`, `x_lt`, `x_lte`, `x_any_of`; `_get_params` turns them into `x.gt` and so on.
- Always end the signature with `params: Optional[Dict[str, Any]] = None, raw: bool = False, options: Optional[RequestOptionBuilder] = None`.
- Precede the method with three `@overload`s that copy its signature, so callers get the precise type for each value of `raw`: `raw: Literal[False] = False` returns the model type (`Iterator[Model]`, `List[Model]` or `Model`); `*, raw: Literal[True]` (keyword-only) returns `HTTPResponse`; and `raw: bool = False` returns the full `Union`. Copy an existing method, such as `list_aggs` in `massive/rest/aggs.py`.
4. **New domain only.** Create a `BaseClient` subclass and add it to `RESTClient`'s bases in `massive/rest/__init__.py`.
5. **Tests, examples, docs.** For core endpoints, add a fixture under `test_rest/mocks/<url path>.json`, a test in `test_rest/test_<domain>.py`, an example in `examples/rest/`, and an `automethod` entry in `docs/source/*.rst`.
6. Run `make lint` and `make test`.
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ style:
## Check static types
static:
poetry run mypy massive test_* examples
@# From test_rest/, massive resolves to the installed package, as it does
@# for users, so this also fails if the package stops shipping py.typed.
cd test_rest && poetry run mypy --strict typing_consumer.py

## Check code style and static types
lint: style static
Expand Down
9 changes: 8 additions & 1 deletion docs/source/Aggs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,17 @@ Get daily open close agg

- `Stocks daily open/close agg`_
- `Options daily open/close agg`_
- `Crypto daily open/close agg`_

.. automethod:: massive.RESTClient.get_daily_open_close_agg

============================
Get daily open close crypto
============================

- `Crypto daily open/close agg`_

.. automethod:: massive.RESTClient.get_daily_open_close_crypto

============================
Get previous close agg
============================
Expand Down
15 changes: 15 additions & 0 deletions docs/source/WebSocket.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,26 @@ Unsubscribe
============================
.. automethod:: massive.WebSocketClient.unsubscribe

============================
Set subscriptions
============================
.. automethod:: massive.WebSocketClient.set_subscriptions

============================
Resubscribe
============================
.. automethod:: massive.WebSocketClient.resubscribe

============================
Close
============================
.. automethod:: massive.WebSocketClient.close

============================
Request close
============================
.. automethod:: massive.WebSocketClient.request_close

.. _Stocks getting started: https://massive.com/docs/stocks/ws_getting-started
.. _Options getting started: https://massive.com/docs/options/ws_getting-started
.. _Forex getting started: https://massive.com/docs/forex/ws_getting-started
Expand Down
7 changes: 4 additions & 3 deletions examples/rest/crypto-daily_open_close.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

# docs
# https://massive.com/docs/crypto/get_v1_open-close_crypto__from___to___date
# https://massive-api-client.readthedocs.io/en/latest/Aggs.html#get-daily-open-close-agg
# https://massive-api-client.readthedocs.io/en/latest/Aggs.html#get-daily-open-close-crypto

# client = RESTClient("XXXXXX") # hardcoded api_key is used
client = RESTClient() # MASSIVE_API_KEY environment variable is used

# make request
request = client.get_daily_open_close_agg(
"X:BTCUSD",
request = client.get_daily_open_close_crypto(
"BTC",
"USD",
"2023-01-09",
)

Expand Down
13 changes: 13 additions & 0 deletions examples/rest/economy-funding_conditions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from massive import RESTClient

# docs
# https://massive.com/docs/rest/economy/funding-conditions

# client = RESTClient("XXXXXX") # hardcoded api_key is used
client = RESTClient() # MASSIVE_API_KEY environment variable is used

conditions = []
for day in client.list_funding_conditions(date_gte="2025-06-01", sort="date.asc"):
conditions.append(day)

print(conditions)
4 changes: 2 additions & 2 deletions massive/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .rest import RESTClient
from .rest import RESTClient as RESTClient
from .rest.base import version
from .websocket import WebSocketClient
from .websocket import WebSocketClient as WebSocketClient
from .exceptions import *

__version__ = version
13 changes: 12 additions & 1 deletion massive/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
from typing import Optional

# the package star-imports this module; keep Optional out of its namespace
__all__ = ["AuthError", "BadResponse"]


class AuthError(Exception):
"""
Empty or invalid API key
Expand All @@ -9,6 +15,11 @@ class AuthError(Exception):
class BadResponse(Exception):
"""
Non-200 response from API

The message is the response body. ``status`` is the HTTP status code, or
None if not given.
"""

pass
def __init__(self, *args: object, status: Optional[int] = None):
super().__init__(*args)
self.status = status
13 changes: 13 additions & 0 deletions massive/modelclass.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import inspect
import sys
import typing
from dataclasses import dataclass

if typing.TYPE_CHECKING:
if sys.version_info >= (3, 11):
from typing import dataclass_transform
else:
from typing_extensions import dataclass_transform
else:

def dataclass_transform(**kwargs):
return lambda f: f


_T = typing.TypeVar("_T")


@dataclass_transform()
def modelclass(cls: typing.Type[_T]) -> typing.Type[_T]:
cls = dataclass(cls)
type_hints = typing.get_type_hints(cls)
Expand Down
Empty file added massive/py.typed
Empty file.
6 changes: 6 additions & 0 deletions massive/rest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(
ca_certs: Optional[str] = None,
proxy: Optional[str] = None,
connection_pool_kw: Optional[Dict[str, Any]] = None,
raise_on_status: bool = True,
):
"""
:param maxsize: Connections kept open per host, for reuse across threads.
Expand All @@ -75,6 +76,9 @@ def __init__(
Environment proxy variables (HTTPS_PROXY etc.) are not used.
:param connection_pool_kw: Other urllib3 PoolManager options. They override
the client's defaults.
:param raise_on_status: When a retried status (413, 429, 499, 500, 502, 503,
504) is still returned after the last retry: if True, urllib3 raises
MaxRetryError; if False, the client raises BadResponse with that status.
"""
# resolve once, so the main and vx clients share it and warn only once
ca_certs = ca_certs or ca_bundle_path(logger)
Expand All @@ -93,6 +97,7 @@ def __init__(
ca_certs=ca_certs,
proxy=proxy,
connection_pool_kw=connection_pool_kw,
raise_on_status=raise_on_status,
)
self.vx = VXClient(
api_key=api_key,
Expand All @@ -109,4 +114,5 @@ def __init__(
ca_certs=ca_certs,
proxy=proxy,
connection_pool_kw=connection_pool_kw,
raise_on_status=raise_on_status,
)
Loading
Loading