Skip to content

Generate Pydantic models from Destinations API spec, validate on release - #1225

Merged
Regan-Koopmans merged 10 commits into
mainfrom
poc/pydantic-models
Sep 24, 2026
Merged

Regan-Koopmans merged 10 commits into
mainfrom
poc/pydantic-models

Conversation

@Regan-Koopmans

@Regan-Koopmans Regan-Koopmans commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

I have introduced typing for the Destination API client methods using Pydantic models. I generated these models from the live Destinations API spec using datamodel-code-generator. I have added a helpful nox command to conveniently regenerate these models going forward:

nox -s generate_models

Having this in place means that we can validate our models against the production API. I have added a nox command to do just this:

nox -s validate_models 

I have configured this to run in the CI for every release, which guarantees that our code does not diverge from the API (at least at the time of release). This will not detect new endpoints in the Destinations API that we have not implemented yet (unless they introduce new DTOs). This could be useful but it is out of scope for this PR.

PR Checklist:

  • This PR is as small and focused as possible
  • If this PR includes proposed changes for inclusion in the changelog, the title of this PR summarizes those changes and is ready for inclusion in the Changelog.
  • I have updated docstrings for function changes and docs in the 'docs' folder for user interface / behavior changes
  • This PR does not break any examples or I have updated them

@Regan-Koopmans Regan-Koopmans self-assigned this Sep 9, 2026
@Regan-Koopmans
Regan-Koopmans force-pushed the poc/pydantic-models branch 4 times, most recently from 26efe67 to 5caafeb Compare September 9, 2026 12:00
- Generate Pydantic models from the Destinations API OpenAPI spec (planet/api_models/destinations.py)
- Use Destination/DestinationsResponse models as return types in DestinationsClient
- Add pre-release model validation in tests/drift/validate_models.py
- Add nox sessions: generate_models, validate_models
- Wire validate_models into the publish-pypi CI workflow
- Scope everything to the Destinations API for now; other APIs noted as TODO
The generated models set extra='forbid', so any field Planet added to the
Destinations API turned a working call into a ValidationError in a shipped
SDK. Responses now allow unknown fields and preserve them through model_dump,
so the CLI reports what the API returned rather than a filtered copy.

The drift check could never pass: the committed models had been reformatted
with yapf after generation, so a byte-comparison against fresh codegen output
always failed. It also could not run at all -- tests/conftest.py imports respx
and setup.cfg injects --cov, neither present in the validate_models extra.
Since this gates PyPI releases, both were release blockers.

Model shape is now controlled entirely by codegen flags rather than by editing
generated files. --strict-nullable matters most: the spec is OpenAPI 3.0.3 and
marks Destination.archived as required and nullable, and codegen was silently
dropping the nullability. --target-python-version and an exact codegen pin make
output reproducible, so a tool release or a different interpreter cannot fail
the gate. The command line lives in one module imported by both the nox session
and the drift test, which previously duplicated it and could drift apart.

Also fixes a yapf exclude that matched nothing (a bare directory is not an
fnmatch for files inside it, which is how the generated file got reformatted
in the first place), a dead None-check that silently dropped `default unset`
output, and six sync docstrings still promising dict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Regan-Koopmans
Regan-Koopmans marked this pull request as ready for review September 11, 2026 08:25
@Regan-Koopmans

Copy link
Copy Markdown
Contributor Author

Hey @asonnenschein, let me know if you want to have a sync call on this to discuss approach.

@ischneider

Copy link
Copy Markdown
Member

Hi Regan,

This would be a breaking change for the Destinations clients but it raises the issue of untyped dicts (throughout the API) as responses...

This is a long-standing critique of mine that also includes a lack of a "request" object (that could be used by both sync/async dispatch, rather than duplicating all request params and docs for each client) and a broader more/useful "response" object, that for example, allows reading raw JSON or as a typed object. This would also support non-body content, such as rate-limiting headers, etc.

I've suggested we could migrate to this approach, without breaking existing clients, by adding a new dispatcher type and defining request/response objects that could be reused under the hood by existing client implementations.

One discussion we had was "is there value in just having users generate clients from openapi specializations?". I think there's something to consider there (as it allows other language users to build on our APIs) unless we provide some value/convenience over generated code.

