hxadmin 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.
- hxadmin-0.1.0/.gitignore +26 -0
- hxadmin-0.1.0/CHANGELOG.md +159 -0
- hxadmin-0.1.0/LICENSE +21 -0
- hxadmin-0.1.0/PKG-INFO +91 -0
- hxadmin-0.1.0/README.md +52 -0
- hxadmin-0.1.0/docs/actions.md +55 -0
- hxadmin-0.1.0/docs/auth.md +46 -0
- hxadmin-0.1.0/docs/export.md +45 -0
- hxadmin-0.1.0/docs/forms.md +63 -0
- hxadmin-0.1.0/docs/getting-started.md +69 -0
- hxadmin-0.1.0/docs/index.md +30 -0
- hxadmin-0.1.0/docs/lists.md +63 -0
- hxadmin-0.1.0/docs/pages.md +33 -0
- hxadmin-0.1.0/docs/templates.md +88 -0
- hxadmin-0.1.0/docs/views.md +72 -0
- hxadmin-0.1.0/pyproject.toml +189 -0
- hxadmin-0.1.0/src/hxadmin/__init__.py +22 -0
- hxadmin-0.1.0/src/hxadmin/actions.py +81 -0
- hxadmin-0.1.0/src/hxadmin/admin.py +370 -0
- hxadmin-0.1.0/src/hxadmin/components.py +74 -0
- hxadmin-0.1.0/src/hxadmin/deps.py +11 -0
- hxadmin-0.1.0/src/hxadmin/export.py +176 -0
- hxadmin-0.1.0/src/hxadmin/fields.py +233 -0
- hxadmin-0.1.0/src/hxadmin/filters.py +341 -0
- hxadmin-0.1.0/src/hxadmin/forms.py +305 -0
- hxadmin-0.1.0/src/hxadmin/icons.py +59 -0
- hxadmin-0.1.0/src/hxadmin/nav.py +109 -0
- hxadmin-0.1.0/src/hxadmin/origin.py +36 -0
- hxadmin-0.1.0/src/hxadmin/pages.py +72 -0
- hxadmin-0.1.0/src/hxadmin/pk.py +102 -0
- hxadmin-0.1.0/src/hxadmin/py.typed +0 -0
- hxadmin-0.1.0/src/hxadmin/query.py +193 -0
- hxadmin-0.1.0/src/hxadmin/routes.py +655 -0
- hxadmin-0.1.0/src/hxadmin/static/hxadmin.css +2 -0
- hxadmin-0.1.0/src/hxadmin/static/src/hxadmin.css +243 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.LICENSE +24 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.css +7 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.min.js +1 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-anchor.min.js +1 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-focus.min.js +18 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-plugins.LICENSE +98 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/alpine.min.js +21 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/htmx.min.js +1 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/lucide.LICENSE +43 -0
- hxadmin-0.1.0/src/hxadmin/static/vendor/lucide.json +1 -0
- hxadmin-0.1.0/src/hxadmin/templates/_macros.html +269 -0
- hxadmin-0.1.0/src/hxadmin/templates/_palette.html +32 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/badge.html +27 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/button.html +79 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/card.html +34 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/date.html +34 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/dialog.html +62 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/feedback.html +85 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/icon.html +14 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/menu.html +117 -0
- hxadmin-0.1.0/src/hxadmin/templates/components/misc.html +40 -0
- hxadmin-0.1.0/src/hxadmin/templates/dashboard.html +26 -0
- hxadmin-0.1.0/src/hxadmin/templates/detail/_panel.html +66 -0
- hxadmin-0.1.0/src/hxadmin/templates/detail/_related_plain.html +7 -0
- hxadmin-0.1.0/src/hxadmin/templates/detail.html +9 -0
- hxadmin-0.1.0/src/hxadmin/templates/error.html +17 -0
- hxadmin-0.1.0/src/hxadmin/templates/form/_field.html +65 -0
- hxadmin-0.1.0/src/hxadmin/templates/form/_fields.html +22 -0
- hxadmin-0.1.0/src/hxadmin/templates/form/_options.html +10 -0
- hxadmin-0.1.0/src/hxadmin/templates/form.html +15 -0
- hxadmin-0.1.0/src/hxadmin/templates/layout.html +527 -0
- hxadmin-0.1.0/src/hxadmin/templates/list/_pagination.html +25 -0
- hxadmin-0.1.0/src/hxadmin/templates/list/_table.html +110 -0
- hxadmin-0.1.0/src/hxadmin/templates/list/_toolbar.html +19 -0
- hxadmin-0.1.0/src/hxadmin/templates/list.html +14 -0
- hxadmin-0.1.0/src/hxadmin/templates/page.html +7 -0
- hxadmin-0.1.0/src/hxadmin/toasts.py +54 -0
- hxadmin-0.1.0/src/hxadmin/users.py +24 -0
- hxadmin-0.1.0/src/hxadmin/views.py +242 -0
- hxadmin-0.1.0/tests/__init__.py +0 -0
- hxadmin-0.1.0/tests/conftest.py +265 -0
- hxadmin-0.1.0/tests/html.py +31 -0
- hxadmin-0.1.0/tests/js.py +57 -0
- hxadmin-0.1.0/tests/test_action_permissions.py +55 -0
- hxadmin-0.1.0/tests/test_action_routes.py +432 -0
- hxadmin-0.1.0/tests/test_action_ui.py +157 -0
- hxadmin-0.1.0/tests/test_actions.py +131 -0
- hxadmin-0.1.0/tests/test_admin.py +117 -0
- hxadmin-0.1.0/tests/test_auth.py +109 -0
- hxadmin-0.1.0/tests/test_badges.py +107 -0
- hxadmin-0.1.0/tests/test_components.py +369 -0
- hxadmin-0.1.0/tests/test_create_edit.py +631 -0
- hxadmin-0.1.0/tests/test_dashboard.py +187 -0
- hxadmin-0.1.0/tests/test_date_input.py +211 -0
- hxadmin-0.1.0/tests/test_delete.py +133 -0
- hxadmin-0.1.0/tests/test_dependencies.py +41 -0
- hxadmin-0.1.0/tests/test_detail.py +351 -0
- hxadmin-0.1.0/tests/test_export.py +205 -0
- hxadmin-0.1.0/tests/test_export_routes.py +149 -0
- hxadmin-0.1.0/tests/test_fields.py +182 -0
- hxadmin-0.1.0/tests/test_filter_ui.py +365 -0
- hxadmin-0.1.0/tests/test_filters.py +349 -0
- hxadmin-0.1.0/tests/test_forms.py +312 -0
- hxadmin-0.1.0/tests/test_icons.py +87 -0
- hxadmin-0.1.0/tests/test_list.py +291 -0
- hxadmin-0.1.0/tests/test_list_contracts.py +260 -0
- hxadmin-0.1.0/tests/test_lookup.py +162 -0
- hxadmin-0.1.0/tests/test_nav.py +80 -0
- hxadmin-0.1.0/tests/test_origin.py +186 -0
- hxadmin-0.1.0/tests/test_package.py +30 -0
- hxadmin-0.1.0/tests/test_pages.py +269 -0
- hxadmin-0.1.0/tests/test_palette.py +214 -0
- hxadmin-0.1.0/tests/test_query.py +305 -0
- hxadmin-0.1.0/tests/test_render.py +57 -0
- hxadmin-0.1.0/tests/test_save_feedback.py +162 -0
- hxadmin-0.1.0/tests/test_shell.py +247 -0
- hxadmin-0.1.0/tests/test_static.py +51 -0
- hxadmin-0.1.0/tests/test_table_models.py +177 -0
- hxadmin-0.1.0/tests/test_toasts.py +172 -0
- hxadmin-0.1.0/tests/test_users.py +47 -0
- hxadmin-0.1.0/tests/test_version.py +5 -0
- hxadmin-0.1.0/tests/test_views.py +299 -0
- hxadmin-0.1.0/zensical.toml +17 -0
hxadmin-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Tooling
|
|
13
|
+
.nox/
|
|
14
|
+
.coverage
|
|
15
|
+
.coverage.*
|
|
16
|
+
htmlcov/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
node_modules/
|
|
20
|
+
|
|
21
|
+
# Demo
|
|
22
|
+
demo/demo.db
|
|
23
|
+
demo_tables/demo_tables.db
|
|
24
|
+
|
|
25
|
+
# Docs
|
|
26
|
+
site/
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-10-08
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Add ModelView base and test scaffolding
|
|
10
|
+
- Mount HxAdmin sub-app with dashboard and layout
|
|
11
|
+
- Redirect unauthenticated admin requests to login_url
|
|
12
|
+
- Build sidebar navigation from registered views
|
|
13
|
+
- Vendor htmx, alpine and compiled tailwind assets
|
|
14
|
+
- Add demo todo app for manual testing
|
|
15
|
+
- Derive Field and RelationField from SQLAlchemy mappers
|
|
16
|
+
- Resolve list, detail, sort and pk config on ModelView
|
|
17
|
+
- Add list query params, search, sort, pagination and fetch_one
|
|
18
|
+
- Add searchable, sortable, paginated model list page
|
|
19
|
+
- Add model detail page with lazy related-collection tabs
|
|
20
|
+
- Carry default, unique, autoincrement, choices and widget on fields
|
|
21
|
+
- Resolve form fields and build pydantic schemas per view
|
|
22
|
+
- Parse, validate and apply form values; narrow fetch_one eager loads
|
|
23
|
+
- Add create, edit and delete with inline validation errors
|
|
24
|
+
- Add relation combobox backed by _lookup search
|
|
25
|
+
- Add toasts via HX-Trigger and a one-shot flash cookie
|
|
26
|
+
- Declare row and bulk actions with @action on ModelView
|
|
27
|
+
- Add row and bulk action endpoints with toast responses
|
|
28
|
+
- Render row actions, bulk selection bar and htmx confirm modal
|
|
29
|
+
- Add admin.page and admin.route custom pages in the admin shell
|
|
30
|
+
- Show row counts on the dashboard and add top-bar global search
|
|
31
|
+
- Declare list filters and apply them from f.* query params
|
|
32
|
+
- Add the list filter panel and removable filter chips
|
|
33
|
+
- Write CSV and XLSX exports with typed cells behind the xlsx extra
|
|
34
|
+
- Export the list as shown or the selection from the list page
|
|
35
|
+
- Toast after create, edit and delete; let on_save raise FormError
|
|
36
|
+
- Gate actions with is_action_allowed; show row controls without hover
|
|
37
|
+
- Semantic design tokens and inline Lucide icons
|
|
38
|
+
- Component macro library with popover, menu and dialog behaviour
|
|
39
|
+
- Command palette route and results partial
|
|
40
|
+
- Redesigned shell with collapsible rail sidebar and user menu
|
|
41
|
+
- Dashboard cards, empty-state error page and stat-based demo stats
|
|
42
|
+
- Merge the dashboard, error page and docs redesign
|
|
43
|
+
- Redesign list filters as per-field popovers with OOB summaries
|
|
44
|
+
- Add ModelView.badges for tone-mapped enum/bool cells
|
|
45
|
+
- Float the bulk-selection bar over the list and share its Alpine scope
|
|
46
|
+
- Redesign list table with a row-action menu and mobile card view
|
|
47
|
+
- Redesign detail page header with actions menu and badges
|
|
48
|
+
- Redesign form layout, confirm dialog and toasts
|
|
49
|
+
- Add a themed date/datetime/time picker component
|
|
50
|
+
- Merge the detail, form, dialog, toast and date picker redesign
|
|
51
|
+
- Support plain Core tables mapped with __table__
|
|
52
|
+
- Add a demo built only on plain Core tables
|
|
53
|
+
- Refuse cross-origin writes from browsers
|
|
54
|
+
|
|
55
|
+
### Bug fixes
|
|
56
|
+
|
|
57
|
+
- Resolve admin links from request root_path, not mount prefix
|
|
58
|
+
- Install tailwindcss before compiling in nox css session
|
|
59
|
+
- Enforce can_view on related rows and keep list state across swaps
|
|
60
|
+
- Scope relation lookups on save through the target view and harden forms
|
|
61
|
+
- Roll back half-applied edits on unknown selection; keep identifying relations read-only on edit
|
|
62
|
+
- Honour model defaults on create, require non-nullable values on edit, roll back failed hooks
|
|
63
|
+
- Toast HTTP errors on htmx requests and redirect to login only for auth failures
|
|
64
|
+
- Serialise JSON-able page results, allow split-method page routes, reject duplicate pages
|
|
65
|
+
- Send Allow on action 405s and keep HTTPException headers in error responses
|
|
66
|
+
- Keep a handler's own page context key; expose the registration as admin_page
|
|
67
|
+
- Truncate long flash messages so the cookie is not dropped
|
|
68
|
+
- Return native list actions to the list with its search, sort and page
|
|
69
|
+
- Count dashboard rows in one query and isolate views whose counts fail
|
|
70
|
+
- Re-render the detail heading and buttons with the panel after an action
|
|
71
|
+
- Drop filter and pk values outside the 64-bit integer range
|
|
72
|
+
- Read only visible filters, and none on the related tab
|
|
73
|
+
- Reject reverse one-to-one relations as list filters
|
|
74
|
+
- Write NaN and infinite numbers as Excel errors in XLSX exports
|
|
75
|
+
- Keep range bounds and pks that Postgres would reject out of the query
|
|
76
|
+
- Keep range bounds in the form their inputs write
|
|
77
|
+
- Compute toast labels where lazy loads work
|
|
78
|
+
- Return to the list as shown after delete and detail-less save
|
|
79
|
+
- Show the active filter count on the Filters toggle
|
|
80
|
+
- Move sidebar toggle to icon button and restore full breadcrumbs
|
|
81
|
+
- Force the date picker's built-in locale to English
|
|
82
|
+
- Disable htmx settle so swapped popovers keep their Alpine state
|
|
83
|
+
- Render list relation cells as muted text and link only the primary column
|
|
84
|
+
- Use the themed date picker in date, datetime and time range filters
|
|
85
|
+
- Load the date picker stylesheet before the theme so its overrides apply
|
|
86
|
+
- Size list columns to content so the primary column takes the remaining width
|
|
87
|
+
- Show the list row count and stop repeating the page title in breadcrumbs
|
|
88
|
+
- Align the list toolbar on one 32px row and scroll filters on mobile
|
|
89
|
+
- Inset icon-less action items so menu labels line up
|
|
90
|
+
- Make the select-all checkbox select every row and show its indeterminate state
|
|
91
|
+
- Highlight selected list rows and cards
|
|
92
|
+
- Make form inputs 36px high and align the sticky footer with the form
|
|
93
|
+
- Listen for htmx 4 event names so the palette, bulk bar and date picker react to swaps
|
|
94
|
+
- Load palette results when it opens and anchor it near the top
|
|
95
|
+
- Give the error page a clear heading, explanation and way back
|
|
96
|
+
- Drop the date picker's white pointer arrow and tighten its offset
|
|
97
|
+
- Reach the palette on mobile and hide the filter row's scrollbar
|
|
98
|
+
- Stack the detail header on mobile so the title is not truncated
|
|
99
|
+
- Give form selects the same chevron and padding as the comboboxes
|
|
100
|
+
- Close the mobile drawer with an X instead of the desktop rail toggle
|
|
101
|
+
- Keep list pagination on one row on mobile
|
|
102
|
+
- Render the user menu for any auth return value
|
|
103
|
+
- Keep the theme menu when auth returns no user
|
|
104
|
+
- Stop palette typing from re-querying the list behind it
|
|
105
|
+
- Clear only the chosen filter, not filters sharing its prefix
|
|
106
|
+
- Reset the toolbar form from the empty state's Reset filters
|
|
107
|
+
- Submit typed dates and stop date-only values shifting a day
|
|
108
|
+
- Stop nesting a neutral badge round the detail header badges
|
|
109
|
+
- Give the mobile card row menus their own ids
|
|
110
|
+
- Show closed nav groups' items in the rail and hide them pre-paint
|
|
111
|
+
- Set the palette's aria-activedescendant when results load
|
|
112
|
+
- Reject badges values a column cannot hold
|
|
113
|
+
- Remove the phase 5 macros and helpers nothing uses
|
|
114
|
+
- Leave inaccessible views out of the sidebar and palette
|
|
115
|
+
- Quote the date input's x-data like every other JSON attribute
|
|
116
|
+
- Announce error toasts as alerts, link related tabs, show Ctrl off Mac
|
|
117
|
+
- Drop empty params from toolbar requests and pushed URLs
|
|
118
|
+
- Draw checkboxes and radios with the theme tokens
|
|
119
|
+
- Keep the table header visible while the rows scroll
|
|
120
|
+
- Match the spec's page title size, pager label and form alignment
|
|
121
|
+
- Lay the detail body out as a two-column description list
|
|
122
|
+
- Put short form fields two to a row
|
|
123
|
+
- Stop heading a single dashboard card with its category
|
|
124
|
+
- Keep the rail's brand row aligned with the 56px top bar
|
|
125
|
+
- Tidy the mobile list toolbar, selection and bulk bar
|
|
126
|
+
- Open filter calendars beside their popover, not over it
|
|
127
|
+
- Show the pointer cursor on buttons, links and menu items
|
|
128
|
+
- Write the palette shortcut as Ctrl+K like Ctrl+B
|
|
129
|
+
- Disable autoflush inside forms.apply
|
|
130
|
+
- Harden list ordering, lookups, form bounds and database error handling
|
|
131
|
+
|
|
132
|
+
### Refactoring
|
|
133
|
+
|
|
134
|
+
- Make ModelView and register generic, inherit view naming
|
|
135
|
+
- Move primary-key helpers to a leaf module; add demo __str__
|
|
136
|
+
- One relation scope resolver, built once per relation filter
|
|
137
|
+
- Group one-model dashboard categories in Python
|
|
138
|
+
- Resolve form widgets and widths in Python
|
|
139
|
+
- Keep the date input formats in one map
|
|
140
|
+
- Render both select-all boxes from one macro
|
|
141
|
+
- Split the list reset into two named helpers
|
|
142
|
+
|
|
143
|
+
### Documentation
|
|
144
|
+
|
|
145
|
+
- Add HxAdmin usage to README
|
|
146
|
+
- Configure demo views and document list/detail options
|
|
147
|
+
- Configure demo forms and document form options
|
|
148
|
+
- Add Task.priority to the demo to exercise defaulted non-nullable columns
|
|
149
|
+
- Demo actions and custom pages; document them in the README
|
|
150
|
+
- GET actions must be read-only
|
|
151
|
+
- Add user docs built with Zensical and slim the README
|
|
152
|
+
- Demo filters, built-in export and FormError
|
|
153
|
+
- Relation option order, bound normalisation and Excel limits
|
|
154
|
+
- Describe the redesigned list, detail, forms, shell and components
|
|
155
|
+
- Mention the list row count, row highlight and breadcrumb rule
|
|
156
|
+
- Describe the list selection and mobile toolbar as shipped
|
|
157
|
+
- Turn the besidePanel comment into JSDoc
|
|
158
|
+
|
|
159
|
+
<!-- generated by git-cliff -->
|
hxadmin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gledi Caushaj
|
|
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.
|
hxadmin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hxadmin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Admin interface for FastAPI and SQLAlchemy 2.0+, built with Tailwind CSS and htmx
|
|
5
|
+
Project-URL: Homepage, https://github.com/gledi-ai/hxadmin
|
|
6
|
+
Project-URL: Documentation, https://github.com/gledi-ai/hxadmin/tree/main/docs
|
|
7
|
+
Project-URL: Source, https://github.com/gledi-ai/hxadmin
|
|
8
|
+
Project-URL: Issues, https://github.com/gledi-ai/hxadmin/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/gledi-ai/hxadmin/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Gledi Caushaj
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: admin,fastapi,htmx,sqlalchemy,tailwind
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: AsyncIO
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Classifier: Framework :: Pydantic :: 2
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.13
|
|
28
|
+
Requires-Dist: anyio<5.0,>=4.10
|
|
29
|
+
Requires-Dist: fastapi<1.0,>=0.139
|
|
30
|
+
Requires-Dist: jinja2<4.0,>=3.1.6
|
|
31
|
+
Requires-Dist: markupsafe<4.0,>=3.0
|
|
32
|
+
Requires-Dist: pydantic<3.0,>=2.12
|
|
33
|
+
Requires-Dist: python-multipart<1.0.0,>=0.0.30
|
|
34
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0.45
|
|
35
|
+
Requires-Dist: starlette<2.0,>=1.2
|
|
36
|
+
Provides-Extra: xlsx
|
|
37
|
+
Requires-Dist: xlsxwriter<4,>=3.2; extra == 'xlsx'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# hxadmin
|
|
41
|
+
|
|
42
|
+
Admin interface for FastAPI and async SQLAlchemy 2.0+, built with Jinja2, htmx, Alpine.js and Tailwind CSS.
|
|
43
|
+
|
|
44
|
+
List, detail, create, edit and delete from class attributes, with search, sort, pagination, per-column filters, row and bulk actions, custom pages, CSV / Excel export and light / dark themes. Async only and fully typed.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install hxadmin # CSV export included
|
|
50
|
+
pip install 'hxadmin[xlsx]' # adds Excel export
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from fastapi import FastAPI
|
|
57
|
+
from hxadmin import HxAdmin, ModelView
|
|
58
|
+
|
|
59
|
+
app = FastAPI()
|
|
60
|
+
admin = HxAdmin(app, session=get_session, auth=current_admin_user, title="My admin")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@admin.register
|
|
64
|
+
class UserView(ModelView[User]):
|
|
65
|
+
model = User
|
|
66
|
+
list_columns = ("email", "name", "group")
|
|
67
|
+
searchable = ("email", "name")
|
|
68
|
+
list_filters = ("active", "group")
|
|
69
|
+
export_formats = ("csv", "xlsx")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`session` yields an `AsyncSession`; `auth` returns the current user or raises `HTTPException(401)`. Open `/admin/`.
|
|
73
|
+
|
|
74
|
+
Documentation: [docs](https://github.com/gledi-ai/hxadmin/tree/main/docs) (build locally with `nox -s docs`). Try everything in the demo: `uv run python -m demo`, then open http://127.0.0.1:8001/admin/. For projects built on plain Core `Table`s instead of ORM models, see `uv run python -m demo_tables` (http://127.0.0.1:8002/admin/).
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uv sync # installs the dev group (lint + test + ipython)
|
|
80
|
+
prek install # git hooks: ruff, pyrefly, uv-lock
|
|
81
|
+
nox # lint + tests on every supported Python
|
|
82
|
+
nox -s tests-3.14 # single Python version
|
|
83
|
+
nox -s tests_lowest # tests against the lowest supported dependency versions
|
|
84
|
+
nox -s tests_latest # tests against the newest allowed releases (weekly in CI)
|
|
85
|
+
nox -s wheel # tests against the built wheel
|
|
86
|
+
nox -s audit # dependency vulnerability audit
|
|
87
|
+
nox -s docs # build the docs strictly into site/
|
|
88
|
+
nox -s css # rebuild hxadmin.css after template changes (needs Node.js)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Versions come from git tags (`v0.1.0`) via uv-dynamic-versioning.
|
hxadmin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# hxadmin
|
|
2
|
+
|
|
3
|
+
Admin interface for FastAPI and async SQLAlchemy 2.0+, built with Jinja2, htmx, Alpine.js and Tailwind CSS.
|
|
4
|
+
|
|
5
|
+
List, detail, create, edit and delete from class attributes, with search, sort, pagination, per-column filters, row and bulk actions, custom pages, CSV / Excel export and light / dark themes. Async only and fully typed.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hxadmin # CSV export included
|
|
11
|
+
pip install 'hxadmin[xlsx]' # adds Excel export
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from hxadmin import HxAdmin, ModelView
|
|
19
|
+
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
admin = HxAdmin(app, session=get_session, auth=current_admin_user, title="My admin")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@admin.register
|
|
25
|
+
class UserView(ModelView[User]):
|
|
26
|
+
model = User
|
|
27
|
+
list_columns = ("email", "name", "group")
|
|
28
|
+
searchable = ("email", "name")
|
|
29
|
+
list_filters = ("active", "group")
|
|
30
|
+
export_formats = ("csv", "xlsx")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`session` yields an `AsyncSession`; `auth` returns the current user or raises `HTTPException(401)`. Open `/admin/`.
|
|
34
|
+
|
|
35
|
+
Documentation: [docs](https://github.com/gledi-ai/hxadmin/tree/main/docs) (build locally with `nox -s docs`). Try everything in the demo: `uv run python -m demo`, then open http://127.0.0.1:8001/admin/. For projects built on plain Core `Table`s instead of ORM models, see `uv run python -m demo_tables` (http://127.0.0.1:8002/admin/).
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync # installs the dev group (lint + test + ipython)
|
|
41
|
+
prek install # git hooks: ruff, pyrefly, uv-lock
|
|
42
|
+
nox # lint + tests on every supported Python
|
|
43
|
+
nox -s tests-3.14 # single Python version
|
|
44
|
+
nox -s tests_lowest # tests against the lowest supported dependency versions
|
|
45
|
+
nox -s tests_latest # tests against the newest allowed releases (weekly in CI)
|
|
46
|
+
nox -s wheel # tests against the built wheel
|
|
47
|
+
nox -s audit # dependency vulnerability audit
|
|
48
|
+
nox -s docs # build the docs strictly into site/
|
|
49
|
+
nox -s css # rebuild hxadmin.css after template changes (needs Node.js)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Versions come from git tags (`v0.1.0`) via uv-dynamic-versioning.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Actions
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
from hxadmin import ActionResult, action
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@admin.register
|
|
10
|
+
class UserView(ModelView[User]):
|
|
11
|
+
model = User
|
|
12
|
+
|
|
13
|
+
@action("deactivate", label="Deactivate", bulk=True, confirm="Deactivate selected users?")
|
|
14
|
+
async def deactivate(
|
|
15
|
+
self, request: Request, session: AsyncSession, objs: Sequence[User]
|
|
16
|
+
) -> ActionResult:
|
|
17
|
+
for user in objs:
|
|
18
|
+
user.active = False
|
|
19
|
+
return ActionResult.message(f"Deactivated {len(objs)} users")
|
|
20
|
+
|
|
21
|
+
@action("reset_password", label="Reset password")
|
|
22
|
+
async def reset_password(
|
|
23
|
+
self, request: Request, session: AsyncSession, obj: User
|
|
24
|
+
) -> ActionResult:
|
|
25
|
+
...
|
|
26
|
+
return ActionResult.message("Reset link sent", level="info")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Row actions (the default) receive one object. They appear in each list row's `⋯` menu and on the detail page: as buttons when a view has at most two and none has `confirm`, otherwise in the detail `⋯` menu. Bulk actions receive the checked rows, always loaded through `get_query`, and appear in the floating bar at the bottom of the viewport once rows are selected (see [Bulk actions](lists.md#bulk-actions)). `confirm` opens the shared confirmation dialog.
|
|
30
|
+
|
|
31
|
+
The result shows as a toast in the bottom-right corner with an icon per level. Toasts pause while hovered, and error toasts stay until dismissed.
|
|
32
|
+
|
|
33
|
+
Return one of:
|
|
34
|
+
|
|
35
|
+
- `ActionResult.message(text, level="success" | "info" | "warning" | "error")`: re-render the view with a toast;
|
|
36
|
+
- `ActionResult.redirect(url)`;
|
|
37
|
+
- `ActionResult.response(response)`: any Starlette response, for example a download.
|
|
38
|
+
|
|
39
|
+
hxadmin commits after the handler returns. Any exception rolls back and shows a "<label> failed." toast (the traceback goes to the `hxadmin` logger), except `HTTPException`, which propagates.
|
|
40
|
+
|
|
41
|
+
POST actions run over htmx. `method="GET"` actions are plain links and form submits, so a returned file downloads. A GET action runs from a plain link, so it must not change data: another site could trigger it. Use POST for anything with side effects.
|
|
42
|
+
|
|
43
|
+
Handlers get objects without eager-loaded relationships: `await session.refresh(obj, ["group"])` before touching one. A row action that deletes its object should return a redirect; if it returns a message, hxadmin falls back to the list.
|
|
44
|
+
|
|
45
|
+
## Permissions
|
|
46
|
+
|
|
47
|
+
Override `is_action_allowed` to limit who may run an action:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
class UserView(ModelView[User]):
|
|
51
|
+
def is_action_allowed(self, request: Request, name: str) -> bool:
|
|
52
|
+
return name != "deactivate" or request.state.hxadmin_user.is_superuser
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A disallowed action is not rendered anywhere, and its routes answer 403. `request.state.hxadmin_user` is the value your `auth` dependency returned.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Auth and sessions
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
admin = HxAdmin(
|
|
5
|
+
app,
|
|
6
|
+
session=get_session, # dependency yielding an AsyncSession
|
|
7
|
+
auth=current_admin_user, # dependency returning the user, or raising HTTPException
|
|
8
|
+
prefix="/admin",
|
|
9
|
+
login_url="/login", # optional
|
|
10
|
+
logout_url="/logout", # optional; shown in the top bar
|
|
11
|
+
trusted_origins=(), # optional; other origins allowed to send writes
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Sessions
|
|
16
|
+
|
|
17
|
+
`session` is any FastAPI dependency that yields or returns an `AsyncSession`. hxadmin uses one session per request, commits after a successful save, delete or action, and rolls back on errors. hxadmin re-queries after commits, so `expire_on_commit=True` sessionmakers work.
|
|
18
|
+
|
|
19
|
+
## Auth
|
|
20
|
+
|
|
21
|
+
`auth` runs on every admin request. Its return value is available as `request.state.hxadmin_user` and as `user` in templates. The top bar's user menu shows `user.name` (an attribute or a mapping key) when it is set, otherwise `str(user)`, with initials derived from it. Any return value works: `None` or a bool (an `auth` that only answers "allowed") shows a generic avatar with no name.
|
|
22
|
+
|
|
23
|
+
To deny access, raise `HTTPException(401)` or `HTTPException(403)`. With `login_url` set, these send the browser to the login page (303, or `HX-Redirect` for htmx requests). Without it, they render an error page.
|
|
24
|
+
|
|
25
|
+
Per view, `is_accessible(request)` and `is_visible(request)` narrow access further. Per action, `is_action_allowed(request, name)` does; see [Actions](actions.md).
|
|
26
|
+
|
|
27
|
+
## Cross-site requests
|
|
28
|
+
|
|
29
|
+
hxadmin refuses state-changing requests (`POST`, `PUT`, `PATCH`, `DELETE`) that a browser sends from another origin, so another site cannot make a signed-in admin's browser create, edit, delete or run an action. This covers every admin route, custom pages included, whatever your `auth` uses (cookies included), and needs no tokens in your templates.
|
|
30
|
+
|
|
31
|
+
- Browsers send `Sec-Fetch-Site`; a write passes when it is `same-origin` (or `none`, a request the user started directly). `same-site` is refused too, so a sibling subdomain cannot write.
|
|
32
|
+
- Browsers too old for `Sec-Fetch-Site` send `Origin`, which must match the request's `Host`.
|
|
33
|
+
- Requests with neither header do not come from a browser (scripts, tests, server-to-server calls) and pass; `auth` still applies.
|
|
34
|
+
- `GET`, `HEAD` and `OPTIONS` always pass, which is why `method="GET"` actions must not change data (see [Actions](actions.md)).
|
|
35
|
+
|
|
36
|
+
A refused request gets a 403 ("Cross-origin request blocked.": an error page, or an error toast for htmx) and a warning on the `hxadmin` logger naming the method, path, `Origin` and `Sec-Fetch-Site`.
|
|
37
|
+
|
|
38
|
+
To accept writes from another origin you control, such as a separate frontend, list it:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
admin = HxAdmin(app, ..., trusted_origins=["https://ops.example.com"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each entry is a bare origin, `scheme://host[:port]`, with no path; anything else raises `ValueError`.
|
|
45
|
+
|
|
46
|
+
Behind a reverse proxy, browsers that only send `Origin` need the proxy to pass the public `Host` header through (e.g. nginx `proxy_set_header Host $host;`). Current browsers send `Sec-Fetch-Site`, which does not depend on the proxy.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Export
|
|
2
|
+
|
|
3
|
+
```python
|
|
4
|
+
@admin.register
|
|
5
|
+
class TaskView(ModelView[Task]):
|
|
6
|
+
model = Task
|
|
7
|
+
export_formats = ("csv", "xlsx") # default (): no export
|
|
8
|
+
export_columns = () # default: list_columns
|
|
9
|
+
export_max_rows = 10_000 # default; None removes the limit
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
CSV needs nothing extra. Excel export needs XlsxWriter:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install 'hxadmin[xlsx]'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Listing `"xlsx"` without it installed raises `ImportError` when the view is registered.
|
|
19
|
+
|
|
20
|
+
## What is exported
|
|
21
|
+
|
|
22
|
+
The list footer links to `/{prefix}/{identity}/_export/csv` (and `xlsx`) with the current search, filters and sort, and the export contains every matching row across all pages. With rows checked, the selection bar offers an export of just those rows. Rows always come through `get_query`.
|
|
23
|
+
|
|
24
|
+
When more rows match than `export_max_rows`, no file is produced: the browser returns to the list with a warning toast.
|
|
25
|
+
|
|
26
|
+
## Cells
|
|
27
|
+
|
|
28
|
+
The header row has the field labels. A column with a `format_<name>` method exports its output; otherwise:
|
|
29
|
+
|
|
30
|
+
| Value | CSV | XLSX |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| empty | empty | blank cell |
|
|
33
|
+
| bool | `true` / `false` | boolean |
|
|
34
|
+
| number | as written | number; NaN is `#NUM!`, infinity `#DIV/0!` |
|
|
35
|
+
| date, datetime, time | ISO 8601 | date cell; aware datetimes converted to UTC |
|
|
36
|
+
| enum | its value | text |
|
|
37
|
+
| many-to-one relation | the related row's `display()` | text |
|
|
38
|
+
| to-many relation | number of rows | number |
|
|
39
|
+
| JSON | JSON text | text |
|
|
40
|
+
|
|
41
|
+
CSV files are UTF-8 with a byte-order mark so Excel detects the encoding. Text cells starting with `=`, `+`, `-`, `@`, tab or carriage return are prefixed with `'` so spreadsheet programs do not run them as formulas; XLSX cells are always written as text, never as formulas.
|
|
42
|
+
|
|
43
|
+
Excel's own limits apply to XLSX files: a cell holds at most 32,767 characters (longer text is cut), dates before 1900 are not real dates to Excel, and a sheet has at most 1,048,576 rows.
|
|
44
|
+
|
|
45
|
+
Files are named `{identity}-{YYYYMMDD-HHMMSS}.{csv|xlsx}` (UTC).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Forms
|
|
2
|
+
|
|
3
|
+
Create and edit forms are derived from the mapper:
|
|
4
|
+
|
|
5
|
+
- autoincrement primary keys, foreign-key columns covered by a relationship, and columns of a type hxadmin has no input for (`LargeBinary`, `ARRAY`, `Interval`, ...) are skipped; naming such a column in `form_fields` raises at registration;
|
|
6
|
+
- `String(n)` columns accept at most `n` characters, and integer columns the range of their type (`SmallInteger` 16-bit, `Integer` 32-bit, `BigInteger` 64-bit);
|
|
7
|
+
- enum columns become selects, booleans checkboxes, dates and times the themed [date picker](#dates-and-times);
|
|
8
|
+
- relationships become a search-as-you-type combobox, backed by the related view's `searchable` columns (without any, it matches each row's display text, scanning the first 2,000 rows, so give large related tables `searchable` columns): a single relation looks like a select showing the current value with a clear `×`, a multiple one shows its values as removable chips;
|
|
9
|
+
- primary keys, including relationships whose foreign keys form the primary key, are read-only on edit.
|
|
10
|
+
|
|
11
|
+
The form is a left-aligned column up to 720px wide. Short fields (selects, numbers, checkboxes, dates and times, single relations) sit two to a row from the `sm` breakpoint; text, JSON and multiple relations take the full width.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from hxadmin import Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@admin.register
|
|
18
|
+
class UserView(ModelView[User]):
|
|
19
|
+
model = User
|
|
20
|
+
form_fields = (
|
|
21
|
+
Field("name", "str", help_text="Full name"),
|
|
22
|
+
Field("email", "str", "E-mail", widget="email"),
|
|
23
|
+
"projects",
|
|
24
|
+
)
|
|
25
|
+
form_exclude = ("password_hash",)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`form_fields` takes names or `Field(...)` overrides for `label`, `widget`, `required`, `help_text` and `readonly`. An empty non-nullable input is left to the model default on create and reported as required on edit. `DateTime(timezone=True)` columns receive naive values from the datetime picker.
|
|
29
|
+
|
|
30
|
+
Each field shows its label (with a red `*` when required), the input, the help text and then the error. The Cancel / Save footer sticks to the bottom of the viewport on long forms; the edit page title carries the row's `#pk`.
|
|
31
|
+
|
|
32
|
+
## Dates and times
|
|
33
|
+
|
|
34
|
+
Date, datetime and time fields use a vendored [Air Datepicker](https://air-datepicker.com), themed with the admin tokens in light and dark. The visible input shows `mm/dd/yyyy` (plus `hh:mm`), while a hidden input carries the submitted ISO value: `YYYY-MM-DD`, `YYYY-MM-DDTHH:MM` or `HH:MM`. Clearing the input with its `×` clears the value. Use it in your own templates through the `date_input` macro:
|
|
35
|
+
|
|
36
|
+
```jinja
|
|
37
|
+
{% import "_macros.html" as m with context %}
|
|
38
|
+
{{ m.date_input("published_at", "2026-09-23T14:30", kind="datetime") }}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`date_input(name, value=None, kind="date" | "datetime" | "time", id=None, required=False, error=False, readonly=False, form=None, class="", value_class="")`: `class` styles the visible input, `value_class` goes on the hidden one, and `form` sets the hidden input's `form=` attribute. Every change fires a bubbling `change` event on the hidden input. Inside a popover, the calendar opens within it on click or ArrowDown.
|
|
42
|
+
|
|
43
|
+
## Saving
|
|
44
|
+
|
|
45
|
+
Validation errors re-render the form in place with status 422, with each message under its field and form-level errors in an alert above the fields. On success, `on_save(request, session, obj, *, created)` runs, then hxadmin commits and redirects with a toast such as *Task "Write docs" created.*
|
|
46
|
+
|
|
47
|
+
Raise `FormError` from `on_save` to reject a save with a message:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from hxadmin import FormError
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TaskView(ModelView[Task]):
|
|
54
|
+
async def on_save(
|
|
55
|
+
self, request: Request, session: AsyncSession, obj: Task, *, created: bool
|
|
56
|
+
) -> None:
|
|
57
|
+
if obj.due_date is not None and obj.due_date < date.today():
|
|
58
|
+
raise FormError("Due date is in the past.", field="due_date")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The session is rolled back and the form re-renders with the message under `field`, or at the top of the form when `field` is omitted or not on the form. Integrity errors (such as a duplicate unique value) and values the database rejects also roll back and show a generic form-level error; the database's own message, which can name constraints and values, goes to the `hxadmin` logger instead. Any other exception rolls back and propagates: htmx requests get a "Something went wrong." toast, page loads the error page.
|
|
62
|
+
|
|
63
|
+
Delete lives in the detail page's `⋯` menu and in each list row's menu, and asks first in the shared confirm dialog. Deleting runs `on_delete(request, session, obj)` first; a row that other rows still reference answers 409 with "other records still refer to it" (an error toast over htmx, else the error page).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install hxadmin # CSV export included
|
|
7
|
+
pip install 'hxadmin[xlsx]' # adds Excel export (XlsxWriter)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
hxadmin needs Python 3.13+, FastAPI and SQLAlchemy 2.0+ with an async driver (`asyncpg`, `aiosqlite`, ...).
|
|
11
|
+
|
|
12
|
+
## A minimal admin
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from collections.abc import AsyncIterator
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
19
|
+
|
|
20
|
+
from hxadmin import HxAdmin, ModelView
|
|
21
|
+
from myapp.models import User
|
|
22
|
+
|
|
23
|
+
engine = create_async_engine("sqlite+aiosqlite:///app.db")
|
|
24
|
+
sessionmaker = async_sessionmaker(engine, expire_on_commit=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_session() -> AsyncIterator[AsyncSession]:
|
|
28
|
+
async with sessionmaker() as session:
|
|
29
|
+
yield session
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def current_admin_user() -> dict[str, str]:
|
|
33
|
+
return {"name": "admin"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
app = FastAPI()
|
|
37
|
+
admin = HxAdmin(app, session=get_session, auth=current_admin_user, title="My admin")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@admin.register
|
|
41
|
+
class UserView(ModelView[User]):
|
|
42
|
+
model = User
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run it with `uvicorn myapp:app` and open `/admin/`. The admin is mounted at `prefix` (default `/admin`); the dashboard lists every registered view with its row count, grouped under a heading per `category`; a category holding a single view joins the ungrouped cards, labelled with its category, instead of heading one card.
|
|
46
|
+
|
|
47
|
+
`session` and `auth` are ordinary FastAPI dependencies; see [Auth and sessions](auth.md).
|
|
48
|
+
|
|
49
|
+
## Branding
|
|
50
|
+
|
|
51
|
+
Pass `logo_url` for an image in the sidebar brand block:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
admin = HxAdmin(
|
|
55
|
+
app, session=get_session, auth=current_admin_user, title="My admin", logo_url="/static/logo.svg"
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Without it, the brand block shows the title's first letter in an accent tile.
|
|
60
|
+
|
|
61
|
+
## The demo
|
|
62
|
+
|
|
63
|
+
The repository ships a small todo app that exercises every feature:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv run python -m demo
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then open <http://127.0.0.1:8001/admin/>.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# hxadmin
|
|
2
|
+
|
|
3
|
+
Admin interface for FastAPI and async SQLAlchemy 2.0+, rendered with Jinja2, htmx, Alpine.js and Tailwind CSS.
|
|
4
|
+
|
|
5
|
+
- One `ModelView` class per model: list, detail, create, edit and delete from class attributes.
|
|
6
|
+
- Search, sort, pagination and per-column filters on every list.
|
|
7
|
+
- Row and bulk actions with a shared confirmation dialog and toasts.
|
|
8
|
+
- Custom pages that share the admin shell.
|
|
9
|
+
- CSV export built in; Excel `.xlsx` export with the `xlsx` extra.
|
|
10
|
+
- Light, dark and system themes.
|
|
11
|
+
- Async only, fully typed (`py.typed`), no WTForms, no sync engines.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from hxadmin import HxAdmin, ModelView
|
|
16
|
+
|
|
17
|
+
app = FastAPI()
|
|
18
|
+
admin = HxAdmin(app, session=get_session, auth=current_admin_user)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@admin.register
|
|
22
|
+
class UserView(ModelView[User]):
|
|
23
|
+
model = User
|
|
24
|
+
list_columns = ("email", "name", "group")
|
|
25
|
+
searchable = ("email", "name")
|
|
26
|
+
list_filters = ("active", "group")
|
|
27
|
+
export_formats = ("csv",)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Start with [Getting started](getting-started.md).
|