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.
Files changed (163) hide show
  1. fastapi_admin_kit/__init__.py +73 -0
  2. fastapi_admin_kit/actions/__init__.py +63 -0
  3. fastapi_admin_kit/actions/base.py +68 -0
  4. fastapi_admin_kit/actions/registry.py +43 -0
  5. fastapi_admin_kit/admin/__init__.py +17 -0
  6. fastapi_admin_kit/admin/admin_config.py +95 -0
  7. fastapi_admin_kit/admin/admin_database.py +138 -0
  8. fastapi_admin_kit/admin/admin_router.py +74 -0
  9. fastapi_admin_kit/admin/admin_template.py +203 -0
  10. fastapi_admin_kit/admin/builtin_models.py +284 -0
  11. fastapi_admin_kit/admin/core.py +1036 -0
  12. fastapi_admin_kit/admin/decorators.py +70 -0
  13. fastapi_admin_kit/admin/state.py +76 -0
  14. fastapi_admin_kit/admin.py +728 -0
  15. fastapi_admin_kit/api/__init__.py +44 -0
  16. fastapi_admin_kit/api/auth.py +342 -0
  17. fastapi_admin_kit/api/crud.py +128 -0
  18. fastapi_admin_kit/api/deps.py +79 -0
  19. fastapi_admin_kit/api/roles.py +128 -0
  20. fastapi_admin_kit/api/schema_generator.py +171 -0
  21. fastapi_admin_kit/api/schemas.py +81 -0
  22. fastapi_admin_kit/api/search.py +132 -0
  23. fastapi_admin_kit/audit/__init__.py +36 -0
  24. fastapi_admin_kit/audit/context.py +62 -0
  25. fastapi_admin_kit/audit/diff.py +77 -0
  26. fastapi_admin_kit/audit/event_bus.py +96 -0
  27. fastapi_admin_kit/audit/events.py +48 -0
  28. fastapi_admin_kit/audit/listener.py +159 -0
  29. fastapi_admin_kit/audit/logger.py +28 -0
  30. fastapi_admin_kit/audit/middleware.py +39 -0
  31. fastapi_admin_kit/audit/models.py +53 -0
  32. fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
  33. fastapi_admin_kit/auth/__init__.py +34 -0
  34. fastapi_admin_kit/auth/backend.py +95 -0
  35. fastapi_admin_kit/auth/csrf.py +240 -0
  36. fastapi_admin_kit/auth/dependencies.py +150 -0
  37. fastapi_admin_kit/auth/identity.py +181 -0
  38. fastapi_admin_kit/auth/models.py +246 -0
  39. fastapi_admin_kit/auth/password.py +35 -0
  40. fastapi_admin_kit/auth/permissions.py +205 -0
  41. fastapi_admin_kit/auth/protocol.py +22 -0
  42. fastapi_admin_kit/auth/ratelimit.py +88 -0
  43. fastapi_admin_kit/auth/router.py +10 -0
  44. fastapi_admin_kit/auth/session.py +79 -0
  45. fastapi_admin_kit/auth/totp.py +83 -0
  46. fastapi_admin_kit/auth/views.py +165 -0
  47. fastapi_admin_kit/cli.py +229 -0
  48. fastapi_admin_kit/config/__init__.py +19 -0
  49. fastapi_admin_kit/config/audit.py +18 -0
  50. fastapi_admin_kit/config/auth.py +54 -0
  51. fastapi_admin_kit/config/behavior.py +27 -0
  52. fastapi_admin_kit/config/nav.py +32 -0
  53. fastapi_admin_kit/config/storage.py +22 -0
  54. fastapi_admin_kit/config/theme.py +215 -0
  55. fastapi_admin_kit/config/ui.py +147 -0
  56. fastapi_admin_kit/dashboard/__init__.py +64 -0
  57. fastapi_admin_kit/db.py +133 -0
  58. fastapi_admin_kit/exceptions.py +5 -0
  59. fastapi_admin_kit/field_types.py +81 -0
  60. fastapi_admin_kit/filters/__init__.py +21 -0
  61. fastapi_admin_kit/filters/base.py +170 -0
  62. fastapi_admin_kit/filters/registry.py +68 -0
  63. fastapi_admin_kit/flash.py +45 -0
  64. fastapi_admin_kit/form/__init__.py +1 -0
  65. fastapi_admin_kit/form/pipeline.py +106 -0
  66. fastapi_admin_kit/inspection/__init__.py +117 -0
  67. fastapi_admin_kit/inspection/registry.py +253 -0
  68. fastapi_admin_kit/inspection.py +115 -0
  69. fastapi_admin_kit/modeladmin.py +375 -0
  70. fastapi_admin_kit/models/__init__.py +7 -0
  71. fastapi_admin_kit/models/base.py +7 -0
  72. fastapi_admin_kit/nav.py +208 -0
  73. fastapi_admin_kit/pagination/__init__.py +14 -0
  74. fastapi_admin_kit/pagination/base.py +40 -0
  75. fastapi_admin_kit/pagination/cursor.py +97 -0
  76. fastapi_admin_kit/pagination/dynamic.py +48 -0
  77. fastapi_admin_kit/pagination/offset.py +42 -0
  78. fastapi_admin_kit/plugins/__init__.py +1 -0
  79. fastapi_admin_kit/py.typed +0 -0
  80. fastapi_admin_kit/registry/__init__.py +5 -0
  81. fastapi_admin_kit/registry/core.py +287 -0
  82. fastapi_admin_kit/registry/validation.py +107 -0
  83. fastapi_admin_kit/registry.py +15 -0
  84. fastapi_admin_kit/router.py +335 -0
  85. fastapi_admin_kit/static/css/admin.css +4736 -0
  86. fastapi_admin_kit/static/css/presets.css +317 -0
  87. fastapi_admin_kit/static/css/tokens.css +217 -0
  88. fastapi_admin_kit/static/css/variables.css +74 -0
  89. fastapi_admin_kit/static/icons/heroicons.svg +160 -0
  90. fastapi_admin_kit/static/js/admin.js +692 -0
  91. fastapi_admin_kit/static/js/htmx-config.js +42 -0
  92. fastapi_admin_kit/storage/__init__.py +6 -0
  93. fastapi_admin_kit/storage/base.py +48 -0
  94. fastapi_admin_kit/storage/local.py +73 -0
  95. fastapi_admin_kit/templates/base.html +142 -0
  96. fastapi_admin_kit/templates/macros/form_fields.html +660 -0
  97. fastapi_admin_kit/templates/macros/icons.html +50 -0
  98. fastapi_admin_kit/templates/macros/table.html +108 -0
  99. fastapi_admin_kit/templates/macros/widgets.html +159 -0
  100. fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
  101. fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
  102. fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
  103. fastapi_admin_kit/templates/pages/audit_log.html +102 -0
  104. fastapi_admin_kit/templates/pages/dashboard.html +295 -0
  105. fastapi_admin_kit/templates/pages/detail.html +183 -0
  106. fastapi_admin_kit/templates/pages/form.html +119 -0
  107. fastapi_admin_kit/templates/pages/list.html +277 -0
  108. fastapi_admin_kit/templates/pages/login.html +85 -0
  109. fastapi_admin_kit/templates/pages/profile/password.html +78 -0
  110. fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
  111. fastapi_admin_kit/templates/pages/role_form.html +75 -0
  112. fastapi_admin_kit/templates/pages/roles/form.html +117 -0
  113. fastapi_admin_kit/templates/pages/roles/list.html +69 -0
  114. fastapi_admin_kit/templates/pages/roles.html +77 -0
  115. fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
  116. fastapi_admin_kit/templates/pages/users/form.html +229 -0
  117. fastapi_admin_kit/templates/pages/users/list.html +83 -0
  118. fastapi_admin_kit/templates/partials/command_palette.html +52 -0
  119. fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
  120. fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
  121. fastapi_admin_kit/templates/partials/head.html +21 -0
  122. fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
  123. fastapi_admin_kit/templates/partials/list_table.html +178 -0
  124. fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
  125. fastapi_admin_kit/templates/partials/pagination.html +82 -0
  126. fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
  127. fastapi_admin_kit/templates/partials/scripts.html +13 -0
  128. fastapi_admin_kit/templates/partials/sidebar.html +94 -0
  129. fastapi_admin_kit/templates/partials/topbar.html +95 -0
  130. fastapi_admin_kit/types.py +145 -0
  131. fastapi_admin_kit/validation.py +43 -0
  132. fastapi_admin_kit/views/__init__.py +78 -0
  133. fastapi_admin_kit/views/audit.py +134 -0
  134. fastapi_admin_kit/views/bulk.py +28 -0
  135. fastapi_admin_kit/views/class_views.py +1040 -0
  136. fastapi_admin_kit/views/context.py +588 -0
  137. fastapi_admin_kit/views/dashboard.py +162 -0
  138. fastapi_admin_kit/views/delete.py +31 -0
  139. fastapi_admin_kit/views/extra.py +65 -0
  140. fastapi_admin_kit/views/factory.py +667 -0
  141. fastapi_admin_kit/views/form.py +159 -0
  142. fastapi_admin_kit/views/list.py +28 -0
  143. fastapi_admin_kit/views/profile.py +219 -0
  144. fastapi_admin_kit/views/protocols.py +54 -0
  145. fastapi_admin_kit/views/renderers.py +634 -0
  146. fastapi_admin_kit/views/roles.py +230 -0
  147. fastapi_admin_kit/views/search.py +31 -0
  148. fastapi_admin_kit/views/settings.py +31 -0
  149. fastapi_admin_kit/views/sidebar.py +101 -0
  150. fastapi_admin_kit/views/totp.py +249 -0
  151. fastapi_admin_kit/views/users.py +347 -0
  152. fastapi_admin_kit/views.py +117 -0
  153. fastapi_admin_kit/widgets/__init__.py +44 -0
  154. fastapi_admin_kit/widgets/base.py +44 -0
  155. fastapi_admin_kit/widgets/inputs.py +363 -0
  156. fastapi_admin_kit/widgets/registry.py +110 -0
  157. fastapi_admin_kit/widgets/relation.py +70 -0
  158. fastapi_admin_kit/widgets/resolver.py +102 -0
  159. fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
  160. fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
  161. fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
  162. fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
  163. 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
@@ -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,5 @@
1
+ """Custom exceptions for fastapi_admin_kit."""
2
+
3
+
4
+ class ConfigError(RuntimeError):
5
+ """Raised when Admin configuration is invalid."""
@@ -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
+ ]