Skip to main content

Derive operation-specific API models from existing Python domain models.

Project description

Projector

Eliminate model explosion by deriving fully typed, operation-specific Python models from existing domain models by declaratively specifying the properties you want.

Your application domain models (the input to projector) can be written using: dataclasses, Pydantic models, TypedDict classes, attrs classes, and plain annotated classes.

Projector can output the derived models in: Pydantic, dataclass, attrs, or TypedDict.

Installation

pip install model-projector

The distribution name is model-projector, and the import package is projector:

from projector import project, renderer, views_for

Example

import sqlite3
from dataclasses import dataclass

from projector import project, optional, renderer, required, views_for


# Application specific domain models:
@dataclass(kw_only=True)
class Address:
    city: str
    zip: str


@dataclass(kw_only=True)
class User:
    name: str
    address: Address


# Derive operation-specific models.
views = views_for(User)
UserModels = project(
    User,
    renderer=renderer.Pydantic,
    Create=required(views.name) + optional(views.address.city + views.address.zip),
    Read=optional(views.name) + optional(views.address.city),
    Update=views.name,
)
UserCreate = UserModels.CreateModel


# Use the derived user models.
conn = sqlite3.connect(":memory:")
conn.execute("create table users (name text, city text, zip text)")

def add_user_to_db(user: UserCreate) -> None:
    conn.execute(
        "insert into users (name, city, zip) values (?, ?, ?)",
        (user.name, user.address.city, user.address.zip),
    )
    conn.commit()


# Create an instance with the exact keyword argument provided
new_user = UserModels.Create(name="John", address={"city": "Paris", "zip": "75001"})
add_user_to_db(new_user)

More Info

Create, Read, and Update above are arbitrary. Use whatever names fit your application.

For example:

UserModels = project(
    User,
    renderer=renderer.Pydantic,
    CreateUserCommand=views.name + views.address.city + views.address.zip,
    Read=views.name + views.address.city,
    UpdateNameCommand=views.name,
)

That gives you:

  • UserModels.CreateUserCommandModel
  • UserModels.ReadModel
  • UserModels.UpdateNameCommandModel

And functions to instantiate instances:

  • UserModels.CreateUserCommand(...)
  • UserModels.Read(...)
  • UserModels.UpdateNameCommand(...)

If a source model field can be None, you can still decide whether a specific output must require it or keep it optional:

UserModels = project(
    User,
    renderer=renderer.Pydantic,
    Create=required(views.address.city),
    Read=optional(views.address.city),
)

required(...) applies to the whole selected subtree. optional(...) keeps that subtree nullable in the generated output.

snake_case output names are supported too. If you use create or create_user_command, the generated accessors stay snake_case:

  • UserModels.create
  • UserModels.create_model
  • UserModels.create_user_command
  • UserModels.create_user_command_model

Supported input and output types

Input:

  • dataclasses
  • Pydantic models
  • plain annotated classes
  • attrs classes
  • TypedDict classes

Output:

  • Pydantic models
  • dataclass models
  • attrs classes
  • TypedDict classes

Core Concepts

project(...) is the main runtime entry point. It takes a source model class, selected fields, and a renderer, then returns a namespace containing generated model classes and factory functions.

views_for(...) creates typed selectors for a source model. Use these selectors to describe which fields belong in each generated output model.

projector type-stubs is the type-checker build step. It writes sibling .pyi files so LSPs and type checkers can understand dynamic views_for(...) attributes. These files are not executed at runtime.

build_entity(...) is the lower-level IR helper. Normal users should not need it for project(...), but it remains public for debugging, tests, and tooling that wants to inspect the derived schema.

Type Checking Build Step

Projector currently derives models dynamically at runtime. Runtime use does not require generated files, but static type checkers like ty and pyrefly need .pyi files to understand the dynamic view objects returned by views_for(...).

Generate those type checker stubs with the CLI:

projector type-stubs path/to/models.py path/to/other_models.py

The command accepts one or more Python file paths and writes sibling .pyi files:

examples/demo_example/models.py -> examples/demo_example/models.pyi

Example Layout

The examples/ directory contains fully isolated consumer examples.

examples/demo_example/        Demo domain models and runnable example
examples/fast_api_example/    An example with a FastAPI HTTP server with CRUD and command style generated models

Run the demo example:

just demo-example
# or
PYTHONPATH=src uv run python -m examples.demo_example.main

Run the FastAPI example:

just fast-api-example
# or
PYTHONPATH=src .venv/bin/python -m examples.fast_api_example.http.main

Helpful just commands

just test
just check
just stubs
just demo-example
just fast-api-example

Publishing

Projector is configured as a Python package in pyproject.toml. The package uses the model-projector distribution name because projector is already taken on PyPI.

Build the source distribution and wheel:

uv build
# or
python -m build

Validate the built package metadata:

python -m twine check dist/*

Upload to TestPyPI first:

python -m twine upload --repository testpypi dist/*

Install-test from TestPyPI:

python -m pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  model-projector

Upload the same distributions to PyPI after the TestPyPI package installs and imports correctly:

python -m twine upload dist/*

PyPI and TestPyPI require separate accounts and API tokens.

Library Structure

projector.ir

Builds the schema IR from user models.

  • Entity is a structured schema node
  • Field is a primitive leaf node
  • build_entity() introspects supported input models into the lower-level IR

projector.projection

Builds and compiles typed projections.

  • views_for() returns a tree of selectable fields
  • Leaf supports + composition
  • compile_projection() turns selections into nested specs

projector.renderers

Turns the IR/spec into concrete output classes.

  • PydanticRenderer
  • DataclassRenderer
  • AttrsRenderer
  • TypedDictRenderer
  • renderer.Pydantic
  • renderer.Dataclass
  • renderer.Attrs
  • renderer.TypedDict

projector.encode

Public orchestration helpers.

  • project() takes a source model class and builds operation-specific factories
  • build_model_and_factory() wires renderer output to factories

projector.stubgen

Stub generation helpers.

  • generate_views_pyi() writes a .pyi file next to a consumer model module

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

model_projector-0.1.0.tar.gz (19.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

model_projector-0.1.0-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file model_projector-0.1.0.tar.gz.

File metadata

  • Download URL: model_projector-0.1.0.tar.gz
  • Upload date:
  • Size: 19.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for model_projector-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3679da93a3de5855961da254263ecd1c582f47813bb5ebe6a38a1cd1b3c324e4
MD5 b820ec2fca3fa1e2b4aa28e678e91031
BLAKE2b-256 952187c199727b2d2af38d3278812130a9898c12e1a9509d3d71f6989123e171

See more details on using hashes here.

Provenance

The following attestation bundles were made for model_projector-0.1.0.tar.gz:

Publisher: publish.yml on sbernheim4/projector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file model_projector-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: model_projector-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for model_projector-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 977fd6426b4c7e6daa5027f3791472ce4a6018b61a120cfb0d4782c59ece5673
MD5 ef316c6362e8f7b0ab68134244196cca
BLAKE2b-256 927a60a0ba63cad138a947ffd3b084e93198d845b397b254d3caddc255985255

See more details on using hashes here.

Provenance

The following attestation bundles were made for model_projector-0.1.0-py3-none-any.whl:

Publisher: publish.yml on sbernheim4/projector

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page