Developing an External Adapter¶
So you've built a custom algorithm or target adapter and you'd rather keep it in its own package than fork dbterd? Good news — dbterd supports external adapter plugins via Python entry points. Ship your adapter as a separate Python package — private or public — and dbterd will pick it up automatically at runtime. No pull request required (though we'd love one if it's useful to others).
No PyPI required
Your external adapter does not need to be published to PyPI. Any installation method that registers the package in the environment works:
- Local editable install:
pip install -e ./my-adapter - Git URL:
pip install git+https://github.com/your-org/my-adapter.git - Private registry:
pip install my-adapter --index-url https://your-registry/simple - Wheel / sdist file:
pip install ./dist/my_adapter-0.1.0.tar.gz
As long as the package metadata (with [project.entry-points]) is installed in the same environment as dbterd, the entry points are discovered.
How It Works¶
When dbterd starts, it discovers adapters from two sources:
- Built-in adapters under
dbterd/adapters/algos/anddbterd/adapters/targets/ - External packages that declare entry points in the
dbterd.adaptersgroup
The discovery flow looks like this:
dbterd starts
│
├── Scan built-in adapter modules
│ └── import dbterd.adapters.algos.*
│ └── import dbterd.adapters.targets.*
│
└── Scan entry points in "dbterd.adapters" group
└── Load each entry point module
└── @register_algo / @register_target decorators fire
└── Adapter registered in PluginRegistry
Your external module just needs to be imported — the @register_algo or @register_target decorator handles the actual registration. Think of the entry point as a doorbell: dbterd rings it, your module wakes up, and the decorator announces itself to the registry.
Quick Start¶
If you're the impatient type (no judgment), here's the shortest path to a working external adapter:
Create pyproject.toml:
[project]
name = "dbterd-target-myformat"
version = "0.1.0"
dependencies = ["dbterd"]
[project.entry-points."dbterd.adapters"]
myformat = "dbterd_target_myformat.adapter"
Create dbterd_target_myformat/adapter.py:
"""External target adapter for dbterd."""
from typing import ClassVar
from dbterd.core.adapters.target import BaseTargetAdapter
from dbterd.core.models import Ref, Table
from dbterd.core.registry.decorators import register_target
@register_target("myformat", description="My custom ERD format")
class MyFormatAdapter(BaseTargetAdapter):
file_extension = ".myext"
default_filename = "output.myext"
RELATIONSHIP_SYMBOLS: ClassVar[dict[str, str]] = {
"01": "?--", "11": "---", "0n": "?--<", "1n": "---<", "nn": ">--<",
}
DEFAULT_SYMBOL = ">--"
def build_erd(self, tables: list[Table], relationships: list[Ref], **kwargs) -> str:
lines = [self.format_table(t, **kwargs) for t in tables]
lines += [self.format_relationship(r, **kwargs) for r in relationships]
return "\n".join(lines)
def format_table(self, table: Table, **kwargs) -> str:
return f"TABLE {table.name}"
def format_relationship(self, relationship: Ref, **kwargs) -> str:
symbol = self.get_rel_symbol(relationship.type)
return f"{relationship.table_map[1]} {symbol} {relationship.table_map[0]}"
Install and run:
That's it. Your adapter is live.
Project Structure¶
A typical external adapter package looks like:
dbterd-target-myformat/
├── pyproject.toml
├── dbterd_target_myformat/
│ ├── __init__.py
│ └── adapter.py # Your adapter class with @register_target
└── tests/
└── test_adapter.py
For an algorithm adapter, swap target for algo:
dbterd-algo-myalgo/
├── pyproject.toml
├── dbterd_algo_myalgo/
│ ├── __init__.py
│ └── adapter.py # Your adapter class with @register_algo
└── tests/
└── test_adapter.py
Step-by-Step Guide¶
1. Create Your Package¶
Initialize a standard Python package. Use whatever build system you prefer — the entry point specification is standardized across all of them.
[project]
name = "dbterd-target-myformat"
version = "0.1.0"
description = "MyFormat target adapter for dbterd"
requires-python = ">=3.9"
dependencies = ["dbterd"]
[project.entry-points."dbterd.adapters"]
myformat = "dbterd_target_myformat.adapter"
2. Implement Your Adapter¶
Your adapter module must import and use the appropriate decorator. When dbterd loads the entry point, the module is imported, which triggers the decorator, which registers your adapter in the PluginRegistry.
For target adapters, inherit from BaseTargetAdapter and use @register_target:
from dbterd.core.adapters.target import BaseTargetAdapter
from dbterd.core.registry.decorators import register_target
@register_target("myformat", description="My custom format")
class MyFormatAdapter(BaseTargetAdapter):
...
For algorithm adapters, inherit from BaseAlgoAdapter and use @register_algo:
from dbterd.core.adapters.algo import BaseAlgoAdapter
from dbterd.core.registry.decorators import register_algo
@register_algo("my_algo", description="My custom relationship detection")
class MyAlgoAdapter(BaseAlgoAdapter):
...
See Developing a Target Adapter and Developing an Algorithm Adapter for the full implementation details on each adapter type.
3. Register the Entry Point¶
The entry point name (left side of =) is an identifier used for logging. The value (right side) must point to the module containing your decorated adapter class.
Module, not class
The entry point should reference the module, not the class directly. The module import triggers the @register_target / @register_algo decorator, which is what performs the actual registration. Pointing at the class works too (dbterd_target_myformat.adapter:MyFormatAdapter), but it's unnecessary since the decorator fires on import either way.
You can register multiple adapters from a single package:
[project.entry-points."dbterd.adapters"]
myformat = "my_package.targets.myformat"
my_algo = "my_package.algos.my_algo"
4. Install and Verify¶
Install your package (editable mode is handy during development):
Verify that dbterd sees your adapter:
# For target adapters
dbterd run --target myformat --help
# For algorithm adapters
dbterd run --algo my_algo --help
If the adapter loaded successfully, dbterd will accept it as a valid option. If it failed to load, you'll see a warning in the logs:
Complete Example: External Target Adapter¶
Here's a full working example of an external target adapter that generates a CSV-based ERD format — because sometimes you just want to throw your relationships into a spreadsheet.
pyproject.toml:
[project]
name = "dbterd-target-csv"
version = "0.1.0"
description = "CSV target adapter for dbterd"
requires-python = ">=3.9"
dependencies = ["dbterd"]
[project.entry-points."dbterd.adapters"]
csv = "dbterd_target_csv.adapter"
dbterd_target_csv/adapter.py:
"""CSV target adapter for dbterd.
Generates ERD data in CSV format for spreadsheet-friendly consumption.
"""
from typing import ClassVar
from dbterd.core.adapters.target import BaseTargetAdapter
from dbterd.core.models import Ref, Table
from dbterd.core.registry.decorators import register_target
@register_target("csv", description="CSV format for spreadsheet tools")
class CsvAdapter(BaseTargetAdapter):
"""CSV target adapter."""
file_extension = ".csv"
default_filename = "erd.csv"
RELATIONSHIP_SYMBOLS: ClassVar[dict[str, str]] = {
"01": "zero-to-one",
"11": "one-to-one",
"0n": "zero-to-many",
"1n": "one-to-many",
"nn": "many-to-many",
}
DEFAULT_SYMBOL = "many-to-one"
def build_erd(self, tables: list[Table], relationships: list[Ref], **kwargs) -> str:
"""Build CSV ERD content."""
lines = ["from_table,from_column,relationship,to_table,to_column"]
for rel in relationships:
lines.append(self.format_relationship(rel, **kwargs))
return "\n".join(lines)
def format_table(self, table: Table, **kwargs) -> str:
"""Format a single table (not used in CSV output)."""
return table.name
def format_relationship(self, relationship: Ref, **kwargs) -> str:
"""Format a single relationship as a CSV row."""
symbol = self.get_rel_symbol(relationship.type)
return (
f"{relationship.table_map[1]},"
f"{relationship.column_map[1]},"
f"{symbol},"
f"{relationship.table_map[0]},"
f"{relationship.column_map[0]}"
)
Usage:
Complete Example: External Algorithm Adapter¶
An external algorithm adapter that detects relationships using dbt meta tags:
pyproject.toml:
[project]
name = "dbterd-algo-meta-refs"
version = "0.1.0"
description = "Meta-based relationship detection for dbterd"
requires-python = ">=3.9"
dependencies = ["dbterd"]
[project.entry-points."dbterd.adapters"]
meta_refs = "dbterd_algo_meta_refs.adapter"
dbterd_algo_meta_refs/adapter.py:
"""Meta-based relationship detection algorithm for dbterd.
Detects relationships using custom meta tags on dbt columns:
columns:
- name: user_id
meta:
references:
table: users
column: id
type: n1
"""
from dbterd.core.adapters.algo import BaseAlgoAdapter
from dbterd.core.models import Ref, Table
from dbterd.core.registry.decorators import register_algo
from dbterd.helpers.log import logger
from dbterd.types import Catalog, Manifest
@register_algo("meta_refs", description="Detect relationships via column meta tags")
class MetaRefsAlgo(BaseAlgoAdapter):
"""Algorithm adapter using dbt column meta tags."""
def parse_artifacts(
self, manifest: Manifest, catalog: Catalog, **kwargs
) -> tuple[list[Table], list[Ref]]:
"""Parse from file-based manifest/catalog artifacts."""
tables = self.get_tables(manifest=manifest, catalog=catalog, **kwargs)
tables = self.filter_tables_based_on_selection(tables=tables, **kwargs)
relationships = self._get_meta_relationships(manifest=manifest)
relationships = self.make_up_relationships(
relationships=relationships, tables=tables
)
logger.info(
f"Collected {len(tables)} table(s) and {len(relationships)} relationship(s)"
)
return (
sorted(tables, key=lambda tbl: tbl.node_name),
sorted(relationships, key=lambda rel: rel.name),
)
def parse_metadata(self, data: dict, **kwargs) -> tuple[list[Table], list[Ref]]:
"""Parse from dbt Cloud metadata API response."""
raise NotImplementedError("Metadata API not supported by meta_refs algorithm")
def _get_meta_relationships(self, manifest: Manifest) -> list[Ref]:
"""Extract relationships from column meta tags."""
refs = []
for node_id, node in manifest.nodes.items():
if not node_id.startswith("model."):
continue
for col_name, col in node.columns.items():
meta_ref = col.meta.get("references") if col.meta else None
if not meta_ref:
continue
target_table = meta_ref.get("table")
target_column = meta_ref.get("column", "id")
rel_type = meta_ref.get("type", "n1")
# Find the target node by table name
target_node_id = self._resolve_node(manifest, target_table)
if not target_node_id:
logger.debug(f"Could not resolve target table '{target_table}'")
continue
refs.append(
Ref(
name=f"meta.{node_id}.{col_name}",
table_map=(target_node_id, node_id),
column_map=(target_column, col_name),
type=rel_type,
)
)
return self.get_unique_refs(refs=refs)
def _resolve_node(self, manifest: Manifest, table_name: str) -> str | None:
"""Resolve a table name to a full node ID."""
for node_id in manifest.nodes:
if node_id.endswith(f".{table_name}"):
return node_id
return None
Usage:
Testing Your Plugin¶
You can test your external adapter independently, without needing to install dbterd from source. The key is to mock the parts you don't control and test your logic directly:
"""Tests for external adapter plugin."""
import pytest
from dbterd.core.models import Column, Ref, Table
@pytest.fixture
def sample_table():
return Table(
name="orders",
database="analytics",
schema="public",
columns=[
Column(name="id", data_type="integer"),
Column(name="user_id", data_type="integer"),
],
node_name="model.shop.orders",
)
@pytest.fixture
def sample_ref():
return Ref(
name="test.ref",
table_map=("model.shop.users", "model.shop.orders"),
column_map=("id", "user_id"),
type="n1",
)
class TestMyAdapter:
def test_format_relationship(self, sample_ref):
from dbterd_target_myformat.adapter import MyFormatAdapter
adapter = MyFormatAdapter()
result = adapter.format_relationship(sample_ref)
assert "model.shop.orders" in result
assert "model.shop.users" in result
def test_build_erd(self, sample_table, sample_ref):
from dbterd_target_myformat.adapter import MyFormatAdapter
adapter = MyFormatAdapter()
result = adapter.build_erd([sample_table], [sample_ref])
assert "orders" in result
To verify entry point registration end-to-end:
def test_entry_point_registered():
"""Verify the adapter registers via entry point."""
from importlib.metadata import entry_points
eps = entry_points(group="dbterd.adapters")
names = [ep.name for ep in eps]
assert "myformat" in names
Distribution¶
Your adapter works with any standard Python installation method. Pick whatever fits your workflow:
Private / Internal Use¶
For team-internal or private adapters, just install directly from source:
# Editable install from local path (great for development)
pip install -e ./my-adapter
# Install from a private Git repo
pip install git+https://github.com/your-org/my-adapter.git
# Install from a private PyPI registry
pip install my-adapter --index-url https://your-registry/simple
# Install from a built wheel or sdist
pip install ./dist/my_adapter-0.1.0.tar.gz
Publishing to PyPI¶
When your adapter is ready for the world:
-
Choose a clear name — follow the convention
dbterd-target-<name>ordbterd-algo-<name>so users can find it easily. -
Pin your dbterd dependency — use a compatible release specifier to avoid breaking on major versions:
-
Build and publish:
-
Users install it alongside dbterd:
Troubleshooting¶
| Symptom | Likely Cause | Fix |
|---|---|---|
WARNING - Failed to load external adapter entry point 'xxx' | Import error in your module | Run python -c "import your_module.adapter" to see the full traceback |
| Adapter not recognized as a valid option | Entry point not installed | Run pip show your-package and check the entry points are listed |
ModuleNotFoundError: No module named 'dbterd' | dbterd not in your dependencies | Add dbterd to [project.dependencies] |
| Adapter works in dev but not after install | Entry point module path is wrong | Double-check the dotted path matches your package layout |
Debug entry point discovery
You can list all registered entry points from Python to verify your adapter is visible:
Tips and Best Practices¶
-
Keep
dbterdas a dependency, not a devDependency — your adapter imports fromdbterdat runtime, so it needs to be a real dependency. -
Don't duplicate adapter names — if you register a name that conflicts with a built-in adapter, behavior is undefined. Pick a unique name.
-
Fail gracefully — if your adapter has optional heavy dependencies (like a database driver), catch the import error and provide a helpful message instead of crashing
dbterd. -
Follow the naming convention —
dbterd-target-*for targets,dbterd-algo-*for algorithms. This keeps things discoverable whether your package is internal or public. -
Test against multiple dbterd versions — entry points are stable, but the base classes may evolve. CI matrix testing saves you from surprise breakage.
-
Read the existing adapter guides — Developing a Target Adapter and Developing an Algorithm Adapter cover the full adapter API in detail. This guide focuses on the external packaging and discovery mechanism.