Finally, while there's certainly value in having a stronger typed response, I think validation of requests is more valuable than validation of responses - our services should ensure the contract and validity of their responses.

Comment thread planet/api_models/destinations.py Outdated
@asonnenschein

Copy link
Copy Markdown
Contributor

I agree with @ischneider's point about this being a breaking change, but otherwise I think this is a big step in the right direction! Perhaps we can refactor the approach here so that the default response objects are still JSON, and typed response object are opt-in?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical packaging omissions could make released wheels fail to import, and CLI tests do not verify model serialization.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds generated Pydantic models for the Destinations API, typed client responses, CLI serialization, and release-time model drift validation.

Changes:

  • Adds generated models and Nox regeneration/validation sessions.
  • Updates clients, CLI, tests, and upgrade documentation.
  • Adds dependencies and release workflow validation.
File summaries
File Summary Findings
tests/integration/test_destinations_cli.py Updates CLI fixtures and assertions. Moderate (1 vote): Add parsed-output assertions covering _links, pl:ref, serialization, and defaults.
tests/integration/test_destinations_api.py Verifies typed API responses. —
tests/drift/validate_models.py Detects model drift against the live spec. —
tests/drift/codegen_config.py Defines shared code-generation settings. —
pyproject.toml Adds Pydantic and validation dependencies. Critical (1 vote): The package list omits planet.api_models, so published wheels omit the models and imports fail.
planet/sync/destinations.py Adds typed synchronous responses. —
planet/clients/destinations.py Validates asynchronous responses into models. Critical (2 votes): The new planet.api_models package is omitted from the built package, causing installed imports to fail.
planet/cli/destinations.py Serializes Pydantic responses. —
planet/api_models/destinations.py Adds generated Destination models. —
planet/api_models/__init__.py Marks the models directory as a package. —
noxfile.py Adds generation and validation sessions. —
docs/get-started/upgrading-v3.md Documents the response-model API change. —
.github/workflows/publish-pypi.yml Validates models before release. —
Review details

Suppressed comments (1)

