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.
Files changed (118) hide show
  1. hxadmin-0.1.0/.gitignore +26 -0
  2. hxadmin-0.1.0/CHANGELOG.md +159 -0
  3. hxadmin-0.1.0/LICENSE +21 -0
  4. hxadmin-0.1.0/PKG-INFO +91 -0
  5. hxadmin-0.1.0/README.md +52 -0
  6. hxadmin-0.1.0/docs/actions.md +55 -0
  7. hxadmin-0.1.0/docs/auth.md +46 -0
  8. hxadmin-0.1.0/docs/export.md +45 -0
  9. hxadmin-0.1.0/docs/forms.md +63 -0
  10. hxadmin-0.1.0/docs/getting-started.md +69 -0
  11. hxadmin-0.1.0/docs/index.md +30 -0
  12. hxadmin-0.1.0/docs/lists.md +63 -0
  13. hxadmin-0.1.0/docs/pages.md +33 -0
  14. hxadmin-0.1.0/docs/templates.md +88 -0
  15. hxadmin-0.1.0/docs/views.md +72 -0
  16. hxadmin-0.1.0/pyproject.toml +189 -0
  17. hxadmin-0.1.0/src/hxadmin/__init__.py +22 -0
  18. hxadmin-0.1.0/src/hxadmin/actions.py +81 -0
  19. hxadmin-0.1.0/src/hxadmin/admin.py +370 -0
  20. hxadmin-0.1.0/src/hxadmin/components.py +74 -0
  21. hxadmin-0.1.0/src/hxadmin/deps.py +11 -0
  22. hxadmin-0.1.0/src/hxadmin/export.py +176 -0
  23. hxadmin-0.1.0/src/hxadmin/fields.py +233 -0
  24. hxadmin-0.1.0/src/hxadmin/filters.py +341 -0
  25. hxadmin-0.1.0/src/hxadmin/forms.py +305 -0
  26. hxadmin-0.1.0/src/hxadmin/icons.py +59 -0
  27. hxadmin-0.1.0/src/hxadmin/nav.py +109 -0
  28. hxadmin-0.1.0/src/hxadmin/origin.py +36 -0
  29. hxadmin-0.1.0/src/hxadmin/pages.py +72 -0
  30. hxadmin-0.1.0/src/hxadmin/pk.py +102 -0
  31. hxadmin-0.1.0/src/hxadmin/py.typed +0 -0
  32. hxadmin-0.1.0/src/hxadmin/query.py +193 -0
  33. hxadmin-0.1.0/src/hxadmin/routes.py +655 -0
  34. hxadmin-0.1.0/src/hxadmin/static/hxadmin.css +2 -0
  35. hxadmin-0.1.0/src/hxadmin/static/src/hxadmin.css +243 -0
  36. hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.LICENSE +24 -0
  37. hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.css +7 -0
  38. hxadmin-0.1.0/src/hxadmin/static/vendor/air-datepicker.min.js +1 -0
  39. hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-anchor.min.js +1 -0
  40. hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-focus.min.js +18 -0
  41. hxadmin-0.1.0/src/hxadmin/static/vendor/alpine-plugins.LICENSE +98 -0
  42. hxadmin-0.1.0/src/hxadmin/static/vendor/alpine.min.js +21 -0
  43. hxadmin-0.1.0/src/hxadmin/static/vendor/htmx.min.js +1 -0
  44. hxadmin-0.1.0/src/hxadmin/static/vendor/lucide.LICENSE +43 -0
  45. hxadmin-0.1.0/src/hxadmin/static/vendor/lucide.json +1 -0
  46. hxadmin-0.1.0/src/hxadmin/templates/_macros.html +269 -0
  47. hxadmin-0.1.0/src/hxadmin/templates/_palette.html +32 -0
  48. hxadmin-0.1.0/src/hxadmin/templates/components/badge.html +27 -0
  49. hxadmin-0.1.0/src/hxadmin/templates/components/button.html +79 -0
  50. hxadmin-0.1.0/src/hxadmin/templates/components/card.html +34 -0
  51. hxadmin-0.1.0/src/hxadmin/templates/components/date.html +34 -0
  52. hxadmin-0.1.0/src/hxadmin/templates/components/dialog.html +62 -0
  53. hxadmin-0.1.0/src/hxadmin/templates/components/feedback.html +85 -0
  54. hxadmin-0.1.0/src/hxadmin/templates/components/icon.html +14 -0
  55. hxadmin-0.1.0/src/hxadmin/templates/components/menu.html +117 -0
  56. hxadmin-0.1.0/src/hxadmin/templates/components/misc.html +40 -0
  57. hxadmin-0.1.0/src/hxadmin/templates/dashboard.html +26 -0
  58. hxadmin-0.1.0/src/hxadmin/templates/detail/_panel.html +66 -0
  59. hxadmin-0.1.0/src/hxadmin/templates/detail/_related_plain.html +7 -0
  60. hxadmin-0.1.0/src/hxadmin/templates/detail.html +9 -0
  61. hxadmin-0.1.0/src/hxadmin/templates/error.html +17 -0
  62. hxadmin-0.1.0/src/hxadmin/templates/form/_field.html +65 -0
  63. hxadmin-0.1.0/src/hxadmin/templates/form/_fields.html +22 -0
  64. hxadmin-0.1.0/src/hxadmin/templates/form/_options.html +10 -0
  65. hxadmin-0.1.0/src/hxadmin/templates/form.html +15 -0
  66. hxadmin-0.1.0/src/hxadmin/templates/layout.html +527 -0
  67. hxadmin-0.1.0/src/hxadmin/templates/list/_pagination.html +25 -0
  68. hxadmin-0.1.0/src/hxadmin/templates/list/_table.html +110 -0
  69. hxadmin-0.1.0/src/hxadmin/templates/list/_toolbar.html +19 -0
  70. hxadmin-0.1.0/src/hxadmin/templates/list.html +14 -0
  71. hxadmin-0.1.0/src/hxadmin/templates/page.html +7 -0
  72. hxadmin-0.1.0/src/hxadmin/toasts.py +54 -0
  73. hxadmin-0.1.0/src/hxadmin/users.py +24 -0
  74. hxadmin-0.1.0/src/hxadmin/views.py +242 -0
  75. hxadmin-0.1.0/tests/__init__.py +0 -0
  76. hxadmin-0.1.0/tests/conftest.py +265 -0
  77. hxadmin-0.1.0/tests/html.py +31 -0
  78. hxadmin-0.1.0/tests/js.py +57 -0
  79. hxadmin-0.1.0/tests/test_action_permissions.py +55 -0
  80. hxadmin-0.1.0/tests/test_action_routes.py +432 -0
  81. hxadmin-0.1.0/tests/test_action_ui.py +157 -0
  82. hxadmin-0.1.0/tests/test_actions.py +131 -0
  83. hxadmin-0.1.0/tests/test_admin.py +117 -0
  84. hxadmin-0.1.0/tests/test_auth.py +109 -0
  85. hxadmin-0.1.0/tests/test_badges.py +107 -0
  86. hxadmin-0.1.0/tests/test_components.py +369 -0
  87. hxadmin-0.1.0/tests/test_create_edit.py +631 -0
  88. hxadmin-0.1.0/tests/test_dashboard.py +187 -0
  89. hxadmin-0.1.0/tests/test_date_input.py +211 -0
  90. hxadmin-0.1.0/tests/test_delete.py +133 -0
  91. hxadmin-0.1.0/tests/test_dependencies.py +41 -0
  92. hxadmin-0.1.0/tests/test_detail.py +351 -0
  93. hxadmin-0.1.0/tests/test_export.py +205 -0
  94. hxadmin-0.1.0/tests/test_export_routes.py +149 -0
  95. hxadmin-0.1.0/tests/test_fields.py +182 -0
  96. hxadmin-0.1.0/tests/test_filter_ui.py +365 -0
  97. hxadmin-0.1.0/tests/test_filters.py +349 -0
  98. hxadmin-0.1.0/tests/test_forms.py +312 -0
  99. hxadmin-0.1.0/tests/test_icons.py +87 -0
  100. hxadmin-0.1.0/tests/test_list.py +291 -0
  101. hxadmin-0.1.0/tests/test_list_contracts.py +260 -0
  102. hxadmin-0.1.0/tests/test_lookup.py +162 -0
  103. hxadmin-0.1.0/tests/test_nav.py +80 -0
  104. hxadmin-0.1.0/tests/test_origin.py +186 -0
  105. hxadmin-0.1.0/tests/test_package.py +30 -0
  106. hxadmin-0.1.0/tests/test_pages.py +269 -0
  107. hxadmin-0.1.0/tests/test_palette.py +214 -0
  108. hxadmin-0.1.0/tests/test_query.py +305 -0
  109. hxadmin-0.1.0/tests/test_render.py +57 -0
  110. hxadmin-0.1.0/tests/test_save_feedback.py +162 -0
  111. hxadmin-0.1.0/tests/test_shell.py +247 -0
  112. hxadmin-0.1.0/tests/test_static.py +51 -0
  113. hxadmin-0.1.0/tests/test_table_models.py +177 -0
  114. hxadmin-0.1.0/tests/test_toasts.py +172 -0
  115. hxadmin-0.1.0/tests/test_users.py +47 -0
  116. hxadmin-0.1.0/tests/test_version.py +5 -0
  117. hxadmin-0.1.0/tests/test_views.py +299 -0
  118. hxadmin-0.1.0/zensical.toml +17 -0
@@ -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.
@@ -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).