telamon 0.1.0a0__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 (35) hide show
  1. telamon-0.1.0a0/LICENSE +21 -0
  2. telamon-0.1.0a0/PKG-INFO +169 -0
  3. telamon-0.1.0a0/README.md +127 -0
  4. telamon-0.1.0a0/pyproject.toml +102 -0
  5. telamon-0.1.0a0/src/telamon/__init__.py +51 -0
  6. telamon-0.1.0a0/src/telamon/backends/__init__.py +0 -0
  7. telamon-0.1.0a0/src/telamon/backends/beanie/__init__.py +3 -0
  8. telamon-0.1.0a0/src/telamon/backends/beanie/backend.py +147 -0
  9. telamon-0.1.0a0/src/telamon/backends/memory/__init__.py +201 -0
  10. telamon-0.1.0a0/src/telamon/backends/mongo/__init__.py +4 -0
  11. telamon-0.1.0a0/src/telamon/backends/mongo/backend.py +301 -0
  12. telamon-0.1.0a0/src/telamon/backends/mongo/common.py +320 -0
  13. telamon-0.1.0a0/src/telamon/backends/sqlalchemy/__init__.py +3 -0
  14. telamon-0.1.0a0/src/telamon/backends/sqlalchemy/backend.py +497 -0
  15. telamon-0.1.0a0/src/telamon/backends/tortoise/__init__.py +3 -0
  16. telamon-0.1.0a0/src/telamon/backends/tortoise/backend.py +369 -0
  17. telamon-0.1.0a0/src/telamon/core/__init__.py +50 -0
  18. telamon-0.1.0a0/src/telamon/core/auth.py +183 -0
  19. telamon-0.1.0a0/src/telamon/core/coerce.py +157 -0
  20. telamon-0.1.0a0/src/telamon/core/exceptions.py +49 -0
  21. telamon-0.1.0a0/src/telamon/core/filters.py +100 -0
  22. telamon-0.1.0a0/src/telamon/core/http.py +117 -0
  23. telamon-0.1.0a0/src/telamon/core/model_admin.py +205 -0
  24. telamon-0.1.0a0/src/telamon/core/query.py +45 -0
  25. telamon-0.1.0a0/src/telamon/core/repository.py +49 -0
  26. telamon-0.1.0a0/src/telamon/core/schema.py +126 -0
  27. telamon-0.1.0a0/src/telamon/core/site.py +461 -0
  28. telamon-0.1.0a0/src/telamon/py.typed +0 -0
  29. telamon-0.1.0a0/src/telamon/static/assets/index-DRNd8E2f.css +2 -0
  30. telamon-0.1.0a0/src/telamon/static/assets/index-rsbgmkee.js +11 -0
  31. telamon-0.1.0a0/src/telamon/static/favicon.svg +1 -0
  32. telamon-0.1.0a0/src/telamon/static/icons.svg +24 -0
  33. telamon-0.1.0a0/src/telamon/static/index.html +15 -0
  34. telamon-0.1.0a0/src/telamon/transports/__init__.py +0 -0
  35. telamon-0.1.0a0/src/telamon/transports/starlette.py +125 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 z.nurym
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,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: telamon
3
+ Version: 0.1.0a0
4
+ Summary: Framework-agnostic admin panel for Python: SQLAlchemy, Tortoise ORM, MongoDB and Beanie, with a built-in React UI
5
+ Keywords: admin,admin-panel,fastapi,starlette,sqlalchemy,tortoise-orm,mongodb,beanie
6
+ Author: z.nurym
7
+ Author-email: z.nurym <z.nurym@mycar.kz>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Framework :: FastAPI
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
20
+ Classifier: Typing :: Typed
21
+ Requires-Dist: anyio>=4.0
22
+ Requires-Dist: telamon[beanie,fastapi,mongo,sqlalchemy,tortoise] ; extra == 'all'
23
+ Requires-Dist: beanie>=1.26 ; extra == 'beanie'
24
+ Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
25
+ Requires-Dist: pymongo>=4.9 ; extra == 'mongo'
26
+ Requires-Dist: sqlalchemy>=2.0 ; extra == 'sqlalchemy'
27
+ Requires-Dist: python-multipart>=0.0.9 ; extra == 'starlette'
28
+ Requires-Dist: starlette>=0.37 ; extra == 'starlette'
29
+ Requires-Dist: tortoise-orm>=0.21 ; extra == 'tortoise'
30
+ Requires-Python: >=3.11
31
+ Project-URL: Homepage, https://github.com/znurym/telamon
32
+ Project-URL: Repository, https://github.com/znurym/telamon
33
+ Project-URL: Issues, https://github.com/znurym/telamon/issues
34
+ Provides-Extra: all
35
+ Provides-Extra: beanie
36
+ Provides-Extra: fastapi
37
+ Provides-Extra: mongo
38
+ Provides-Extra: sqlalchemy
39
+ Provides-Extra: starlette
40
+ Provides-Extra: tortoise
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Telamon Admin
44
+
45
+ Framework‑agnostic admin panel for Python: a headless core with a JSON API, pluggable data
46
+ backends (SQLAlchemy sync/async, Tortoise ORM, MongoDB via PyMongo/Motor, Beanie ODM) and thin
47
+ transports (Starlette/FastAPI today; Flask and Django next). Ships with a built‑in React UI, no
48
+ Node required at install time.
49
+
50
+ > Status: pre‑alpha. API may change.
51
+
52
+ ## Quickstart (FastAPI + SQLAlchemy)
53
+
54
+ ```bash
55
+ uv add "telamon[fastapi,sqlalchemy]"
56
+ ```
57
+
58
+ ```python
59
+ from fastapi import FastAPI
60
+ from sqlalchemy.ext.asyncio import create_async_engine
61
+
62
+ from telamon import AdminSite, ModelAdmin, SimpleAuth, action
63
+ from telamon.backends.sqlalchemy import SQLAlchemyBackend
64
+ from telamon.transports.starlette import mount
65
+
66
+ engine = create_async_engine("sqlite+aiosqlite:///app.db")
67
+ site = AdminSite("My Admin", backend=SQLAlchemyBackend(engine),
68
+ auth=SimpleAuth("change-me", {"admin": "admin"}))
69
+
70
+ @site.register(Post)
71
+ class PostAdmin(ModelAdmin):
72
+ list_display = ["id", "title", "status", "author"]
73
+ list_filter = ["status", "author", "tags"]
74
+ search_fields = ["title", "body"]
75
+ ordering = ["-id"]
76
+
77
+ @action("Publish", confirm="Publish selected posts?")
78
+ async def publish(self, request, objs):
79
+ repo = self.site.resource("post").repository
80
+ for post in objs:
81
+ await repo.update(post, {"status": "published"})
82
+ return f"Published {len(objs)} post(s)"
83
+
84
+ app = FastAPI()
85
+ mount(app, site, path="/admin")
86
+ ```
87
+
88
+ ## Backends
89
+
90
+ | Backend | Install | Usage |
91
+ |---|---|---|
92
+ | SQLAlchemy 2.x, sync or async | `telamon[sqlalchemy]` | `SQLAlchemyBackend(engine_or_sessionmaker)` |
93
+ | Tortoise ORM | `telamon[tortoise]` | `TortoiseBackend()`; run `Tortoise.init()` in your lifespan |
94
+ | MongoDB (PyMongo sync, PyMongo async, Motor) | `telamon[mongo]` | `MongoBackend(client["db"])`, models are Pydantic classes |
95
+ | Beanie ODM | `telamon[beanie]` | `BeanieBackend()`; run `init_beanie()` in your lifespan |
96
+
97
+ MongoDB models are plain Pydantic classes; references are declared with `Ref`:
98
+
99
+ ```python
100
+ from typing import Annotated
101
+ from telamon.backends.mongo import MongoBackend, Ref
102
+
103
+ class Order(BaseModel):
104
+ id: str # maps to _id (ObjectId hex, int or str)
105
+ number: str = Field(max_length=20)
106
+ customer: Annotated[str | None, Ref(Customer)] = None # many_to_one
107
+ tags: Annotated[list[str], Ref(Tag)] = [] # many_to_many
108
+ ```
109
+
110
+ With Beanie, `Link[T]`, `list[Link[T]]` and `BackLink[T]` are picked up automatically and links are
111
+ stored as DBRefs, so documents written by the admin load through Beanie unchanged.
112
+
113
+ Runnable examples live in `examples/`:
114
+
115
+ ```bash
116
+ uv run uvicorn examples.fastapi_sqlalchemy.app:app --reload # async SQLAlchemy + SQLite
117
+ uv run uvicorn examples.fastapi_sqlalchemy_sync.app:app --reload # sync engine
118
+ uv run uvicorn examples.fastapi_tortoise.app:app --reload
119
+ docker run -d --rm -p 27017:27017 --name mongo mongo:7
120
+ uv run uvicorn examples.fastapi_mongo.app:app --reload # PyMongo async
121
+ uv run uvicorn examples.fastapi_beanie.app:app --reload
122
+ # open http://127.0.0.1:8000/admin (admin / admin)
123
+ ```
124
+
125
+ ## Architecture
126
+
127
+ ```
128
+ transports/ Starlette ──┐
129
+ Flask (planned) ──► core/ (AdminSite, ModelAdmin, schema, filters, auth)
130
+ Django (planned) ──┘ ▲
131
+ │ Repository / Backend protocols
132
+ backends/ sqlalchemy, tortoise, mongo, beanie, memory ─┘
133
+ ui/ React SPA, built into src/telamon/static
134
+ ```
135
+
136
+ * **core** never imports a web framework or an ORM. It exposes `AdminSite.dispatch(AdminRequest)`.
137
+ * **backends** implement `Backend.introspect(model)` → `ModelSchema` and the async `Repository`
138
+ contract (`list`, `count`, `get`, `get_many`, `create`, `update`, `delete`, `to_dict`).
139
+ * **transports** map framework requests to `AdminRequest` and serve the UI.
140
+
141
+ ### JSON API
142
+
143
+ | Method | Path | |
144
+ |---|---|---|
145
+ | GET | `/_api/site` | resources, current user |
146
+ | POST/GET | `/_api/auth/login`, `/_api/auth/logout`, `/_api/auth/me` | session auth |
147
+ | GET | `/_api/{resource}/schema` | fields, actions, permissions |
148
+ | GET | `/_api/{resource}?offset&limit&sort=-id,name&search=&filters=<json>` | list |
149
+ | POST / GET / PATCH / DELETE | `/_api/{resource}`, `/_api/{resource}/{pk}` | CRUD |
150
+ | DELETE | `/_api/{resource}` body `{"pks": [...]}` | bulk delete |
151
+ | POST | `/_api/{resource}/actions/{name}` body `{"pks": [...]}` | custom action |
152
+ | GET | `/_api/{resource}/options?q=` | autocomplete for relations |
153
+ | GET | `/_api/{resource}/export?format=csv\|json` | export current view |
154
+
155
+ Filter tree: `{"and": [{"field": "age", "op": "gte", "value": 18}, {"or": [...]}]}` with ops
156
+ `eq ne lt lte gt gte in not_in contains icontains startswith endswith is_null between`.
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ uv sync --all-extras # python deps
162
+ uv run pytest # core + backend contract tests + transport tests
163
+ docker run -d --rm -p 27017:27017 mongo:7 # Mongo/Beanie tests skip when no server is reachable
164
+ uv run ruff check src tests
165
+ pnpm --dir ui install && pnpm --dir ui build # rebuilds src/telamon/static
166
+ pnpm --dir ui dev # UI dev server, proxies /admin/_api to :8000
167
+ ```
168
+
169
+ See `docs/PLAN.md` for the roadmap.
@@ -0,0 +1,127 @@
1
+ # Telamon Admin
2
+
3
+ Framework‑agnostic admin panel for Python: a headless core with a JSON API, pluggable data
4
+ backends (SQLAlchemy sync/async, Tortoise ORM, MongoDB via PyMongo/Motor, Beanie ODM) and thin
5
+ transports (Starlette/FastAPI today; Flask and Django next). Ships with a built‑in React UI, no
6
+ Node required at install time.
7
+
8
+ > Status: pre‑alpha. API may change.
9
+
10
+ ## Quickstart (FastAPI + SQLAlchemy)
11
+
12
+ ```bash
13
+ uv add "telamon[fastapi,sqlalchemy]"
14
+ ```
15
+
16
+ ```python
17
+ from fastapi import FastAPI
18
+ from sqlalchemy.ext.asyncio import create_async_engine
19
+
20
+ from telamon import AdminSite, ModelAdmin, SimpleAuth, action
21
+ from telamon.backends.sqlalchemy import SQLAlchemyBackend
22
+ from telamon.transports.starlette import mount
23
+
24
+ engine = create_async_engine("sqlite+aiosqlite:///app.db")
25
+ site = AdminSite("My Admin", backend=SQLAlchemyBackend(engine),
26
+ auth=SimpleAuth("change-me", {"admin": "admin"}))
27
+
28
+ @site.register(Post)
29
+ class PostAdmin(ModelAdmin):
30
+ list_display = ["id", "title", "status", "author"]
31
+ list_filter = ["status", "author", "tags"]
32
+ search_fields = ["title", "body"]
33
+ ordering = ["-id"]
34
+
35
+ @action("Publish", confirm="Publish selected posts?")
36
+ async def publish(self, request, objs):
37
+ repo = self.site.resource("post").repository
38
+ for post in objs:
39
+ await repo.update(post, {"status": "published"})
40
+ return f"Published {len(objs)} post(s)"
41
+
42
+ app = FastAPI()
43
+ mount(app, site, path="/admin")
44
+ ```
45
+
46
+ ## Backends
47
+
48
+ | Backend | Install | Usage |
49
+ |---|---|---|
50
+ | SQLAlchemy 2.x, sync or async | `telamon[sqlalchemy]` | `SQLAlchemyBackend(engine_or_sessionmaker)` |
51
+ | Tortoise ORM | `telamon[tortoise]` | `TortoiseBackend()`; run `Tortoise.init()` in your lifespan |
52
+ | MongoDB (PyMongo sync, PyMongo async, Motor) | `telamon[mongo]` | `MongoBackend(client["db"])`, models are Pydantic classes |
53
+ | Beanie ODM | `telamon[beanie]` | `BeanieBackend()`; run `init_beanie()` in your lifespan |
54
+
55
+ MongoDB models are plain Pydantic classes; references are declared with `Ref`:
56
+
57
+ ```python
58
+ from typing import Annotated
59
+ from telamon.backends.mongo import MongoBackend, Ref
60
+
61
+ class Order(BaseModel):
62
+ id: str # maps to _id (ObjectId hex, int or str)
63
+ number: str = Field(max_length=20)
64
+ customer: Annotated[str | None, Ref(Customer)] = None # many_to_one
65
+ tags: Annotated[list[str], Ref(Tag)] = [] # many_to_many
66
+ ```
67
+
68
+ With Beanie, `Link[T]`, `list[Link[T]]` and `BackLink[T]` are picked up automatically and links are
69
+ stored as DBRefs, so documents written by the admin load through Beanie unchanged.
70
+
71
+ Runnable examples live in `examples/`:
72
+
73
+ ```bash
74
+ uv run uvicorn examples.fastapi_sqlalchemy.app:app --reload # async SQLAlchemy + SQLite
75
+ uv run uvicorn examples.fastapi_sqlalchemy_sync.app:app --reload # sync engine
76
+ uv run uvicorn examples.fastapi_tortoise.app:app --reload
77
+ docker run -d --rm -p 27017:27017 --name mongo mongo:7
78
+ uv run uvicorn examples.fastapi_mongo.app:app --reload # PyMongo async
79
+ uv run uvicorn examples.fastapi_beanie.app:app --reload
80
+ # open http://127.0.0.1:8000/admin (admin / admin)
81
+ ```
82
+
83
+ ## Architecture
84
+
85
+ ```
86
+ transports/ Starlette ──┐
87
+ Flask (planned) ──► core/ (AdminSite, ModelAdmin, schema, filters, auth)
88
+ Django (planned) ──┘ ▲
89
+ │ Repository / Backend protocols
90
+ backends/ sqlalchemy, tortoise, mongo, beanie, memory ─┘
91
+ ui/ React SPA, built into src/telamon/static
92
+ ```
93
+
94
+ * **core** never imports a web framework or an ORM. It exposes `AdminSite.dispatch(AdminRequest)`.
95
+ * **backends** implement `Backend.introspect(model)` → `ModelSchema` and the async `Repository`
96
+ contract (`list`, `count`, `get`, `get_many`, `create`, `update`, `delete`, `to_dict`).
97
+ * **transports** map framework requests to `AdminRequest` and serve the UI.
98
+
99
+ ### JSON API
100
+
101
+ | Method | Path | |
102
+ |---|---|---|
103
+ | GET | `/_api/site` | resources, current user |
104
+ | POST/GET | `/_api/auth/login`, `/_api/auth/logout`, `/_api/auth/me` | session auth |
105
+ | GET | `/_api/{resource}/schema` | fields, actions, permissions |
106
+ | GET | `/_api/{resource}?offset&limit&sort=-id,name&search=&filters=<json>` | list |
107
+ | POST / GET / PATCH / DELETE | `/_api/{resource}`, `/_api/{resource}/{pk}` | CRUD |
108
+ | DELETE | `/_api/{resource}` body `{"pks": [...]}` | bulk delete |
109
+ | POST | `/_api/{resource}/actions/{name}` body `{"pks": [...]}` | custom action |
110
+ | GET | `/_api/{resource}/options?q=` | autocomplete for relations |
111
+ | GET | `/_api/{resource}/export?format=csv\|json` | export current view |
112
+
113
+ Filter tree: `{"and": [{"field": "age", "op": "gte", "value": 18}, {"or": [...]}]}` with ops
114
+ `eq ne lt lte gt gte in not_in contains icontains startswith endswith is_null between`.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ uv sync --all-extras # python deps
120
+ uv run pytest # core + backend contract tests + transport tests
121
+ docker run -d --rm -p 27017:27017 mongo:7 # Mongo/Beanie tests skip when no server is reachable
122
+ uv run ruff check src tests
123
+ pnpm --dir ui install && pnpm --dir ui build # rebuilds src/telamon/static
124
+ pnpm --dir ui dev # UI dev server, proxies /admin/_api to :8000
125
+ ```
126
+
127
+ See `docs/PLAN.md` for the roadmap.
@@ -0,0 +1,102 @@
1
+ [project]
2
+ name = "telamon"
3
+ version = "0.1.0a0"
4
+ description = "Framework-agnostic admin panel for Python: SQLAlchemy, Tortoise ORM, MongoDB and Beanie, with a built-in React UI"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "z.nurym", email = "z.nurym@mycar.kz" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ keywords = ["admin", "admin-panel", "fastapi", "starlette", "sqlalchemy", "tortoise-orm", "mongodb", "beanie"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Framework :: FastAPI",
22
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
23
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "anyio>=4.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ sqlalchemy = [
32
+ "sqlalchemy>=2.0",
33
+ ]
34
+ tortoise = [
35
+ "tortoise-orm>=0.21",
36
+ ]
37
+ starlette = [
38
+ "python-multipart>=0.0.9",
39
+ "starlette>=0.37",
40
+ ]
41
+ fastapi = [
42
+ "fastapi>=0.110",
43
+ ]
44
+ mongo = [
45
+ "pymongo>=4.9",
46
+ ]
47
+ beanie = [
48
+ "beanie>=1.26",
49
+ ]
50
+ all = [
51
+ "telamon[beanie,fastapi,mongo,sqlalchemy,tortoise]",
52
+ ]
53
+
54
+ [project.urls]
55
+ Homepage = "https://github.com/znurym/telamon"
56
+ Repository = "https://github.com/znurym/telamon"
57
+ Issues = "https://github.com/znurym/telamon/issues"
58
+
59
+ [build-system]
60
+ requires = ["uv_build>=0.11.27,<0.12.0"]
61
+ build-backend = "uv_build"
62
+
63
+ [dependency-groups]
64
+ dev = [
65
+ "aiosqlite>=0.22.1",
66
+ "beanie>=2.2.0",
67
+ "fastapi>=0.156.8",
68
+ "httpx>=0.28.1",
69
+ "mongomock>=4.3.0",
70
+ "motor>=3.7.1",
71
+ "mypy>=2.4.0",
72
+ "pymongo>=4.19.0",
73
+ "pytest>=9.1.1",
74
+ "pytest-asyncio>=1.4.0",
75
+ "python-multipart>=0.0.32",
76
+ "ruff>=0.17.1",
77
+ "sqlalchemy[asyncio]>=2.0",
78
+ "tortoise-orm>=1.1.9",
79
+ "uvicorn>=0.54.0",
80
+ ]
81
+
82
+ [tool.pytest.ini_options]
83
+ asyncio_mode = "auto"
84
+ testpaths = ["tests"]
85
+
86
+ [tool.ruff]
87
+ line-length = 100
88
+ target-version = "py311"
89
+
90
+ [tool.ruff.lint]
91
+ select = ["E", "F", "I", "UP", "B", "SIM"]
92
+
93
+ [tool.mypy]
94
+ python_version = "3.11"
95
+ strict = true
96
+ ignore_missing_imports = true
97
+
98
+ [tool.uv.sources]
99
+ telamon = { workspace = true }
100
+
101
+ [tool.ruff.lint.per-file-ignores]
102
+ "tests/*" = ["E501"]
@@ -0,0 +1,51 @@
1
+ """Telamon Admin: framework-agnostic admin panel for SQLAlchemy and Tortoise ORM."""
2
+
3
+ from .core import (
4
+ Action,
5
+ AdminError,
6
+ AdminRequest,
7
+ AdminResponse,
8
+ AdminSite,
9
+ AuthProvider,
10
+ Backend,
11
+ FieldType,
12
+ FilterGroup,
13
+ ListQuery,
14
+ ModelAdmin,
15
+ ModelSchema,
16
+ NotFound,
17
+ PermissionDenied,
18
+ Repository,
19
+ SessionAuth,
20
+ SimpleAuth,
21
+ User,
22
+ ValidationError,
23
+ action,
24
+ hash_password,
25
+ )
26
+
27
+ __version__ = "0.1.0a0"
28
+
29
+ __all__ = [
30
+ "Action",
31
+ "AdminError",
32
+ "AdminRequest",
33
+ "AdminResponse",
34
+ "AdminSite",
35
+ "AuthProvider",
36
+ "Backend",
37
+ "FieldType",
38
+ "FilterGroup",
39
+ "ListQuery",
40
+ "ModelAdmin",
41
+ "ModelSchema",
42
+ "NotFound",
43
+ "PermissionDenied",
44
+ "Repository",
45
+ "SessionAuth",
46
+ "SimpleAuth",
47
+ "User",
48
+ "ValidationError",
49
+ "action",
50
+ "hash_password",
51
+ ]
File without changes
@@ -0,0 +1,3 @@
1
+ from .backend import BeanieBackend, BeanieRepository
2
+
3
+ __all__ = ["BeanieBackend", "BeanieRepository"]
@@ -0,0 +1,147 @@
1
+ """Beanie ODM backend. ``Link[T]`` -> many_to_one, ``list[Link[T]]`` -> many_to_many,
2
+ ``BackLink[T]`` -> one_to_many (read-only). Requires ``init_beanie()`` before the first request.
3
+
4
+ Documents are read and written through the raw pymongo collection that Beanie manages; links are
5
+ stored as DBRefs exactly as Beanie stores them, so both sides stay compatible.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import builtins
11
+ from typing import Any, cast, get_args, get_origin
12
+
13
+ from beanie import Document
14
+ from beanie.odm.fields import BackLink, Link
15
+ from bson import DBRef
16
+
17
+ from telamon.core.model_admin import ModelAdmin
18
+ from telamon.core.schema import FieldSchema, FieldType, ModelSchema, Relation
19
+
20
+ from ..mongo.backend import Driver, MongoBackend, MongoRepository
21
+ from ..mongo.common import Ref, _unwrap, field_from_annotation, parse_pk
22
+
23
+ _HIDDEN = {"revision_id"}
24
+
25
+
26
+ class BeanieBackend(MongoBackend):
27
+ name = "beanie"
28
+
29
+ def __init__(self) -> None: # database comes from init_beanie()
30
+ self._driver: Driver | None = None
31
+
32
+ @property
33
+ def driver(self) -> Driver:
34
+ if self._driver is None:
35
+ raise RuntimeError(
36
+ "Beanie is not initialised; call init_beanie() before using the admin"
37
+ )
38
+ return self._driver
39
+
40
+ @driver.setter
41
+ def driver(self, value: Driver) -> None:
42
+ self._driver = value
43
+
44
+ def supports(self, model: type) -> bool:
45
+ return isinstance(model, type) and issubclass(model, Document)
46
+
47
+ def introspect(self, model: type, admin: ModelAdmin) -> ModelSchema:
48
+ fields: list[FieldSchema] = []
49
+ doc = cast(type[Document], model)
50
+ for name, info in doc.model_fields.items():
51
+ if name in _HIDDEN:
52
+ continue
53
+ if name == "id":
54
+ f = field_from_annotation("id", info.annotation, info)
55
+ f.primary_key, f.read_only, f.has_default, f.required, f.nullable = (
56
+ True,
57
+ True,
58
+ True,
59
+ False,
60
+ False,
61
+ )
62
+ fields.insert(0, f)
63
+ continue
64
+ core, nullable, _ = _unwrap(info.annotation)
65
+ origin = get_origin(core)
66
+ inner = get_args(core)[0] if origin in (list, set, tuple) and get_args(core) else None
67
+ if origin is Link:
68
+ target = get_args(core)[0]
69
+ ann: Any = (str | None) if nullable else str
70
+ fields.append(field_from_annotation(name, ann, info, extra_metadata=[Ref(target)]))
71
+ elif inner is not None and get_origin(inner) is Link:
72
+ target = get_args(inner)[0]
73
+ fields.append(
74
+ field_from_annotation(name, list[str], info, extra_metadata=[Ref(target)])
75
+ )
76
+ elif origin is BackLink or (inner is not None and get_origin(inner) is BackLink):
77
+ link = core if origin is BackLink else inner
78
+ target = get_args(link)[0] if get_args(link) else Any
79
+ fields.append(
80
+ FieldSchema(
81
+ name,
82
+ FieldType.ONE_TO_MANY,
83
+ read_only=True,
84
+ relation=Relation(model=target, kind=FieldType.ONE_TO_MANY),
85
+ sortable=False,
86
+ filterable=False,
87
+ )
88
+ )
89
+ else:
90
+ fields.append(field_from_annotation(name, info.annotation, info))
91
+ label = model.__name__
92
+ return ModelSchema(
93
+ key=label.lower(), label=label, label_plural=f"{label}s", pk_field="id", fields=fields
94
+ )
95
+
96
+ def repository(self, model: type, schema: ModelSchema, admin: ModelAdmin) -> BeanieRepository:
97
+ return BeanieRepository(self, model, schema, admin)
98
+
99
+
100
+ class BeanieRepository(MongoRepository):
101
+ id_path = ".$id"
102
+
103
+ @property
104
+ def driver(self) -> Driver:
105
+ _ = self.coll # resolving the collection also wires the driver from init_beanie()
106
+ return self.backend.driver
107
+
108
+ @property
109
+ def coll(self) -> Any:
110
+ if self._coll is None:
111
+ doc = cast(type[Document], self.model)
112
+ self._coll = doc.get_pymongo_collection() # raises if init_beanie() was not run
113
+ self.backend.driver = Driver(self._coll.database)
114
+ return self._coll
115
+
116
+ @property
117
+ def collection_name(self) -> str:
118
+ return str(cast(type[Document], self.model).get_collection_name())
119
+
120
+ def _target_collection(self, f: FieldSchema) -> str:
121
+ assert f.relation is not None
122
+ return str(f.relation.model.get_collection_name())
123
+
124
+ def _rel_value(self, f: FieldSchema, value: Any) -> Any:
125
+ if value is None:
126
+ return None
127
+ coll = self._target_collection(f)
128
+ if f.type == FieldType.MANY_TO_MANY:
129
+ return [DBRef(coll, parse_pk(v)) for v in value]
130
+ return DBRef(coll, parse_pk(value))
131
+
132
+ def _rel_ids(self, f: FieldSchema, stored: Any) -> builtins.list[Any]:
133
+ if stored is None:
134
+ return []
135
+ items = stored if isinstance(stored, list) else [stored]
136
+ out = []
137
+ for i in items:
138
+ if isinstance(i, DBRef):
139
+ out.append(i.id)
140
+ elif isinstance(i, dict) and "$id" in i:
141
+ out.append(i["$id"])
142
+ else:
143
+ out.append(i)
144
+ return out
145
+
146
+
147
+ __all__ = ["BeanieBackend", "BeanieRepository"]