django-joist 0.1.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.
Files changed (95) hide show
  1. django_joist-0.1.0/.github/workflows/release.yml +60 -0
  2. django_joist-0.1.0/.github/workflows/tests.yml +23 -0
  3. django_joist-0.1.0/.gitignore +7 -0
  4. django_joist-0.1.0/LICENSE +55 -0
  5. django_joist-0.1.0/PKG-INFO +230 -0
  6. django_joist-0.1.0/README.md +198 -0
  7. django_joist-0.1.0/pyproject.toml +60 -0
  8. django_joist-0.1.0/src/django_joist/__init__.py +54 -0
  9. django_joist-0.1.0/src/django_joist/apps.py +12 -0
  10. django_joist-0.1.0/src/django_joist/cache.py +153 -0
  11. django_joist-0.1.0/src/django_joist/cli.py +46 -0
  12. django_joist-0.1.0/src/django_joist/conf.py +213 -0
  13. django_joist-0.1.0/src/django_joist/diff/__init__.py +6 -0
  14. django_joist-0.1.0/src/django_joist/diff/baseline.py +113 -0
  15. django_joist-0.1.0/src/django_joist/diff/differ.py +114 -0
  16. django_joist-0.1.0/src/django_joist/doctor/__init__.py +26 -0
  17. django_joist-0.1.0/src/django_joist/doctor/collection.py +38 -0
  18. django_joist-0.1.0/src/django_joist/doctor/enums.py +44 -0
  19. django_joist-0.1.0/src/django_joist/doctor/finding.py +44 -0
  20. django_joist-0.1.0/src/django_joist/doctor/formatters.py +92 -0
  21. django_joist-0.1.0/src/django_joist/doctor/registry.py +98 -0
  22. django_joist-0.1.0/src/django_joist/doctor/report.py +158 -0
  23. django_joist-0.1.0/src/django_joist/doctor/rules/__init__.py +40 -0
  24. django_joist-0.1.0/src/django_joist/doctor/rules/base.py +54 -0
  25. django_joist-0.1.0/src/django_joist/doctor/rules/indexes.py +315 -0
  26. django_joist-0.1.0/src/django_joist/doctor/rules/integrity.py +404 -0
  27. django_joist-0.1.0/src/django_joist/doctor/rules/types.py +99 -0
  28. django_joist-0.1.0/src/django_joist/doctor/runner.py +53 -0
  29. django_joist-0.1.0/src/django_joist/export/__init__.py +10 -0
  30. django_joist-0.1.0/src/django_joist/export/annotator.py +105 -0
  31. django_joist-0.1.0/src/django_joist/export/builder.py +190 -0
  32. django_joist-0.1.0/src/django_joist/export/exporter.py +94 -0
  33. django_joist-0.1.0/src/django_joist/export/generators/__init__.py +1 -0
  34. django_joist-0.1.0/src/django_joist/export/generators/csv.py +66 -0
  35. django_joist-0.1.0/src/django_joist/export/generators/dbml.py +124 -0
  36. django_joist-0.1.0/src/django_joist/export/generators/json.py +25 -0
  37. django_joist-0.1.0/src/django_joist/export/generators/llm.py +89 -0
  38. django_joist-0.1.0/src/django_joist/export/generators/markdown.py +115 -0
  39. django_joist-0.1.0/src/django_joist/export/generators/mermaid.py +84 -0
  40. django_joist-0.1.0/src/django_joist/export/transforms.py +97 -0
  41. django_joist-0.1.0/src/django_joist/introspection/__init__.py +14 -0
  42. django_joist-0.1.0/src/django_joist/introspection/builder.py +227 -0
  43. django_joist-0.1.0/src/django_joist/introspection/data.py +45 -0
  44. django_joist-0.1.0/src/django_joist/introspection/fallback.py +140 -0
  45. django_joist-0.1.0/src/django_joist/introspection/native.py +248 -0
  46. django_joist-0.1.0/src/django_joist/introspection/serializer.py +62 -0
  47. django_joist-0.1.0/src/django_joist/management/__init__.py +0 -0
  48. django_joist-0.1.0/src/django_joist/management/commands/__init__.py +0 -0
  49. django_joist-0.1.0/src/django_joist/management/commands/joist_diff.py +148 -0
  50. django_joist-0.1.0/src/django_joist/management/commands/joist_doctor.py +110 -0
  51. django_joist-0.1.0/src/django_joist/management/commands/joist_export.py +159 -0
  52. django_joist-0.1.0/src/django_joist/management/commands/joist_open.py +56 -0
  53. django_joist-0.1.0/src/django_joist/management/commands/joist_rebuild.py +45 -0
  54. django_joist-0.1.0/src/django_joist/management/commands/joist_show.py +61 -0
  55. django_joist-0.1.0/src/django_joist/security.py +85 -0
  56. django_joist-0.1.0/src/django_joist/selection.py +37 -0
  57. django_joist-0.1.0/src/django_joist/signals.py +86 -0
  58. django_joist-0.1.0/src/django_joist/static/joist/css/joist.css +679 -0
  59. django_joist-0.1.0/src/django_joist/static/joist/fonts/IBM-Plex.LICENSE +93 -0
  60. django_joist-0.1.0/src/django_joist/static/joist/fonts/ibm-plex-mono-400.woff2 +0 -0
  61. django_joist-0.1.0/src/django_joist/static/joist/fonts/ibm-plex-mono-500.woff2 +0 -0
  62. django_joist-0.1.0/src/django_joist/static/joist/fonts/ibm-plex-mono-600.woff2 +0 -0
  63. django_joist-0.1.0/src/django_joist/static/joist/js/diff-view.js +78 -0
  64. django_joist-0.1.0/src/django_joist/static/joist/js/doctor-view.js +89 -0
  65. django_joist-0.1.0/src/django_joist/static/joist/js/export-menu.js +95 -0
  66. django_joist-0.1.0/src/django_joist/static/joist/js/export-request.js +38 -0
  67. django_joist-0.1.0/src/django_joist/static/joist/js/focus-combobox.js +0 -0
  68. django_joist-0.1.0/src/django_joist/static/joist/js/joist.js +1513 -0
  69. django_joist-0.1.0/src/django_joist/static/joist/js/label-face-gate.js +71 -0
  70. django_joist-0.1.0/src/django_joist/static/joist/js/mermaid-definition.js +145 -0
  71. django_joist-0.1.0/src/django_joist/static/joist/js/selection.js +128 -0
  72. django_joist-0.1.0/src/django_joist/static/joist/js/table-match.js +41 -0
  73. django_joist-0.1.0/src/django_joist/static/joist/js/type-labels.js +101 -0
  74. django_joist-0.1.0/src/django_joist/static/joist/js/url-state.js +36 -0
  75. django_joist-0.1.0/src/django_joist/static/joist/js/viewport.js +60 -0
  76. django_joist-0.1.0/src/django_joist/static/joist/vendor/mermaid.LICENSE +24 -0
  77. django_joist-0.1.0/src/django_joist/static/joist/vendor/mermaid.min.js +3587 -0
  78. django_joist-0.1.0/src/django_joist/templates/joist/index.html +153 -0
  79. django_joist-0.1.0/src/django_joist/theme.py +150 -0
  80. django_joist-0.1.0/src/django_joist/urls.py +27 -0
  81. django_joist-0.1.0/src/django_joist/views.py +232 -0
  82. django_joist-0.1.0/tests/__init__.py +0 -0
  83. django_joist-0.1.0/tests/conftest.py +74 -0
  84. django_joist-0.1.0/tests/settings.py +51 -0
  85. django_joist-0.1.0/tests/test_diff.py +168 -0
  86. django_joist-0.1.0/tests/test_doctor.py +525 -0
  87. django_joist-0.1.0/tests/test_export.py +471 -0
  88. django_joist-0.1.0/tests/test_facade.py +81 -0
  89. django_joist-0.1.0/tests/test_foundation.py +99 -0
  90. django_joist-0.1.0/tests/test_http.py +221 -0
  91. django_joist-0.1.0/tests/test_http_helpers.py +6 -0
  92. django_joist-0.1.0/tests/test_static.py +109 -0
  93. django_joist-0.1.0/tests/testapp/__init__.py +0 -0
  94. django_joist-0.1.0/tests/testapp/models.py +72 -0
  95. django_joist-0.1.0/tests/urls.py +3 -0
