Skip to main content

Add your description here

Project description

Genrepo

Generate typed, readable data-access repositories for FastAPI apps using SQLModel or SQLAlchemy. Genrepo removes CRUD boilerplate by rendering Jinja2 templates into real Python code that follows the Repository Pattern. It supports three generation modes (standalone, base-only, combined) and an optional stub-only mode (no ORM, structure only).

Explore concrete configurations in the examples/ folder to see common scenarios and how to tailor genrepo.yaml.


What it is / isn’t

  • Is: a small CLI that
    • Reads a genrepo.yaml configuration.
    • Renders Jinja2 templates to per-model repository files.
    • Standardizes naming, docstrings, and common CRUD methods.
  • Isn’t:
    • An ORM.
    • An API router/service generator.
    • A migration or database session/engine manager.

Requirements

  • Python 3.10+ (tested on 3.10–3.13).
  • Dependencies managed via uv (recommended) or classic pip.

Installation

Install from PyPI (recommended):

pip install genrepo

Install via pipx (isolated):

pipx install genrepo

Install from local artifacts (dist/):

pip install dist/genrepo-0.1.6-py3-none-any.whl
# or
pip install dist/genrepo-0.1.6.tar.gz

Quickstart (installed)

# Verify installation
genrepo healthcheck --verbose

# 1) Create a starter genrepo.yaml (combined + example)
genrepo init-config

# 2) Open and configure genrepo.yaml (models, methods, etc.)
#    Then generate repositories from the YAML
genrepo generate

Quickstart (uv)

# Install deps from uv.lock (creates .venv)
uv sync

# Verify environment
uv run genrepo healthcheck --verbose

# 1) Create a starter genrepo.yaml (combined + example)
uv run genrepo init-config

# 2) Open and configure genrepo.yaml (models, methods, etc.)
#    Then generate repositories from the YAML
uv run genrepo generate

Or activate the venv manually:

source .venv/bin/activate
genrepo healthcheck --verbose

Upgrading from older Python: after bumping to 3.12.9, regenerate the lockfile and environment:

uv sync --python 3.12.9

CLI Commands

healthcheck

Checks CLI readiness. With --verbose prints versions.

Installed:

genrepo healthcheck
genrepo healthcheck --verbose

From repo (dev):

uv run genrepo healthcheck
uv run genrepo healthcheck --verbose

init-config

Creates a genrepo.yaml sample (combined mode by default). If the file already exists, it does not overwrite it unless --force is provided.

Options:

  • --path/-p: target path (default genrepo.yaml).
  • --force/-f: overwrite existing file.

Installed:

genrepo init-config
genrepo init-config --path config/genrepo.yaml

From repo (dev):

uv run genrepo init-config
uv run genrepo init-config --path config/genrepo.yaml

generate

Reads genrepo.yaml and generates repositories according to the selected mode.

Options:

  • --config/-c: path to genrepo.yaml (default genrepo.yaml).
  • --templates-dir: override templates directory (e.g., ./templates).
  • --force/-f: overwrite existing generated files (only where applicable).
  • --stub-only: generate stub-only repositories (structure only, no ORM logic).

Installed:

genrepo generate
genrepo generate --stub-only

From repo (dev):

uv run genrepo generate
uv run genrepo generate --stub-only

Configuration (genrepo.yaml)

Top-level fields:

  • orm: sqlmodel or sqlalchemy (ignored if generation.stub_only: true).
  • async_mode: true|false to enable AsyncSession and async/await (per ORM).
  • output_dir: destination folder for generated repositories.
  • generation:
    • mode: standalone | base | combined.
    • base_filename: base filename (default base_repository.py).
    • base_class_name: base class name (default BaseRepository).
    • overwrite_base: overwrite base on regeneration (default false).
    • stub_only: generate skeletons only (structure; no ORM logic).
  • Discovery: models: all with models_package and models_dir to discover all models under a package/directory.
  • Explicit list: models: [] to define per-model config.
  • commit_strategy: commit|flush|none (default: none). Typically your app/service controls transactions.
  • allow_missing_models: if true, do not fail when an explicit import_path cannot be imported.

