admin-litestar 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.
- admin_litestar-0.1.0/.gitignore +11 -0
- admin_litestar-0.1.0/LICENSE +21 -0
- admin_litestar-0.1.0/PKG-INFO +239 -0
- admin_litestar-0.1.0/README.md +189 -0
- admin_litestar-0.1.0/pyproject.toml +54 -0
- admin_litestar-0.1.0/src/admin_litestar/__init__.py +50 -0
- admin_litestar-0.1.0/src/admin_litestar/admin.py +183 -0
- admin_litestar-0.1.0/src/admin_litestar/auth.py +107 -0
- admin_litestar-0.1.0/src/admin_litestar/constants.py +47 -0
- admin_litestar-0.1.0/src/admin_litestar/controllers/__init__.py +6 -0
- admin_litestar-0.1.0/src/admin_litestar/controllers/models.py +176 -0
- admin_litestar-0.1.0/src/admin_litestar/controllers/session.py +77 -0
- admin_litestar-0.1.0/src/admin_litestar/export.py +45 -0
- admin_litestar-0.1.0/src/admin_litestar/pages.py +28 -0
- admin_litestar-0.1.0/src/admin_litestar/passwords.py +76 -0
- admin_litestar-0.1.0/src/admin_litestar/protocols.py +63 -0
- admin_litestar-0.1.0/src/admin_litestar/py.typed +0 -0
- admin_litestar-0.1.0/src/admin_litestar/queries.py +161 -0
- admin_litestar-0.1.0/src/admin_litestar/render.py +41 -0
- admin_litestar-0.1.0/src/admin_litestar/spec.py +145 -0
- admin_litestar-0.1.0/src/admin_litestar/static/__init__.py +7 -0
- admin_litestar-0.1.0/src/admin_litestar/static/admin.css +205 -0
- admin_litestar-0.1.0/src/admin_litestar/static/htmx.min.js +1 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/__init__.py +7 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/_table.html +18 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/base.html +16 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/base_bare.html +10 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/detail.html +21 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/list.html +25 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/login.html +13 -0
- admin_litestar-0.1.0/src/admin_litestar/templates/nav.html +20 -0
- admin_litestar-0.1.0/tests/__init__.py +1 -0
- admin_litestar-0.1.0/tests/conftest.py +1 -0
- admin_litestar-0.1.0/tests/hostapp.py +111 -0
- admin_litestar-0.1.0/tests/models.py +48 -0
- admin_litestar-0.1.0/tests/test_admin.py +140 -0
- admin_litestar-0.1.0/tests/test_api.py +88 -0
- admin_litestar-0.1.0/tests/test_auth.py +159 -0
- admin_litestar-0.1.0/tests/test_boundary.py +162 -0
- admin_litestar-0.1.0/tests/test_export.py +51 -0
- admin_litestar-0.1.0/tests/test_models_controller.py +233 -0
- admin_litestar-0.1.0/tests/test_passwords.py +65 -0
- admin_litestar-0.1.0/tests/test_queries.py +158 -0
- admin_litestar-0.1.0/tests/test_render.py +84 -0
- admin_litestar-0.1.0/tests/test_spec.py +222 -0
- admin_litestar-0.1.0/tests/test_templates.py +58 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omirtay Adilkhan
|
|
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,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: admin-litestar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Server-rendered admin for Litestar + SQLAlchemy applications
|
|
5
|
+
Project-URL: Homepage, https://github.com/adllkhan/admin-litestar
|
|
6
|
+
Project-URL: Repository, https://github.com/adllkhan/admin-litestar
|
|
7
|
+
Project-URL: Issues, https://github.com/adllkhan/admin-litestar/issues
|
|
8
|
+
Author-email: Omirtay Adilkhan <aomertayevich@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Omirtay Adilkhan
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: admin,asgi,htmx,litestar,sqlalchemy
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
42
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: jinja2>=3.1
|
|
47
|
+
Requires-Dist: litestar>=2.24.0
|
|
48
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# admin-litestar
|
|
52
|
+
|
|
53
|
+
A server-rendered admin panel for [Litestar](https://litestar.dev) applications backed by
|
|
54
|
+
SQLAlchemy. No build step, no JavaScript toolchain, no CDN at runtime — the CSS is
|
|
55
|
+
hand-written and HTMX is vendored as package data.
|
|
56
|
+
|
|
57
|
+
It knows SQLAlchemy and Litestar. It knows nothing about your schema, your authentication,
|
|
58
|
+
or where you keep your audit trail — those arrive through protocols you implement. A test
|
|
59
|
+
in this repository fails if the package ever imports a host application.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv add admin-litestar
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python 3.10+. Depends only on `litestar`, `sqlalchemy` and `jinja2`.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Declare a `ModelSpec` per model, implement three small protocols, and mount what `Admin`
|
|
72
|
+
gives you.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from litestar import Litestar
|
|
76
|
+
from litestar.stores.memory import MemoryStore
|
|
77
|
+
|
|
78
|
+
from admin_litestar import (
|
|
79
|
+
Admin,
|
|
80
|
+
AdminConfig,
|
|
81
|
+
DETAIL,
|
|
82
|
+
EXPORT,
|
|
83
|
+
LIST,
|
|
84
|
+
ModelSpec,
|
|
85
|
+
hash_password,
|
|
86
|
+
verify_password,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
INVOICE = ModelSpec(
|
|
90
|
+
model=Invoice,
|
|
91
|
+
slug="invoice",
|
|
92
|
+
label="Invoices",
|
|
93
|
+
group="Billing",
|
|
94
|
+
list_columns=("id", "reference", "issued_at"),
|
|
95
|
+
detail_columns=("id", "reference", "note", "issued_at"),
|
|
96
|
+
capabilities=frozenset({LIST, DETAIL, EXPORT}),
|
|
97
|
+
order_by="id",
|
|
98
|
+
searchable=("reference",),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Auth:
|
|
103
|
+
"""Decides who may enter the admin, and whether they still may."""
|
|
104
|
+
|
|
105
|
+
async def authenticate(self, session, username, password):
|
|
106
|
+
user = await lookup(session, username)
|
|
107
|
+
if user and verify_password(password, user.admin_password):
|
|
108
|
+
return user
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def identity_of(self, user):
|
|
112
|
+
return user.id
|
|
113
|
+
|
|
114
|
+
async def is_valid(self, session, actor_id) -> bool:
|
|
115
|
+
return await still_permitted(session, actor_id)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
admin = Admin(
|
|
119
|
+
config=AdminConfig(path="/admin", static_path="/admin-static"),
|
|
120
|
+
specs=[INVOICE],
|
|
121
|
+
auth=Auth(),
|
|
122
|
+
audit=YourAuditSink(),
|
|
123
|
+
cache=lambda request: your_cache,
|
|
124
|
+
session_factory=async_sessionmaker(engine),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
app = Litestar(
|
|
128
|
+
route_handlers=[admin.router(), admin.static_router()],
|
|
129
|
+
template_config=admin.template_config(),
|
|
130
|
+
middleware=[admin.session_config(MemoryStore()).middleware],
|
|
131
|
+
csrf_config=admin.csrf_config(SECRET),
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That yields a login page, a gated sidebar shell, and list / detail / delete / CSV-export
|
|
136
|
+
routes for every spec that declares the matching capability.
|
|
137
|
+
|
|
138
|
+
## The column boundary
|
|
139
|
+
|
|
140
|
+
`ModelSpec` distinguishes three kinds of column, and the distinction is enforced where
|
|
141
|
+
statements are built rather than where values are rendered:
|
|
142
|
+
|
|
143
|
+
| Field | Behaviour |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `list_columns` | Loaded and shown in list views |
|
|
146
|
+
| `detail_columns` | Loaded and shown in detail views |
|
|
147
|
+
| `hidden_columns` | Permitted in detail views, **never** loaded by a list query |
|
|
148
|
+
| `excluded_columns` | Never selected, rendered or exported, anywhere |
|
|
149
|
+
|
|
150
|
+
List queries use `load_only()` over `list_columns`, so a hidden column is absent from the
|
|
151
|
+
SQL itself. That matters when a column's SQLAlchemy type decrypts on load: a list page
|
|
152
|
+
neither pays the cost nor can leak the value, even if a template is wrong. `ModelSpec`
|
|
153
|
+
rejects contradictory declarations at construction, so a hidden column named in
|
|
154
|
+
`list_columns` — or an excluded column named as searchable or filterable — is an error you
|
|
155
|
+
get at import time, not a leak you find later.
|
|
156
|
+
|
|
157
|
+
## Search
|
|
158
|
+
|
|
159
|
+
`searchable` columns match with `ILIKE`. `exact_searchable` columns match by equality, and
|
|
160
|
+
`search_transform` is applied to the term first — which is how you search a keyed-digest
|
|
161
|
+
column without this package knowing anything about your hashing:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
ModelSpec(
|
|
165
|
+
...,
|
|
166
|
+
exact_searchable=("iin_digest",),
|
|
167
|
+
search_transform=your_digest_function,
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Exact search takes precedence when both are declared, because a digest cannot be matched
|
|
172
|
+
partially.
|
|
173
|
+
|
|
174
|
+
## Pagination
|
|
175
|
+
|
|
176
|
+
Keyset, never `OFFSET`. The cursor is the last row's `order_by` value, coerced to the
|
|
177
|
+
column's Python type — dates and datetimes are parsed with `fromisoformat`. A malformed or
|
|
178
|
+
timezone-naive cursor is treated as absent and yields an unpaginated first page rather than
|
|
179
|
+
an error, because cursors arrive from URLs and URLs get edited.
|
|
180
|
+
|
|
181
|
+
## Custom pages
|
|
182
|
+
|
|
183
|
+
Generic tables cannot do everything. `CustomPage` lets a host contribute its own routes,
|
|
184
|
+
rendered inside the same shell and listed in the same nav:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from admin_litestar import CustomPage
|
|
188
|
+
|
|
189
|
+
dashboard = CustomPage(
|
|
190
|
+
slug="dashboard", label="Dashboard", group="Overview", handlers=[DashboardController]
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Host templates take precedence over the package's, so `AdminConfig(template_dirs=(...))`
|
|
195
|
+
lets you override any template by name while extending `base.html`.
|
|
196
|
+
|
|
197
|
+
## Authentication
|
|
198
|
+
|
|
199
|
+
The package owns the mechanism; you own the policy.
|
|
200
|
+
|
|
201
|
+
- `hash_password` / `verify_password` use `hashlib.scrypt` with `n=16384, r=8, p=1,
|
|
202
|
+
dklen=32`. The encoding is 86 characters, so it fits a `String(128)` column. Anything not
|
|
203
|
+
in that format fails verification — there is no fallback to another scheme.
|
|
204
|
+
- Login failures are counted per username **and** client IP, locking after 5 attempts for
|
|
205
|
+
15 minutes. Deliberately separate from any lockout counter on your own user rows, so
|
|
206
|
+
admin brute-force cannot lock someone out of your main application.
|
|
207
|
+
- Sessions are server-side over a store you supply. `AuthBackend.is_valid` is re-checked on
|
|
208
|
+
every request, cached briefly, so revoking access takes effect in seconds rather than at
|
|
209
|
+
session expiry.
|
|
210
|
+
- CSRF protection covers every mutating route. Templates call `{{ csrf_token() }}`.
|
|
211
|
+
|
|
212
|
+
## Design
|
|
213
|
+
|
|
214
|
+
Dark, near-monochrome, one amber accent, monospace for identifiers — chosen because admin
|
|
215
|
+
data is mostly ids, hashes, addresses and timestamps, which align and scan far better in a
|
|
216
|
+
monospaced column. Light and dark both ship, honouring `prefers-color-scheme` with an
|
|
217
|
+
explicit `data-theme` override.
|
|
218
|
+
|
|
219
|
+
`ModelSpec` validates at construction: unknown column names, a hidden column listed in
|
|
220
|
+
`list_columns`, an excluded column named as searchable, or an unknown capability all raise
|
|
221
|
+
immediately rather than producing an admin that quietly misbehaves.
|
|
222
|
+
|
|
223
|
+
## Status
|
|
224
|
+
|
|
225
|
+
Early. The API has one real consumer, so every protocol here is a considered guess about
|
|
226
|
+
the second one. Expect `0.x` releases to move interfaces, and pin exactly if that matters.
|
|
227
|
+
|
|
228
|
+
`admin_litestar.__all__` is the compatibility promise. Deeper import paths work but carry
|
|
229
|
+
none — see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
230
|
+
|
|
231
|
+
## Documentation
|
|
232
|
+
|
|
233
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — layout, layering, where each guarantee is enforced
|
|
234
|
+
- [CHANGELOG.md](CHANGELOG.md) — what moved and when
|
|
235
|
+
- [RELEASING.md](RELEASING.md) — how a release is cut
|
|
236
|
+
|
|
237
|
+
## Licence
|
|
238
|
+
|
|
239
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# admin-litestar
|
|
2
|
+
|
|
3
|
+
A server-rendered admin panel for [Litestar](https://litestar.dev) applications backed by
|
|
4
|
+
SQLAlchemy. No build step, no JavaScript toolchain, no CDN at runtime — the CSS is
|
|
5
|
+
hand-written and HTMX is vendored as package data.
|
|
6
|
+
|
|
7
|
+
It knows SQLAlchemy and Litestar. It knows nothing about your schema, your authentication,
|
|
8
|
+
or where you keep your audit trail — those arrive through protocols you implement. A test
|
|
9
|
+
in this repository fails if the package ever imports a host application.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add admin-litestar
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.10+. Depends only on `litestar`, `sqlalchemy` and `jinja2`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Declare a `ModelSpec` per model, implement three small protocols, and mount what `Admin`
|
|
22
|
+
gives you.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from litestar import Litestar
|
|
26
|
+
from litestar.stores.memory import MemoryStore
|
|
27
|
+
|
|
28
|
+
from admin_litestar import (
|
|
29
|
+
Admin,
|
|
30
|
+
AdminConfig,
|
|
31
|
+
DETAIL,
|
|
32
|
+
EXPORT,
|
|
33
|
+
LIST,
|
|
34
|
+
ModelSpec,
|
|
35
|
+
hash_password,
|
|
36
|
+
verify_password,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
INVOICE = ModelSpec(
|
|
40
|
+
model=Invoice,
|
|
41
|
+
slug="invoice",
|
|
42
|
+
label="Invoices",
|
|
43
|
+
group="Billing",
|
|
44
|
+
list_columns=("id", "reference", "issued_at"),
|
|
45
|
+
detail_columns=("id", "reference", "note", "issued_at"),
|
|
46
|
+
capabilities=frozenset({LIST, DETAIL, EXPORT}),
|
|
47
|
+
order_by="id",
|
|
48
|
+
searchable=("reference",),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Auth:
|
|
53
|
+
"""Decides who may enter the admin, and whether they still may."""
|
|
54
|
+
|
|
55
|
+
async def authenticate(self, session, username, password):
|
|
56
|
+
user = await lookup(session, username)
|
|
57
|
+
if user and verify_password(password, user.admin_password):
|
|
58
|
+
return user
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def identity_of(self, user):
|
|
62
|
+
return user.id
|
|
63
|
+
|
|
64
|
+
async def is_valid(self, session, actor_id) -> bool:
|
|
65
|
+
return await still_permitted(session, actor_id)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
admin = Admin(
|
|
69
|
+
config=AdminConfig(path="/admin", static_path="/admin-static"),
|
|
70
|
+
specs=[INVOICE],
|
|
71
|
+
auth=Auth(),
|
|
72
|
+
audit=YourAuditSink(),
|
|
73
|
+
cache=lambda request: your_cache,
|
|
74
|
+
session_factory=async_sessionmaker(engine),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
app = Litestar(
|
|
78
|
+
route_handlers=[admin.router(), admin.static_router()],
|
|
79
|
+
template_config=admin.template_config(),
|
|
80
|
+
middleware=[admin.session_config(MemoryStore()).middleware],
|
|
81
|
+
csrf_config=admin.csrf_config(SECRET),
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That yields a login page, a gated sidebar shell, and list / detail / delete / CSV-export
|
|
86
|
+
routes for every spec that declares the matching capability.
|
|
87
|
+
|
|
88
|
+
## The column boundary
|
|
89
|
+
|
|
90
|
+
`ModelSpec` distinguishes three kinds of column, and the distinction is enforced where
|
|
91
|
+
statements are built rather than where values are rendered:
|
|
92
|
+
|
|
93
|
+
| Field | Behaviour |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `list_columns` | Loaded and shown in list views |
|
|
96
|
+
| `detail_columns` | Loaded and shown in detail views |
|
|
97
|
+
| `hidden_columns` | Permitted in detail views, **never** loaded by a list query |
|
|
98
|
+
| `excluded_columns` | Never selected, rendered or exported, anywhere |
|
|
99
|
+
|
|
100
|
+
List queries use `load_only()` over `list_columns`, so a hidden column is absent from the
|
|
101
|
+
SQL itself. That matters when a column's SQLAlchemy type decrypts on load: a list page
|
|
102
|
+
neither pays the cost nor can leak the value, even if a template is wrong. `ModelSpec`
|
|
103
|
+
rejects contradictory declarations at construction, so a hidden column named in
|
|
104
|
+
`list_columns` — or an excluded column named as searchable or filterable — is an error you
|
|
105
|
+
get at import time, not a leak you find later.
|
|
106
|
+
|
|
107
|
+
## Search
|
|
108
|
+
|
|
109
|
+
`searchable` columns match with `ILIKE`. `exact_searchable` columns match by equality, and
|
|
110
|
+
`search_transform` is applied to the term first — which is how you search a keyed-digest
|
|
111
|
+
column without this package knowing anything about your hashing:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
ModelSpec(
|
|
115
|
+
...,
|
|
116
|
+
exact_searchable=("iin_digest",),
|
|
117
|
+
search_transform=your_digest_function,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Exact search takes precedence when both are declared, because a digest cannot be matched
|
|
122
|
+
partially.
|
|
123
|
+
|
|
124
|
+
## Pagination
|
|
125
|
+
|
|
126
|
+
Keyset, never `OFFSET`. The cursor is the last row's `order_by` value, coerced to the
|
|
127
|
+
column's Python type — dates and datetimes are parsed with `fromisoformat`. A malformed or
|
|
128
|
+
timezone-naive cursor is treated as absent and yields an unpaginated first page rather than
|
|
129
|
+
an error, because cursors arrive from URLs and URLs get edited.
|
|
130
|
+
|
|
131
|
+
## Custom pages
|
|
132
|
+
|
|
133
|
+
Generic tables cannot do everything. `CustomPage` lets a host contribute its own routes,
|
|
134
|
+
rendered inside the same shell and listed in the same nav:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from admin_litestar import CustomPage
|
|
138
|
+
|
|
139
|
+
dashboard = CustomPage(
|
|
140
|
+
slug="dashboard", label="Dashboard", group="Overview", handlers=[DashboardController]
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Host templates take precedence over the package's, so `AdminConfig(template_dirs=(...))`
|
|
145
|
+
lets you override any template by name while extending `base.html`.
|
|
146
|
+
|
|
147
|
+
## Authentication
|
|
148
|
+
|
|
149
|
+
The package owns the mechanism; you own the policy.
|
|
150
|
+
|
|
151
|
+
- `hash_password` / `verify_password` use `hashlib.scrypt` with `n=16384, r=8, p=1,
|
|
152
|
+
dklen=32`. The encoding is 86 characters, so it fits a `String(128)` column. Anything not
|
|
153
|
+
in that format fails verification — there is no fallback to another scheme.
|
|
154
|
+
- Login failures are counted per username **and** client IP, locking after 5 attempts for
|
|
155
|
+
15 minutes. Deliberately separate from any lockout counter on your own user rows, so
|
|
156
|
+
admin brute-force cannot lock someone out of your main application.
|
|
157
|
+
- Sessions are server-side over a store you supply. `AuthBackend.is_valid` is re-checked on
|
|
158
|
+
every request, cached briefly, so revoking access takes effect in seconds rather than at
|
|
159
|
+
session expiry.
|
|
160
|
+
- CSRF protection covers every mutating route. Templates call `{{ csrf_token() }}`.
|
|
161
|
+
|
|
162
|
+
## Design
|
|
163
|
+
|
|
164
|
+
Dark, near-monochrome, one amber accent, monospace for identifiers — chosen because admin
|
|
165
|
+
data is mostly ids, hashes, addresses and timestamps, which align and scan far better in a
|
|
166
|
+
monospaced column. Light and dark both ship, honouring `prefers-color-scheme` with an
|
|
167
|
+
explicit `data-theme` override.
|
|
168
|
+
|
|
169
|
+
`ModelSpec` validates at construction: unknown column names, a hidden column listed in
|
|
170
|
+
`list_columns`, an excluded column named as searchable, or an unknown capability all raise
|
|
171
|
+
immediately rather than producing an admin that quietly misbehaves.
|
|
172
|
+
|
|
173
|
+
## Status
|
|
174
|
+
|
|
175
|
+
Early. The API has one real consumer, so every protocol here is a considered guess about
|
|
176
|
+
the second one. Expect `0.x` releases to move interfaces, and pin exactly if that matters.
|
|
177
|
+
|
|
178
|
+
`admin_litestar.__all__` is the compatibility promise. Deeper import paths work but carry
|
|
179
|
+
none — see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
180
|
+
|
|
181
|
+
## Documentation
|
|
182
|
+
|
|
183
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md) — layout, layering, where each guarantee is enforced
|
|
184
|
+
- [CHANGELOG.md](CHANGELOG.md) — what moved and when
|
|
185
|
+
- [RELEASING.md](RELEASING.md) — how a release is cut
|
|
186
|
+
|
|
187
|
+
## Licence
|
|
188
|
+
|
|
189
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "admin-litestar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Server-rendered admin for Litestar + SQLAlchemy applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { file = "LICENSE" }
|
|
8
|
+
authors = [{ name = "Omirtay Adilkhan", email = "aomertayevich@gmail.com" }]
|
|
9
|
+
keywords = ["litestar", "sqlalchemy", "admin", "htmx", "asgi"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Framework :: AsyncIO",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"litestar>=2.24.0",
|
|
27
|
+
"sqlalchemy>=2.0",
|
|
28
|
+
"jinja2>=3.1",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/adllkhan/admin-litestar"
|
|
33
|
+
Repository = "https://github.com/adllkhan/admin-litestar"
|
|
34
|
+
Issues = "https://github.com/adllkhan/admin-litestar/issues"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=9.1.1",
|
|
39
|
+
"pytest-asyncio>=1.4.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["hatchling"]
|
|
44
|
+
build-backend = "hatchling.build"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/admin_litestar"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = ["src", "tests", "README.md", "LICENSE", "pyproject.toml"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Server-rendered admin for Litestar + SQLAlchemy applications.
|
|
2
|
+
|
|
3
|
+
Everything a host application needs is exported here. Deeper import paths such
|
|
4
|
+
as ``admin_litestar.queries`` happen to work, but the compatibility promise is
|
|
5
|
+
this module's ``__all__`` and nothing below it — see ARCHITECTURE.md.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
from .admin import Admin, AdminConfig
|
|
11
|
+
from .auth import actor_of
|
|
12
|
+
from .constants import CAPABILITIES, DELETE, DETAIL, EXPORT, LIST
|
|
13
|
+
from .export import csv_rows
|
|
14
|
+
from .pages import CustomPage
|
|
15
|
+
from .passwords import hash_password, verify_password
|
|
16
|
+
from .protocols import AuditSink, AuthBackend, CacheBackend
|
|
17
|
+
from .queries import count_statement, detail_statement, list_statement
|
|
18
|
+
from .render import is_htmx, project, render_value
|
|
19
|
+
from .spec import ModelSpec
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__ = version("admin-litestar")
|
|
23
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
24
|
+
__version__ = "0.0.0.dev0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Admin",
|
|
28
|
+
"AdminConfig",
|
|
29
|
+
"AuditSink",
|
|
30
|
+
"AuthBackend",
|
|
31
|
+
"CAPABILITIES",
|
|
32
|
+
"CacheBackend",
|
|
33
|
+
"CustomPage",
|
|
34
|
+
"DELETE",
|
|
35
|
+
"DETAIL",
|
|
36
|
+
"EXPORT",
|
|
37
|
+
"LIST",
|
|
38
|
+
"ModelSpec",
|
|
39
|
+
"__version__",
|
|
40
|
+
"actor_of",
|
|
41
|
+
"count_statement",
|
|
42
|
+
"csv_rows",
|
|
43
|
+
"detail_statement",
|
|
44
|
+
"hash_password",
|
|
45
|
+
"is_htmx",
|
|
46
|
+
"list_statement",
|
|
47
|
+
"project",
|
|
48
|
+
"render_value",
|
|
49
|
+
"verify_password",
|
|
50
|
+
]
|