Skip to content

Refactor usage models to Pydantic with graceful degradation#109

Merged
alexkroman merged 3 commits into
mainfrom
claude/sweet-maxwell-igsoib
Jun 12, 2026
Merged

Refactor usage models to Pydantic with graceful degradation#109
alexkroman merged 3 commits into
mainfrom
claude/sweet-maxwell-igsoib

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Replace imperative helper functions with declarative Pydantic models for parsing AMS usage data, improving type safety and maintainability while preserving graceful degradation on malformed input.

Summary

This refactor introduces three Pydantic models (_LineItem, _Window, _Usage) to replace seven standalone functions that parsed and formatted AMS usage responses. The models are deliberately tolerant of schema drift: unknown fields are ignored, missing/invalid prices default to 0.0, and non-list/non-object values degrade to empty collections rather than crashing. This allows assembly usage to render partial data instead of failing entirely on unexpected API responses.

Key changes

  • New Pydantic models (_LineItem, _Window, _Usage):

    • _LineItem: wraps price (cents) with a label property that searches for product/feature keys in preferred order
    • _Window: aggregates line items, computes total_cents from prices, formats label from timestamps, and generates breakdown (per-product spend summary)
    • _Usage: top-level container for usage_items (list of windows)
    • All use BeforeValidator to coerce raw AMS dicts through jsonshape helpers, and extra="allow" to tolerate unknown fields
  • Removed functions: _usage_items, _window_total_cents, _window_label, _line_item_name, _line_items_summary — their logic is now properties/methods on the models

  • Updated call sites in body() render function to use model validation and property access instead of function calls

  • Refactored config_builder.py coercion to use Pydantic's TypeAdapter validators instead of custom parsing functions, reducing duplication and leveraging Pydantic's standard error handling

  • Updated tests to use model constructors (_Window.model_validate(), _LineItem.model_validate()) and assert on properties rather than function returns; added explicit test for graceful degradation on junk shapes

Notable details

  • The _MappingList validator is reused across models to handle the AMS pattern of non-list/non-object values degrading to []
  • Price defaults to 0.0 via BeforeValidator(jsonshape.as_float), so missing/invalid prices don't break aggregation
  • The breakdown property remains a string (not a structured type) to match the existing table rendering; it's computed on-demand from line items
  • --json output is unaffected: the raw AMS dict is passed through untouched before model validation

https://claude.ai/code/session_01CbHTsyv1Z4nPgH4EEHbPpM

claude added 2 commits June 12, 2026 13:44
The five per-kind coercion functions (_coerce_bool/int/float/list/json)
and their dispatch table collapse into a single kind -> (validator,
expectation) table backed by pydantic's lax-mode TypeAdapters, with one
shared UsageError path. Error messages are unchanged; accepted input is
a small superset (pydantic's bool spellings also include t/f/y/n, and
integral strings like "2.0" now parse as ints). Net -27 lines.

https://claude.ai/code/session_01CbHTsyv1Z4nPgH4EEHbPpM
The five dict-walking helpers (_usage_items, _window_total_cents,
_window_label, _line_item_name, _line_items_summary) become _Usage /
_Window / _LineItem models with total_cents/label/breakdown properties,
so the usage render path reads typed attributes instead of
Mapping.get() chains. The models stay deliberately tolerant via
jsonshape-backed BeforeValidators: junk shapes degrade (bad windows
dropped, junk price -> 0.0) instead of raising, and --json still passes
the raw AMS dict through untouched. Behavior is unchanged; the helper
tests are rewritten against the models with the same pinned cases.

https://claude.ai/code/session_01CbHTsyv1Z4nPgH4EEHbPpM
@alexkroman alexkroman enabled auto-merge (squash) June 12, 2026 13:56
@alexkroman alexkroman merged commit c51b3db into main Jun 12, 2026
16 checks passed
@alexkroman alexkroman deleted the claude/sweet-maxwell-igsoib branch June 12, 2026 14:01
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.

2 participants