Per-model (models[]):

  • name, import_path (module.path:Class), id_field, id_type.
  • methods: only base CRUD presets are allowed: get, get_or_raise, list, find_one, create, update, delete, delete_by_id, exists, count, plus presets all and none.
  • personalize_methods: custom repo-only stubs (combined: user repo, standalone: appended at bottom).

Default sample created by generate (combined + wildcard):

orm: sqlmodel
async_mode: false
commit_strategy: none

output_dir: app/repositories

generation:
  mode: combined
  base_filename: base_repository.py
  base_class_name: BaseRepository

models:
  - name: All
    import_path: app.models
    id_field: id
    id_type: int
    methods: [none]
    personalize_methods: [calculate_something]

Alternative discovery:

models: all
models_package: app.models
models_dir: app/models

Explicit per-model customization (some base methods + one personalized):

orm: sqlmodel
output_dir: app/repositories
generation:
  mode: standalone

models:
  - name: User
    import_path: app.models.user:User  # module:Class
    id_field: id
    id_type: int
    methods: [get, list]              # pick from the base set
    personalize_methods: [calculate_age]

Methods you can generate (base set)

Reading:

  • get(session, id) -> Optional[Model]: Fetch by primary key, or None.
  • get_or_raise(session, id) -> Model: Same as get but raises NotFoundError when missing.
  • list(session, *where, limit=100, offset=0) -> list[Model]: Paginated list with optional SQLAlchemy filter clauses.
  • find_one(session, *where) -> Optional[Model]: First row matching filters, or None.

Writing:

  • create(session, obj) -> Model: Persist and refresh.
  • update(session, db_obj, obj_in: dict[str, Any]) -> Model: Apply changes and refresh.
  • delete(session, db_obj) -> None: Delete by instance.
  • delete_by_id(session, id) -> bool: Delete by PK; returns True if removed.

Utilities:

  • exists(session, *where) -> bool: Any row matches filters.
  • count(session, *where) -> int: Count rows matching filters.

Notes:

  • In standalone, methods limits which of the above are generated in each repository.
  • In combined, the base repository exposes the full set; personalize_methods adds repo-only stubs in the user repo.
  • In stub-only, only method signatures are generated (TODO + pass), without ORM imports or logic.

Templates

Packaged defaults cover the following scenarios:

  • Base repositories per ORM and sync/async.
  • Standalone repositories per ORM (async controlled by context).
  • Combined user repository stub.
  • Stub-only base and standalone (no ORM).

Local overrides (optional): use --templates-dir ./templates in generate to point to your own copies.

Template Map

  • Base + SQLModel + sync: base_repository_sqlmodel_sync.j2
  • Base + SQLModel + async: base_repository_sqlmodel_async.j2
  • Base + SQLAlchemy + sync: base_repository_sqlalchemy_sync.j2
  • Base + SQLAlchemy + async: base_repository_sqlalchemy_async.j2
  • Standalone + SQLModel: repository_sqlmodel.j2
  • Standalone + SQLAlchemy: repository_sqlalchemy.j2
  • Combined (user repo): model_repository_user_stub.j2
  • Stub-only (base): repository_base_stub.j2
  • Stub-only (standalone): repository_standalone_stub.j2

Output

  • Location: output_dir (default app/repositories).
  • File name: <model>_repository.py (snake_case).
  • Class name: <Model>NameRepository (PascalCase).

Example (User, standalone):

from sqlmodel import Session, select
from app.models.user import User

class UserRepository:
    def get(self, session: Session, id: int) -> User | None: ...
    def list(self, session: Session, *where, limit: int = 100, offset: int = 0) -> list[User]: ...
    def create(self, session: Session, obj_in: User) -> User: ...
    def update(self, session: Session, db_obj: User, obj_in: dict[str, Any]) -> User: ...
    def delete_by_id(self, session: Session, id: int) -> bool: ...

