Skip to content

fix: align rest_proxy with schwab-py 1.5.1 enum API + pin dependency#1

Merged
rdemeritt merged 1 commit into
mainfrom
fix/schwab-py-api-drift
Jun 16, 2026
Merged

fix: align rest_proxy with schwab-py 1.5.1 enum API + pin dependency#1
rdemeritt merged 1 commit into
mainfrom
fix/schwab-py-api-drift

Conversation

@rdemeritt

Copy link
Copy Markdown
Member

Root cause

requirements.txt left schwab-py unpinned, so the Docker build pulled a newer enum API than rest_proxy.py was written against. The installed version is schwab-py 1.5.1. Two live 502s resulted (greeksmith → proxy @ 127.0.0.1:8080):

  • GET /v1/quotes?fields=quotetype object 'Quote' has no attribute 'FIELD_QUOTE'
  • GET /v1/pricehistory?...frequency_type=daily&period=1expected type "Period", got type "int"

Audited all four /v1 endpoints; chains and markets were also wrong (chains used the dropped range= kwarg + an unmapped entitlement; markets relied on name-only enum lookup).

API corrections (old → new), validated against schwab-py 1.5.1

Endpoint Old New
quotes client.Quote.FIELD_QUOTE / FIELD_REFERENCE / FIELD_EXTENDED / FIELD_FUNDAMENTAL / FIELD_REGULAR client.Quote.Fields.QUOTE / .REFERENCE / .EXTENDED / .FUNDAMENTAL / .REGULAR
pricehistory raw int for period, frequency PriceHistory.Period(int), PriceHistory.Frequency(int) (typed enums; both int-valued)
pricehistory period_type/frequency_type best-effort name lookup typed PeriodType/FrequencyType (by value day/daily or by name)
chains kwargs["range"] = ... kwargs["strike_range"] = ... (get_option_chain renamed the kwarg; range= was rejected)
chains entitlement passed through as raw str mapped to Options.Entitlement
chains Options.ContractType/Strategy/StrikeRange/ExpirationMonth/Type via [NAME] only mapped via shared coercer (member name or wire value, e.g. ITM)
markets MarketHours.Market[NAME] only mapped via shared coercer (value equity or name EQUITY)

Fix design

  • New app/schwab_data_proxy/enum_mapping.py: centralized, unit-testable mappers. Each accepts the live/mocked client (reads enum classes off it), accepts member name or wire value, and raises UnknownEnumValue on bad input.
  • rest_proxy.py: all four endpoints rewired to the mappers. UnknownEnumValue now surfaces as HTTP 400 (bad query string) instead of an opaque 502.
  • I kept enforce_enums at the schwab-py default (on) and passed correct typed enums — that's the intended usage for 1.5.1 and keeps bad params loud rather than silently shipping wrong types upstream.

Dependency pin

requirements.txt: schwab-py==1.5.1 (with rationale comment) so the enum API can't silently drift again.

Tests / CI

  • New tests/ pytest suite (19 tests): param→enum mapping + handler-level call construction with a mocked client (no network/auth). Asserts correct enum types and kwarg names, and that bad params → 400.
  • Tests run against the real installed schwab-py enums, so a future upgrade that renames/retypes a member fails CI loudly.
  • New CI test job (pytest) + requirements-dev.txt + pyproject.toml pytest config.
  • Local results: 19 passed; ruff check app/ + ruff format --check app/ clean.

Deploy note

This is a code-only change. The running proxy container must be rebuilt/restarted by the operator for the fix to take effect (so greeksmith's data smoke can pass). No container was rebuilt or deployed in this PR. Do not merge until reviewed.

The /v1 endpoints were written against an older schwab-py enum API than the
unpinned build installed (schwab-py 1.5.1), causing live 502s:

  - quotes:  AttributeError "Quote has no attribute FIELD_QUOTE"
  - pricehistory: ValueError "expected type Period, got type int"

Root cause: requirements.txt left schwab-py unpinned, so the API drifted.

Changes
- New app/schwab_data_proxy/enum_mapping.py: centralized, unit-testable param
  -> schwab-py enum mappers. Accepts member name OR wire value; raises
  UnknownEnumValue (surfaced as HTTP 400) on unmappable input instead of
  passing raw strings/ints downstream.
- rest_proxy.py rewired for all four endpoints:
  * quotes:        Quote.FIELD_* -> Quote.Fields.*
  * pricehistory:  raw int period/frequency -> PriceHistory.Period/.Frequency;
                   period_type/frequency_type -> typed enums
  * chains:        Options.* enums; FIX kwarg range= -> strike_range= (the old
                   name was rejected by get_option_chain); entitlement now mapped
  * markets:       MarketHours.Market enums
  * UnknownEnumValue -> 400 (bad query string), not 502 (upstream)
- requirements.txt: pin schwab-py==1.5.1 (with rationale comment) so the enum
  API can't silently drift again.
- tests/: pytest regression suite (19 tests) covering the param->enum mapping
  and handler-level call construction (mocked client, no network).
- CI: add pytest job; requirements-dev.txt + pyproject.toml pytest config.
@rdemeritt rdemeritt merged commit c6d935c into main Jun 16, 2026
4 checks passed
@rdemeritt rdemeritt deleted the fix/schwab-py-api-drift branch June 16, 2026 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant