Skip to content

fix(ws-models): OrderbookDeltaPayload.ts AwareDatetime + envelope type Literal narrowing#365

Merged
TexasCoding merged 1 commit into
mainfrom
r3/W2-D
May 22, 2026
Merged

fix(ws-models): OrderbookDeltaPayload.ts AwareDatetime + envelope type Literal narrowing#365
TexasCoding merged 1 commit into
mainfrom
r3/W2-D

Conversation

@TexasCoding

Copy link
Copy Markdown
Owner

Summary

Widen OrderbookDeltaPayload.ts to pydantic.AwareDatetime (closing the gap left by the v2.5 #270 WS datetime sweep) and narrow each WS envelope's type field from bare str to its channel-specific Literal[...]. No wire-format change.

Issues closed

Behavioral changes

  • OrderbookDeltaPayload.ts is now AwareDatetime | None. Naive RFC3339 strings on this field now raise ValidationError instead of silently parsing as str. Aware RFC3339 strings continue to parse, but as datetime instead of str.
  • WS envelope type is now a Literal. Five existing tests/ws/test_models.py fixtures sent "type": "user_orders" (plural, channel name) where the dispatch table — and therefore the wire type value — is "user_order" (singular). Those fixtures are corrected to match the dispatch table.

Tests

  • tests/ws/test_models.py::TestIssue331OrderbookDeltaTsAwareDatetime — RFC3339 string parses to tz-aware datetime; naive string raises ValidationError.
  • tests/ws/test_models.py::TestIssue353EnvelopeTypeLiteralNarrowing — All seven narrowed envelopes accept their own type literal and reject any other channel's literal; model_fields["type"].annotation is no longer bare str.
  • Updated test_delta_with_optional_fields assertion to compare against a datetime value.

Source

Round-3 independent audit closure plan, wave W2.

…iteral

`OrderbookDeltaPayload.ts` was missed by the v2.5 #270 WS AwareDatetime
sweep that closed the v2.4 #234 REST gap. Widen it to `AwareDatetime |
None` so naive RFC3339 strings on the highest-volume WS channel raise
`ValidationError` rather than silently passing as `str`, matching
`user_orders` and `communications`.

Narrow every WS message envelope's `type` field from bare `str` to its
channel-specific `Literal[...]` across `OrderbookSnapshotMessage`,
`OrderbookDeltaMessage`, `TickerMessage`, `FillMessage`,
`UserOrdersMessage`, `MarketLifecycleMessage`, and
`CommunicationsMessage`. The default-value form keeps no-arg
construction ergonomics but constructing an envelope with a mismatched
`type` string now raises `ValidationError`. This also unlocks
`pydantic.Discriminator("type")` for the dispatcher's union without a
follow-up wire-format change.

Five `tests/ws/test_models.py` fixtures sent `"type": "user_orders"`
(plural, channel name) where the wire `type` value per the dispatcher
table is `"user_order"` (singular). Those fixtures are corrected to
match the dispatch table that real frames go through.

Closes #331, #353
@TexasCoding TexasCoding added bug Something isn't working spec-drift Upstream OpenAPI/AsyncAPI spec changed since last sync ws WebSocket-related labels May 22, 2026
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review — PR #365

Overview

Two focused spec-drift fixes:

  1. OrderbookDeltaPayload.ts still typed as str — v2.5 #270 WS AwareDatetime sweep missed it #331OrderbookDeltaPayload.ts promoted from str | None to AwareDatetime | None, closing the gap left by the v2.5 datetime sweep.
  2. WS message envelope type field is bare str with a default — should be Literal #353 — WS envelope type narrowed from bare str to Literal[<channel>] across 7 message envelopes.

The changes are minimal, surgical, and correct. The user_ordersuser_order test fixture correction is the right fix — the dispatch table already used the singular form, so those 5 test fixtures were never exercising accurate wire data.


Code Quality

Strengths:

  • Each affected file adds exactly from typing import Literal and changes type: str = "<val>" to type: Literal["<val>"] = "<val>". No unnecessary churn.
  • match="timezone" in the ts rejection test is consistent with the existing TestWsPayloadsRejectNaiveDatetime class (lines 1474, 1478, 1489).
  • The import of AwareDatetime in orderbook_delta.py follows the same pattern already used in communications.py and user_orders.py.

One issue — test loop stops on first failure:

TestIssue353EnvelopeTypeLiteralNarrowing.test_issue_353_envelope_type_literal_narrowing iterates over all 7 cases in a single for loop. A failure on case 1 silently skips cases 2–7, making it hard to know how many envelopes actually broke. The project's existing parametrize usage (see test_contracts.py) makes this easy to fix:

@pytest.mark.parametrize("cls,expected_type,ok_payload,bad_payload", [
    (OrderbookSnapshotMessage, "orderbook_snapshot", {...}, {...}),
    ...
])
def test_issue_353_envelope_type_literal_narrowing(cls, expected_type, ok_payload, bad_payload):
    ...

Not a blocker, but parametrized tests give much better failure messages.


Potential Issues

Annotation assertion is weak:

annotation = cls.model_fields["type"].annotation
assert annotation is not str, f"{cls.__name__} still bare str"

This confirms the field isn't bare str, but doesn't confirm it's the right Literal. A typo like Literal["orderbook_snapsho"] would pass. The ValidationError rejection test below it partially covers this, but you could tighten with:

from typing import get_args
assert get_args(annotation) == (expected_type,)

Minor, but it closes a gap for free.

Remaining type: str models are out of scope but worth tracking:

Five more envelopes still carry type: str:

  • TradeMessage (trade.py)
  • MarketPositionsMessage (market_positions.py)
  • OrderGroupMessage (order_group.py)
  • MultivariateMessage / MultivariateLifecycleMessage (multivariate.py)

If there isn't already a tracking issue for these, opening one would keep the sweep visible.


Behavioral Change Risk

The ts: AwareDatetime | None promotion is technically a breaking change for any caller reading msg.ts as a string (e.g., passing to a string formatter). The PR description calls this out explicitly, which is good. Risk is low since ts is optional and superseded by ts_ms, but worth a CHANGELOG entry.


Security / Performance

No concerns. Pure model validation tightening.


Summary

Approve with two non-blocking suggestions:

  1. Switch test_issue_353_envelope_type_literal_narrowing to @pytest.mark.parametrize for better failure isolation.
  2. Optionally strengthen the annotation assertion using get_args(annotation) == (expected_type,).

The core fix is correct, the test patterns match established conventions, and the user_order fixture correction closes a latent spec mismatch that was hiding behind the old bare str.

@TexasCoding
TexasCoding merged commit 3278786 into main May 22, 2026
5 checks passed
@TexasCoding
TexasCoding deleted the r3/W2-D branch May 22, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working spec-drift Upstream OpenAPI/AsyncAPI spec changed since last sync ws WebSocket-related

Projects

None yet

1 participant