The imported model (e.g., app.models.user:User) must exist in your target project; Genrepo does not create models or configure sessions/engines. Use allow_missing_models: true if you want to generate repos even when imports are not yet resolvable.

In combined mode:

  • base_repository.pyclass BaseRepository[T] (editable).
  • <model>_repository.py (user) → class <Model>Repository(BaseRepository[<Model>]) (created once; extend for your domain).

Stub-only mode generates the same file layout, but with TODO + pass bodies (no ORM logic).

Flow diagram

flowchart TD
  A["genrepo CLI<br/>init-config"] -->|writes once| B["genrepo.yaml<br/>(sample)"]
  B -->|edit / configure| C["genrepo CLI<br/>generate"]

  C -->|reads| T["Templates (packaged)<br/>- base per ORM/async<br/>- standalone per ORM<br/>- user stub<br/>- stub-only (no ORM)"]
  T -->|render| G["Generated files<br/>app/repositories/*.py"]

  C -.-> M["select mode:<br/>standalone / base / combined"]

  C -->|stub_only=true| S["Use stub-only templates<br/>(signatures only; TODO + pass)"]
  C -->|stub_only=false| N["Normal generation<br/>(full templates)"]
  S --> T
  N --> T

Legend: “sa” = standalone.

Packaging the templates (.j2)

When distributing the library, ensure .j2 files are included in the wheel/sdist.

Hatchling example:

[tool.hatch.build.targets.wheel]
packages = ["src/genrepo"]

[tool.hatch.build.targets.wheel.force-include]
"src/genrepo/templates" = "genrepo/templates"

Setuptools example:

recursive-include src/genrepo/templates *.j2
recursive-include src/genrepo/assets *.yaml

Repository structure (maintainers)

  • src/genrepo/cli/app.py: Typer CLI. Commands: init-config (writes sample YAML), generate (generates code), healthcheck.
  • src/genrepo/config.py: Pydantic schema and loader/validation for genrepo.yaml (modes, discovery, methods/personalize_methods, errors).
  • src/genrepo/generator.py: Orchestrates Jinja2 rendering, selects templates by mode/ORM/async or stub-only, writes outputs.
  • src/genrepo/constants.py: Central constants (messages/errors, CRUD method set, template filenames, sample asset path, ORM IDs).
  • src/genrepo/templates/: Packaged Jinja2 templates:
    • Base per ORM/async: base_repository_sqlmodel_sync.j2, base_repository_sqlmodel_async.j2, base_repository_sqlalchemy_sync.j2, base_repository_sqlalchemy_async.j2.
    • Standalone per ORM: repository_sqlmodel.j2, repository_sqlalchemy.j2.
    • Combined user repo: model_repository_user_stub.j2.
    • Stub-only (no ORM): repository_base_stub.j2, repository_standalone_stub.j2.
  • src/genrepo/assets/genrepo.sample.yaml: Default YAML sample used by generate.

Notes for contributors

  • Keep templates focused (no business logic), one responsibility per file.
  • Extend via new templates or constants (e.g., adding ORMs) rather than scattering literals.
  • Prefer errors/messages from constants.py to keep CLI output consistent and localizable.

And configure package_data/include_package_data accordingly.

Docker

  • Build locally: docker build -t genrepo:local .
  • Run the CLI against your project (mount current dir):
docker run --rm \
  -v "$PWD":"$PWD" -w "$PWD" \
  genrepo:local generate --check

CI (--check)

Example GitHub Actions job to ensure repositories are up to date:

jobs:
  validate-architecture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install Genrepo
        run: pip install genrepo
      - name: Verify repositories are up to date
        run: genrepo generate --check

Exit Codes and CI behavior

  • --dry-run: computes the plan and prints a summary (or JSON when --format json), without writing files.
  • --check: exits with code 1 if any file would be written (drift detected), otherwise 0.
  • --format json: stdout is strict JSON suitable for parsing; avoid mixing logs into stdout. If you need logs, send them to stderr.