planet/cli/destinations.py:58

  • The destination CLI tests only assert exit_code, so they do not verify the new model-to-JSON contract introduced here. A regression could emit Python field names (field_links, pl_ref), fail to serialize datetimes/enums, or change default-field handling while all these tests still pass; assert representative parsed output (including _links and pl:ref) for at least the list/get path and one mutation.
            echo_json(
                response.model_dump(mode='json',
                                    by_alias=True,
                                    exclude_unset=True),
  • Files reviewed: 12/13 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread planet/clients/destinations.py Outdated
Comment thread pyproject.toml Outdated
- Clients return raw dict (matches pattern of all other API clients).
  Pydantic models remain available in planet.api_models for callers that
  want typed responses via Destination.model_validate(...).
- CLI drops model_dump(); passes the dict directly to echo_json.
- Add planet.api_models to setup.cfg packages list so the module is
  included in built wheels.
- Fix DestinationPatchRequest1/2/3 naming: strip the pure-constraint
  anyOf block from the spec before codegen so the generator emits a
  single flat DestinationPatchRequest instead of numbered variants.
- Codegen pipeline now fetches and patches the spec in-process rather
  than passing --url directly; both nox sessions and the drift test use
  the same fetch_and_patch_spec() helper.
Our changes unnecessarily altered Dict -> Dict[str, Any] annotations and
rewrote docstrings that were already correct on the branch. Restore both
files to match main exactly, keeping only the Pydantic import removal.
Clients now return plain dicts; the upgrading guide section describing
attribute access and model_dump() no longer applies.
@Regan-Koopmans

Copy link
Copy Markdown
Contributor Author

Hi @ischneider, @asonnenschein. I have reverted changes to the Destinations client code. The changes are now backwards compatible for existing users, but the models are still available for users who would like to validate against them ( viaDestination.model_validate(...)). Having this in place will allow incremental adoption, but already adds some safety for detecting changes in the underlying API (even if we do not implement the client code in terms of these new models).

Comment thread .github/workflows/publish-pypi.yml Outdated
Comment thread planet/api_models/destinations.py Outdated
Comment thread planet/api_models/destinations.py Outdated
Requests and responses want opposite handling of unknown fields, so the
global `--extra-fields allow` was wrong for half the models.

Codegen now runs without that override and honours the spec, so the
schemas marked `additionalProperties: false` generate as `extra='forbid'`.
A typo'd key now fails client side instead of on the round trip.

Response schemas need the opposite. A shipped SDK must not raise when
Planet adds a field, so every schema reachable from a response body is
patched to `additionalProperties: true` before codegen. Reachability is
computed from the spec, not hardcoded, so it carries to other APIs.

The two sets overlap: AmazonS3Params and its siblings are echoed back in
responses, so tolerance wins and they stay `allow`. Only the *PatchParams
and the top-level request bodies are request-only, and those get `forbid`.

Model validation moves out of the release workflow and into test.yml, so
drift is caught on the PR that causes it rather than gating a release on
a live API call.
Comment thread pyproject.toml
Comment thread pyproject.toml Outdated
@asonnenschein

Copy link
Copy Markdown
Contributor

Can we change the models import path from planet.api_models.* to something more succinct like planet.types.*?

Comment thread scripts/codegen_config.py Outdated
Comment thread tests/drift/codegen_config.py Outdated
…n constants

Models move from planet.api_models to planet.types.

pydantic moves out of the core dependency list into a `models` extra, so
`pip install planet` no longer pulls in pydantic-core and its compiled Rust
extension. Typed models are `pip install planet[models]`. Nothing outside
planet/types imports pydantic -- clients still return plain dicts -- and a
unit test walks the AST of every module under planet/ to keep it that way.
planet/types/__init__.py raises a pointed ImportError naming the extra
rather than letting a bare ModuleNotFoundError surface.

The codegen script moves from tests/drift/ to scripts/, since it generates
production code rather than test fixtures. Spec URLs, output paths, the
generated-file header and the codegen target version split out into
scripts/codegen_constants.py; adding an API is now one line there. The drift
test reaches the script through a conftest that puts scripts/ on sys.path.

The validate_models extra folds into dev.

Regenerated output is byte-identical: the refactor changed no models.
Comment thread setup.cfg
Comment thread tests/unit/test_api_models.py Outdated
Comment thread tests/unit/test_types.py
Comment thread tests/drift/validate_models.py Outdated
"""Pre-release drift detection: regenerate Pydantic models and diff against committed files.

How it works:
- datamodel-codegen fetches the live OpenAPI spec and generates models into a temp file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datamodel-codegen fetches the live OpenAPI spec ...

Is this still true? IIRC fetch_and_patch_spec fetches the spec and codegen reads it from a temp file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore. Updated: the spec is fetched and patched into a temp file, and codegen reads that file.

Comment thread scripts/codegen_config.py Outdated
Comment thread noxfile.py Outdated
@asonnenschein

Copy link
Copy Markdown
Contributor

Note - this MR doesn't include a py.typed marker file in the package. I think that's OK for this MR because the typed models aren't actually invoked in any hot code paths. Calling this out though because eventually we'll need to add a py.typed marker file in the distribution to indicate that the package supports type checking (https://typing.python.org/en/latest/spec/distributing.html#packaging-type-information).

Comment thread noxfile.py Outdated
Comment thread noxfile.py Outdated
Comment thread noxfile.py Outdated
Comment thread tests/unit/test_types.py
Comment thread noxfile.py Outdated
Comment thread scripts/codegen_constants.py Outdated
Comment thread scripts/codegen_config.py Outdated
Comment thread scripts/codegen_config.py Outdated
Comment thread scripts/type_gen.py
Comment thread scripts/constants.py
Comment thread noxfile.py Outdated
Comment on lines +142 to +143
import json
import tempfile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports should be at the top of the file.

@Regan-Koopmans Regan-Koopmans Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment thread noxfile.py Outdated
Comment thread .github/workflows/test.yml Outdated
Rename scripts/codegen_config.py to scripts/type_gen.py,
scripts/codegen_constants.py to scripts/constants.py, and
tests/unit/test_api_models.py to tests/unit/test_types.py.

Hoist noxfile imports, use flake8 --extend-exclude, pin the codegen
sessions to Python 3.12, and use MODELS_DIR for output paths.

@asonnenschein asonnenschein left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I think we've given this MR appropriate technical due diligence, great job @Regan-Koopmans!

@Regan-Koopmans
Regan-Koopmans merged commit 43af53c into main Sep 24, 2026
10 checks passed
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.

4 participants