erdscope 0.1.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.
@@ -0,0 +1,359 @@
1
+ Metadata-Version: 2.4
2
+ Name: erdscope
3
+ Version: 0.3.0
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
+ Author: tas6
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/orapli/erdscope
8
+ Project-URL: Documentation, https://orapli.github.io/erdscope/manual.html
9
+ Project-URL: Live demo, https://orapli.github.io/erdscope/
10
+ Project-URL: Repository, https://github.com/orapli/erdscope
11
+ Project-URL: Issues, https://github.com/orapli/erdscope/issues
12
+ Keywords: erd,er-diagram,database,schema,mysql,rails,prisma,django,documentation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Database
18
+ Classifier: Topic :: Documentation
19
+ Classifier: Topic :: Software Development :: Documentation
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: mysql
24
+ Requires-Dist: PyMySQL; extra == "mysql"
25
+ Provides-Extra: postgres
26
+ Requires-Dist: psycopg[binary]; extra == "postgres"
27
+ Provides-Extra: yaml
28
+ Requires-Dist: PyYAML; extra == "yaml"
29
+ Provides-Extra: all
30
+ Requires-Dist: PyMySQL; extra == "all"
31
+ Requires-Dist: psycopg[binary]; extra == "all"
32
+ Requires-Dist: PyYAML; extra == "all"
33
+ Dynamic: license-file
34
+
35
+ # erdscope
36
+
37
+ [![CI](https://github.com/orapli/erdscope/actions/workflows/ci.yml/badge.svg)](https://github.com/orapli/erdscope/actions/workflows/ci.yml)
38
+ [![PyPI](https://img.shields.io/pypi/v/erdscope)](https://pypi.org/project/erdscope/)
39
+
40
+ Generate a **self-contained, interactive ER diagram** — and an **Excel table-definition
41
+ workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
42
+ zero-dependency Python CLI.
43
+
44
+ ```bash
45
+ pip install erdscope
46
+ erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
47
+ erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
48
+ erdscope sqlite:///path/to/app.db -o erd.html
49
+ ```
50
+
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.
65
+
66
+ ## Demo
67
+
68
+ **[Try the live demo →](https://orapli.github.io/erdscope/)** — a small e-commerce
69
+ schema with comments, indexes, real FKs and an inferred relation. Everything below
70
+ is one self-contained HTML file.
71
+
72
+ [![erdscope demo](docs/screenshot.png)](https://orapli.github.io/erdscope/)
73
+
74
+ Regenerate it anytime with `python3 docs/gen_demo.py`.
75
+
76
+ **[Read the user manual →](https://orapli.github.io/erdscope/manual.html)** — installation,
77
+ CLI/config reference, a full viewer guide, and troubleshooting. ([日本語版 →](https://orapli.github.io/erdscope/manual.ja.html))
78
+
79
+ ## Install & usage
80
+
81
+ `pip install erdscope` (or `pipx install erdscope`) gives you the `erdscope` command.
82
+ Prefer not to install anything? `erd.py` is a single, dependency-free file — grab it
83
+ and run it with any Python 3.9+:
84
+
85
+ ```bash
86
+ curl -O https://raw.githubusercontent.com/orapli/erdscope/main/erd.py
87
+ python3 erd.py ... # identical to the erdscope command below
88
+ ```
89
+
90
+ ```bash
91
+ erdscope mysql://readonly@127.0.0.1:3306/myapp_production -o erd.html
92
+
93
+ # PostgreSQL: same thing (schema defaults to public; override with ?schema=name)
94
+ erdscope postgres://readonly@127.0.0.1:5432/myapp_production -o erd.html
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
+
99
+ # enrich with association semantics parsed from application code (optional)
100
+ erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
101
+ --models /path/to/rails/app -o erd.html
102
+
103
+ # also write a table-definition workbook
104
+ erdscope mysql://readonly@127.0.0.1:3306/myapp_production \
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
115
+ ```
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
+
121
+ Behind a bastion? Open an SSH tunnel first and point at localhost:
122
+
123
+ ```bash
124
+ ssh -N -L 3307:db-host:3306 bastion &
125
+ erdscope mysql://readonly@127.0.0.1:3307/myapp_production -o erd.html
126
+ ```
127
+
128
+ Use a read-only account, and leave the password out of the URL — if it's not in
129
+ `MYSQL_PWD` either, you'll be prompted for it (hidden input, never touches argv or
130
+ shell history). See [Dependencies](#dependencies) below for how the DB connection
131
+ itself is made.
132
+
133
+ ### Options
134
+
135
+ | Option | Description |
136
+ |---|---|
137
+ | `-o FILE` | Output HTML path (default: `erd.html`) |
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 |
139
+ | `--excel FILE.xlsx` | Also write a table-definition workbook: an overview sheet plus one sheet per table (columns, defaults, keys, comments, indexes, associations) |
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) |
141
+ | `--max-rows N` | Max column rows shown per table (default: 15; the rest scroll) |
142
+ | `--only 'user*,post*'` | Include only tables matching the glob pattern(s) |
143
+ | `--exclude '*_logs'` | Exclude tables matching the glob pattern(s) |
144
+ | `--infer-fk` | Guess relations from `*_id` column names when no real association/FK backs them (off by default — see below) |
145
+ | `--table-map 'Widget=crm_widgets'` | Rails only: override a model's table when static analysis can't determine it (e.g. `table_name` set inside a concern that lives in a gem, not the app). Repeatable; comma-separated lists accepted |
146
+ | `--config PATH` | Load defaults from a config file instead of repeating flags — see below. Auto-discovered as `.erdscope.json`/`.yml`/`.yaml` in the current directory if not given |
147
+ | `--no-config` | Skip config auto-discovery even if `.erdscope.*` exists in the cwd |
148
+
149
+ ## Config file
150
+
151
+ Once the flag list above gets long, put it in a config file instead — `.erdscope.json`
152
+ (or `.yml`/`.yaml` with PyYAML) next to where you run the tool is picked up automatically.
153
+ Most keys mirror a CLI option (an explicit flag always wins over the config); the DB
154
+ connection is config-only as `engine`/`host`/`port`/`user`/`database` — deliberately with
155
+ no password field — and `relations` manually declares relations no FK, code, or inference
156
+ can find. See the [Config file chapter of the manual](https://orapli.github.io/erdscope/manual.html#config-file) for the full key
157
+ list and semantics, and [`erdscope.example.yml`](erdscope.example.yml) for a fully
158
+ annotated sample based on the live demo's schema.
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
+
210
+ ## Dependencies
211
+
212
+ `erd.py` runs with **zero required dependencies** — everything below is optional, and
213
+ the tool degrades gracefully (falls back, or fails with a clear message) when a piece
214
+ is missing. If you installed via pip, extras pull them in for you:
215
+ `pip install 'erdscope[mysql]'` (PyMySQL), `'erdscope[postgres]'` (psycopg),
216
+ `'erdscope[yaml]'` (PyYAML), or `'erdscope[all]'`.
217
+
218
+ | Library | Used for | If not installed |
219
+ |---|---|---|
220
+ | [PyMySQL](https://pypi.org/project/PyMySQL/) | MySQL connections | Falls back to shelling out to the `mysql` CLI (must be on `PATH`) |
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 |
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 |
224
+
225
+ Excel output (`--excel`) needs none of these — it's written directly via the stdlib
226
+ `zipfile`/XML, not a spreadsheet library.
227
+
228
+ Test-only, and only if you run that particular suite:
229
+
230
+ | Library | Used for |
231
+ |---|---|
232
+ | [openpyxl](https://pypi.org/project/openpyxl/) | Roundtrip-verifying `--excel` output in the unit tests (`tests/test_erd.py`); that one test skips itself if it's missing |
233
+ | [Playwright](https://playwright.dev/python/) | The browser E2E suite (`tests/test_e2e.py`) — see [Tests](#tests) below |
234
+
235
+ ## What you get
236
+
237
+ Feature highlights — each link goes to the relevant [manual](https://orapli.github.io/erdscope/manual.html) chapter:
238
+
239
+ - **Database truth** — tables, columns (full SQL types, defaults, extras), table and
240
+ column comments, indexes, and real FK constraints, read from the database catalog
241
+ (`information_schema` on MySQL, `pg_catalog` on PostgreSQL)
242
+ - **Code semantics on top** — `--models` merges Rails / Prisma / Django associations;
243
+ declared, DB-FK, and inferred edges stay [visually distinct](https://orapli.github.io/erdscope/manual.html#viewer-edges)
244
+ - **[Interactive exploration](https://orapli.github.io/erdscope/manual.html#viewer-guide)** — focus with depth and dependency
245
+ direction, two-level hiding, table *and column* search (with regex/case toggles), a
246
+ non-filtering Highlight search that survives into exports, named views, share links
247
+ - **[Readable layouts](https://orapli.github.io/erdscope/manual.html#viewer-layout)** — viewport-aware packing with crossing
248
+ reduction, drag-to-snap with guide lines, multi-select align/distribute, Auto-tidy,
249
+ layout undo/redo
250
+ - **[Exports](https://orapli.github.io/erdscope/manual.html#exports)** — PNG (2x), SVG, Mermaid, and PlantUML, each with its own
251
+ copy and download buttons and image options, plus the Excel workbook
252
+ (customizable via `--excel-template`)
253
+ - **[Logical names](https://orapli.github.io/erdscope/manual.html#viewer-names)** — a table's DB comment doubles as a searchable
254
+ logical name (e.g. `users(Customer accounts)`), with independent display modes for
255
+ the live view and exports
256
+ - **Extras** — a built-in `?` shortcuts/help popup, dark mode, print stylesheet,
257
+ resizable/collapsible panes
258
+
259
+ ## Tests
260
+
261
+ ```bash
262
+ python3 -m unittest discover -s tests -v
263
+ ```
264
+
265
+ The IR builders and the Excel writer are covered by pure unit tests; the overlay
266
+ parsers have minimal fixtures under `tests/fixture_*`. No database is required to
267
+ run the tests.
268
+
269
+ `tests/test_e2e.py` drives the generated HTML's client-side JS (grid layout,
270
+ multi-select align/distribute, drag-to-snap, Auto-tidy) in a real headless browser. It's optional and skips
271
+ itself if not set up:
272
+
273
+ ```bash
274
+ pip install playwright && playwright install chromium
275
+ python3 -m unittest tests.test_e2e -v
276
+ ```
277
+
278
+ ## Extending
279
+
280
+ Everything downstream (UI, layouts, exports) consumes the intermediate representation
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.
356
+
357
+ ## License
358
+
359
+ MIT