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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: erdscope
3
- Version: 0.2.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
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](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 PostgreSQL database, with a single-file,
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
- The database is the source of truth (tables, columns, comments, indexes, real foreign
51
- keys). Application code (Rails / Prisma / Django) can optionally be layered on top to
52
- add association semantics the database cannot express (`has_many :through`,
53
- polymorphic, ...).
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`. Adding another database engine means adding one
204
- parser that produces that shape `parse_postgres()` (which reuses the MySQL
205
- adapter's IR builder wholesale) is the working example of the pattern.
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
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](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 PostgreSQL database, with a single-file,
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
- The database is the source of truth (tables, columns, comments, indexes, real foreign
17
- keys). Application code (Rails / Prisma / Django) can optionally be layered on top to
18
- add association semantics the database cannot express (`has_many :through`,
19
- polymorphic, ...).
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`. Adding another database engine means adding one
170
- parser that produces that shape `parse_postgres()` (which reuses the MySQL
171
- adapter's IR builder wholesale) is the working example of the pattern.
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