Refactor usage models to Pydantic with graceful degradation#109
Merged
Conversation
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
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.
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 allowsassembly usageto render partial data instead of failing entirely on unexpected API responses.Key changes
New Pydantic models (
_LineItem,_Window,_Usage):_LineItem: wrapsprice(cents) with alabelproperty that searches for product/feature keys in preferred order_Window: aggregates line items, computestotal_centsfrom prices, formatslabelfrom timestamps, and generatesbreakdown(per-product spend summary)_Usage: top-level container forusage_items(list of windows)BeforeValidatorto coerce raw AMS dicts throughjsonshapehelpers, andextra="allow"to tolerate unknown fieldsRemoved functions:
_usage_items,_window_total_cents,_window_label,_line_item_name,_line_items_summary— their logic is now properties/methods on the modelsUpdated call sites in
body()render function to use model validation and property access instead of function callsRefactored
config_builder.pycoercion to use Pydantic'sTypeAdaptervalidators instead of custom parsing functions, reducing duplication and leveraging Pydantic's standard error handlingUpdated 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 shapesNotable details
_MappingListvalidator is reused across models to handle the AMS pattern of non-list/non-object values degrading to[]BeforeValidator(jsonshape.as_float), so missing/invalid prices don't break aggregationbreakdownproperty remains a string (not a structured type) to match the existing table rendering; it's computed on-demand from line items--jsonoutput is unaffected: the raw AMS dict is passed through untouched before model validationhttps://claude.ai/code/session_01CbHTsyv1Z4nPgH4EEHbPpM