@@ -0,0 +1,60 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python: ["3.13", "3.14"]
18
+ django: ["~=5.2.0", "~=6.0.0", "~=6.1.0"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ - run: pip install "Django${{ matrix.django }}" pytest pytest-django
25
+ - run: pip install -e .
26
+ - run: python -m pytest -q
27
+
28
+ build:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.14"
35
+ - name: Check tag matches package version
36
+ run: >-
37
+ python -c "import os, tomllib;
38
+ version = tomllib.load(open('pyproject.toml', 'rb'))['project']['version'];
39
+ assert os.environ['GITHUB_REF_NAME'] == f'v{version}',
40
+ f\"tag {os.environ['GITHUB_REF_NAME']} does not match version {version}\""
41
+ - run: python -m pip install build twine
42
+ - run: python -m build
43
+ - run: python -m twine check dist/*
44
+ - uses: actions/upload-artifact@v4
45
+ with:
46
+ name: python-package-distributions
47
+ path: dist/
48
+
49
+ publish:
50
+ needs: [test, build]
51
+ runs-on: ubuntu-latest
52
+ environment: pypi
53
+ permissions:
54
+ id-token: write
55
+ steps:
56
+ - uses: actions/download-artifact@v4
57
+ with:
58
+ name: python-package-distributions
59
+ path: dist/
60
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python: ["3.13", "3.14"]
15
+ django: ["~=5.2.0", "~=6.0.0", "~=6.1.0"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - run: pip install "Django${{ matrix.django }}" pytest pytest-django
22
+ - run: pip install -e .
23
+ - run: python -m pytest -q
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ /var/
7
+ .pytest_cache/
@@ -0,0 +1,55 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simone Fornasiere
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Portions of the front end (src/django_joist/static/joist/) are adapted from
26
+ Laravel Truss (https://github.com/albertoarena/laravel-truss), which is
27
+ distributed under the MIT License:
28
+
29
+ MIT License
30
+
31
+ Copyright (c) 2026 Alberto Arena
32
+
33
+ Permission is hereby granted, free of charge, to any person obtaining a copy
34
+ of this software and associated documentation files (the "Software"), to deal
35
+ in the Software without restriction, including without limitation the rights
36
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
37
+ copies of the Software, and to permit persons to whom the Software is
38
+ furnished to do so, subject to the following conditions:
39
+
40
+ The above copyright notice and this permission notice shall be included in all
41
+ copies or substantial portions of the Software.
42
+
43
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
44
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
45
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
46
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
47
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
48
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
49
+ SOFTWARE.
50
+
51
+ ---
52
+
53
+ The vendored Mermaid library and the IBM Plex Mono font carry their own
54
+ licenses, reproduced under src/django_joist/static/joist/ (mermaid.LICENSE and
55
+ IBM-Plex.LICENSE).
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-joist
3
+ Version: 0.1.0
4
+ Summary: A live database structure viewer for Django. Renders your schema (never data) as a zoomable ER diagram, with exports, a schema doctor and a schema diff.
5
+ Project-URL: Homepage, https://github.com/sforna/django-joist
6
+ Project-URL: Issues, https://github.com/sforna/django-joist/issues
7
+ Author-email: Simone Fornasiere <s.fornasiere@gmail.com>
8
+ License-Expression: MIT AND OFL-1.1
9
+ License-File: LICENSE
10
+ License-File: src/django_joist/static/joist/fonts/IBM-Plex.LICENSE
11
+ License-File: src/django_joist/static/joist/vendor/mermaid.LICENSE
12
+ Keywords: data-dictionary,database,database-diagram,dbml,django,er-diagram,erd,introspection,mermaid,schema
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 5.2
17
+ Classifier: Framework :: Django :: 6.0
18
+ Classifier: Framework :: Django :: 6.1
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Database
25
+ Classifier: Topic :: Software Development :: Documentation
26
+ Requires-Python: >=3.13
27
+ Requires-Dist: django>=5.2
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: pytest-django; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # django-joist
34
+
35
+ **A live database structure viewer for Django.** Joist introspects your live
36
+ schema and renders it as a scrollable, zoomable ER diagram right inside your
37
+ app, so you can see how tables actually connect without opening a DB client.
38
+ It reads **structure only** — tables, columns, keys, indexes, foreign keys;
39
+ row data is never queried or exposed.
40
+
41
+ > django-joist is a Django port of the concept implemented by
42
+ > [Laravel Truss](https://github.com/albertoarena/laravel-truss) (MIT,
43
+ > Alberto Arena), which inspired it. The name, branding and Django wiring are
44
+ > original; selected front-end code is adapted under MIT with attribution.
45
+
46
+ ## Features
47
+
48
+ - Live ER diagram of your database, rendered with Mermaid — self-hosted, no
49
+ CDN, no build step.
50
+ - Focus mode: a table and its foreign-key neighbours, with a searchable
51
+ picker that stays usable on schemas with hundreds of tables.
52
+ - Filter by table name; toggle native DB types against Django-style labels.
53
+ - Map-style pan and zoom, auto-fit with a legible floor, plus a Fit button.
54
+ - Exports: PNG/SVG from the browser; DBML, JSON, CSV, Markdown data
55
+ dictionary, Mermaid, or a token-trimmed `llm` format — from the dashboard
56
+ or from CI with `manage.py joist_export`. Deterministic: the same schema
57
+ always produces the same bytes, so `joist_export --check` fails the build
58
+ when a committed schema file drifts.
59
+ - Feed your real structure to a coding agent as grounding context:
60
+ annotations for business meaning, `--compact` to trim tokens, `--focus` to
61
+ narrow to one neighbourhood. Only the schema is read — row data is never touched.
62
+ - Schema diff: what changed since the last migration, in the dashboard's
63
+ Changes panel and via `manage.py joist_diff`.
64
+ - Schema doctor: `manage.py joist_doctor` reviews the structure for problems
65
+ (missing primary keys, unindexed foreign keys, duplicate indexes, risky
66
+ types) — deterministic, structure-only, safe in CI, with `--fail-on` exit
67
+ codes; the same findings power the dashboard's Health panel.
68
+ - Multiple databases: list aliases under `JOIST["connections"]` and switch
69
+ diagrams from the toolbar, each scoped to its own database.
70
+ - Light/dark "blueprint" theme, or bring your own: a handful of semantic
71
+ colour/font knobs re-skin the whole dashboard, served as a same-origin
72
+ stylesheet (CSP-safe, no inline styles).
73
+ - Rebuilds itself: the cached snapshot refreshes after every `migrate` and
74
+ degrades gracefully when the cache store is unreachable.
75
+ - Production-safe by design: routes answer 404 unless enabled, denials never
76
+ confirm the dashboard exists, and excluded tables are filtered server-side
77
+ so their structure never reaches the browser.
78
+
79
+ ## Installation
80
+
81
+ ```bash
82
+ pip install django-joist
83
+ ```
84
+
85
+ Add the app and mount the dashboard:
86
+
87
+ ```python
88
+ # settings.py
89
+ INSTALLED_APPS = [
90
+ ...,
91
+ "django_joist",
92
+ ]
93
+
94
+ # urls.py
95
+ from django.urls import include, path
96
+
97
+ urlpatterns = [
98
+ ...,
99
+ path("joist/", include("django_joist.urls")),
100
+ ]
101
+ ```
102
+
103
+ Requires Python 3.13+ and Django 5.2+. No migrations, no models, no
104
+ `collectstatic` needed (assets are served by the package, gated with the
105
+ rest of the dashboard). The dashboard HTML is a template shipped inside the
106
+ app, so your `TEMPLATES` must keep `"APP_DIRS": True` for `django_joist`
107
+ (the default in `django-admin startproject`); if you disabled it, add the
108
+ package's template directory to `DIRS`.
109
+
110
+ ## Quick start
111
+
112
+ By default Joist is enabled when `DEBUG = True`. Start your dev server and
113
+ visit `/joist/`.
114
+
115
+ To run Joist gated on staging or production, set both switches:
116
+
117
+ ```python
118
+ JOIST = {
119
+ "enabled": True,
120
+ "authorization": {
121
+ "allowed_emails": ["ada@example.com", "grace@example.com"],
122
+ },
123
+ }
124
+ ```
125
+
126
+ Outside `DEBUG` the shipped default authorizer admits only allow-listed
127
+ emails and **fails closed** on an empty list. To authorize by role instead,
128
+ point `authorization.callable` at your own `request -> bool` (a callable or a
129
+ dotted path); it fully replaces the default. Denials always return **404**.
130
+
131
+ > Your project's `AuthenticationMiddleware` must run for the mounted URLs
132
+ > (the normal case). Joist's own guard is part of the views and cannot be
133
+ > configured away.
134
+
135
+ ## Command line
136
+
137
+ ```bash
138
+ python manage.py joist_show # the structure as a terminal table
139
+ python manage.py joist_open # print/open the dashboard URL
140
+ python manage.py joist_rebuild # rebuild the cached snapshot (CI)
141
+ python manage.py joist_diff # what changed since the last migration
142
+ python manage.py joist_doctor # structure review; exits non-zero on findings
143
+ python manage.py joist_export # DBML to stdout by default
144
+ python manage.py joist_export --format=json --output=docs/schema.json
145
+ python manage.py joist_export --format=dbml --output=docs/schema.dbml --check # CI drift check
146
+ python manage.py joist_export --tables=orders,order_lines --focus=orders --depth=1 --compact
147
+ ```
148
+
149
+ `joist_doctor --format=json` and `--fail-on=warning` make it a CI gate;
150
+ `joist_export --check` regenerates, compares, writes nothing and exits `1` on
151
+ drift (`2` on usage errors), so a migration that changes the schema without
152
+ refreshing the committed file fails the build.
153
+
154
+ ## As AI context
155
+
156
+ The same export doubles as grounding context for a coding agent, so it stops
157
+ inventing columns:
158
+
159
+ ```bash
160
+ python manage.py joist_export --format=llm # dense, token-trimmed plaintext
161
+ python manage.py joist_export --compact # drop defaults and non-unique indexes
162
+ python manage.py joist_export --focus=orders --depth=1
163
+ ```
164
+
165
+ Annotations add the business meaning a type cannot — declare them in
166
+ settings under `JOIST["annotations"]` (per-table, per-column, global notes)
167
+ or let Joist read native database comments (Postgres/MySQL) by keeping
168
+ `"database"` in `annotations["source"]`. The `llm` format and Markdown/DBML
169
+ all render annotations; `--no-annotations` strips them.
170
+
171
+ Programmatically, the builder is immutable and mirrors the CLI byte-for-byte:
172
+
173
+ ```python
174
+ from django_joist import snapshot
175
+
176
+ text = (
177
+ snapshot()
178
+ .only(["orders", "order_lines"])
179
+ .focus("orders", depth=1)
180
+ .compact()
181
+ .to_dbml()
182
+ )
183
+ ```
184
+
185
+ ## Configuration
186
+
187
+ Everything lives in one `JOIST` dict; unset keys keep their defaults (nested
188
+ dicts merge key-by-key). The common knobs:
189
+
190
+ | Key | Purpose |
191
+ |---|---|
192
+ | `enabled` | Master switch; defaults to `DEBUG`. Off = every route 404s |
193
+ | `connections` | Visualizable database aliases + per-alias overrides, e.g. `{"analytics": {"excluded_tables": [...]}}`; empty = just `default` |
194
+ | `excluded_tables` | Hidden server-side (never sent to the browser). Django bookkeeping tables are pre-listed |
195
+ | `authorization.callable` / `.allowed_emails` | Who may view outside `DEBUG`; a custom callable replaces the email allow-list |
196
+ | `cache.ttl` / `cache.alias` | Snapshot cache (seconds, `<= 0` = forever) and which `CACHES` entry to use |
197
+ | `diagram.type_labels` | `native` (default) or `django` short labels |
198
+ | `diagram.mermaid_url` | Point at your own copy/CDN; default self-hosts from the gated asset route |
199
+ | `focus.default_depth` | FK-neighbour hops shown on focus |
200
+ | `large_schema.warn_above` | Table count that triggers the "use focus/filter" hint |
201
+ | `diff.enabled` / `diff.dir` | Schema-diff switch and where the structure-only baseline lives (default `BASE_DIR/var/joist`) |
202
+ | `doctor.*` | Preset, per-rule severity/enable, ignore patterns, `fail_on`, dashboard panel switches |
203
+ | `annotations.*` | Global notes, per-table/per-column meaning, comment sources |
204
+ | `theme.*` | Semantic colour/font knobs for both light and dark |
205
+ | `base_url` | Used by `joist_open` to build a browsable URL |
206
+
207
+ The baseline file under `diff.dir` is the only thing Joist writes to disk;
208
+ it is derived and safe to delete — worth gitignoring alongside `var/`.
209
+
210
+ ## The no-data promise
211
+
212
+ Structure is the `CREATE TABLE` definition: tables, columns (name, native
213
+ type, nullability, defaults), primary keys, indexes, foreign keys with
214
+ referential actions, and native comments. Row contents are never queried —
215
+ not by the dashboard, the API, the CLI, or the doctor. Config
216
+ `excluded_tables` are stripped server-side, so an excluded table never
217
+ reaches the client, the diff, or an export.
218
+
219
+ ## Development
220
+
221
+ ```bash
222
+ pip install -e ".[dev]"
223
+ python -m pytest
224
+ ```
225
+
226
+ ## License
227
+
228
+ MIT. See [LICENSE](LICENSE) (includes the attribution to Laravel Truss).
229
+ Vendored assets carry their own licenses (Mermaid MIT, IBM Plex Mono SIL OFL)
230
+ under `src/django_joist/static/joist/`.
@@ -0,0 +1,198 @@
1
+ # django-joist
2
+
3
+ **A live database structure viewer for Django.** Joist introspects your live
4
+ schema and renders it as a scrollable, zoomable ER diagram right inside your
5
+ app, so you can see how tables actually connect without opening a DB client.
6
+ It reads **structure only** — tables, columns, keys, indexes, foreign keys;
7
+ row data is never queried or exposed.
8
+
9
+ > django-joist is a Django port of the concept implemented by
10
+ > [Laravel Truss](https://github.com/albertoarena/laravel-truss) (MIT,
11
+ > Alberto Arena), which inspired it. The name, branding and Django wiring are
12
+ > original; selected front-end code is adapted under MIT with attribution.
13
+
14
+ ## Features
15
+
16
+ - Live ER diagram of your database, rendered with Mermaid — self-hosted, no
17
+ CDN, no build step.
18
+ - Focus mode: a table and its foreign-key neighbours, with a searchable
19
+ picker that stays usable on schemas with hundreds of tables.
20
+ - Filter by table name; toggle native DB types against Django-style labels.
21
+ - Map-style pan and zoom, auto-fit with a legible floor, plus a Fit button.
22
+ - Exports: PNG/SVG from the browser; DBML, JSON, CSV, Markdown data
23
+ dictionary, Mermaid, or a token-trimmed `llm` format — from the dashboard
24
+ or from CI with `manage.py joist_export`. Deterministic: the same schema
25
+ always produces the same bytes, so `joist_export --check` fails the build
26
+ when a committed schema file drifts.
27
+ - Feed your real structure to a coding agent as grounding context:
28
+ annotations for business meaning, `--compact` to trim tokens, `--focus` to
29
+ narrow to one neighbourhood. Only the schema is read — row data is never touched.
30
+ - Schema diff: what changed since the last migration, in the dashboard's
31
+ Changes panel and via `manage.py joist_diff`.
32
+ - Schema doctor: `manage.py joist_doctor` reviews the structure for problems
33
+ (missing primary keys, unindexed foreign keys, duplicate indexes, risky
34
+ types) — deterministic, structure-only, safe in CI, with `--fail-on` exit
35
+ codes; the same findings power the dashboard's Health panel.
36
+ - Multiple databases: list aliases under `JOIST["connections"]` and switch
37
+ diagrams from the toolbar, each scoped to its own database.
38
+ - Light/dark "blueprint" theme, or bring your own: a handful of semantic
39
+ colour/font knobs re-skin the whole dashboard, served as a same-origin
40
+ stylesheet (CSP-safe, no inline styles).
41
+ - Rebuilds itself: the cached snapshot refreshes after every `migrate` and
42
+ degrades gracefully when the cache store is unreachable.
43
+ - Production-safe by design: routes answer 404 unless enabled, denials never
44
+ confirm the dashboard exists, and excluded tables are filtered server-side
45
+ so their structure never reaches the browser.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install django-joist
51
+ ```
52
+
53
+ Add the app and mount the dashboard:
54
+
55
+ ```python
56
+ # settings.py
57
+ INSTALLED_APPS = [
58
+ ...,
59
+ "django_joist",
60
+ ]
61
+
62
+ # urls.py
63
+ from django.urls import include, path
64
+
65
+ urlpatterns = [
66
+ ...,
67
+ path("joist/", include("django_joist.urls")),
68
+ ]
69
+ ```
70
+
71
+ Requires Python 3.13+ and Django 5.2+. No migrations, no models, no
72
+ `collectstatic` needed (assets are served by the package, gated with the
73
+ rest of the dashboard). The dashboard HTML is a template shipped inside the
74
+ app, so your `TEMPLATES` must keep `"APP_DIRS": True` for `django_joist`
75
+ (the default in `django-admin startproject`); if you disabled it, add the
76
+ package's template directory to `DIRS`.
77
+
78
+ ## Quick start
79
+
80
+ By default Joist is enabled when `DEBUG = True`. Start your dev server and
81
+ visit `/joist/`.
82
+
83
+ To run Joist gated on staging or production, set both switches:
84
+
85
+ ```python
86
+ JOIST = {
87
+ "enabled": True,
88
+ "authorization": {
89
+ "allowed_emails": ["ada@example.com", "grace@example.com"],
90
+ },
91
+ }
92
+ ```
93
+
94
+ Outside `DEBUG` the shipped default authorizer admits only allow-listed
95
+ emails and **fails closed** on an empty list. To authorize by role instead,
96
+ point `authorization.callable` at your own `request -> bool` (a callable or a
97
+ dotted path); it fully replaces the default. Denials always return **404**.
98
+
99
+ > Your project's `AuthenticationMiddleware` must run for the mounted URLs
100
+ > (the normal case). Joist's own guard is part of the views and cannot be
101
+ > configured away.
102
+
103
+ ## Command line
104
+
105
+ ```bash
106
+ python manage.py joist_show # the structure as a terminal table
107
+ python manage.py joist_open # print/open the dashboard URL
108
+ python manage.py joist_rebuild # rebuild the cached snapshot (CI)
109
+ python manage.py joist_diff # what changed since the last migration
110
+ python manage.py joist_doctor # structure review; exits non-zero on findings
111
+ python manage.py joist_export # DBML to stdout by default
112
+ python manage.py joist_export --format=json --output=docs/schema.json
113
+ python manage.py joist_export --format=dbml --output=docs/schema.dbml --check # CI drift check
114
+ python manage.py joist_export --tables=orders,order_lines --focus=orders --depth=1 --compact
115
+ ```
116
+
117
+ `joist_doctor --format=json` and `--fail-on=warning` make it a CI gate;
118
+ `joist_export --check` regenerates, compares, writes nothing and exits `1` on
119
+ drift (`2` on usage errors), so a migration that changes the schema without
120
+ refreshing the committed file fails the build.
121
+
122
+ ## As AI context
123
+
124
+ The same export doubles as grounding context for a coding agent, so it stops
125
+ inventing columns:
126
+
127
+ ```bash
128
+ python manage.py joist_export --format=llm # dense, token-trimmed plaintext
129
+ python manage.py joist_export --compact # drop defaults and non-unique indexes
130
+ python manage.py joist_export --focus=orders --depth=1
131
+ ```
132
+
133
+ Annotations add the business meaning a type cannot — declare them in
134
+ settings under `JOIST["annotations"]` (per-table, per-column, global notes)
135
+ or let Joist read native database comments (Postgres/MySQL) by keeping
136
+ `"database"` in `annotations["source"]`. The `llm` format and Markdown/DBML
137
+ all render annotations; `--no-annotations` strips them.
138
+
139
+ Programmatically, the builder is immutable and mirrors the CLI byte-for-byte:
140
+
141
+ ```python
142
+ from django_joist import snapshot
143
+
144
+ text = (
145
+ snapshot()
146
+ .only(["orders", "order_lines"])
147
+ .focus("orders", depth=1)
148
+ .compact()
149
+ .to_dbml()
150
+ )
151
+ ```
152
+
153
+ ## Configuration
154
+
155
+ Everything lives in one `JOIST` dict; unset keys keep their defaults (nested
156
+ dicts merge key-by-key). The common knobs:
157
+
158
+ | Key | Purpose |
159
+ |---|---|
160
+ | `enabled` | Master switch; defaults to `DEBUG`. Off = every route 404s |
161
+ | `connections` | Visualizable database aliases + per-alias overrides, e.g. `{"analytics": {"excluded_tables": [...]}}`; empty = just `default` |
162
+ | `excluded_tables` | Hidden server-side (never sent to the browser). Django bookkeeping tables are pre-listed |
163
+ | `authorization.callable` / `.allowed_emails` | Who may view outside `DEBUG`; a custom callable replaces the email allow-list |
164
+ | `cache.ttl` / `cache.alias` | Snapshot cache (seconds, `<= 0` = forever) and which `CACHES` entry to use |
165
+ | `diagram.type_labels` | `native` (default) or `django` short labels |
166
+ | `diagram.mermaid_url` | Point at your own copy/CDN; default self-hosts from the gated asset route |
167
+ | `focus.default_depth` | FK-neighbour hops shown on focus |
168
+ | `large_schema.warn_above` | Table count that triggers the "use focus/filter" hint |
169
+ | `diff.enabled` / `diff.dir` | Schema-diff switch and where the structure-only baseline lives (default `BASE_DIR/var/joist`) |
170
+ | `doctor.*` | Preset, per-rule severity/enable, ignore patterns, `fail_on`, dashboard panel switches |
171
+ | `annotations.*` | Global notes, per-table/per-column meaning, comment sources |
172
+ | `theme.*` | Semantic colour/font knobs for both light and dark |
173
+ | `base_url` | Used by `joist_open` to build a browsable URL |
174
+
175
+ The baseline file under `diff.dir` is the only thing Joist writes to disk;
176
+ it is derived and safe to delete — worth gitignoring alongside `var/`.
177
+
178
+ ## The no-data promise
179
+
180
+ Structure is the `CREATE TABLE` definition: tables, columns (name, native
181
+ type, nullability, defaults), primary keys, indexes, foreign keys with
182
+ referential actions, and native comments. Row contents are never queried —
183
+ not by the dashboard, the API, the CLI, or the doctor. Config
184
+ `excluded_tables` are stripped server-side, so an excluded table never
185
+ reaches the client, the diff, or an export.
186
+
187
+ ## Development
188
+
189
+ ```bash
190
+ pip install -e ".[dev]"
191
+ python -m pytest
192
+ ```
193
+
194
+ ## License
195
+
196
+ MIT. See [LICENSE](LICENSE) (includes the attribution to Laravel Truss).
197
+ Vendored assets carry their own licenses (Mermaid MIT, IBM Plex Mono SIL OFL)
198
+ under `src/django_joist/static/joist/`.
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "django-joist"
7
+ version = "0.1.0"
8
+ description = "A live database structure viewer for Django. Renders your schema (never data) as a zoomable ER diagram, with exports, a schema doctor and a schema diff."
9
+ readme = "README.md"
10
+ license = "MIT AND OFL-1.1"
11
+ license-files = [
12
+ "LICENSE",
13
+ "src/django_joist/static/joist/vendor/mermaid.LICENSE",
14
+ "src/django_joist/static/joist/fonts/IBM-Plex.LICENSE",
15
+ ]
16
+ requires-python = ">=3.13"
17
+ authors = [{ name = "Simone Fornasiere", email = "s.fornasiere@gmail.com" }]
18
+ keywords = [
19
+ "django",
20
+ "database",
21
+ "schema",
22
+ "er-diagram",
23
+ "erd",
24
+ "database-diagram",
25
+ "dbml",
26
+ "data-dictionary",
27
+ "introspection",
28
+ "mermaid",
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 4 - Beta",
32
+ "Environment :: Web Environment",
33
+ "Framework :: Django",
34
+ "Framework :: Django :: 5.2",
35
+ "Framework :: Django :: 6.0",
36
+ "Framework :: Django :: 6.1",
37
+ "Intended Audience :: Developers",
38
+ "Operating System :: OS Independent",
39
+ "Programming Language :: Python :: 3",
40
+ "Programming Language :: Python :: 3.13",
41
+ "Programming Language :: Python :: 3.14",
42
+ "Topic :: Database",
43
+ "Topic :: Software Development :: Documentation",
44
+ ]
45
+ dependencies = ["Django>=5.2"]
46
+
47
+ [project.optional-dependencies]
48
+ dev = ["pytest", "pytest-django"]
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/sforna/django-joist"
52
+ Issues = "https://github.com/sforna/django-joist/issues"
53
+
54
+ [tool.hatch.build.targets.wheel]
55
+ packages = ["src/django_joist"]
56
+
57
+ [tool.pytest.ini_options]
58
+ DJANGO_SETTINGS_MODULE = "tests.settings"
59
+ pythonpath = ["src", "."]
60
+ testpaths = ["tests"]