feat(typed): add optional Pydantic namespace wrapper (#354)#359
Draft
sorlen008 wants to merge 1 commit intocyberjunky:masterfrom
Draft
feat(typed): add optional Pydantic namespace wrapper (#354)#359sorlen008 wants to merge 1 commit intocyberjunky:masterfrom
sorlen008 wants to merge 1 commit intocyberjunky:masterfrom
Conversation
Adds ``g.typed`` — a lazy namespace accessor that wraps a curated set of
high-value read endpoints with Pydantic response models, while leaving the
132 existing methods entirely unchanged.
Design follows Option D from the issue discussion:
g = Garmin(email, password)
raw = g.get_stats(date) # dict[str, Any] — unchanged
stats = g.typed.get_stats(date) # DailyStats (Pydantic model)
Scope of the first cut (7 methods, 6 response models):
- get_stats / get_user_summary -> DailyStats
- get_sleep_data -> SleepData (with nested DailySleepDTO)
- get_hrv_data -> HrvData | None
- get_body_battery -> list[BodyBatteryEntry]
- get_training_readiness -> list[TrainingReadiness]
- get_activities_by_date -> list[Activity]
Design notes:
- Lazy import. ``g.typed`` is a ``@functools.cached_property`` that imports
``garminconnect.typed`` on first access, so pydantic stays optional. If
pydantic is missing, the import raises a clear install hint.
- Kept in a new ``[typed]`` extra (pydantic>=2.0.0) rather than promoted to
a core dep, so the default ``pip install garminconnect`` footprint doesn't
change.
- ``extra='allow'`` + all fields ``Optional`` — models tolerate Garmin schema
drift (new firmware / subscription tiers add fields) and partial responses
(privacy-protected accounts, missing device data). Unknown fields land in
``model_extra`` for callers who want them.
- Validation failures raise ``GarminConnectResponseValidationError`` with the
unvalidated response preserved on ``.raw`` so users can fall back without
losing data.
- ``get_training_readiness`` wrapper reflects the real list-shaped response
(the existing ``dict[str, Any]`` annotation on the raw method is wrong but
out of scope here).
Marked **experimental** in module and class docstrings — model shapes may
change between minor releases while the pattern stabilises.
Tests (16 mocked, all green):
- accessor returns cached ``TypedGarmin``
- each wrapper calls through to the raw method and validates the response
- extra fields tolerated (preserved in ``model_extra``)
- missing fields default to ``None``
- validation errors raise ``GarminConnectResponseValidationError`` with
``.raw`` populated
- list endpoints with non-list responses return ``[]``
- ``get_hrv_data`` ``None`` passes through unchanged
Lint clean under ruff + mypy + black + isort.
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Closes #354 (once merged).
Implements Option D from the issue discussion: a lazy
g.typednamespace that wraps a small set of high-value read endpoints with Pydantic response models, while leaving the 132 existing methods entirely untouched.Opening as draft so you can veto any of the design choices below before I polish / land.
Decisions I made (please flag any you'd change)
GarminConnectResponseValidationErrorwith the unvalidated response preserved as.rawtry/exceptand still access the data. The alternative (silent fallback to dict) was the one open UX question you hadn't weighed in on — happy to flip to silent-fallback if you'd prefer.extra='allow'+ all fieldsOptional[typed]extra, not core depg.typedlazy-imports the module and raises a clear install hint if pydantic is missingpopulate_by_name=Truelist[TrainingReadiness]Garmin.get_training_readinessis annotateddict[str, Any]Scope
get_stats,get_user_summaryDailyStatsget_sleep_dataSleepData(nestedDailySleepDTO)get_hrv_dataHrvData | Noneget_body_batterylist[BodyBatteryEntry]get_training_readinesslist[TrainingReadiness]get_activities_by_datelist[Activity]Tests
16 new mocked tests in
tests/test_typed.py. All 107 mock-based tests still pass.Covered scenarios:
TypedGarminmodel_extra(no validation failure)NoneGarminConnectResponseValidationErrorwith.raw[]get_hrv_dataNonepasses through unchangedOut of scope (follow-ups)
dict[str, Any]annotation onget_training_readiness— separate PR, doesn't need to block this.pragma: no cover; happy to add an integration test if you want.Verification
pdm run ruff check garminconnect/typed.py tests/test_typed.py— cleanpdm run mypy garminconnect/typed.py— cleanblack -l 88 --target-version py313— cleanisort --profile black— cleanpytest tests/test_typed.py— 16 passedtest_garmin_unit,test_retry_decorator,test_workout_constants,test_typed) — 107 passed, 0 regressionsLet me know if the direction is off and I'll rework.