CLI & Python API¶
erdify ships a command-line interface, a module entry point, and a Python API for programmatic use.
CLI Options¶
usage: erdify [-h] [-o OUTPUT] [--title TITLE] [--exclude [PATTERN ...]]
[--exclude-paths [PATTERN ...]] [--no-default-excludes]
[--sources [KIND ...]] [--include PATTERN [PATTERN ...]]
[--base-classes NAME [NAME ...]] [--sql-dialect NAME]
[--infer-keys] [--django-raw-types] [--no-enums]
[--no-relationships] [--format FMT [FMT ...]] [--inject FILE]
[--allow-empty] [--check] [-v]
input
Generate PlantUML ERD diagrams from SQLModel, SQLAlchemy, Django, Pydantic and
dataclass models
positional arguments:
input Directory or file with models (.py) or SQL DDL (.sql)
options:
-h, --help show this help message and exit
-o, --output OUTPUT Output .puml file (default: stdout)
--title TITLE Diagram title (default: 'Database ERD')
--exclude [PATTERN ...]
Glob patterns (case-sensitive) to exclude entities by
class name or table name, e.g. --exclude '*Link'
audit_log
--exclude-paths [PATTERN ...]
Glob patterns (case-sensitive) for models.py files to
skip before parsing, matched against the path relative
to input or any path segment, e.g. --exclude-paths
migrations legacy 'apps/experimental/*'
--no-default-excludes
Do not auto-skip models.py under venv/site-
packages/cache dirs (.venv, site-packages,
__pycache__, ...); scan them too
--sources [KIND ...] Restrict which model kinds become entities. Choices:
sqlmodel, sqlalchemy, django, dataclass, pydantic.
Default: all. e.g. --sources sqlmodel sqlalchemy for
DB tables only
--include PATTERN [PATTERN ...]
Glob patterns for files to scan (default: models.py).
A pattern with '/' matches the path relative to input
('**' crosses dirs); a pattern without '/' matches a
filename at any depth. Replaces the default, so list
models.py too if you want it, e.g. --include models.py
'**/models/*.py' tables.py
--base-classes NAME [NAME ...]
Extra base-class names to treat as Pydantic models,
for bases defined outside the scanned files (erdify
resolves ancestors only across those), e.g. --base-
classes Schema for django-ninja, or a shared
BaseSchema from an internal library
--sql-dialect NAME SQL dialect hint for parsing .sql DDL with the [sql]
extra (e.g. postgres, mysql, sqlite). Default: a
permissive generic read
--infer-keys For keyless models (Pydantic/dataclass), infer a
primary key from a field named 'id' and a foreign key
from '<x>_id' (target table '<x>')
--django-raw-types For Django models, show original field names
(CharField, TextField) instead of mapped Python types
(str, int, datetime)
--no-enums Skip enum definitions in output
--no-relationships Skip relationship lines in output
--format FMT [FMT ...]
Output format(s): plantuml (.puml), mermaid (.mmd),
json (.json), html (.html). Default: plantuml. With -o
the extension is set per format; multiple formats
require -o, e.g. --format plantuml mermaid
--inject FILE Inject the diagram into a markdown file between '<!--
erdify:start -->' and '<!-- erdify:end -->' markers
(only that region is rewritten). Uses a single
--format (default mermaid). Combine with --check to
fail on drift, e.g. --inject README.md
--allow-empty Treat 'no tables found' as a warning instead of an
error: write the empty diagram and exit 0 (default
since 0.13.0: exit 1)
--check Don't write; exit non-zero if the --output file is
missing or differs from the freshly generated diagram
(for CI / pre-commit drift checks)
-v, --version show program's version number and exit
Example: erdify ./src/database -o database_erd.puml
The block above is generated from
erdify --help; runpython scripts/gen_cli_docs.pyafter changing CLI flags.
Configuration via pyproject.toml¶
Instead of passing flags every time, commit them under [tool.erdify] in your
project's pyproject.toml. erdify searches upward from the input directory for
the nearest pyproject.toml and reads that table.
[tool.erdify]
title = "My Database Schema"
output = "docs/database_erd" # relative paths resolve from the project root
format = ["plantuml", "mermaid"] # one or both
sources = ["django"]
exclude = ["audit_log", "*Link"]
exclude_paths = ["migrations", "legacy"]
base_classes = ["Schema"] # extra Pydantic bases defined outside the scan
infer_keys = true
django_raw_types = false
allow_empty = true # downgrade "no tables found" to a warning
sql_dialect = "postgres" # required for CREATE TYPE … AS ENUM support
With that in place, erdify . uses these settings. Precedence is explicit CLI
flag > [tool.erdify] value > built-in default. (Boolean flags merge by OR: a
flag enabled in config can be added to on the CLI but not turned off there.)
Bases defined outside the scan (--base-classes)¶
erdify classifies a class as a Pydantic model when BaseModel appears in its
bases — and it resolves ancestors only across the files it scanned. A base
class that lives in an installed package, or in a module --include does not
match, is therefore unresolvable and its subclasses are skipped entirely.
The common case is django-ninja. ninja.Schema
does subclass pydantic.BaseModel, but that inheritance is inside the installed
package, which erdify never reads:
from ninja import Schema
class AuthorOut(Schema): # not recognized by default
id: int
name: str
Name the base and it is treated as a Pydantic model:
erdify ./api --base-classes Schema --infer-keys
[tool.erdify]
base_classes = ["Schema", "BaseSchema"]
This works for a shared base in your own internal library just as well — the
BaseSchema every service imports from a company package is the same problem.
Both the bare form (Schema) and the qualified form (ninja.Schema) are
matched, and a class that reaches the named base through an intermediate
defined in a scanned file is picked up too.
Model-derived schemas are a different matter
ninja.ModelSchema and ninja_schema.ModelSchema take their fields from a
Django model via an inner Meta / Config class, leaving the class body
empty. erdify does not resolve that reference, so naming those bases would
produce entities with no fields. It detects the shape and skips them
instead, with a note on stderr:
Warning: skipped UserSchema - its fields come from Meta.model (User), which
erdify does not resolve. Drawing it would add an entity with no fields.
The Django model itself is still drawn as usual. Whether a response schema belongs in an ERD at all — it is a projection of a table, not a table — is an open question in issue #171; if you have a view, that is the place for it.
Empty results (--allow-empty)¶
When a run finds no tables, erdify reports it on stderr — naming the active
--include patterns, how many .py/.sql files it scanned and how many of
them matched — and exits 1 without writing anything, so a file already on
disk is left untouched.
Changed in 0.13.0
Up to 0.12.3 this was a warning: erdify wrote an empty diagram and exited
0. In a CI job that commits the result, that silently replaced a good ERD
with an empty one whenever --include stopped matching — a rename or a
moved package was enough. Zero entities is now an error by default.
A schema can legitimately be empty — every entity filtered out by --exclude,
a --sources filter that matches nothing, a schema mid-migration. Opt out
there:
erdify ./src/database -o docs/erd.puml --allow-empty
or, permanently:
[tool.erdify]
allow_empty = true
That restores the old behavior exactly: the message becomes a warning, the
empty diagram is written, and the exit code is 0.
Keeping the diagram in sync (--check)¶
--check regenerates the diagram in memory and compares it to the existing
--output file or the injected region in an --inject target without writing.
It exits 0 when the content is up to date and non-zero when the file is missing
or stale — ideal for CI or a pre-commit hook.
erdify ./src/database -o docs/erd.puml --check
erdify ./src/database --inject README.md --check
Running as Module¶
python -m erdify ./src/database -o erd.puml
Python API¶
from pathlib import Path
from erdify import parse_models_directory, generate_plantuml
# Parse Python models
entities, enums = parse_models_directory(Path("./src/database"))
# Parse SQL DDL (requires erdify[sql])
entities, enums = parse_models_directory(Path("schema.sql"), sql_dialect="postgres")
# Generate PlantUML
diagram = generate_plantuml(
entities=entities,
enums=enums,
title="My Database ERD"
)
# Save or use the diagram
Path("erd.puml").write_text(diagram)
parse_models_directory accepts both a Python models directory and a .sql file
(or a directory scanned with --include '*.sql'). For Python models only, the
lower-level ASTDatabaseParser class is also available — it does not handle .sql
input, so use parse_models_directory when working with SQL DDL.
Programmatic Access¶
For lower-level control, use the parser and generator classes directly:
from erdify import (
ASTDatabaseParser,
PlantUMLGenerator,
EntityInfo,
FieldInfo,
EnumInfo,
)
# Low-level parser access (Python models only — use parse_models_directory for .sql)
parser = ASTDatabaseParser(Path("./models"))
entities, enums = parser.parse_all_models()
# Access entity details
for name, entity in entities.items():
print(f"Table: {entity.table_name}")
for field in entity.fields:
if field.is_primary_key:
print(f" PK: {field.name}")
elif field.is_foreign_key:
print(f" FK: {field.name} -> {field.foreign_table}")
# Custom generator with options
generator = PlantUMLGenerator(
entities=entities,
enums=enums,
title="Custom ERD"
)
output = generator.generate()