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] [--strict] [--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 Draw no relationship lines (edges) in any format,
foreign-key edges included
--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)
--strict Fail (exit 1, write nothing) if any model or .sql file
cannot be parsed. Without it, such a file is reported
on stderr and skipped
--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
strict = true # fail if any model/.sql file cannot be parsed
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. A response schema is a projection of a table, not a table, so erdify does not draw it; see issue #171 for the reasoning. If a projection view would help you, say so there.
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.
Files that cannot be parsed (--strict)¶
By default, a model or .sql file that erdify cannot parse (a syntax error, a
file that isn't valid SQL) does not stop the run. erdify prints
Error parsing <file>: ... on stderr, skips the file and renders the rest, and
the exit code is 0. That keeps a single broken or work-in-progress file from
blocking everything else, but the diagram is then silently missing that file's
entities.
--strict turns this into an error. erdify still reports every failing file,
then exits 1 without writing anything, so an existing diagram is left
untouched:
erdify ./src/database -o docs/erd.puml --strict
or, permanently:
[tool.erdify]
strict = true
--strict is a good fit for CI: together with --check, it fails the job
both when the committed diagram is stale and when part of the schema could not
be read. From Python, pass strict=True to parse_models_directory (or to
ASTDatabaseParser / SqlSchemaParser). It raises erdify.ModelParseError,
whose errors attribute lists every (file, message) pair.
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, 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'). Its options mirror the CLI flags
(include_patterns, exclude_patterns, sources, infer_keys, sql_dialect, ...).
generate_mermaid, generate_json and generate_html take the same arguments as
generate_plantuml and return the other output formats as a
string.
Programmatic Access¶
For lower-level control, use the parser and generator classes directly:
from pathlib import Path
from erdify import (
ASTDatabaseParser,
SqlSchemaParser,
PlantUMLGenerator,
MermaidGenerator,
)
# Python models only
parser = ASTDatabaseParser(Path("./models"), include_patterns=["models.py", "models/*.py"])
entities, enums = parser.parse_all_models()
# SQL DDL only (requires erdify[sql]); takes an explicit list of files
sql_entities, sql_enums = SqlSchemaParser([Path("schema.sql")], dialect="postgres").parse()
# 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}")
puml = PlantUMLGenerator(entities, enums, title="Custom ERD").generate()
mmd = MermaidGenerator(entities, enums, title="Custom ERD").generate()
SqlSchemaParser.parse() raises erdify.SqlDependencyError when the optional
sqlglot dependency is missing.
What is public¶
Everything listed in erdify.__all__ is the public Python API. Within it:
| Name | Public contract |
|---|---|
parse_models_directory, generate_plantuml, generate_mermaid, generate_json, generate_html |
The function and its parameters. Only the first argument (and enums for the generate_* functions) may be passed positionally; everything else is keyword-only. |
ASTDatabaseParser |
The constructor (database_path positional, options keyword-only) and parse_all_models(). |
SqlSchemaParser |
The constructor (files positional, options keyword-only) and parse(). |
PlantUMLGenerator, MermaidGenerator |
The constructor (entities, enums positional, title keyword-only) and generate(). |
EntityInfo, FieldInfo, EnumInfo |
The dataclass fields. New fields may be added with defaults. |
SqlDependencyError |
The exception type. |
ModelParseError |
The exception type and its errors attribute ((file, message) pairs). |
JSON_SCHEMA_VERSION |
The current JSON output schema_version. |
Any other attribute or method on these classes, and anything imported from a
submodule (erdify.parser, erdify.generator, ...) rather than from erdify,
is internal and may change in any release.