django-orm-lens 1.0.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,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-orm-lens
3
+ Version: 1.0.0
4
+ Summary: Static analysis + MCP server for Django models. Sidebar tree, ER diagrams, and JSON output for terminals and AI coding agents.
5
+ Author: frowningdev
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/FROWNINGdev/django-orm-lens
8
+ Project-URL: Repository, https://github.com/FROWNINGdev/django-orm-lens
9
+ Project-URL: Bug Tracker, https://github.com/FROWNINGdev/django-orm-lens/issues
10
+ Project-URL: Changelog, https://github.com/FROWNINGdev/django-orm-lens/blob/main/CHANGELOG.md
11
+ Project-URL: VS Code Marketplace, https://marketplace.visualstudio.com/items?itemName=frowningdev.django-orm-lens
12
+ Keywords: django,orm,models,er-diagram,mermaid,mcp,ai-agents,cursor,aider,code-analysis,static-analysis
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Console
15
+ Classifier: Framework :: Django
16
+ Classifier: Framework :: Django :: 4.0
17
+ Classifier: Framework :: Django :: 4.1
18
+ Classifier: Framework :: Django :: 4.2
19
+ Classifier: Framework :: Django :: 5.0
20
+ Classifier: Framework :: Django :: 5.1
21
+ Classifier: Intended Audience :: Developers
22
+ Classifier: License :: OSI Approved :: MIT License
23
+ Classifier: Operating System :: OS Independent
24
+ Classifier: Programming Language :: Python
25
+ Classifier: Programming Language :: Python :: 3
26
+ Classifier: Programming Language :: Python :: 3 :: Only
27
+ Classifier: Programming Language :: Python :: 3.9
28
+ Classifier: Programming Language :: Python :: 3.10
29
+ Classifier: Programming Language :: Python :: 3.11
30
+ Classifier: Programming Language :: Python :: 3.12
31
+ Classifier: Programming Language :: Python :: 3.13
32
+ Classifier: Topic :: Software Development :: Documentation
33
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
34
+ Classifier: Topic :: Utilities
35
+ Classifier: Typing :: Typed
36
+ Requires-Python: >=3.9
37
+ Description-Content-Type: text/markdown
38
+ Provides-Extra: mcp
39
+ Requires-Dist: mcp>=1.0; extra == "mcp"
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=8; extra == "dev"
42
+ Requires-Dist: ruff>=0.5; extra == "dev"
43
+ Requires-Dist: mypy>=1.10; extra == "dev"
44
+ Requires-Dist: build>=1.2; extra == "dev"
45
+ Requires-Dist: twine>=5.1; extra == "dev"
46
+
47
+ # django-orm-lens
48
+
49
+ **Static analysis + MCP server for Django models.** Terminal- and AI-agent-friendly.
50
+
51
+ Ships with a zero-dependency parser, a JSON/Markdown/table CLI, and an optional
52
+ MCP (Model Context Protocol) server so any AI coding agent — Cursor, Aider,
53
+ Continue, and any other MCP client — can navigate your Django schema without
54
+ importing Django or spinning up your app.
55
+
56
+ Companion to the [Django ORM Lens VS Code
57
+ extension](https://marketplace.visualstudio.com/items?itemName=frowningdev.django-orm-lens).
58
+
59
+ ---
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ # Core CLI (zero third-party deps)
65
+ pip install django-orm-lens
66
+
67
+ # With the MCP server (adds the `mcp` package)
68
+ pip install "django-orm-lens[mcp]"
69
+ ```
70
+
71
+ Requires Python **3.9+**. Works on Linux, macOS, and Windows.
72
+
73
+ ---
74
+
75
+ ## CLI usage
76
+
77
+ ```bash
78
+ # Scan a Django project for models (JSON, Markdown, or table)
79
+ django-orm-lens scan -f json
80
+ django-orm-lens scan -f markdown
81
+ django-orm-lens scan -f table
82
+
83
+ # Describe one model
84
+ django-orm-lens describe blog.Post
85
+ django-orm-lens describe Post -f json
86
+
87
+ # Compact hover card (great for pipeing into your editor)
88
+ django-orm-lens hover blog.Post
89
+
90
+ # Flat list — pipes into fzf, grep, etc.
91
+ django-orm-lens list | fzf
92
+
93
+ # Emit a Mermaid ER diagram
94
+ django-orm-lens er > schema.mmd
95
+ django-orm-lens er -o schema.mmd
96
+ ```
97
+
98
+ Every command accepts `--path <dir>` and repeatable `--exclude <glob>`. Defaults
99
+ skip `migrations/`, `venv/`, `.venv/`, `env/`, and `node_modules/`.
100
+
101
+ ---
102
+
103
+ ## MCP server — for AI coding agents
104
+
105
+ The MCP server exposes five read-only tools that any MCP-compatible agent can
106
+ call while it edits your Django project:
107
+
108
+ | Tool | Purpose |
109
+ | --- | --- |
110
+ | `list_apps` | Every Django app in the workspace with model counts |
111
+ | `list_models` | Flat `app.Model` list, optional app filter |
112
+ | `describe_model` | Full field / relation / Meta detail for one model |
113
+ | `find_relations` | Inbound + outbound relations for one model |
114
+ | `er_diagram` | Mermaid `erDiagram` string for the whole workspace |
115
+
116
+ ### Start the server manually
117
+
118
+ ```bash
119
+ django-orm-lens-mcp # dedicated entry point
120
+ # or
121
+ django-orm-lens mcp # subcommand
122
+ ```
123
+
124
+ By default the server scans the current working directory. Override with
125
+ `DJANGO_ORM_LENS_ROOT=/abs/path/to/project`.
126
+
127
+ ### Register it with an agent
128
+
129
+ **Cursor** — add to `~/.cursor/mcp.json`:
130
+
131
+ ```json
132
+ {
133
+ "mcpServers": {
134
+ "django-orm-lens": {
135
+ "command": "django-orm-lens-mcp",
136
+ "env": { "DJANGO_ORM_LENS_ROOT": "/abs/path/to/your/project" }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ **Aider / Continue.dev / Zed / any MCP client** — same shape, the tool is
143
+ generic. Point `command` at the installed `django-orm-lens-mcp` binary and set
144
+ the workspace root via env.
145
+
146
+ ---
147
+
148
+ ## Why?
149
+
150
+ Django's ORM is Python, and Python is dynamic. AI agents that only see
151
+ `models.py` as raw text miss:
152
+
153
+ * which fields belong to which model;
154
+ * the direction and cardinality of every relation;
155
+ * what `Meta.ordering`, `unique_together`, and `constraints` actually contain;
156
+ * which app owns which model when the project uses split `models/` packages.
157
+
158
+ `django-orm-lens` gives them a **static, deterministic, JSON view** of the
159
+ schema — no Django boot, no database, no side effects. And you get a nice CLI
160
+ for humans too.
161
+
162
+ ---
163
+
164
+ ## Programmatic API
165
+
166
+ ```python
167
+ from django_orm_lens import scan_workspace
168
+
169
+ index = scan_workspace(".")
170
+ for app in index.apps:
171
+ for model in app.models:
172
+ print(f"{app.name}.{model.name} — {len(model.fields)} fields")
173
+
174
+ # The full parsed tree serialises to the same JSON schema the VS Code
175
+ # extension emits, so tools can share it interchangeably.
176
+ import json
177
+ json.dumps(index.to_dict(), indent=2)
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Links
183
+
184
+ * Repo: <https://github.com/FROWNINGdev/django-orm-lens>
185
+ * Issues: <https://github.com/FROWNINGdev/django-orm-lens/issues>
186
+ * Changelog: <https://github.com/FROWNINGdev/django-orm-lens/blob/main/CHANGELOG.md>
187
+ * VS Code extension: <https://marketplace.visualstudio.com/items?itemName=frowningdev.django-orm-lens>
188
+
189
+ MIT licensed.
@@ -0,0 +1,143 @@
1
+ # django-orm-lens
2
+
3
+ **Static analysis + MCP server for Django models.** Terminal- and AI-agent-friendly.
4
+
5
+ Ships with a zero-dependency parser, a JSON/Markdown/table CLI, and an optional
6
+ MCP (Model Context Protocol) server so any AI coding agent — Cursor, Aider,
7
+ Continue, and any other MCP client — can navigate your Django schema without
8
+ importing Django or spinning up your app.
9
+
10
+ Companion to the [Django ORM Lens VS Code
11
+ extension](https://marketplace.visualstudio.com/items?itemName=frowningdev.django-orm-lens).
12
+
13
+ ---
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ # Core CLI (zero third-party deps)
19
+ pip install django-orm-lens
20
+
21
+ # With the MCP server (adds the `mcp` package)
22
+ pip install "django-orm-lens[mcp]"
23
+ ```
24
+
25
+ Requires Python **3.9+**. Works on Linux, macOS, and Windows.
26
+
27
+ ---
28
+
29
+ ## CLI usage
30
+
31
+ ```bash
32
+ # Scan a Django project for models (JSON, Markdown, or table)
33
+ django-orm-lens scan -f json
34
+ django-orm-lens scan -f markdown
35
+ django-orm-lens scan -f table
36
+
37
+ # Describe one model
38
+ django-orm-lens describe blog.Post
39
+ django-orm-lens describe Post -f json
40
+
41
+ # Compact hover card (great for pipeing into your editor)
42
+ django-orm-lens hover blog.Post
43
+
44
+ # Flat list — pipes into fzf, grep, etc.
45
+ django-orm-lens list | fzf
46
+
47
+ # Emit a Mermaid ER diagram
48
+ django-orm-lens er > schema.mmd
49
+ django-orm-lens er -o schema.mmd
50
+ ```
51
+
52
+ Every command accepts `--path <dir>` and repeatable `--exclude <glob>`. Defaults
53
+ skip `migrations/`, `venv/`, `.venv/`, `env/`, and `node_modules/`.
54
+
55
+ ---
56
+
57
+ ## MCP server — for AI coding agents
58
+
59
+ The MCP server exposes five read-only tools that any MCP-compatible agent can
60
+ call while it edits your Django project:
61
+
62
+ | Tool | Purpose |
63
+ | --- | --- |
64
+ | `list_apps` | Every Django app in the workspace with model counts |
65
+ | `list_models` | Flat `app.Model` list, optional app filter |
66
+ | `describe_model` | Full field / relation / Meta detail for one model |
67
+ | `find_relations` | Inbound + outbound relations for one model |
68
+ | `er_diagram` | Mermaid `erDiagram` string for the whole workspace |
69
+
70
+ ### Start the server manually
71
+
72
+ ```bash
73
+ django-orm-lens-mcp # dedicated entry point
74
+ # or
75
+ django-orm-lens mcp # subcommand
76
+ ```
77
+
78
+ By default the server scans the current working directory. Override with
79
+ `DJANGO_ORM_LENS_ROOT=/abs/path/to/project`.
80
+
81
+ ### Register it with an agent
82
+
83
+ **Cursor** — add to `~/.cursor/mcp.json`:
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "django-orm-lens": {
89
+ "command": "django-orm-lens-mcp",
90
+ "env": { "DJANGO_ORM_LENS_ROOT": "/abs/path/to/your/project" }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ **Aider / Continue.dev / Zed / any MCP client** — same shape, the tool is
97
+ generic. Point `command` at the installed `django-orm-lens-mcp` binary and set
98
+ the workspace root via env.
99
+
100
+ ---
101
+
102
+ ## Why?
103
+
104
+ Django's ORM is Python, and Python is dynamic. AI agents that only see
105
+ `models.py` as raw text miss:
106
+
107
+ * which fields belong to which model;
108
+ * the direction and cardinality of every relation;
109
+ * what `Meta.ordering`, `unique_together`, and `constraints` actually contain;
110
+ * which app owns which model when the project uses split `models/` packages.
111
+
112
+ `django-orm-lens` gives them a **static, deterministic, JSON view** of the
113
+ schema — no Django boot, no database, no side effects. And you get a nice CLI
114
+ for humans too.
115
+
116
+ ---
117
+
118
+ ## Programmatic API
119
+
120
+ ```python
121
+ from django_orm_lens import scan_workspace
122
+
123
+ index = scan_workspace(".")
124
+ for app in index.apps:
125
+ for model in app.models:
126
+ print(f"{app.name}.{model.name} — {len(model.fields)} fields")
127
+
128
+ # The full parsed tree serialises to the same JSON schema the VS Code
129
+ # extension emits, so tools can share it interchangeably.
130
+ import json
131
+ json.dumps(index.to_dict(), indent=2)
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Links
137
+
138
+ * Repo: <https://github.com/FROWNINGdev/django-orm-lens>
139
+ * Issues: <https://github.com/FROWNINGdev/django-orm-lens/issues>
140
+ * Changelog: <https://github.com/FROWNINGdev/django-orm-lens/blob/main/CHANGELOG.md>
141
+ * VS Code extension: <https://marketplace.visualstudio.com/items?itemName=frowningdev.django-orm-lens>
142
+
143
+ MIT licensed.
@@ -0,0 +1,20 @@
1
+ """Django ORM Lens — sidebar tree, ER diagrams, and JSON for Django models.
2
+
3
+ Terminal / editor-agnostic parser for Django models.py files.
4
+ Ships a CLI and an optional MCP server for AI coding agents.
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+
9
+ from .models import ParsedApp, ParsedField, ParsedModel, WorkspaceIndex
10
+ from .parser import parse_models_file, scan_workspace
11
+
12
+ __all__ = [
13
+ "ParsedApp",
14
+ "ParsedField",
15
+ "ParsedModel",
16
+ "WorkspaceIndex",
17
+ "parse_models_file",
18
+ "scan_workspace",
19
+ "__version__",
20
+ ]
@@ -0,0 +1,238 @@
1
+ """Command-line entry point for ``django-orm-lens``.
2
+
3
+ Zero-dep argparse CLI. Commands mirror what the VS Code extension does:
4
+
5
+ django-orm-lens scan — walk workspace, list every model
6
+ django-orm-lens describe — one model in detail
7
+ django-orm-lens er — Mermaid ER diagram (stdout or file)
8
+ django-orm-lens hover — compact hover card for one model
9
+ django-orm-lens list — flat list ``app.Model`` for shell piping
10
+ django-orm-lens mcp — start the MCP stdio server (extras required)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+ from typing import List, Optional, Sequence
18
+
19
+ from . import __version__
20
+ from .formatters import format_hover, format_index, format_model
21
+ from .models import ParsedModel, WorkspaceIndex
22
+ from .parser import DEFAULT_EXCLUDES, scan_workspace
23
+
24
+
25
+ def _add_scan_flags(p: argparse.ArgumentParser) -> None:
26
+ p.add_argument(
27
+ "--path",
28
+ "-p",
29
+ default=".",
30
+ help="Workspace root to scan (default: current directory)",
31
+ )
32
+ p.add_argument(
33
+ "--exclude",
34
+ "-x",
35
+ action="append",
36
+ default=None,
37
+ help="Glob to exclude, repeatable (default: migrations/, venv/, node_modules/)",
38
+ )
39
+
40
+
41
+ def _resolve_excludes(cli_excludes: Optional[Sequence[str]]) -> Sequence[str]:
42
+ return tuple(cli_excludes) if cli_excludes else DEFAULT_EXCLUDES
43
+
44
+
45
+ def _find_model(index: WorkspaceIndex, ref: str) -> Optional[ParsedModel]:
46
+ if "." in ref:
47
+ app_name, model_name = ref.split(".", 1)
48
+ for app in index.apps:
49
+ if app.name == app_name:
50
+ for m in app.models:
51
+ if m.name == model_name:
52
+ return m
53
+ for app in index.apps:
54
+ for m in app.models:
55
+ if m.name == ref:
56
+ return m
57
+ return None
58
+
59
+
60
+ def _cmd_scan(args: argparse.Namespace) -> int:
61
+ idx = scan_workspace(args.path, _resolve_excludes(args.exclude))
62
+ print(format_index(idx, args.format))
63
+ return 0
64
+
65
+
66
+ def _cmd_describe(args: argparse.Namespace) -> int:
67
+ idx = scan_workspace(args.path, _resolve_excludes(args.exclude))
68
+ model = _find_model(idx, args.model)
69
+ if model is None:
70
+ print(f"error: model {args.model!r} not found", file=sys.stderr)
71
+ return 2
72
+ print(format_model(model, args.format))
73
+ return 0
74
+
75
+
76
+ def _cmd_hover(args: argparse.Namespace) -> int:
77
+ idx = scan_workspace(args.path, _resolve_excludes(args.exclude))
78
+ model = _find_model(idx, args.model)
79
+ if model is None:
80
+ print(f"error: model {args.model!r} not found", file=sys.stderr)
81
+ return 2
82
+ print(format_hover(model))
83
+ return 0
84
+
85
+
86
+ def _cmd_list(args: argparse.Namespace) -> int:
87
+ idx = scan_workspace(args.path, _resolve_excludes(args.exclude))
88
+ for app in idx.apps:
89
+ for m in app.models:
90
+ print(f"{app.name}.{m.name}")
91
+ return 0
92
+
93
+
94
+ def _cmd_er(args: argparse.Namespace) -> int:
95
+ idx = scan_workspace(args.path, _resolve_excludes(args.exclude))
96
+ mermaid = _build_mermaid(idx)
97
+ if args.output:
98
+ with open(args.output, "w", encoding="utf-8") as fh:
99
+ fh.write(mermaid)
100
+ print(f"Wrote {args.output}", file=sys.stderr)
101
+ else:
102
+ print(mermaid)
103
+ return 0
104
+
105
+
106
+ def _build_mermaid(index: WorkspaceIndex) -> str:
107
+ lines: List[str] = ["erDiagram"]
108
+ model_names = {m.name for app in index.apps for m in app.models}
109
+
110
+ def safe(s: str) -> str:
111
+ return "".join(ch if (ch.isalnum() or ch == "_") else "_" for ch in s)
112
+
113
+ for app in index.apps:
114
+ for model in app.models:
115
+ lines.append(f" {safe(model.name)} {{")
116
+ for f in model.fields:
117
+ if f.is_relation:
118
+ continue
119
+ type_safe = "".join(ch for ch in f.type if ch.isalnum())
120
+ name_safe = safe(f.name)
121
+ lines.append(f" {type_safe} {name_safe}")
122
+ lines.append(" }")
123
+
124
+ for app in index.apps:
125
+ for model in app.models:
126
+ for f in model.fields:
127
+ if not f.is_relation or not f.related_model:
128
+ continue
129
+ target = (
130
+ model.name
131
+ if f.related_model == "self"
132
+ else f.related_model.split(".")[-1]
133
+ )
134
+ if target not in model_names:
135
+ continue
136
+ if f.relation_kind == "ManyToManyField":
137
+ arrow = "}o--o{"
138
+ elif f.relation_kind == "OneToOneField":
139
+ arrow = "||--||"
140
+ else:
141
+ arrow = "}o--||"
142
+ lines.append(
143
+ f' {safe(model.name)} {arrow} {safe(target)} : "{f.name}"'
144
+ )
145
+ return "\n".join(lines)
146
+
147
+
148
+ def build_parser() -> argparse.ArgumentParser:
149
+ p = argparse.ArgumentParser(
150
+ prog="django-orm-lens",
151
+ description="Static analysis for Django models. Terminal + AI agent friendly.",
152
+ )
153
+ p.add_argument(
154
+ "--version", action="version", version=f"django-orm-lens {__version__}"
155
+ )
156
+ sub = p.add_subparsers(dest="command", required=True)
157
+
158
+ scan = sub.add_parser("scan", help="Scan a workspace for Django models")
159
+ _add_scan_flags(scan)
160
+ scan.add_argument(
161
+ "--format", "-f", choices=("json", "markdown", "table"), default="json"
162
+ )
163
+ scan.set_defaults(func=_cmd_scan)
164
+
165
+ describe = sub.add_parser(
166
+ "describe", help="Describe a single model (app.Model or Model)"
167
+ )
168
+ _add_scan_flags(describe)
169
+ describe.add_argument("model", help="Model reference, e.g. blog.Post or Post")
170
+ describe.add_argument(
171
+ "--format", "-f", choices=("json", "markdown", "table"), default="markdown"
172
+ )
173
+ describe.set_defaults(func=_cmd_describe)
174
+
175
+ hover = sub.add_parser("hover", help="Compact hover-card markdown for a model")
176
+ _add_scan_flags(hover)
177
+ hover.add_argument("model", help="Model reference, e.g. blog.Post or Post")
178
+ hover.set_defaults(func=_cmd_hover)
179
+
180
+ lst = sub.add_parser("list", help="Flat list of app.Model — pipes well into shell")
181
+ _add_scan_flags(lst)
182
+ lst.set_defaults(func=_cmd_list)
183
+
184
+ er = sub.add_parser("er", help="Emit a Mermaid ER diagram (stdout or file)")
185
+ _add_scan_flags(er)
186
+ er.add_argument(
187
+ "--output", "-o", default=None, help="Write diagram to file instead of stdout"
188
+ )
189
+ er.set_defaults(func=_cmd_er)
190
+
191
+ mcp = sub.add_parser(
192
+ "mcp",
193
+ help="Run the MCP stdio server (requires 'pip install django-orm-lens[mcp]')",
194
+ )
195
+ mcp.set_defaults(func=_cmd_mcp)
196
+
197
+ return p
198
+
199
+
200
+ def _cmd_mcp(_args: argparse.Namespace) -> int:
201
+ try:
202
+ from . import mcp_server
203
+ except ImportError as e:
204
+ print(
205
+ "error: MCP server dependencies not installed.\n"
206
+ "install with: pip install 'django-orm-lens[mcp]'\n"
207
+ f"({e})",
208
+ file=sys.stderr,
209
+ )
210
+ return 3
211
+ return mcp_server.main()
212
+
213
+
214
+ def _force_utf8_stdio() -> None:
215
+ for stream in (sys.stdout, sys.stderr):
216
+ reconfigure = getattr(stream, "reconfigure", None)
217
+ if reconfigure is not None:
218
+ try:
219
+ reconfigure(encoding="utf-8", errors="replace")
220
+ except (OSError, ValueError):
221
+ pass
222
+
223
+
224
+ def main(argv: Optional[Sequence[str]] = None) -> int:
225
+ _force_utf8_stdio()
226
+ parser = build_parser()
227
+ args = parser.parse_args(argv)
228
+ try:
229
+ return int(args.func(args))
230
+ except KeyboardInterrupt:
231
+ return 130
232
+ except Exception as e:
233
+ print(f"error: {e}", file=sys.stderr)
234
+ return 1
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(main())