Repository navigation
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
chipkent
force-pushed
the
feat/typing
branch
2 times, most recently
from
October 24, 2026 04:22
7773e8d to
185c717
Compare
Several hints describe values the API never sends or returns:
- CryptoQuote.bid_price, ask_price and ask_size were int; XQ sends
fractional numbers ("bp": 33052.79, "as": 0.601). They are float.
- IndexValue.timestamp was str; V sends integer Unix milliseconds.
- list_tickers(cusip=...) accepted only int. The API treats a CUSIP as a
string: it is alphanumeric, and leading zeros matter ("037833100").
The hint now accepts str as well as int, so existing callers keep
type-checking, and the docstring recommends a string.
- get_previous_close_agg() was annotated as returning one
PreviousCloseAgg, but it returns the "results" array as a list, as the
existing test shows.
- list_treasury_yields(), list_short_interest(), list_short_volume() and
list_ipos() were annotated as returning a List, but like every other
list_* method they return a paginating iterator.
Hints that are imprecise but still admit every value the API sends, such
as integer timestamps hinted as float, are left alone: narrowing them
would gain little and could break callers that type-check today.
Only annotations and docstrings change; runtime behaviour is the same.
@modelclass builds an __init__ that accepts every field positionally or by keyword, but type checkers could not see it: a call such as CryptoQuote(pair="BTC-USD", bid_price=1.5) was reported as "Unexpected keyword argument" for every field. Mark modelclass with dataclass_transform (PEP 681) so checkers derive the constructor from the field annotations. The decorator is imported only under TYPE_CHECKING, from typing on 3.11+ and from typing_extensions on 3.10, and is a no-op at runtime, so there is no new runtime dependency and Python 3.10 keeps working.
Every REST method is annotated as returning Union[Model, HTTPResponse]
(or Union[Iterator[Model], HTTPResponse]), because raw=True returns the
undecoded urllib3 response. Once the package is checked as typed, that
union makes ordinary code fail, because the checker cannot rule out
HTTPResponse:
for t in client.list_tickers():
t.ticker # "HTTPResponse" has no attribute "ticker"
client.get_previous_close_agg("AAPL")[0] # not indexable
Add three overloads to each of the 92 methods that take raw:
- raw: Literal[False] = False returns the model type, so the default
call is precise.
- *, raw: Literal[True] returns HTTPResponse. raw is keyword-only in
this overload because it follows parameters with defaults; a
Literal[True] with a default would overlap the first overload, which
pyright reports.
- raw: bool = False returns the full union, so a non-literal flag, or
raw passed positionally, type-checks exactly as before.
The overloads copy each implementation's signature and differ only in
the raw parameter and the return type. They are declarations only: the
implementations, and the signatures that _get_params inspects at
runtime, are unchanged.
AGENTS.md now describes the pattern for new endpoints.
Without the PEP 561 marker, mypy skips the installed package and reports
import-untyped, and strict users must set ignore_missing_imports, which
turns every massive type into Any.
- Add massive/py.typed. The existing packages entry in pyproject.toml
already includes it in the wheel and sdist.
- Re-export RESTClient and WebSocketClient explicitly ("X as X"), so
"from massive import RESTClient" passes --strict, which does not treat
plain imports in a typed package as re-exports. This adds no __all__,
so "from massive import *" exports exactly the names it did before.
- Add test_rest/test_exports.py, which fails if any package stops
exporting, through "from <package> import *", a name it exported
before this change.
- Add test_rest/typing_consumer.py, a small strict user of the public
API that is type-checked but never run. It iterates list_* results,
indexes get_* lists, checks that raw=True gives HTTPResponse, builds
models with keyword arguments, pins each corrected hint with
assert_type, and keeps a sample of code written for earlier releases
(isinstance narrowing, an int CUSIP) to show it still type-checks.
- make static checks it with mypy --strict from inside test_rest/, so
massive resolves to the installed package as it does for users. It
fails if py.typed goes missing or any of the above regresses.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #<issue: F4>
Summary
Runtime behavior is unchanged. Every change is a type annotation, an
@overloaddeclaration, a docstring, a test, or the emptypy.typedmarker file. No runtime dependency is added. The differences a type checker sees are listed under Typing-visible differences.Type checkers cannot use the package as published: it ships no
py.typedmarker, so mypy skips it. Shipping the marker alone would make things worse, because every REST method is annotatedUnion[Model, HTTPResponse], so ordinary code would stop type-checking:This PR makes the annotations precise and then ships the marker. It has four commits, each reviewable on its own.
Commits
1.
fix: correct type hints that disagree with the API. Only hints that describe values the API never sends or returns are changed:CryptoQuote.bid_price,ask_price,ask_sizeOptional[int]Optional[float]"bp": 33052.79,"as": 0.601IndexValue.timestampOptional[str]Optional[int]tas Unix milliseconds, e.g.1678220098130list_tickers(cusip=...)Optional[int]Optional[Union[int, str]]"037833100").intis still accepted; the docstring recommends a stringget_previous_close_agg()returnsPreviousCloseAggList[PreviousCloseAgg]resultsarray;test_get_previous_close_aggalready expects a listlist_treasury_yields(),list_short_interest(),list_short_volume(),vx.list_ipos()returnList[...]Iterator[...]list_*method, they return the paginating generator from_paginateHints that are imprecise but still admit every value the API sends, such as integer timestamps hinted as
float(CryptoQuote.timestamp,PreviousCloseAgg.timestamp,LastTrade.sequence_number), are deliberately left alone: narrowing them would gain little and could break callers.2.
feat: let type checkers see model constructors.@modelclassbuilds an__init__that type checkers cannot see, soCryptoQuote(pair="BTC-USD", bid_price=1.5)is reported as "Unexpected keyword argument". This commit marksmodelclasswithdataclass_transform(PEP 681), imported only underTYPE_CHECKING(fromtypingon 3.11 and later, fromtyping_extensionson 3.10). At runtime it is a no-op decorator.3.
feat: overload REST methods on raw so callers get precise return types. Each of the 92 methods that takerawgets three@overloaddeclarations ahead of its unchanged implementation:for t in client.list_tickers()andclient.get_aggs(...)[0]check without anisinstanceguard.raw=True: returnsHTTPResponse. In this overloadrawis keyword-only: it follows parameters that have defaults, so it cannot be a required positional parameter, and giving it a default would overlap the first overload, which pyright reports as an error.bool(raw=flag, orrawpassed positionally): the fullUnion, exactly as today.The overloads are copied from each method's own signature and differ only in
rawand the return type. They are generated mechanically, which is why the diff is large (about 6,800 added lines across 14 files). To review, compare one method's overloads against its signature; the rest follow the same pattern.AGENTS.mdnow describes the pattern for new endpoints.4.
feat: ship py.typed so the package is checked as typed.massive/py.typed(PEP 561). The existingpackagesentry inpyproject.tomlalready puts it in the wheel and sdist.RESTClientandWebSocketClientwithimport X as X, the explicit re-export form, sofrom massive import RESTClientpassesmypy --strict. No__all__is added, sofrom massive import *exports exactly the same names as before.test_rest/test_exports.py, which fails ifmassive,massive.rest,massive.rest.models,massive.websocketormassive.websocket.modelsstops exporting, throughfrom <package> import *, any name it exported before this PR.test_rest/typing_consumer.py: ordinary user code that is type-checked but never run. It iterateslist_*results and indexesget_*lists; checks thatraw=TruegivesHTTPResponseand a plainboolgives the union; builds models with keyword arguments; pins each corrected hint withassert_type(an exact check, soOptional[int]does not pass forOptional[float]); and includes code written for earlier releases (narrowing withisinstance, passing anintCUSIP) to show it still type-checks.make staticchecks that file withmypy --strictfrom insidetest_rest/, wheremassiveresolves to the installed package as it does for users, so the check also fails ifpy.typedgoes missing.Runtime behavior: unchanged
I compared
masterwith this branch by importing both trees and recording everything observable:dataclass_transformhelper inmassive/modelclass.py.from <module> import *loses no names. It gains only thetypinghelpers the new annotations import (Literal,overload, andsysinmodelclass).typingimport line.Annotations are visible to runtime introspection (
__annotations__,typing.get_overloads), so those differ by design.@modelclassreads only the names of its fields' hints, not their types, so changing a hint cannot change behavior.Typing-visible differences
These affect only static type checkers.
Wider (no existing code breaks):
list_tickers(cusip=...)acceptsstras well asint.raw) now returns the model type instead ofUnion[Model, HTTPResponse]. Code that narrows withisinstance, or stores the result in aUnion-typed variable, still checks.Narrower (existing code may now be flagged). Each case fixes a hint that was wrong at runtime; the code it flags was already wrong, or is unusual:
CryptoQuote.bid_price,ask_price,ask_sizearefloat. Reading one into anintvariable is now flagged, but the value was never anint.IndexValue.timestampisint. Treating it asstris now flagged, but it was never astr.get_previous_close_agg()returns a list. Storing it in a variable typedUnion[PreviousCloseAgg, HTTPResponse]is now flagged. At runtime it was always a list.list_*methods in commit 1 return an iterator. Storing the result in a variable typedList[...]is now flagged. Indexing it already failed at runtime.TickerChange,TickerChangeEventandTickerChangeResultsare the only models with fields that have no default. Calling one without those fields is now flagged by commit 2. The runtime accepts the call, but reading the missing field then raisesAttributeError.raw=Truecall (x = c.list_trades(...), thenx = c.list_trades(..., raw=True)) is now flagged by mypy, because the first assignment fixes the variable's type. Annotate the variable with theUnion, or use two variables.py.typeditself: mypy now checks calls intomassive. Before, it skipped them, or treated them asAnyunderignore_missing_imports. Code that relied onAnymay now show errors.Still flagged, as before: type checkers reject unknown keyword arguments, and more positional arguments than fields, in model constructors, while the runtime
__init__silently ignores them. Checkers rejected these calls before this PR too, since they could not see the constructor at all.Model(**some_dict)type-checks before and after. No checker-visible signature can say "these fields, plus anything else" without giving up the keyword checking that commit 2 adds, so this is left in place.Not changed: passing
raw=Truepositionally still gets theUnion, as today._getstill returns[]when a response cannot be decoded or lacks theresultskey, even for methods annotated to return a single model. That is existing runtime behavior.Test plan
master,make static's check reportsimport-untypedfor everymassiveimport. Run against themastersources with the marker present, it reports 33 errors in the consumer: missing re-exports, every union-typed call, each wrong hint, thestrCUSIP, and the model constructors. The errors drop with each commit, to 0 on the last one.py.typed: with the marker removed from the final tree, the check reportsimport-untyped.test_exports.pypasses, and fails whenmassive/__init__.pydefines an__all__that drops names.masterwas run through mypy and pyright on this branch. Every common pattern still checks; only the cases listed above are flagged.massive/py.typed. Installed with mypy in clean environments on Python 3.10 and 3.14, it passesmypy --stricton the consumer.massive/rest, the overloads add no new pyright errors.make test,make styleandmake staticpass on Python 3.10 and 3.14.Independent of the other PRs.