Tips

  • Local template overrides (--templates-dir): copy packaged templates, tweak, and point the CLI to your folder.

    # Copy packaged templates locally
    uv run genrepo init-templates --dest ./templates/genrepo
    
    # Add a tiny marker to verify overrides are being used
    printf "# LOCAL_TPL\n" | cat - templates/genrepo/repository_sqlmodel.j2 > /tmp/t && mv /tmp/t templates/genrepo/repository_sqlmodel.j2
    
    # Generate using local templates
    uv run genrepo generate --templates-dir ./templates/genrepo
    
  • Stub-only (skeletons, no ORM):

    uv run genrepo generate --stub-only
    # Repositories contain method signatures + TODO/pass, with no SQLModel/SQLAlchemy imports
    
  • Discover all models (models: all):

    In your genrepo.yaml:

    models: all
    models_dir: app/models
    models_package: app.models
    

    All Python files under models_dir (excluding dunders) will be mapped as models_package.<file>:<Class>.

  • Shell completion:

    genrepo --install-completion  # install for your shell
    # zsh: ensure fpath+=($HOME/.zfunc); autoload -U compinit; compinit; source ~/.zshrc
    

Examples

See the examples/ folder for ready-to-use genrepo.yaml samples:

  • examples/standalone_sqlmodel_sync.yaml: Standalone repos with SQLModel (sync).
  • examples/standalone_sqlalchemy_async.yaml: Standalone repos with SQLAlchemy (async).
  • examples/combined_sqlmodel.yaml: Combined mode (base + user repo stubs).
  • examples/combined_sqlmodel_multi.yaml: Combined with multiple models and per-model methods (SQLModel, sync).
  • examples/combined_sqlmodel_multi_async.yaml: Combined with multiple models (SQLModel, async).
  • examples/combined_sqlalchemy_multi.yaml: Combined with multiple models (SQLAlchemy, sync).
  • examples/stub_only.yaml: Stub-only (signatures + TODO/pass; no ORM).
  • examples/discover_all.yaml: Discover models automatically from a package.
  • examples/base_only_sqlmodel_sync.yaml: BaseRepository only (SQLModel, sync).

Copy one to genrepo.yaml, adjust import_path to your models, and run genrepo generate.

Troubleshooting

  • “No module named pydantic/typer”: run inside the venv (uv run ...) or source .venv/bin/activate.
  • sqlmodel/sqlalchemy missing in your target app: install them in that project.
  • “No files generated”: likely exist already; use --force.

License

This project is licensed under the terms of the MIT License. See the LICENSE file for details.

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

genrepo-0.1.6.tar.gz (19.4 kB view details)

Uploaded Source

Built Distribution

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

genrepo-0.1.6-py3-none-any.whl (30.0 kB view details)

Uploaded Python 3

File details

Details for the file genrepo-0.1.6.tar.gz.

File metadata

  • Download URL: genrepo-0.1.6.tar.gz
  • Upload date:
  • Size: 19.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for genrepo-0.1.6.tar.gz
Algorithm Hash digest
SHA256 353f7bcd5701e9ec07e5a076849f2a2e439e204c36945642b6abff999419fac9
MD5 f76ed7d8555f0ff63c62b0968f5dcfe6
BLAKE2b-256 3eb6ad144bddef7e36aa08bcc160cf268315b9d026bc5e023591a052db4ee753

See more details on using hashes here.

File details

Details for the file genrepo-0.1.6-py3-none-any.whl.

File metadata

  • Download URL: genrepo-0.1.6-py3-none-any.whl
  • Upload date:
  • Size: 30.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for genrepo-0.1.6-py3-none-any.whl
Algorithm Hash digest
SHA256 95492f76253b8f84a1620c918d40f252d3a7236d40d520795bd3ef596ca68c9a
MD5 38abf7e6b5a5eb4f39a2aa1a240f3c2d
BLAKE2b-256 a5a585d9b69197438291172b82e0db8036f5c291a05ac2d70818b9d38b4c8f1f

See more details on using hashes here.

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