fastapi-admin-kit 0.1.0__py3-none-any.whl
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.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Theme configuration — maps to CSS custom properties."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
PRESET_DEFAULTS: dict[str, dict[str, str]] = {
|
|
6
|
+
"editorial": {
|
|
7
|
+
"surface_base": "#FAF8F5",
|
|
8
|
+
"surface_raised": "#FFFFFF",
|
|
9
|
+
"text_primary": "#1C1917",
|
|
10
|
+
"text_secondary": "#78716C",
|
|
11
|
+
"border_color": "#E8E4DE",
|
|
12
|
+
"primary_color": "#059669",
|
|
13
|
+
"font_display": "'Instrument Serif', Georgia, serif",
|
|
14
|
+
"font_body": "'DM Sans', system-ui, sans-serif",
|
|
15
|
+
"font_mono": "'JetBrains Mono', monospace",
|
|
16
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
17
|
+
"radius_sm": "3px",
|
|
18
|
+
"radius_md": "5px",
|
|
19
|
+
"radius_lg": "8px",
|
|
20
|
+
},
|
|
21
|
+
"modern": {
|
|
22
|
+
"surface_base": "#F8FAFC",
|
|
23
|
+
"surface_raised": "#FFFFFF",
|
|
24
|
+
"text_primary": "#0F172A",
|
|
25
|
+
"text_secondary": "#64748B",
|
|
26
|
+
"border_color": "#E2E8F0",
|
|
27
|
+
"primary_color": "#6366F1",
|
|
28
|
+
"font_display": "'Inter', system-ui, sans-serif",
|
|
29
|
+
"font_body": "'Inter', system-ui, sans-serif",
|
|
30
|
+
"font_mono": "'JetBrains Mono', ui-monospace, monospace",
|
|
31
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
32
|
+
"radius_sm": "6px",
|
|
33
|
+
"radius_md": "8px",
|
|
34
|
+
"radius_lg": "12px",
|
|
35
|
+
},
|
|
36
|
+
"midnight": {
|
|
37
|
+
"surface_base": "#0B0F19",
|
|
38
|
+
"surface_raised": "#151B2B",
|
|
39
|
+
"text_primary": "#E2E8F0",
|
|
40
|
+
"text_secondary": "#94A3B8",
|
|
41
|
+
"border_color": "#2A3248",
|
|
42
|
+
"primary_color": "#818CF8",
|
|
43
|
+
"font_display": "'Inter', system-ui, sans-serif",
|
|
44
|
+
"font_body": "'Inter', system-ui, sans-serif",
|
|
45
|
+
"font_mono": "'JetBrains Mono', ui-monospace, monospace",
|
|
46
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
47
|
+
"radius_sm": "4px",
|
|
48
|
+
"radius_md": "6px",
|
|
49
|
+
"radius_lg": "10px",
|
|
50
|
+
},
|
|
51
|
+
"paper": {
|
|
52
|
+
"surface_base": "#FAF8F5",
|
|
53
|
+
"surface_raised": "#FFFFFF",
|
|
54
|
+
"text_primary": "#1C1917",
|
|
55
|
+
"text_secondary": "#78716C",
|
|
56
|
+
"border_color": "#E8E4DE",
|
|
57
|
+
"primary_color": "#059669",
|
|
58
|
+
"font_display": "'Instrument Serif', Georgia, serif",
|
|
59
|
+
"font_body": "'DM Sans', system-ui, sans-serif",
|
|
60
|
+
"font_mono": "'JetBrains Mono', monospace",
|
|
61
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
62
|
+
"radius_sm": "3px",
|
|
63
|
+
"radius_md": "5px",
|
|
64
|
+
"radius_lg": "8px",
|
|
65
|
+
},
|
|
66
|
+
"forest": {
|
|
67
|
+
"surface_base": "#F0FDF4",
|
|
68
|
+
"surface_raised": "#FFFFFF",
|
|
69
|
+
"text_primary": "#14532D",
|
|
70
|
+
"text_secondary": "#166534",
|
|
71
|
+
"border_color": "#BBF7D0",
|
|
72
|
+
"primary_color": "#22C55E",
|
|
73
|
+
"font_display": "'Instrument Serif', Georgia, serif",
|
|
74
|
+
"font_body": "'DM Sans', system-ui, sans-serif",
|
|
75
|
+
"font_mono": "'JetBrains Mono', monospace",
|
|
76
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
77
|
+
"radius_sm": "4px",
|
|
78
|
+
"radius_md": "6px",
|
|
79
|
+
"radius_lg": "10px",
|
|
80
|
+
},
|
|
81
|
+
"minimal": {
|
|
82
|
+
"surface_base": "#FFFFFF",
|
|
83
|
+
"surface_raised": "#FFFFFF",
|
|
84
|
+
"text_primary": "#171717",
|
|
85
|
+
"text_secondary": "#737373",
|
|
86
|
+
"border_color": "#E5E5E5",
|
|
87
|
+
"primary_color": "#404040",
|
|
88
|
+
"font_display": "'Inter', system-ui, sans-serif",
|
|
89
|
+
"font_body": "'Inter', system-ui, sans-serif",
|
|
90
|
+
"font_mono": "'JetBrains Mono', ui-monospace, monospace",
|
|
91
|
+
"font_import_url": "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
92
|
+
"radius_sm": "0px",
|
|
93
|
+
"radius_md": "0px",
|
|
94
|
+
"radius_lg": "0px",
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ThemeConfig:
|
|
100
|
+
"""Complete theme configuration — maps to CSS custom properties.
|
|
101
|
+
|
|
102
|
+
When preset is set, its defaults are used. Any explicit attribute
|
|
103
|
+
override takes precedence over the preset defaults.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
preset: str = "editorial",
|
|
109
|
+
*,
|
|
110
|
+
primary_color: str | None = None,
|
|
111
|
+
surface_base: str | None = None,
|
|
112
|
+
surface_raised: str | None = None,
|
|
113
|
+
text_primary: str | None = None,
|
|
114
|
+
text_secondary: str | None = None,
|
|
115
|
+
border_color: str | None = None,
|
|
116
|
+
font_display: str | None = None,
|
|
117
|
+
font_body: str | None = None,
|
|
118
|
+
font_mono: str | None = None,
|
|
119
|
+
font_import_url: str | None = None,
|
|
120
|
+
radius_sm: str | None = None,
|
|
121
|
+
radius_md: str | None = None,
|
|
122
|
+
radius_lg: str | None = None,
|
|
123
|
+
shadow_sm: str | None = None,
|
|
124
|
+
shadow_md: str | None = None,
|
|
125
|
+
shadow_lg: str | None = None,
|
|
126
|
+
topbar_height: str = "56px",
|
|
127
|
+
sidebar_width: str = "248px",
|
|
128
|
+
sidebar_collapsed_width: str = "60px",
|
|
129
|
+
content_max_width: str = "1360px",
|
|
130
|
+
content_padding: str = "32px",
|
|
131
|
+
duration_fast: str = "100ms",
|
|
132
|
+
duration_base: str = "180ms",
|
|
133
|
+
duration_slow: str = "280ms",
|
|
134
|
+
easing: str = "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
135
|
+
show_grain_texture: bool = True,
|
|
136
|
+
show_accent_line: bool = True,
|
|
137
|
+
compact_mode: bool = False,
|
|
138
|
+
):
|
|
139
|
+
defaults = PRESET_DEFAULTS.get(preset, PRESET_DEFAULTS["editorial"])
|
|
140
|
+
self.preset = preset
|
|
141
|
+
self.primary_color = primary_color or defaults["primary_color"]
|
|
142
|
+
self.surface_base = surface_base or defaults["surface_base"]
|
|
143
|
+
self.surface_raised = surface_raised or defaults["surface_raised"]
|
|
144
|
+
self.text_primary = text_primary or defaults["text_primary"]
|
|
145
|
+
self.text_secondary = text_secondary or defaults["text_secondary"]
|
|
146
|
+
self.border_color = border_color or defaults["border_color"]
|
|
147
|
+
self.font_display = font_display or defaults["font_display"]
|
|
148
|
+
self.font_body = font_body or defaults["font_body"]
|
|
149
|
+
self.font_mono = font_mono or defaults["font_mono"]
|
|
150
|
+
self.font_import_url = font_import_url or defaults["font_import_url"]
|
|
151
|
+
self.radius_sm = radius_sm or defaults["radius_sm"]
|
|
152
|
+
self.radius_md = radius_md or defaults["radius_md"]
|
|
153
|
+
self.radius_lg = radius_lg or defaults["radius_lg"]
|
|
154
|
+
self.shadow_sm = shadow_sm
|
|
155
|
+
self.shadow_md = shadow_md
|
|
156
|
+
self.shadow_lg = shadow_lg
|
|
157
|
+
self.topbar_height = topbar_height
|
|
158
|
+
self.sidebar_width = sidebar_width
|
|
159
|
+
self.sidebar_collapsed_width = sidebar_collapsed_width
|
|
160
|
+
self.content_max_width = content_max_width
|
|
161
|
+
self.content_padding = content_padding
|
|
162
|
+
self.duration_fast = duration_fast
|
|
163
|
+
self.duration_base = duration_base
|
|
164
|
+
self.duration_slow = duration_slow
|
|
165
|
+
self.easing = easing
|
|
166
|
+
self.show_grain_texture = show_grain_texture
|
|
167
|
+
self.show_accent_line = show_accent_line
|
|
168
|
+
self.compact_mode = compact_mode
|
|
169
|
+
|
|
170
|
+
def to_css_variables(self) -> str:
|
|
171
|
+
"""Generate CSS :root{} block from config."""
|
|
172
|
+
lines = [
|
|
173
|
+
f" --primary-500: {self.primary_color};",
|
|
174
|
+
f" --surface-base: {self.surface_base};",
|
|
175
|
+
f" --surface-raised: {self.surface_raised};",
|
|
176
|
+
f" --text-primary: {self.text_primary};",
|
|
177
|
+
f" --text-secondary: {self.text_secondary};",
|
|
178
|
+
f" --surface-border: {self.border_color};",
|
|
179
|
+
f" --font-display: {self.font_display};",
|
|
180
|
+
f" --font-body: {self.font_body};",
|
|
181
|
+
f" --font-mono: {self.font_mono};",
|
|
182
|
+
f" --topbar-height: {self.topbar_height};",
|
|
183
|
+
f" --sidebar-width: {self.sidebar_width};",
|
|
184
|
+
f" --sidebar-collapsed: {self.sidebar_collapsed_width};",
|
|
185
|
+
f" --content-max-width: {self.content_max_width};",
|
|
186
|
+
f" --content-padding: {self.content_padding};",
|
|
187
|
+
f" --radius-sm: {self.radius_sm};",
|
|
188
|
+
f" --radius-md: {self.radius_md};",
|
|
189
|
+
f" --radius-lg: {self.radius_lg};",
|
|
190
|
+
f" --duration-fast: {self.duration_fast};",
|
|
191
|
+
f" --duration-base: {self.duration_base};",
|
|
192
|
+
f" --duration-slow: {self.duration_slow};",
|
|
193
|
+
f" --easing-out: {self.easing};",
|
|
194
|
+
f" --admin-grain-opacity: {'0.025' if self.show_grain_texture else '0'};",
|
|
195
|
+
f" --admin-accent-line-opacity: {'0.4' if self.show_accent_line else '0'};",
|
|
196
|
+
]
|
|
197
|
+
if self.shadow_sm:
|
|
198
|
+
lines.append(f" --shadow-sm: {self.shadow_sm};")
|
|
199
|
+
if self.shadow_md:
|
|
200
|
+
lines.append(f" --shadow-md: {self.shadow_md};")
|
|
201
|
+
if self.shadow_lg:
|
|
202
|
+
lines.append(f" --shadow-lg: {self.shadow_lg};")
|
|
203
|
+
body = "\n".join(lines)
|
|
204
|
+
return f":root {{\n{body}\n}}"
|
|
205
|
+
|
|
206
|
+
def to_context(self) -> dict:
|
|
207
|
+
"""Return dict suitable for template context."""
|
|
208
|
+
return {
|
|
209
|
+
"theme": self,
|
|
210
|
+
"theme_preset": self.preset,
|
|
211
|
+
"theme_css": self.to_css_variables(),
|
|
212
|
+
"theme_font_import_url": self.font_import_url,
|
|
213
|
+
"theme_show_grain": self.show_grain_texture,
|
|
214
|
+
"theme_show_accent_line": self.show_accent_line,
|
|
215
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""UI configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from fastapi_admin_kit.config.theme import ThemeConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UIConfig:
|
|
12
|
+
"""UI configuration — wraps ThemeConfig + component-level options."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
title: str = "FastAPI Admin Kit",
|
|
17
|
+
logo_url: str | None = None,
|
|
18
|
+
favicon_url: str | None = None,
|
|
19
|
+
primary_color: str = "#0ea5e9",
|
|
20
|
+
primary_color_dark: str = "#0284c7",
|
|
21
|
+
dark_mode_default: bool = False,
|
|
22
|
+
per_page_default: int = 25,
|
|
23
|
+
# Theme
|
|
24
|
+
theme: ThemeConfig | None = None,
|
|
25
|
+
# Component config
|
|
26
|
+
sidebar_style: str = "default",
|
|
27
|
+
sidebar_show_icons: bool = True,
|
|
28
|
+
sidebar_show_badges: bool = True,
|
|
29
|
+
sidebar_group_style: str = "label",
|
|
30
|
+
sidebar_position: str = "left",
|
|
31
|
+
table_style: str = "default",
|
|
32
|
+
table_hover_effect: bool = True,
|
|
33
|
+
table_row_height: str = "normal",
|
|
34
|
+
form_layout: str = "two-column",
|
|
35
|
+
form_label_position: str = "top",
|
|
36
|
+
form_spacing: str = "normal",
|
|
37
|
+
form_card_style: bool = True,
|
|
38
|
+
dashboard_grid: str = "auto",
|
|
39
|
+
dashboard_card_style: str = "default",
|
|
40
|
+
dashboard_stat_size: str = "normal",
|
|
41
|
+
content_width: str = "default",
|
|
42
|
+
topbar_style: str = "default",
|
|
43
|
+
sticky_header: bool = True,
|
|
44
|
+
# Custom injection
|
|
45
|
+
custom_css: str = "",
|
|
46
|
+
custom_css_url: str = "",
|
|
47
|
+
custom_js: str = "",
|
|
48
|
+
custom_js_url: str = "",
|
|
49
|
+
# Feature toggles
|
|
50
|
+
show_history: bool = True,
|
|
51
|
+
show_view_on_site: bool = True,
|
|
52
|
+
show_back_button: bool = False,
|
|
53
|
+
environment_label: str | None = None,
|
|
54
|
+
environment_color: str = "info",
|
|
55
|
+
site_url: str = "/",
|
|
56
|
+
site_symbol: str | None = None,
|
|
57
|
+
login_background_url: str | None = None,
|
|
58
|
+
# Mobile
|
|
59
|
+
mobile_sidebar: str = "overlay",
|
|
60
|
+
mobile_topbar_height: str = "48px",
|
|
61
|
+
mobile_content_padding: str = "16px",
|
|
62
|
+
):
|
|
63
|
+
self.title = title
|
|
64
|
+
self.logo_url = logo_url
|
|
65
|
+
self.favicon_url = favicon_url
|
|
66
|
+
self.primary_color = primary_color
|
|
67
|
+
self.primary_color_dark = primary_color_dark
|
|
68
|
+
self.dark_mode_default = dark_mode_default
|
|
69
|
+
self.per_page_default = per_page_default
|
|
70
|
+
self.theme = theme
|
|
71
|
+
self.sidebar_style = sidebar_style
|
|
72
|
+
self.sidebar_show_icons = sidebar_show_icons
|
|
73
|
+
self.sidebar_show_badges = sidebar_show_badges
|
|
74
|
+
self.sidebar_group_style = sidebar_group_style
|
|
75
|
+
self.sidebar_position = sidebar_position
|
|
76
|
+
self.table_style = table_style
|
|
77
|
+
self.table_hover_effect = table_hover_effect
|
|
78
|
+
self.table_row_height = table_row_height
|
|
79
|
+
self.form_layout = form_layout
|
|
80
|
+
self.form_label_position = form_label_position
|
|
81
|
+
self.form_spacing = form_spacing
|
|
82
|
+
self.form_card_style = form_card_style
|
|
83
|
+
self.dashboard_grid = dashboard_grid
|
|
84
|
+
self.dashboard_card_style = dashboard_card_style
|
|
85
|
+
self.dashboard_stat_size = dashboard_stat_size
|
|
86
|
+
self.content_width = content_width
|
|
87
|
+
self.topbar_style = topbar_style
|
|
88
|
+
self.sticky_header = sticky_header
|
|
89
|
+
self.custom_css = custom_css
|
|
90
|
+
self.custom_css_url = custom_css_url
|
|
91
|
+
self.custom_js = custom_js
|
|
92
|
+
self.custom_js_url = custom_js_url
|
|
93
|
+
self.show_history = show_history
|
|
94
|
+
self.show_view_on_site = show_view_on_site
|
|
95
|
+
self.show_back_button = show_back_button
|
|
96
|
+
self.environment_label = environment_label
|
|
97
|
+
self.environment_color = environment_color
|
|
98
|
+
self.site_url = site_url
|
|
99
|
+
self.site_symbol = site_symbol
|
|
100
|
+
self.login_background_url = login_background_url
|
|
101
|
+
self.mobile_sidebar = mobile_sidebar
|
|
102
|
+
self.mobile_topbar_height = mobile_topbar_height
|
|
103
|
+
self.mobile_content_padding = mobile_content_padding
|
|
104
|
+
|
|
105
|
+
def apply_to_template_context(self) -> dict:
|
|
106
|
+
"""Apply UI configuration to template context."""
|
|
107
|
+
ctx = {
|
|
108
|
+
"title": self.title,
|
|
109
|
+
"logo_url": self.logo_url,
|
|
110
|
+
"favicon_url": self.favicon_url,
|
|
111
|
+
"primary_color": self.primary_color,
|
|
112
|
+
"primary_color_dark": self.primary_color_dark,
|
|
113
|
+
"dark_mode_default": self.dark_mode_default,
|
|
114
|
+
"per_page_default": self.per_page_default,
|
|
115
|
+
"sidebar_style": self.sidebar_style,
|
|
116
|
+
"sidebar_show_icons": self.sidebar_show_icons,
|
|
117
|
+
"sidebar_show_badges": self.sidebar_show_badges,
|
|
118
|
+
"sidebar_group_style": self.sidebar_group_style,
|
|
119
|
+
"sidebar_position": self.sidebar_position,
|
|
120
|
+
"table_style": self.table_style,
|
|
121
|
+
"table_hover_effect": self.table_hover_effect,
|
|
122
|
+
"table_row_height": self.table_row_height,
|
|
123
|
+
"form_layout": self.form_layout,
|
|
124
|
+
"form_label_position": self.form_label_position,
|
|
125
|
+
"form_spacing": self.form_spacing,
|
|
126
|
+
"form_card_style": self.form_card_style,
|
|
127
|
+
"dashboard_grid": self.dashboard_grid,
|
|
128
|
+
"dashboard_card_style": self.dashboard_card_style,
|
|
129
|
+
"dashboard_stat_size": self.dashboard_stat_size,
|
|
130
|
+
"content_width": self.content_width,
|
|
131
|
+
"topbar_style": self.topbar_style,
|
|
132
|
+
"sticky_header": self.sticky_header,
|
|
133
|
+
"show_history": self.show_history,
|
|
134
|
+
"show_view_on_site": self.show_view_on_site,
|
|
135
|
+
"show_back_button": self.show_back_button,
|
|
136
|
+
"environment_label": self.environment_label,
|
|
137
|
+
"environment_color": self.environment_color,
|
|
138
|
+
"site_url": self.site_url,
|
|
139
|
+
"site_symbol": self.site_symbol,
|
|
140
|
+
"login_background_url": self.login_background_url,
|
|
141
|
+
"mobile_sidebar": self.mobile_sidebar,
|
|
142
|
+
"mobile_topbar_height": self.mobile_topbar_height,
|
|
143
|
+
"mobile_content_padding": self.mobile_content_padding,
|
|
144
|
+
}
|
|
145
|
+
if self.theme:
|
|
146
|
+
ctx.update(self.theme.to_context())
|
|
147
|
+
return ctx
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Dashboard component classes for Unfold-style dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class CardComponent:
|
|
11
|
+
"""Stat card component."""
|
|
12
|
+
type: str = "card"
|
|
13
|
+
title: str = ""
|
|
14
|
+
value: Any = ""
|
|
15
|
+
description: str = ""
|
|
16
|
+
url: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ChartComponent:
|
|
21
|
+
"""Chart component (placeholder for JS chart library integration)."""
|
|
22
|
+
type: str = "chart"
|
|
23
|
+
title: str = ""
|
|
24
|
+
chart_type: str = "line" # line, bar, pie, doughnut
|
|
25
|
+
data: dict = field(default_factory=dict)
|
|
26
|
+
height: int = 300
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class TableComponent:
|
|
31
|
+
"""Table component for dashboard."""
|
|
32
|
+
type: str = "table"
|
|
33
|
+
title: str = ""
|
|
34
|
+
headers: list[str] = field(default_factory=list)
|
|
35
|
+
rows: list[list[str]] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ProgressComponent:
|
|
40
|
+
"""Progress bar component."""
|
|
41
|
+
type: str = "progress"
|
|
42
|
+
title: str = ""
|
|
43
|
+
value: int = 0 # 0-100
|
|
44
|
+
description: str = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class LinkComponent:
|
|
49
|
+
"""Link/button component."""
|
|
50
|
+
type: str = "button"
|
|
51
|
+
title: str = ""
|
|
52
|
+
description: str = ""
|
|
53
|
+
url: str = "#"
|
|
54
|
+
icon: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ButtonComponent:
|
|
59
|
+
"""Alias for LinkComponent."""
|
|
60
|
+
type: str = "button"
|
|
61
|
+
title: str = ""
|
|
62
|
+
description: str = ""
|
|
63
|
+
url: str = "#"
|
|
64
|
+
icon: str | None = None
|
fastapi_admin_kit/db.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Per-request database session management.
|
|
2
|
+
|
|
3
|
+
Replaces the single shared ``AsyncSession`` on ``app.state`` with a
|
|
4
|
+
``sessionmaker`` factory and ASGI middleware that creates + tears down
|
|
5
|
+
a fresh session for every incoming request.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
13
|
+
from starlette.requests import Request
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_session_factory(
|
|
17
|
+
engine: Any,
|
|
18
|
+
) -> async_sessionmaker[AsyncSession]:
|
|
19
|
+
"""Create an ``async_sessionmaker`` bound to *engine*."""
|
|
20
|
+
return async_sessionmaker(
|
|
21
|
+
bind=engine,
|
|
22
|
+
class_=AsyncSession,
|
|
23
|
+
expire_on_commit=False,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_db_session(request: Request) -> AsyncSession:
|
|
28
|
+
"""Return the per-request ``AsyncSession``.
|
|
29
|
+
|
|
30
|
+
The session is created by :class:`SessionMiddleware` and stored on
|
|
31
|
+
``scope["state"]["admin_db_session"]`` (accessible via
|
|
32
|
+
``request.state.admin_db_session``). Falls back to the legacy
|
|
33
|
+
``app.state.admin_db_session`` when the middleware is not active.
|
|
34
|
+
"""
|
|
35
|
+
session = getattr(request.state, "admin_db_session", None)
|
|
36
|
+
if session is not None:
|
|
37
|
+
if isinstance(session, AsyncSession):
|
|
38
|
+
return session
|
|
39
|
+
from fastapi_admin_kit.db import SyncSessionWrapper
|
|
40
|
+
|
|
41
|
+
return SyncSessionWrapper(session)
|
|
42
|
+
real_app = getattr(request.scope, "app", None) or request.app
|
|
43
|
+
legacy = getattr(real_app.state, "admin_db_session", None)
|
|
44
|
+
if legacy is not None:
|
|
45
|
+
if isinstance(legacy, AsyncSession):
|
|
46
|
+
return legacy
|
|
47
|
+
from fastapi_admin_kit.db import SyncSessionWrapper
|
|
48
|
+
|
|
49
|
+
return SyncSessionWrapper(legacy)
|
|
50
|
+
return legacy
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SessionMiddleware:
|
|
54
|
+
"""Pure ASGI middleware — one session per request, one commit or rollback.
|
|
55
|
+
|
|
56
|
+
The session factory is read from ``app.state.admin_session_factory``
|
|
57
|
+
at request time (it is not available when middleware is registered).
|
|
58
|
+
On success the session is committed. On exception it is rolled back.
|
|
59
|
+
The session is always closed when the request completes.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, app: Any) -> None:
|
|
63
|
+
self.app = app
|
|
64
|
+
|
|
65
|
+
async def __call__(self, scope: dict, receive: Any, send: Any) -> None:
|
|
66
|
+
if scope["type"] != "http":
|
|
67
|
+
await self.app(scope, receive, send)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
from starlette.datastructures import State
|
|
71
|
+
|
|
72
|
+
state: State = scope.get("state", State()) # type: ignore[assignment]
|
|
73
|
+
factory = getattr(state, "admin_session_factory", None)
|
|
74
|
+
if factory is None:
|
|
75
|
+
real_app = scope.get("app")
|
|
76
|
+
if real_app is not None:
|
|
77
|
+
factory = getattr(real_app.state, "admin_session_factory", None)
|
|
78
|
+
if factory is None:
|
|
79
|
+
app_state = getattr(self.app, "state", None)
|
|
80
|
+
if app_state is not None:
|
|
81
|
+
factory = getattr(app_state, "admin_session_factory", None)
|
|
82
|
+
if factory is None:
|
|
83
|
+
await self.app(scope, receive, send)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
session = factory()
|
|
87
|
+
scope["state"]["admin_db_session"] = session # type: ignore[attr-defined]
|
|
88
|
+
try:
|
|
89
|
+
await self.app(scope, receive, send)
|
|
90
|
+
except Exception:
|
|
91
|
+
if hasattr(session, "rollback"):
|
|
92
|
+
result = session.rollback()
|
|
93
|
+
if hasattr(result, "__await__"):
|
|
94
|
+
await result
|
|
95
|
+
raise
|
|
96
|
+
else:
|
|
97
|
+
if hasattr(session, "commit"):
|
|
98
|
+
result = session.commit()
|
|
99
|
+
if hasattr(result, "__await__"):
|
|
100
|
+
await result
|
|
101
|
+
finally:
|
|
102
|
+
if hasattr(session, "close"):
|
|
103
|
+
result = session.close()
|
|
104
|
+
if hasattr(result, "__await__"):
|
|
105
|
+
await result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SyncSessionWrapper:
|
|
109
|
+
"""Wraps a sync SQLAlchemy Session to provide an async-compatible interface."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, session: Any) -> None:
|
|
112
|
+
self._session = session
|
|
113
|
+
|
|
114
|
+
async def execute(self, *args: Any, **kwargs: Any) -> Any:
|
|
115
|
+
return self._session.execute(*args, **kwargs)
|
|
116
|
+
|
|
117
|
+
async def commit(self) -> None:
|
|
118
|
+
self._session.commit()
|
|
119
|
+
|
|
120
|
+
async def rollback(self) -> None:
|
|
121
|
+
self._session.rollback()
|
|
122
|
+
|
|
123
|
+
async def close(self) -> None:
|
|
124
|
+
self._session.close()
|
|
125
|
+
|
|
126
|
+
async def merge(self, *args: Any, **kwargs: Any) -> Any:
|
|
127
|
+
return self._session.merge(*args, **kwargs)
|
|
128
|
+
|
|
129
|
+
async def flush(self) -> None:
|
|
130
|
+
self._session.flush()
|
|
131
|
+
|
|
132
|
+
def __getattr__(self, name: str) -> Any:
|
|
133
|
+
return getattr(self._session, name)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Column type → widget name mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import (
|
|
6
|
+
JSON,
|
|
7
|
+
UUID,
|
|
8
|
+
BigInteger,
|
|
9
|
+
Boolean,
|
|
10
|
+
Date,
|
|
11
|
+
DateTime,
|
|
12
|
+
Enum,
|
|
13
|
+
Float,
|
|
14
|
+
Integer,
|
|
15
|
+
LargeBinary,
|
|
16
|
+
Numeric,
|
|
17
|
+
String,
|
|
18
|
+
Text,
|
|
19
|
+
Time,
|
|
20
|
+
)
|
|
21
|
+
from sqlalchemy.types import TypeEngine
|
|
22
|
+
|
|
23
|
+
# Widget name constants
|
|
24
|
+
TEXT_INPUT = "text_input"
|
|
25
|
+
TEXTAREA = "textarea"
|
|
26
|
+
NUMBER_INPUT = "number_input"
|
|
27
|
+
TOGGLE = "toggle"
|
|
28
|
+
DATE_PICKER = "date_picker"
|
|
29
|
+
DATETIME_PICKER = "datetime_picker"
|
|
30
|
+
TIME_PICKER = "time_picker"
|
|
31
|
+
SELECT = "select"
|
|
32
|
+
JSON_EDITOR = "json_editor"
|
|
33
|
+
FILE_UPLOAD = "file_upload"
|
|
34
|
+
TAG_INPUT = "tag_input"
|
|
35
|
+
RELATION_PICKER = "relation_picker"
|
|
36
|
+
MULTI_RELATION_PICKER = "multi_relation_picker"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# SQLAlchemy type → widget name mapping
|
|
40
|
+
TYPE_WIDGET_MAP: dict[type[TypeEngine], str] = {
|
|
41
|
+
String: TEXT_INPUT,
|
|
42
|
+
Text: TEXTAREA,
|
|
43
|
+
Integer: NUMBER_INPUT,
|
|
44
|
+
BigInteger: NUMBER_INPUT,
|
|
45
|
+
Float: NUMBER_INPUT,
|
|
46
|
+
Numeric: NUMBER_INPUT,
|
|
47
|
+
Boolean: TOGGLE,
|
|
48
|
+
Date: DATE_PICKER,
|
|
49
|
+
DateTime: DATETIME_PICKER,
|
|
50
|
+
Time: TIME_PICKER,
|
|
51
|
+
Enum: SELECT,
|
|
52
|
+
JSON: JSON_EDITOR,
|
|
53
|
+
LargeBinary: FILE_UPLOAD,
|
|
54
|
+
UUID: TEXT_INPUT,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_widget_for_type(col_type: TypeEngine) -> str:
|
|
59
|
+
"""Map a SQLAlchemy column type to a widget name."""
|
|
60
|
+
# Check for exact type match first
|
|
61
|
+
for sa_type, widget in TYPE_WIDGET_MAP.items():
|
|
62
|
+
if isinstance(col_type, sa_type):
|
|
63
|
+
return widget
|
|
64
|
+
|
|
65
|
+
# Check by class name for custom types
|
|
66
|
+
type_name = type(col_type).__name__
|
|
67
|
+
if "ARRAY" in type_name:
|
|
68
|
+
return TAG_INPUT
|
|
69
|
+
|
|
70
|
+
# Default fallback
|
|
71
|
+
return TEXT_INPUT
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_widget_for_column(column_meta) -> str:
|
|
75
|
+
"""Get the appropriate widget for a column, considering foreign keys."""
|
|
76
|
+
from fastapi_admin_kit.inspection import ColumnMeta
|
|
77
|
+
|
|
78
|
+
if isinstance(column_meta, ColumnMeta) and column_meta.is_foreign_key:
|
|
79
|
+
return RELATION_PICKER
|
|
80
|
+
|
|
81
|
+
return get_widget_for_type(column_meta.type)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Filter system for list views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi_admin_kit.filters.base import (
|
|
6
|
+
BooleanFilter,
|
|
7
|
+
EnumFilter,
|
|
8
|
+
Filter,
|
|
9
|
+
RelationFilter,
|
|
10
|
+
TextFilter,
|
|
11
|
+
)
|
|
12
|
+
from fastapi_admin_kit.filters.registry import FilterRegistry
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Filter",
|
|
16
|
+
"TextFilter",
|
|
17
|
+
"BooleanFilter",
|
|
18
|
+
"RelationFilter",
|
|
19
|
+
"EnumFilter",
|
|
20
|
+
"FilterRegistry",
|
|
21
|
+
]
|