erdscope 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {erdscope-0.2.0/erdscope.egg-info → erdscope-0.3.0}/PKG-INFO +160 -10
- {erdscope-0.2.0 → erdscope-0.3.0}/README.md +159 -9
- {erdscope-0.2.0 → erdscope-0.3.0}/erd.py +1852 -539
- {erdscope-0.2.0 → erdscope-0.3.0/erdscope.egg-info}/PKG-INFO +160 -10
- {erdscope-0.2.0 → erdscope-0.3.0}/erdscope.egg-info/SOURCES.txt +6 -1
- {erdscope-0.2.0 → erdscope-0.3.0}/pyproject.toml +1 -1
- erdscope-0.3.0/tests/test_build.py +45 -0
- erdscope-0.3.0/tests/test_characterization.py +1026 -0
- erdscope-0.3.0/tests/test_config_validation.py +475 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/tests/test_erd.py +201 -86
- erdscope-0.3.0/tests/test_merge_ir.py +929 -0
- erdscope-0.3.0/tests/test_pipeline.py +761 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/LICENSE +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/erdscope.egg-info/dependency_links.txt +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/erdscope.egg-info/entry_points.txt +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/erdscope.egg-info/requires.txt +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/erdscope.egg-info/top_level.txt +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/setup.cfg +0 -0
- {erdscope-0.2.0 → erdscope-0.3.0}/tests/test_e2e.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: erdscope
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Interactive, self-contained ER-diagram HTML and Excel table definitions from a live MySQL or PostgreSQL database — single file, zero required dependencies
|
|
5
5
|
Author: tas6
|
|
6
6
|
License-Expression: MIT
|
|
@@ -38,19 +38,30 @@ Dynamic: license-file
|
|
|
38
38
|
[](https://pypi.org/project/erdscope/)
|
|
39
39
|
|
|
40
40
|
Generate a **self-contained, interactive ER diagram** — and an **Excel table-definition
|
|
41
|
-
workbook** — from a live MySQL or
|
|
41
|
+
workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
|
|
42
42
|
zero-dependency Python CLI.
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
pip install erdscope
|
|
46
46
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
|
|
47
47
|
erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
|
|
48
|
+
erdscope sqlite:///path/to/app.db -o erd.html
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
**Three input sources — any one of them is enough** (a database is no longer required):
|
|
52
|
+
|
|
53
|
+
- **Database** (MySQL / PostgreSQL / SQLite) — the source of truth for tables, columns,
|
|
54
|
+
comments, indexes, and real foreign keys.
|
|
55
|
+
- **Application code** (`--models`: Rails / Prisma / Django) — adds association semantics
|
|
56
|
+
the database cannot express (`has_many :through`, polymorphic, ...), and can stand on
|
|
57
|
+
its own when there is no DB to point at.
|
|
58
|
+
- **Config file** (`tables:`) — declare or patch a schema by hand: add tables, columns,
|
|
59
|
+
indexes, and associations, or override and delete what the DB or code got wrong.
|
|
60
|
+
|
|
61
|
+
They merge in that order — **database → code → config**, each layer refining the previous
|
|
62
|
+
(the database wins on physical facts like column types; code and config win on
|
|
63
|
+
associations and logical names; config always has the final say). With no database URL,
|
|
64
|
+
nothing is connected and no password is prompted.
|
|
54
65
|
|
|
55
66
|
## Demo
|
|
56
67
|
|
|
@@ -82,6 +93,9 @@ erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
|
|
|
82
93
|
# PostgreSQL: same thing (schema defaults to public; override with ?schema=name)
|
|
83
94
|
erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
|
|
84
95
|
|
|
96
|
+
# SQLite: just point at the file (no server, nothing to install — uses stdlib sqlite3)
|
|
97
|
+
erdscope sqlite:///path/to/app.db -o erd.html
|
|
98
|
+
|
|
85
99
|
# enrich with association semantics parsed from application code (optional)
|
|
86
100
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
87
101
|
--models /path/to/rails/app -o erd.html
|
|
@@ -89,8 +103,21 @@ erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
|
89
103
|
# also write a table-definition workbook
|
|
90
104
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
91
105
|
--excel table_definitions.xlsx -o erd.html
|
|
106
|
+
|
|
107
|
+
# no database — generate straight from application code
|
|
108
|
+
erdscope --models /path/to/rails/app -o erd.html
|
|
109
|
+
|
|
110
|
+
# no database — generate from a hand-written config schema (see Config file below)
|
|
111
|
+
erdscope --config schema.yml -o erd.html
|
|
112
|
+
|
|
113
|
+
# multiple code sources merge in order, later wins (--models is repeatable)
|
|
114
|
+
erdscope --models /path/to/rails/app --models /path/to/schema.prisma -o erd.html
|
|
92
115
|
```
|
|
93
116
|
|
|
117
|
+
Want to try it right now with no database of your own? A ready-to-run SQLite
|
|
118
|
+
sample ships in [`examples/`](examples/) — `python3 erd.py
|
|
119
|
+
sqlite:///examples/demo_shop.db -o shop.html`. See [examples/README.md](examples/README.md).
|
|
120
|
+
|
|
94
121
|
Behind a bastion? Open an SSH tunnel first and point at localhost:
|
|
95
122
|
|
|
96
123
|
```bash
|
|
@@ -108,7 +135,7 @@ itself is made.
|
|
|
108
135
|
| Option | Description |
|
|
109
136
|
|---|---|
|
|
110
137
|
| `-o FILE` | Output HTML path (default: `erd.html`) |
|
|
111
|
-
| `--models PATH` | Merge associations parsed from code: a Rails project (or `app/models` dir), a `schema.prisma`, or a Django project — auto-detected |
|
|
138
|
+
| `--models PATH` | Merge associations parsed from code: a Rails project (or `app/models` dir), a `schema.prisma`, or a Django project — auto-detected. **Repeatable** — multiple sources merge in the order given (later wins). Usable with no database URL |
|
|
112
139
|
| `--excel FILE.xlsx` | Also write a table-definition workbook: an overview sheet plus one sheet per table (columns, defaults, keys, comments, indexes, associations) |
|
|
113
140
|
| `--excel-template FILE.xlsx` | Override the workbook's colors/fonts/borders from a template `.xlsx` — see `excel-template.xlsx` and its `Styles` sheet for the 5-cell contract (default: built-in styling) |
|
|
114
141
|
| `--max-rows N` | Max column rows shown per table (default: 15; the rest scroll) |
|
|
@@ -130,6 +157,56 @@ can find. See the [Config file chapter of the manual](https://orapli.github.io/e
|
|
|
130
157
|
list and semantics, and [`erdscope.example.yml`](erdscope.example.yml) for a fully
|
|
131
158
|
annotated sample based on the live demo's schema.
|
|
132
159
|
|
|
160
|
+
### Config as a schema source
|
|
161
|
+
|
|
162
|
+
Beyond settings, the config can carry a **`tables:`** section that is itself a full input
|
|
163
|
+
source — enough to generate a diagram with no database and no code at all, or to patch
|
|
164
|
+
what the other sources produce. It merges as the highest-priority layer, so it can add
|
|
165
|
+
new tables/columns/indexes/associations, override attributes, or delete them:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
title: billing
|
|
169
|
+
tables:
|
|
170
|
+
customers:
|
|
171
|
+
comment: Customer accounts
|
|
172
|
+
primary_key: id
|
|
173
|
+
columns:
|
|
174
|
+
- { name: id, type: bigint, primary: true }
|
|
175
|
+
- { name: email, type: varchar, nullable: false, comment: Login address }
|
|
176
|
+
associations:
|
|
177
|
+
- { type: has_many, name: invoices, target: invoices }
|
|
178
|
+
invoices:
|
|
179
|
+
columns:
|
|
180
|
+
- { name: id, type: bigint, primary: true }
|
|
181
|
+
- { name: customer_id, type: bigint }
|
|
182
|
+
associations:
|
|
183
|
+
- { type: belongs_to, name: customer, target: customers, foreign_key: customer_id }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Patch an existing DB/code result instead of declaring from scratch — override a column,
|
|
187
|
+
delete a stale one, drop a whole table, or replace a table's column list wholesale:
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
tables:
|
|
191
|
+
orders:
|
|
192
|
+
columns:
|
|
193
|
+
- { name: status, comment: Order status } # override just this attribute
|
|
194
|
+
- { name: legacy_flag, drop: true } # delete a column
|
|
195
|
+
temp_scratch:
|
|
196
|
+
drop: true # delete a whole table
|
|
197
|
+
reports:
|
|
198
|
+
columns_mode: replace # discard lower-layer columns first
|
|
199
|
+
columns:
|
|
200
|
+
- { name: id, type: bigint, primary: true }
|
|
201
|
+
- { name: body, type: text }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Config is validated in two passes: **syntactically at load** (unknown keys, bad types,
|
|
205
|
+
malformed operations) and **semantically at run time** (a `drop`/reference must point at
|
|
206
|
+
something that actually exists once all sources are merged) — typos never pass silently.
|
|
207
|
+
The full `tables:` schema, the `drop`/`replace` operations, and the precedence rules are
|
|
208
|
+
documented in the [manual](https://orapli.github.io/erdscope/manual.html#config-file).
|
|
209
|
+
|
|
133
210
|
## Dependencies
|
|
134
211
|
|
|
135
212
|
`erd.py` runs with **zero required dependencies** — everything below is optional, and
|
|
@@ -142,6 +219,7 @@ is missing. If you installed via pip, extras pull them in for you:
|
|
|
142
219
|
|---|---|---|
|
|
143
220
|
| [PyMySQL](https://pypi.org/project/PyMySQL/) | MySQL connections | Falls back to shelling out to the `mysql` CLI (must be on `PATH`) |
|
|
144
221
|
| [psycopg](https://pypi.org/project/psycopg/) (or psycopg2) | PostgreSQL connections | Falls back to shelling out to the `psql` CLI (must be on `PATH`) |
|
|
222
|
+
| _(none)_ | **SQLite** connections (`sqlite:///file.db`) | Uses Python's built-in `sqlite3` — always available, nothing to install or fall back to |
|
|
145
223
|
| [PyYAML](https://pypi.org/project/PyYAML/) | Reading a `.yml`/`.yaml` config file | A `.json` config still works with no dependency; pointing `--config`/auto-discovery at a `.yml`/`.yaml` file without PyYAML installed exits with a clear error |
|
|
146
224
|
|
|
147
225
|
Excel output (`--excel`) needs none of these — it's written directly via the stdlib
|
|
@@ -200,9 +278,81 @@ python3 -m unittest tests.test_e2e -v
|
|
|
200
278
|
## Extending
|
|
201
279
|
|
|
202
280
|
Everything downstream (UI, layouts, exports) consumes the intermediate representation
|
|
203
|
-
documented at the top of `erd.py`.
|
|
204
|
-
|
|
205
|
-
|
|
281
|
+
documented at the top of `erd.py`. Both input layers are pluggable: a **database
|
|
282
|
+
adapter** turns a URL scheme into that shape, and a **framework overlay** turns an
|
|
283
|
+
application-code project into it. Each is a small class registered under the scheme /
|
|
284
|
+
project kind it handles, so adding one never touches the dispatch code.
|
|
285
|
+
|
|
286
|
+
### Custom adapters and overlays (a plugin file)
|
|
287
|
+
|
|
288
|
+
Write a plain Python file that subclasses the base and registers itself, then load it
|
|
289
|
+
with `--adapter path/to/plugin.py` (or config `adapters: [...]`). No need to rebuild
|
|
290
|
+
`erd.py` — the plugin registers into the running process, and a plugin works the same
|
|
291
|
+
against the single-file `erd.py` or a `pip install erdscope`.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
# my_sqlite.py — a custom database adapter
|
|
295
|
+
from erd import DBAdapter, register_adapter, mysql_ir
|
|
296
|
+
|
|
297
|
+
@register_adapter
|
|
298
|
+
class SqliteAdapter(DBAdapter):
|
|
299
|
+
schemes = ('sqlite',) # the URL scheme(s) it answers to
|
|
300
|
+
name = 'sqlite' # provider id recorded in the output
|
|
301
|
+
label = 'SQLite' # pretty name for the progress line
|
|
302
|
+
|
|
303
|
+
def fetch(self, url):
|
|
304
|
+
# ...read the schema for `url` and return the IR (the `tables` dict).
|
|
305
|
+
# mysql_ir() builds it from information_schema-shaped rows for you.
|
|
306
|
+
return mysql_ir(table_rows, col_rows, fk_rows, index_rows)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
python3 erd.py sqlite:///app.db --adapter my_sqlite.py -o erd.html
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
A **framework overlay** is the same idea against a `--models` path:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
# my_framework.py
|
|
317
|
+
from erd import FrameworkOverlay, register_overlay, make_provider_result
|
|
318
|
+
|
|
319
|
+
@register_overlay
|
|
320
|
+
class SequelizeOverlay(FrameworkOverlay):
|
|
321
|
+
name = 'sequelize'
|
|
322
|
+
priority = 5 # lower detect()s first; first match wins
|
|
323
|
+
|
|
324
|
+
def detect(self, root):
|
|
325
|
+
return root.is_dir() and (root / 'models' / 'index.js').exists()
|
|
326
|
+
|
|
327
|
+
def build(self, root, table_map):
|
|
328
|
+
tables = ... # parse `root` into the IR
|
|
329
|
+
return make_provider_result('framework', 'sequelize', tables, location=str(root))
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The built-in `db/mysql.py`, `db/postgres.py` and `frameworks/{rails,prisma,django}.py`
|
|
333
|
+
are the working examples of both patterns.
|
|
334
|
+
|
|
335
|
+
### Building `erd.py`
|
|
336
|
+
|
|
337
|
+
The shipped `erd.py` is a **build artifact**: the development source lives under
|
|
338
|
+
`src/erdscope/`. The Python is organised into concern-named fragments (`ir.py`,
|
|
339
|
+
`merge.py`, `providers.py`, `exporters.py`, `config.py`, `cli.py`, …) plus two
|
|
340
|
+
self-assembling **folders** — `db/` (the DB adapters) and `frameworks/` (the overlays)
|
|
341
|
+
— whose files are all included automatically (`base.py` first, then sorted), so adding a
|
|
342
|
+
built-in adapter or overlay is just dropping a new file in the folder. They are
|
|
343
|
+
concatenated in order, and the ~3,600-line embedded viewer (HTML/CSS/JS) lives in
|
|
344
|
+
`viewer.html`, out of the Python. Edit the relevant file, then regenerate the single file:
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
python3 tools/build_single_file.py # rewrites erd.py from the source
|
|
348
|
+
python3 tools/build_single_file.py --check # CI-style check that erd.py is in sync
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The fragments are an amalgamation of one flat module (SQLite-style) — no cross-module
|
|
352
|
+
imports — and the viewer is inlined into a one-line sentinel, so the whole build is pure
|
|
353
|
+
textual assembly and `erd.py` stays a self-contained, zero-dependency single file:
|
|
354
|
+
grabbing and running it, or `pip install erdscope`, is unchanged. CI runs the `--check`
|
|
355
|
+
above, so a hand-edit of `erd.py` or a forgotten rebuild fails the build.
|
|
206
356
|
|
|
207
357
|
## License
|
|
208
358
|
|
|
@@ -4,19 +4,30 @@
|
|
|
4
4
|
[](https://pypi.org/project/erdscope/)
|
|
5
5
|
|
|
6
6
|
Generate a **self-contained, interactive ER diagram** — and an **Excel table-definition
|
|
7
|
-
workbook** — from a live MySQL or
|
|
7
|
+
workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
|
|
8
8
|
zero-dependency Python CLI.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pip install erdscope
|
|
12
12
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
|
|
13
13
|
erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
|
|
14
|
+
erdscope sqlite:///path/to/app.db -o erd.html
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
**Three input sources — any one of them is enough** (a database is no longer required):
|
|
18
|
+
|
|
19
|
+
- **Database** (MySQL / PostgreSQL / SQLite) — the source of truth for tables, columns,
|
|
20
|
+
comments, indexes, and real foreign keys.
|
|
21
|
+
- **Application code** (`--models`: Rails / Prisma / Django) — adds association semantics
|
|
22
|
+
the database cannot express (`has_many :through`, polymorphic, ...), and can stand on
|
|
23
|
+
its own when there is no DB to point at.
|
|
24
|
+
- **Config file** (`tables:`) — declare or patch a schema by hand: add tables, columns,
|
|
25
|
+
indexes, and associations, or override and delete what the DB or code got wrong.
|
|
26
|
+
|
|
27
|
+
They merge in that order — **database → code → config**, each layer refining the previous
|
|
28
|
+
(the database wins on physical facts like column types; code and config win on
|
|
29
|
+
associations and logical names; config always has the final say). With no database URL,
|
|
30
|
+
nothing is connected and no password is prompted.
|
|
20
31
|
|
|
21
32
|
## Demo
|
|
22
33
|
|
|
@@ -48,6 +59,9 @@ erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
|
|
|
48
59
|
# PostgreSQL: same thing (schema defaults to public; override with ?schema=name)
|
|
49
60
|
erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
|
|
50
61
|
|
|
62
|
+
# SQLite: just point at the file (no server, nothing to install — uses stdlib sqlite3)
|
|
63
|
+
erdscope sqlite:///path/to/app.db -o erd.html
|
|
64
|
+
|
|
51
65
|
# enrich with association semantics parsed from application code (optional)
|
|
52
66
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
53
67
|
--models /path/to/rails/app -o erd.html
|
|
@@ -55,8 +69,21 @@ erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
|
55
69
|
# also write a table-definition workbook
|
|
56
70
|
erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
|
|
57
71
|
--excel table_definitions.xlsx -o erd.html
|
|
72
|
+
|
|
73
|
+
# no database — generate straight from application code
|
|
74
|
+
erdscope --models /path/to/rails/app -o erd.html
|
|
75
|
+
|
|
76
|
+
# no database — generate from a hand-written config schema (see Config file below)
|
|
77
|
+
erdscope --config schema.yml -o erd.html
|
|
78
|
+
|
|
79
|
+
# multiple code sources merge in order, later wins (--models is repeatable)
|
|
80
|
+
erdscope --models /path/to/rails/app --models /path/to/schema.prisma -o erd.html
|
|
58
81
|
```
|
|
59
82
|
|
|
83
|
+
Want to try it right now with no database of your own? A ready-to-run SQLite
|
|
84
|
+
sample ships in [`examples/`](examples/) — `python3 erd.py
|
|
85
|
+
sqlite:///examples/demo_shop.db -o shop.html`. See [examples/README.md](examples/README.md).
|
|
86
|
+
|
|
60
87
|
Behind a bastion? Open an SSH tunnel first and point at localhost:
|
|
61
88
|
|
|
62
89
|
```bash
|
|
@@ -74,7 +101,7 @@ itself is made.
|
|
|
74
101
|
| Option | Description |
|
|
75
102
|
|---|---|
|
|
76
103
|
| `-o FILE` | Output HTML path (default: `erd.html`) |
|
|
77
|
-
| `--models PATH` | Merge associations parsed from code: a Rails project (or `app/models` dir), a `schema.prisma`, or a Django project — auto-detected |
|
|
104
|
+
| `--models PATH` | Merge associations parsed from code: a Rails project (or `app/models` dir), a `schema.prisma`, or a Django project — auto-detected. **Repeatable** — multiple sources merge in the order given (later wins). Usable with no database URL |
|
|
78
105
|
| `--excel FILE.xlsx` | Also write a table-definition workbook: an overview sheet plus one sheet per table (columns, defaults, keys, comments, indexes, associations) |
|
|
79
106
|
| `--excel-template FILE.xlsx` | Override the workbook's colors/fonts/borders from a template `.xlsx` — see `excel-template.xlsx` and its `Styles` sheet for the 5-cell contract (default: built-in styling) |
|
|
80
107
|
| `--max-rows N` | Max column rows shown per table (default: 15; the rest scroll) |
|
|
@@ -96,6 +123,56 @@ can find. See the [Config file chapter of the manual](https://orapli.github.io/e
|
|
|
96
123
|
list and semantics, and [`erdscope.example.yml`](erdscope.example.yml) for a fully
|
|
97
124
|
annotated sample based on the live demo's schema.
|
|
98
125
|
|
|
126
|
+
### Config as a schema source
|
|
127
|
+
|
|
128
|
+
Beyond settings, the config can carry a **`tables:`** section that is itself a full input
|
|
129
|
+
source — enough to generate a diagram with no database and no code at all, or to patch
|
|
130
|
+
what the other sources produce. It merges as the highest-priority layer, so it can add
|
|
131
|
+
new tables/columns/indexes/associations, override attributes, or delete them:
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
title: billing
|
|
135
|
+
tables:
|
|
136
|
+
customers:
|
|
137
|
+
comment: Customer accounts
|
|
138
|
+
primary_key: id
|
|
139
|
+
columns:
|
|
140
|
+
- { name: id, type: bigint, primary: true }
|
|
141
|
+
- { name: email, type: varchar, nullable: false, comment: Login address }
|
|
142
|
+
associations:
|
|
143
|
+
- { type: has_many, name: invoices, target: invoices }
|
|
144
|
+
invoices:
|
|
145
|
+
columns:
|
|
146
|
+
- { name: id, type: bigint, primary: true }
|
|
147
|
+
- { name: customer_id, type: bigint }
|
|
148
|
+
associations:
|
|
149
|
+
- { type: belongs_to, name: customer, target: customers, foreign_key: customer_id }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Patch an existing DB/code result instead of declaring from scratch — override a column,
|
|
153
|
+
delete a stale one, drop a whole table, or replace a table's column list wholesale:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
tables:
|
|
157
|
+
orders:
|
|
158
|
+
columns:
|
|
159
|
+
- { name: status, comment: Order status } # override just this attribute
|
|
160
|
+
- { name: legacy_flag, drop: true } # delete a column
|
|
161
|
+
temp_scratch:
|
|
162
|
+
drop: true # delete a whole table
|
|
163
|
+
reports:
|
|
164
|
+
columns_mode: replace # discard lower-layer columns first
|
|
165
|
+
columns:
|
|
166
|
+
- { name: id, type: bigint, primary: true }
|
|
167
|
+
- { name: body, type: text }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Config is validated in two passes: **syntactically at load** (unknown keys, bad types,
|
|
171
|
+
malformed operations) and **semantically at run time** (a `drop`/reference must point at
|
|
172
|
+
something that actually exists once all sources are merged) — typos never pass silently.
|
|
173
|
+
The full `tables:` schema, the `drop`/`replace` operations, and the precedence rules are
|
|
174
|
+
documented in the [manual](https://orapli.github.io/erdscope/manual.html#config-file).
|
|
175
|
+
|
|
99
176
|
## Dependencies
|
|
100
177
|
|
|
101
178
|
`erd.py` runs with **zero required dependencies** — everything below is optional, and
|
|
@@ -108,6 +185,7 @@ is missing. If you installed via pip, extras pull them in for you:
|
|
|
108
185
|
|---|---|---|
|
|
109
186
|
| [PyMySQL](https://pypi.org/project/PyMySQL/) | MySQL connections | Falls back to shelling out to the `mysql` CLI (must be on `PATH`) |
|
|
110
187
|
| [psycopg](https://pypi.org/project/psycopg/) (or psycopg2) | PostgreSQL connections | Falls back to shelling out to the `psql` CLI (must be on `PATH`) |
|
|
188
|
+
| _(none)_ | **SQLite** connections (`sqlite:///file.db`) | Uses Python's built-in `sqlite3` — always available, nothing to install or fall back to |
|
|
111
189
|
| [PyYAML](https://pypi.org/project/PyYAML/) | Reading a `.yml`/`.yaml` config file | A `.json` config still works with no dependency; pointing `--config`/auto-discovery at a `.yml`/`.yaml` file without PyYAML installed exits with a clear error |
|
|
112
190
|
|
|
113
191
|
Excel output (`--excel`) needs none of these — it's written directly via the stdlib
|
|
@@ -166,9 +244,81 @@ python3 -m unittest tests.test_e2e -v
|
|
|
166
244
|
## Extending
|
|
167
245
|
|
|
168
246
|
Everything downstream (UI, layouts, exports) consumes the intermediate representation
|
|
169
|
-
documented at the top of `erd.py`.
|
|
170
|
-
|
|
171
|
-
|
|
247
|
+
documented at the top of `erd.py`. Both input layers are pluggable: a **database
|
|
248
|
+
adapter** turns a URL scheme into that shape, and a **framework overlay** turns an
|
|
249
|
+
application-code project into it. Each is a small class registered under the scheme /
|
|
250
|
+
project kind it handles, so adding one never touches the dispatch code.
|
|
251
|
+
|
|
252
|
+
### Custom adapters and overlays (a plugin file)
|
|
253
|
+
|
|
254
|
+
Write a plain Python file that subclasses the base and registers itself, then load it
|
|
255
|
+
with `--adapter path/to/plugin.py` (or config `adapters: [...]`). No need to rebuild
|
|
256
|
+
`erd.py` — the plugin registers into the running process, and a plugin works the same
|
|
257
|
+
against the single-file `erd.py` or a `pip install erdscope`.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
# my_sqlite.py — a custom database adapter
|
|
261
|
+
from erd import DBAdapter, register_adapter, mysql_ir
|
|
262
|
+
|
|
263
|
+
@register_adapter
|
|
264
|
+
class SqliteAdapter(DBAdapter):
|
|
265
|
+
schemes = ('sqlite',) # the URL scheme(s) it answers to
|
|
266
|
+
name = 'sqlite' # provider id recorded in the output
|
|
267
|
+
label = 'SQLite' # pretty name for the progress line
|
|
268
|
+
|
|
269
|
+
def fetch(self, url):
|
|
270
|
+
# ...read the schema for `url` and return the IR (the `tables` dict).
|
|
271
|
+
# mysql_ir() builds it from information_schema-shaped rows for you.
|
|
272
|
+
return mysql_ir(table_rows, col_rows, fk_rows, index_rows)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
python3 erd.py sqlite:///app.db --adapter my_sqlite.py -o erd.html
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A **framework overlay** is the same idea against a `--models` path:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
# my_framework.py
|
|
283
|
+
from erd import FrameworkOverlay, register_overlay, make_provider_result
|
|
284
|
+
|
|
285
|
+
@register_overlay
|
|
286
|
+
class SequelizeOverlay(FrameworkOverlay):
|
|
287
|
+
name = 'sequelize'
|
|
288
|
+
priority = 5 # lower detect()s first; first match wins
|
|
289
|
+
|
|
290
|
+
def detect(self, root):
|
|
291
|
+
return root.is_dir() and (root / 'models' / 'index.js').exists()
|
|
292
|
+
|
|
293
|
+
def build(self, root, table_map):
|
|
294
|
+
tables = ... # parse `root` into the IR
|
|
295
|
+
return make_provider_result('framework', 'sequelize', tables, location=str(root))
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The built-in `db/mysql.py`, `db/postgres.py` and `frameworks/{rails,prisma,django}.py`
|
|
299
|
+
are the working examples of both patterns.
|
|
300
|
+
|
|
301
|
+
### Building `erd.py`
|
|
302
|
+
|
|
303
|
+
The shipped `erd.py` is a **build artifact**: the development source lives under
|
|
304
|
+
`src/erdscope/`. The Python is organised into concern-named fragments (`ir.py`,
|
|
305
|
+
`merge.py`, `providers.py`, `exporters.py`, `config.py`, `cli.py`, …) plus two
|
|
306
|
+
self-assembling **folders** — `db/` (the DB adapters) and `frameworks/` (the overlays)
|
|
307
|
+
— whose files are all included automatically (`base.py` first, then sorted), so adding a
|
|
308
|
+
built-in adapter or overlay is just dropping a new file in the folder. They are
|
|
309
|
+
concatenated in order, and the ~3,600-line embedded viewer (HTML/CSS/JS) lives in
|
|
310
|
+
`viewer.html`, out of the Python. Edit the relevant file, then regenerate the single file:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
python3 tools/build_single_file.py # rewrites erd.py from the source
|
|
314
|
+
python3 tools/build_single_file.py --check # CI-style check that erd.py is in sync
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The fragments are an amalgamation of one flat module (SQLite-style) — no cross-module
|
|
318
|
+
imports — and the viewer is inlined into a one-line sentinel, so the whole build is pure
|
|
319
|
+
textual assembly and `erd.py` stays a self-contained, zero-dependency single file:
|
|
320
|
+
grabbing and running it, or `pip install erdscope`, is unchanged. CI runs the `--check`
|
|
321
|
+
above, so a hand-edit of `erd.py` or a forgotten rebuild fails the build.
|
|
172
322
|
|
|
173
323
|
## License
|
|
174
324
|
|