django-conjure 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 (39) hide show
  1. django_conjure-0.1.0/.gitignore +46 -0
  2. django_conjure-0.1.0/LICENSE +21 -0
  3. django_conjure-0.1.0/PKG-INFO +126 -0
  4. django_conjure-0.1.0/README.md +85 -0
  5. django_conjure-0.1.0/conjure/__init__.py +19 -0
  6. django_conjure-0.1.0/conjure/admin_config.py +12 -0
  7. django_conjure-0.1.0/conjure/apps.py +18 -0
  8. django_conjure-0.1.0/conjure/audit.py +55 -0
  9. django_conjure-0.1.0/conjure/auth.py +123 -0
  10. django_conjure-0.1.0/conjure/conf.py +90 -0
  11. django_conjure-0.1.0/conjure/discovery.py +14 -0
  12. django_conjure-0.1.0/conjure/management/__init__.py +0 -0
  13. django_conjure-0.1.0/conjure/management/commands/__init__.py +0 -0
  14. django_conjure-0.1.0/conjure/management/commands/conjure_dump_schema.py +44 -0
  15. django_conjure-0.1.0/conjure/migrations/0001_initial.py +69 -0
  16. django_conjure-0.1.0/conjure/migrations/__init__.py +0 -0
  17. django_conjure-0.1.0/conjure/mixins.py +35 -0
  18. django_conjure-0.1.0/conjure/models.py +56 -0
  19. django_conjure-0.1.0/conjure/permissions.py +59 -0
  20. django_conjure-0.1.0/conjure/registry.py +130 -0
  21. django_conjure-0.1.0/conjure/schema.py +138 -0
  22. django_conjure-0.1.0/conjure/serializers.py +71 -0
  23. django_conjure-0.1.0/conjure/static/conjure/PLACEHOLDER.txt +3 -0
  24. django_conjure-0.1.0/conjure/urls.py +33 -0
  25. django_conjure-0.1.0/conjure/viewsets.py +344 -0
  26. django_conjure-0.1.0/conjure/widgets.py +81 -0
  27. django_conjure-0.1.0/pyproject.toml +78 -0
  28. django_conjure-0.1.0/tests/__init__.py +0 -0
  29. django_conjure-0.1.0/tests/conftest.py +11 -0
  30. django_conjure-0.1.0/tests/settings.py +35 -0
  31. django_conjure-0.1.0/tests/test_auth_widgets.py +93 -0
  32. django_conjure-0.1.0/tests/test_commands.py +31 -0
  33. django_conjure-0.1.0/tests/test_crud.py +87 -0
  34. django_conjure-0.1.0/tests/test_schema.py +42 -0
  35. django_conjure-0.1.0/tests/testapp/__init__.py +0 -0
  36. django_conjure-0.1.0/tests/testapp/admin_config.py +26 -0
  37. django_conjure-0.1.0/tests/testapp/apps.py +7 -0
  38. django_conjure-0.1.0/tests/testapp/models.py +45 -0
  39. django_conjure-0.1.0/tests/urls.py +5 -0
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ .eggs/
7
+ build/
8
+ dist/
9
+ .tox/
10
+ .nox/
11
+ .coverage
12
+ .coverage.*
13
+ htmlcov/
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .venv/
18
+ venv/
19
+ *.sqlite3
20
+ db.sqlite3
21
+
22
+ # Node
23
+ node_modules/
24
+ .pnpm-store/
25
+ *.tsbuildinfo
26
+ .vite/
27
+ .astro/
28
+
29
+ # Build outputs
30
+ apps/landing/dist/
31
+ apps/docs/site/
32
+ packages/web/dist/
33
+ packages/conjure/conjure/static/conjure/assets/
34
+
35
+ # Env
36
+ .env
37
+ .env.local
38
+ .env.*.local
39
+ !.env.example
40
+
41
+ # OS / editor
42
+ .DS_Store
43
+ .idea/
44
+ .vscode/*
45
+ !.vscode/extensions.json
46
+ *.swp
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Terrace Lab
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.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-conjure
3
+ Version: 0.1.0
4
+ Summary: Conjure your Django admin — read your models, summon a CRUD API and dashboard.
5
+ Project-URL: Homepage, https://conjure.terracelab.dev
6
+ Project-URL: Documentation, https://docs.conjure.terracelab.dev
7
+ Project-URL: Repository, https://github.com/terracelab/django-conjure
8
+ Project-URL: Changelog, https://github.com/terracelab/django-conjure/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/terracelab/django-conjure/issues
10
+ Author-email: Terrace Lab <hello@terracelab.dev>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: admin,codegen,crud,dashboard,django,introspection,rest
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: Django
17
+ Classifier: Framework :: Django :: 4.2
18
+ Classifier: Framework :: Django :: 5.0
19
+ Classifier: Framework :: Django :: 5.1
20
+ Classifier: Intended Audience :: Developers
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: django>=4.2
29
+ Requires-Dist: djangorestframework>=3.15
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2; extra == 'dev'
32
+ Requires-Dist: django>=4.2; extra == 'dev'
33
+ Requires-Dist: djangorestframework-simplejwt>=5.3; extra == 'dev'
34
+ Requires-Dist: djangorestframework>=3.15; extra == 'dev'
35
+ Requires-Dist: pytest-django>=4.8; extra == 'dev'
36
+ Requires-Dist: pytest>=8.0; extra == 'dev'
37
+ Requires-Dist: ruff>=0.6; extra == 'dev'
38
+ Provides-Extra: jwt
39
+ Requires-Dist: djangorestframework-simplejwt>=5.3; extra == 'jwt'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # django-conjure
43
+
44
+ > Conjure your Django admin — read your models, summon a CRUD API + schema.
45
+
46
+ The Python package behind [Conjure](https://conjure.terracelab.dev). It introspects your
47
+ models and serves a generic admin **REST API** (`/conjure/`) plus a schema endpoint that a
48
+ frontend — or codegen — can read. A matching React dashboard lives in the repo and is in
49
+ active development; for the `0.1.x` line, point any client at the API below.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install django-conjure # session auth (default)
55
+ pip install "django-conjure[jwt]" # add JWT auth mode
56
+ ```
57
+
58
+ ```python
59
+ # settings.py
60
+ INSTALLED_APPS += ["conjure"]
61
+
62
+ CONJURE = {
63
+ "AUTH": "session", # or "jwt"
64
+ "BRAND": {"name": "My Admin", "accent": "#4f46e5"},
65
+ # "USER_PAYLOAD": "myapp.hooks.payload", # custom user model? return any dict
66
+ }
67
+ ```
68
+
69
+ ```python
70
+ # urls.py
71
+ urlpatterns += [path("conjure/", include("conjure.urls"))]
72
+ ```
73
+
74
+ ```bash
75
+ python manage.py migrate conjure # AdminAuditLog table (audit log, optional but recommended)
76
+ ```
77
+
78
+ ## Register a model
79
+
80
+ Drop an `admin_config.py` into any app — it's discovered automatically, like `admin.py`:
81
+
82
+ ```python
83
+ from conjure import register, AdminConfig
84
+ from myapp.models import Product
85
+
86
+ @register(Product)
87
+ class ProductConfig(AdminConfig):
88
+ list_display = ["name", "price", "is_active"]
89
+ search_fields = ["name"]
90
+ list_filter = ["is_active"]
91
+ inlines = [(ProductImage, "product")] # inline child editing
92
+ is_readonly = False # True => log/history models, blocks writes
93
+ ```
94
+
95
+ Anything you leave unset is inferred from the model's fields.
96
+
97
+ ## What you get
98
+
99
+ - `GET /conjure/schema/` + `…/schema/{app.Model}/` — model introspection (codegen + runtime source)
100
+ - `GET/POST /conjure/r/{app.Model}/` and `…/{pk}/` — generic CRUD
101
+ - `…/autocomplete/`, `…/bulk/` (atomic inline ops), `…/{pk}/related/` (delete impact)
102
+ - Django-permission gating (`view/add/change/delete`, shared with Django admin), `is_staff` required
103
+ - Audit log with before/after diff, staff auth (session or JWT), dashboard widget registry
104
+
105
+ ## Extend without forking
106
+
107
+ ```python
108
+ from conjure import register_widget
109
+
110
+ @register_widget("signup-trend")
111
+ def signup_trend(request):
112
+ ... # return any JSON; served at /conjure/widgets/signup-trend/
113
+ ```
114
+
115
+ Full docs, the settings reference, the REST contract, and the extension SDK live at
116
+ **[docs.conjure.terracelab.dev](https://docs.conjure.terracelab.dev)**.
117
+
118
+ ## Develop
119
+
120
+ ```bash
121
+ pip install -e ".[dev]"
122
+ pytest
123
+ ruff check . && ruff format --check .
124
+ ```
125
+
126
+ MIT © Terrace Lab
@@ -0,0 +1,85 @@
1
+ # django-conjure
2
+
3
+ > Conjure your Django admin — read your models, summon a CRUD API + schema.
4
+
5
+ The Python package behind [Conjure](https://conjure.terracelab.dev). It introspects your
6
+ models and serves a generic admin **REST API** (`/conjure/`) plus a schema endpoint that a
7
+ frontend — or codegen — can read. A matching React dashboard lives in the repo and is in
8
+ active development; for the `0.1.x` line, point any client at the API below.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install django-conjure # session auth (default)
14
+ pip install "django-conjure[jwt]" # add JWT auth mode
15
+ ```
16
+
17
+ ```python
18
+ # settings.py
19
+ INSTALLED_APPS += ["conjure"]
20
+
21
+ CONJURE = {
22
+ "AUTH": "session", # or "jwt"
23
+ "BRAND": {"name": "My Admin", "accent": "#4f46e5"},
24
+ # "USER_PAYLOAD": "myapp.hooks.payload", # custom user model? return any dict
25
+ }
26
+ ```
27
+
28
+ ```python
29
+ # urls.py
30
+ urlpatterns += [path("conjure/", include("conjure.urls"))]
31
+ ```
32
+
33
+ ```bash
34
+ python manage.py migrate conjure # AdminAuditLog table (audit log, optional but recommended)
35
+ ```
36
+
37
+ ## Register a model
38
+
39
+ Drop an `admin_config.py` into any app — it's discovered automatically, like `admin.py`:
40
+
41
+ ```python
42
+ from conjure import register, AdminConfig
43
+ from myapp.models import Product
44
+
45
+ @register(Product)
46
+ class ProductConfig(AdminConfig):
47
+ list_display = ["name", "price", "is_active"]
48
+ search_fields = ["name"]
49
+ list_filter = ["is_active"]
50
+ inlines = [(ProductImage, "product")] # inline child editing
51
+ is_readonly = False # True => log/history models, blocks writes
52
+ ```
53
+
54
+ Anything you leave unset is inferred from the model's fields.
55
+
56
+ ## What you get
57
+
58
+ - `GET /conjure/schema/` + `…/schema/{app.Model}/` — model introspection (codegen + runtime source)
59
+ - `GET/POST /conjure/r/{app.Model}/` and `…/{pk}/` — generic CRUD
60
+ - `…/autocomplete/`, `…/bulk/` (atomic inline ops), `…/{pk}/related/` (delete impact)
61
+ - Django-permission gating (`view/add/change/delete`, shared with Django admin), `is_staff` required
62
+ - Audit log with before/after diff, staff auth (session or JWT), dashboard widget registry
63
+
64
+ ## Extend without forking
65
+
66
+ ```python
67
+ from conjure import register_widget
68
+
69
+ @register_widget("signup-trend")
70
+ def signup_trend(request):
71
+ ... # return any JSON; served at /conjure/widgets/signup-trend/
72
+ ```
73
+
74
+ Full docs, the settings reference, the REST contract, and the extension SDK live at
75
+ **[docs.conjure.terracelab.dev](https://docs.conjure.terracelab.dev)**.
76
+
77
+ ## Develop
78
+
79
+ ```bash
80
+ pip install -e ".[dev]"
81
+ pytest
82
+ ruff check . && ruff format --check .
83
+ ```
84
+
85
+ MIT © Terrace Lab
@@ -0,0 +1,19 @@
1
+ """Conjure — conjure your Django admin.
2
+
3
+ Public API:
4
+ from conjure import register, AdminConfig
5
+ from conjure import register_widget
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ from conjure.registry import AdminConfig, admin_register, register, registry
11
+ from conjure.widgets import register_widget
12
+
13
+ __all__ = ["AdminConfig", "registry", "register", "admin_register", "register_widget"]
14
+
15
+ try:
16
+ # Single source of truth = the installed package metadata (pyproject version).
17
+ __version__ = version("django-conjure")
18
+ except PackageNotFoundError: # running from source without an install
19
+ __version__ = "0.0.0+local"
@@ -0,0 +1,12 @@
1
+ """Conjure registers its own audit-log model (read-only) so the dashboard's audit page works."""
2
+
3
+ from conjure import AdminConfig, register
4
+ from conjure.models import AdminAuditLog
5
+
6
+
7
+ @register(AdminAuditLog)
8
+ class AdminAuditLogConfig(AdminConfig):
9
+ is_readonly = True
10
+ list_display = ["id", "created_at", "actor", "action", "model_label", "object_pk", "object_repr"]
11
+ search_fields = ["model_label", "object_repr", "object_pk"]
12
+ list_filter = ["action"]
@@ -0,0 +1,18 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ConjureConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "conjure"
7
+ verbose_name = "Conjure"
8
+
9
+ def ready(self):
10
+ # Register built-in widgets.
11
+ from conjure import widgets # noqa: F401
12
+ from conjure.conf import conjure_settings
13
+
14
+ # Discover each app's admin_config module (like admin.autodiscover()).
15
+ if conjure_settings.AUTODISCOVER:
16
+ from conjure.discovery import autodiscover
17
+
18
+ autodiscover()
@@ -0,0 +1,55 @@
1
+ """
2
+ @Domain: conjure / audit log
3
+ @BusinessLogic: Helper to record an audit entry for each write. CRUD must keep working even when
4
+ the AdminAuditLog table hasn't been migrated yet, so write failures are swallowed and logged.
5
+ @Context: DECOUPLING — uses the standard library ``logging`` (logger name from settings) instead
6
+ of any project-specific logger. Honors CONJURE["AUDIT"] (off => no-op).
7
+ @Connection: conjure/viewsets.py, conjure/models.py, conjure/conf.py
8
+ """
9
+
10
+ import logging
11
+
12
+ from conjure.conf import conjure_settings
13
+
14
+ logger = logging.getLogger(conjure_settings.LOGGER_NAME)
15
+
16
+
17
+ def get_client_ip(request):
18
+ forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
19
+ if forwarded:
20
+ return forwarded.split(",")[0].strip()
21
+ return request.META.get("REMOTE_ADDR")
22
+
23
+
24
+ def build_diff(before, after):
25
+ """Compare before/after serializer dicts -> {field: [old, new]}. Skips internal fields."""
26
+ diff = {}
27
+ keys = set(before.keys()) | set(after.keys())
28
+ for key in keys:
29
+ if key.startswith("_") or key.endswith("_label") or key.endswith("_display"):
30
+ continue
31
+ b, a = before.get(key), after.get(key)
32
+ if b != a:
33
+ diff[key] = [b, a]
34
+ return diff
35
+
36
+
37
+ def record(request, model_label, object_pk, action, object_repr="", diff=None):
38
+ """Record one audit entry — failure here never blocks the underlying write."""
39
+ if not conjure_settings.AUDIT:
40
+ return
41
+
42
+ from conjure.models import AdminAuditLog
43
+
44
+ try:
45
+ AdminAuditLog.objects.create(
46
+ actor=request.user if request.user.is_authenticated else None,
47
+ model_label=model_label,
48
+ object_pk=str(object_pk),
49
+ object_repr=str(object_repr)[:200],
50
+ action=action,
51
+ diff=diff or None,
52
+ ip=get_client_ip(request),
53
+ )
54
+ except Exception: # table not migrated yet, etc. — auditing must not break CRUD
55
+ logger.exception("[conjure] failed to write audit log")
@@ -0,0 +1,123 @@
1
+ """
2
+ @Domain: conjure / auth
3
+ @BusinessLogic: Staff-only login for the admin frontend. Two modes selected at request time by
4
+ CONJURE["AUTH"]: "session" (reuse Django's session login — works in any project, the default)
5
+ and "jwt" (SimpleJWT, requires the optional [jwt] extra). Non-staff accounts are always
6
+ rejected. The login/me response body comes from CONJURE["USER_PAYLOAD"].
7
+ @Context: DECOUPLING — the payload is a configurable callable defaulting to get_username()/
8
+ get_full_name(), so it never assumes project-specific user fields (e.g. nickname/real_name).
9
+ @Connection: conjure/urls.py, conjure/conf.py, packages/web/src/lib/auth.ts
10
+ """
11
+
12
+ from django.contrib.auth import authenticate
13
+ from django.contrib.auth import login as django_login
14
+ from django.contrib.auth import logout as django_logout
15
+ from django.core.exceptions import ImproperlyConfigured
16
+ from django.http import Http404
17
+ from rest_framework import status
18
+ from rest_framework.exceptions import AuthenticationFailed
19
+ from rest_framework.permissions import AllowAny
20
+ from rest_framework.response import Response
21
+ from rest_framework.views import APIView
22
+
23
+ from conjure.conf import conjure_settings
24
+ from conjure.mixins import ConjureAuthMixin
25
+ from conjure.permissions import IsStaffUser
26
+
27
+
28
+ def default_user_payload(user):
29
+ """Project-agnostic default — only uses the User API guaranteed by contrib.auth."""
30
+ full_name = (user.get_full_name() or "").strip()
31
+ return {
32
+ "id": user.pk,
33
+ "username": user.get_username(),
34
+ "name": full_name or user.get_username(),
35
+ "is_superuser": user.is_superuser,
36
+ }
37
+
38
+
39
+ def user_payload(user):
40
+ fn = conjure_settings.USER_PAYLOAD
41
+ return fn(user) if callable(fn) else default_user_payload(user)
42
+
43
+
44
+ def _require_staff(user):
45
+ if not (user and user.is_active and user.is_staff):
46
+ raise AuthenticationFailed("This account has no admin access.")
47
+
48
+
49
+ def _jwt_module():
50
+ try:
51
+ import rest_framework_simplejwt.serializers as ser
52
+ except ImportError as e: # pragma: no cover - guarded path
53
+ raise ImproperlyConfigured(
54
+ "CONJURE['AUTH'] == 'jwt' requires the optional dependency: pip install 'django-conjure[jwt]'"
55
+ ) from e
56
+ return ser
57
+
58
+
59
+ class LoginView(APIView):
60
+ """POST /auth/login/ — dispatches on CONJURE['AUTH']."""
61
+
62
+ permission_classes = [AllowAny]
63
+ authentication_classes = []
64
+ swagger_schema = None
65
+
66
+ def post(self, request):
67
+ if conjure_settings.AUTH == "jwt":
68
+ serializer_cls = _jwt_module().TokenObtainPairSerializer
69
+ serializer = serializer_cls(data=request.data, context={"request": request})
70
+ serializer.is_valid(raise_exception=True)
71
+ _require_staff(serializer.user)
72
+ data = dict(serializer.validated_data)
73
+ data["user"] = user_payload(serializer.user)
74
+ return Response(data)
75
+
76
+ # session mode
77
+ user = authenticate(
78
+ request._request,
79
+ username=request.data.get("username"),
80
+ password=request.data.get("password"),
81
+ )
82
+ if user is None:
83
+ raise AuthenticationFailed("Invalid credentials.")
84
+ _require_staff(user)
85
+ django_login(request._request, user)
86
+ return Response({"user": user_payload(user)})
87
+
88
+
89
+ class RefreshView(APIView):
90
+ """POST /auth/refresh/ — JWT mode only."""
91
+
92
+ permission_classes = [AllowAny]
93
+ authentication_classes = []
94
+ swagger_schema = None
95
+
96
+ def post(self, request):
97
+ if conjure_settings.AUTH != "jwt":
98
+ raise Http404
99
+ serializer = _jwt_module().TokenRefreshSerializer(data=request.data)
100
+ serializer.is_valid(raise_exception=True)
101
+ return Response(serializer.validated_data)
102
+
103
+
104
+ class LogoutView(ConjureAuthMixin, APIView):
105
+ """POST /auth/logout/ — session mode (no-op for JWT, client just drops the token)."""
106
+
107
+ permission_classes = [IsStaffUser]
108
+ swagger_schema = None
109
+
110
+ def post(self, request):
111
+ if conjure_settings.AUTH != "jwt":
112
+ django_logout(request._request)
113
+ return Response(status=status.HTTP_204_NO_CONTENT)
114
+
115
+
116
+ class MeView(ConjureAuthMixin, APIView):
117
+ """GET /auth/me/ — the currently authenticated staff user."""
118
+
119
+ permission_classes = [IsStaffUser]
120
+ swagger_schema = None
121
+
122
+ def get(self, request):
123
+ return Response(user_payload(request.user))
@@ -0,0 +1,90 @@
1
+ """
2
+ @Domain: conjure / settings
3
+ @BusinessLogic: Single accessor for all Conjure settings. Project overrides live under the
4
+ ``CONJURE`` dict in Django settings and are merged over DEFAULTS. This is the decoupling
5
+ layer — every project-specific knob (auth mode, brand, user payload, page size, audit) is
6
+ read through ``conjure_settings`` instead of being hardcoded, so the app attaches to any
7
+ project without edits.
8
+ @Connection: conjure/auth.py (AUTH, USER_PAYLOAD), conjure/audit.py (AUDIT, LOGGER_NAME),
9
+ conjure/viewsets.py (PAGE_SIZE), conjure/apps.py (AUTODISCOVER)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from django.conf import settings
15
+ from django.core.exceptions import ImproperlyConfigured
16
+ from django.utils.module_loading import import_string
17
+
18
+ DEFAULTS = {
19
+ # "session" (reuse Django login, works anywhere) or "jwt" (SimpleJWT — requires the [jwt] extra)
20
+ "AUTH": "session",
21
+ # Header / login title + accent color surfaced to the frontend config endpoint.
22
+ "BRAND": {"name": "Conjure", "accent": "#4f46e5"},
23
+ # Dotted path or callable -> dict, used as the login/me response. None = built-in default
24
+ # (id / username / name via get_username()/get_full_name() — works with any AUTH_USER_MODEL).
25
+ "USER_PAYLOAD": None,
26
+ # List pagination.
27
+ "PAGE_SIZE": 50,
28
+ "MAX_PAGE_SIZE": 200,
29
+ # Write the audit log (requires `migrate conjure`). When False, writes are skipped entirely.
30
+ "AUDIT": True,
31
+ # Auto-import every installed app's ``admin_config`` module on ready() (like admin.autodiscover).
32
+ "AUTODISCOVER": True,
33
+ # Logger used for non-fatal internal errors (e.g. audit write failures).
34
+ "LOGGER_NAME": "conjure",
35
+ }
36
+
37
+ # Keys whose value is a dotted import path that should be resolved to the referenced object.
38
+ _IMPORT_STRINGS = {"USER_PAYLOAD"}
39
+
40
+
41
+ class ConjureSettings:
42
+ """Lazy, cached accessor over ``settings.CONJURE`` merged onto DEFAULTS."""
43
+
44
+ def __init__(self):
45
+ self._cache: dict = {}
46
+
47
+ @property
48
+ def _user(self) -> dict:
49
+ user = getattr(settings, "CONJURE", {})
50
+ if not isinstance(user, dict):
51
+ raise ImproperlyConfigured("settings.CONJURE must be a dict.")
52
+ return user
53
+
54
+ def __getattr__(self, name: str):
55
+ if name.startswith("_"):
56
+ raise AttributeError(name)
57
+ if name not in DEFAULTS:
58
+ raise AttributeError(f"Unknown Conjure setting: {name!r}")
59
+ if name in self._cache:
60
+ return self._cache[name]
61
+
62
+ default = DEFAULTS[name]
63
+ value = self._user.get(name, default)
64
+ # Shallow-merge dict defaults (e.g. BRAND) so a partial override keeps the rest.
65
+ if isinstance(default, dict) and isinstance(value, dict):
66
+ value = {**default, **value}
67
+ if name in _IMPORT_STRINGS and isinstance(value, str):
68
+ value = import_string(value)
69
+
70
+ self._cache[name] = value
71
+ return value
72
+
73
+ def reload(self):
74
+ """Drop the cache — used by tests that override settings."""
75
+ self._cache.clear()
76
+
77
+
78
+ conjure_settings = ConjureSettings()
79
+
80
+
81
+ # Keep settings live under override_settings(CONJURE=...) (same pattern DRF uses for its own settings).
82
+ from django.test.signals import setting_changed # noqa: E402
83
+
84
+
85
+ def _reload_conjure_settings(*, setting, **kwargs):
86
+ if setting == "CONJURE":
87
+ conjure_settings.reload()
88
+
89
+
90
+ setting_changed.connect(_reload_conjure_settings)
@@ -0,0 +1,14 @@
1
+ """
2
+ @Domain: conjure / discovery
3
+ @BusinessLogic: Auto-import each installed app's ``admin_config`` module on startup, exactly like
4
+ Django's ``admin.autodiscover()`` does for ``admin``. This is what lets a project register
5
+ models by dropping an ``admin_config.py`` into any app — no central import list.
6
+ @Context: DECOUPLING — replaces the old hardcoded ``registrations.py`` single-file import.
7
+ @Connection: conjure/apps.py
8
+ """
9
+
10
+ from django.utils.module_loading import autodiscover_modules
11
+
12
+
13
+ def autodiscover():
14
+ autodiscover_modules("admin_config")
File without changes
@@ -0,0 +1,44 @@
1
+ """
2
+ @Domain: conjure / codegen
3
+ @BusinessLogic: Dump the registered models' admin schema to JSON — the snapshot the frontend
4
+ codegen reads. Replaces the ad-hoc shell one-liner. Permissions are forced on so the dump
5
+ is complete regardless of who runs it.
6
+ @Connection: conjure/schema.py (model_schema), packages/web/codegen/schema-snapshot.json
7
+ """
8
+
9
+ import json
10
+
11
+ from django.core.management.base import BaseCommand
12
+
13
+ from conjure.registry import registry
14
+ from conjure.schema import model_schema
15
+
16
+
17
+ class _AllPerms:
18
+ """Stand-in user so the schema dump includes every model's full permission flags."""
19
+
20
+ is_superuser = True
21
+
22
+ def has_perm(self, perm, obj=None):
23
+ return True
24
+
25
+
26
+ class Command(BaseCommand):
27
+ help = "Dump the registered models' admin schema to JSON (the codegen snapshot)."
28
+
29
+ def add_arguments(self, parser):
30
+ parser.add_argument("-o", "--output", default=None, help="Write to this path instead of stdout.")
31
+ parser.add_argument("--indent", type=int, default=2, help="JSON indent (default: 2).")
32
+
33
+ def handle(self, *args, **options):
34
+ user = _AllPerms()
35
+ data = {key: model_schema(key, config, user) for key, config in registry.items()}
36
+ text = json.dumps(data, ensure_ascii=False, indent=options["indent"])
37
+
38
+ output = options["output"]
39
+ if output:
40
+ with open(output, "w", encoding="utf-8") as fh:
41
+ fh.write(text)
42
+ self.stderr.write(self.style.SUCCESS(f"Wrote {len(data)} models to {output}"))
43
+ else:
44
+ self.stdout.write(text)