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.
- telamon-0.1.0a0/LICENSE +21 -0
- telamon-0.1.0a0/PKG-INFO +169 -0
- telamon-0.1.0a0/README.md +127 -0
- telamon-0.1.0a0/pyproject.toml +102 -0
- telamon-0.1.0a0/src/telamon/__init__.py +51 -0
- telamon-0.1.0a0/src/telamon/backends/__init__.py +0 -0
- telamon-0.1.0a0/src/telamon/backends/beanie/__init__.py +3 -0
- telamon-0.1.0a0/src/telamon/backends/beanie/backend.py +147 -0
- telamon-0.1.0a0/src/telamon/backends/memory/__init__.py +201 -0
- telamon-0.1.0a0/src/telamon/backends/mongo/__init__.py +4 -0
- telamon-0.1.0a0/src/telamon/backends/mongo/backend.py +301 -0
- telamon-0.1.0a0/src/telamon/backends/mongo/common.py +320 -0
- telamon-0.1.0a0/src/telamon/backends/sqlalchemy/__init__.py +3 -0
- telamon-0.1.0a0/src/telamon/backends/sqlalchemy/backend.py +497 -0
- telamon-0.1.0a0/src/telamon/backends/tortoise/__init__.py +3 -0
- telamon-0.1.0a0/src/telamon/backends/tortoise/backend.py +369 -0
- telamon-0.1.0a0/src/telamon/core/__init__.py +50 -0
- telamon-0.1.0a0/src/telamon/core/auth.py +183 -0
- telamon-0.1.0a0/src/telamon/core/coerce.py +157 -0
- telamon-0.1.0a0/src/telamon/core/exceptions.py +49 -0
- telamon-0.1.0a0/src/telamon/core/filters.py +100 -0
- telamon-0.1.0a0/src/telamon/core/http.py +117 -0
- telamon-0.1.0a0/src/telamon/core/model_admin.py +205 -0
- telamon-0.1.0a0/src/telamon/core/query.py +45 -0
- telamon-0.1.0a0/src/telamon/core/repository.py +49 -0
- telamon-0.1.0a0/src/telamon/core/schema.py +126 -0
- telamon-0.1.0a0/src/telamon/core/site.py +461 -0
- telamon-0.1.0a0/src/telamon/py.typed +0 -0
- telamon-0.1.0a0/src/telamon/static/assets/index-DRNd8E2f.css +2 -0
- telamon-0.1.0a0/src/telamon/static/assets/index-rsbgmkee.js +11 -0
- telamon-0.1.0a0/src/telamon/static/favicon.svg +1 -0
- telamon-0.1.0a0/src/telamon/static/icons.svg +24 -0
- telamon-0.1.0a0/src/telamon/static/index.html +15 -0
- telamon-0.1.0a0/src/telamon/transports/__init__.py +0 -0
- telamon-0.1.0a0/src/telamon/transports/starlette.py +125 -0
telamon-0.1.0a0/LICENSE
ADDED
|
@@ -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.
|
telamon-0.1.0a0/PKG-INFO
ADDED
|
@@ -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,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"]
|