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,253 @@
1
+ """ModelInspector — inspects SQLAlchemy models and extracts metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from sqlalchemy import inspect
9
+
10
+ from fastapi_admin_kit.types import ColumnMeta, RelationMeta
11
+
12
+ if TYPE_CHECKING:
13
+ pass
14
+
15
+
16
+ class ModelInspector:
17
+ """Inspects SQLAlchemy models and extracts column/relationship metadata.
18
+
19
+ This class centralizes all model inspection logic, making it testable
20
+ and separable from the registry's storage concerns.
21
+ """
22
+
23
+ def inspect_model(
24
+ self, model: type
25
+ ) -> tuple[list[ColumnMeta], list[RelationMeta]]:
26
+ """Inspect a SQLAlchemy or SQLModel model and return column + relationship metadata.
27
+
28
+ Args:
29
+ model: A SQLAlchemy or SQLModel declarative model class.
30
+
31
+ Returns:
32
+ A tuple of (columns, relationships) metadata.
33
+ """
34
+ mapper = inspect(model)
35
+ columns: list[ColumnMeta] = []
36
+ relationships: list[RelationMeta] = []
37
+
38
+ # Check if this is a SQLModel
39
+ is_sqlmodel = self._is_sqlmodel(model)
40
+
41
+ for col in mapper.columns:
42
+ # For SQLModel, we may need to extract type info from Pydantic fields
43
+ col_type = col.type
44
+ if is_sqlmodel:
45
+ col_type = self._resolve_sqlmodel_type(model, col.key, col.type)
46
+
47
+ columns.append(
48
+ ColumnMeta(
49
+ name=col.key,
50
+ type=col_type,
51
+ nullable=col.nullable,
52
+ primary_key=col.primary_key,
53
+ foreign_keys=list(col.foreign_keys),
54
+ default=col.default,
55
+ server_default=col.server_default,
56
+ index=col.index,
57
+ unique=col.unique,
58
+ )
59
+ )
60
+
61
+ for rel in mapper.relationships:
62
+ try:
63
+ relationships.append(
64
+ RelationMeta(
65
+ name=rel.key,
66
+ direction=rel.direction.name,
67
+ target_model=rel.mapper.class_,
68
+ uselist=rel.uselist,
69
+ back_populates=rel.back_populates,
70
+ secondary=rel.secondary,
71
+ )
72
+ )
73
+ except Exception:
74
+ # Skip relationships that fail to configure (e.g. SQLModel
75
+ # relationships with incomplete FK resolution)
76
+ pass
77
+
78
+ return columns, relationships
79
+
80
+ def _is_sqlmodel(self, model: type) -> bool:
81
+ """Check if a model is a SQLModel instance."""
82
+ try:
83
+ from sqlmodel import SQLModel
84
+
85
+ return isinstance(model, type) and issubclass(model, SQLModel)
86
+ except ImportError:
87
+ return False
88
+
89
+ def _resolve_sqlmodel_type(self, model: type, field_name: str, default_type: Any) -> Any:
90
+ """Resolve the column type for a SQLModel field.
91
+
92
+ SQLModel may expose Python types (int, str, etc.) instead of SQLAlchemy types.
93
+ This method maps them to equivalent SQLAlchemy types.
94
+ """
95
+ try:
96
+ from sqlmodel import SQLModel
97
+
98
+ if not (isinstance(model, type) and issubclass(model, SQLModel)):
99
+ return default_type
100
+
101
+ # Get SQLModel field info
102
+ sqlmodel_fields = getattr(model, "__sqlmodel_fields__", {})
103
+ if field_name not in sqlmodel_fields:
104
+ return default_type
105
+
106
+ field_info = sqlmodel_fields[field_name]
107
+ annotation = getattr(field_info, "annotation", None)
108
+
109
+ if annotation is None:
110
+ return default_type
111
+
112
+ # Map Python types to SQLAlchemy types
113
+ type_map = {
114
+ int: self._get_sa_type("Integer"),
115
+ str: self._get_sa_type("String"),
116
+ float: self._get_sa_type("Float"),
117
+ bool: self._get_sa_type("Boolean"),
118
+ }
119
+
120
+ # Handle Optional types
121
+ origin = getattr(annotation, "__origin__", None)
122
+ if origin is not None:
123
+ args = getattr(annotation, "__args__", ())
124
+ if args:
125
+ inner = args[0]
126
+ if inner in type_map:
127
+ return type_map[inner]
128
+
129
+ if annotation in type_map:
130
+ return type_map[annotation]
131
+
132
+ return default_type
133
+ except Exception:
134
+ return default_type
135
+
136
+ def _get_sa_type(self, type_name: str) -> Any:
137
+ """Get a SQLAlchemy type by name."""
138
+ import sqlalchemy as sa
139
+
140
+ return getattr(sa, type_name, None)
141
+
142
+ def validate_model(self, model: type) -> None:
143
+ """Validate that a model is suitable for admin registration.
144
+
145
+ Args:
146
+ model: A SQLAlchemy declarative model class.
147
+
148
+ Raises:
149
+ ValueError: If the model is not a valid SQLAlchemy model.
150
+ """
151
+ if not hasattr(model, "__tablename__"):
152
+ raise ValueError(
153
+ f"{model.__name__} is not a SQLAlchemy model (no __tablename__)"
154
+ )
155
+
156
+ def extract_metadata(
157
+ self,
158
+ model: type,
159
+ columns: list[ColumnMeta],
160
+ relationships: list[RelationMeta],
161
+ ) -> dict[str, Any]:
162
+ """Extract additional metadata from a model.
163
+
164
+ Args:
165
+ model: A SQLAlchemy declarative model class.
166
+ columns: The extracted column metadata.
167
+ relationships: The extracted relationship metadata.
168
+
169
+ Returns:
170
+ A dictionary of extracted metadata.
171
+ """
172
+ pk_field = self.get_pk_field(model)
173
+ table_name = model.__tablename__
174
+
175
+ return {
176
+ "table_name": table_name,
177
+ "pk_field": pk_field,
178
+ "columns": columns,
179
+ "relationships": relationships,
180
+ }
181
+
182
+ def is_abstract(self, model: type) -> bool:
183
+ """Check if a model is abstract and should be skipped during auto-discovery.
184
+
185
+ Args:
186
+ model: A SQLAlchemy declarative model class.
187
+
188
+ Returns:
189
+ True if the model is abstract, False otherwise.
190
+ """
191
+ return getattr(model, "__abstract__", False)
192
+
193
+ def get_pk_field(self, model: type) -> str | tuple[str, ...] | None:
194
+ """Get the primary key field name for a model.
195
+
196
+ Args:
197
+ model: A SQLAlchemy declarative model class.
198
+
199
+ Returns:
200
+ The single PK field name for simple PKs,
201
+ a tuple of names for composite PKs,
202
+ or None if no primary key is found.
203
+ """
204
+ mapper = inspect(model)
205
+ pk_cols = mapper.primary_key
206
+ if not pk_cols:
207
+ return None
208
+ if len(pk_cols) == 1:
209
+ return pk_cols[0].key
210
+ return tuple(col.key for col in pk_cols)
211
+
212
+ def auto_label(self, name: str) -> str:
213
+ """Auto-generate a human-readable label from a field name.
214
+
215
+ Args:
216
+ name: The field name to convert.
217
+
218
+ Returns:
219
+ A human-readable label.
220
+
221
+ Examples:
222
+ "category_id" → "Category"
223
+ "is_active" → "Is Active"
224
+ "created_at" → "Created At"
225
+ "skuCode" → "Sku Code"
226
+ """
227
+ label = name
228
+ if label.endswith("_id"):
229
+ label = label[:-3]
230
+ label = re.sub(r"([A-Z])", r" \1", label)
231
+ return label.replace("_", " ").strip().title()
232
+
233
+ def is_required(self, col: ColumnMeta) -> bool:
234
+ """Determine if a column is required (NOT NULL with no default).
235
+
236
+ A column is required if:
237
+ - It is NOT NULL
238
+ - It has no Python default
239
+ - It has no server_default (DB-side default)
240
+ - It is NOT a primary key (PKs are handled separately)
241
+
242
+ Args:
243
+ col: The column metadata to check.
244
+
245
+ Returns:
246
+ True if the column is required, False otherwise.
247
+ """
248
+ return (
249
+ not col.nullable
250
+ and col.default is None
251
+ and col.server_default is None
252
+ and not col.primary_key
253
+ )
@@ -0,0 +1,115 @@
1
+ """Model inspection — SQLAlchemy model → ColumnMeta / RelationMeta."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from sqlalchemy import inspect
9
+
10
+ from fastapi_admin_kit.types import ColumnMeta, RelationMeta
11
+
12
+
13
+ def inspect_model(model: type) -> tuple[list[ColumnMeta], list[RelationMeta]]:
14
+ """Inspect a SQLAlchemy model and return column + relationship metadata."""
15
+ mapper = inspect(model)
16
+ columns: list[ColumnMeta] = []
17
+ relationships: list[RelationMeta] = []
18
+
19
+ for col in mapper.columns:
20
+ columns.append(
21
+ ColumnMeta(
22
+ name=col.key,
23
+ type=col.type,
24
+ nullable=col.nullable,
25
+ primary_key=col.primary_key,
26
+ foreign_keys=list(col.foreign_keys),
27
+ default=col.default,
28
+ server_default=col.server_default,
29
+ index=col.index,
30
+ unique=col.unique,
31
+ )
32
+ )
33
+
34
+ for rel in mapper.relationships:
35
+ relationships.append(
36
+ RelationMeta(
37
+ name=rel.key,
38
+ direction=rel.direction.name,
39
+ target_model=rel.mapper.class_,
40
+ uselist=rel.uselist,
41
+ back_populates=rel.back_populates,
42
+ secondary=rel.secondary,
43
+ )
44
+ )
45
+
46
+ return columns, relationships
47
+
48
+
49
+ def is_abstract(model: type) -> bool:
50
+ """Check if a model is abstract and should be skipped during auto-discovery."""
51
+ return getattr(model, "__abstract__", False)
52
+
53
+
54
+ def get_pk_field(model: type) -> str | None:
55
+ """Get the primary key field name for a model.
56
+
57
+ Returns the single PK field name for simple PKs,
58
+ or a tuple of names for composite PKs.
59
+ Returns None if no primary key is found.
60
+ """
61
+ mapper = inspect(model)
62
+ pk_cols = mapper.primary_key
63
+ if not pk_cols:
64
+ return None
65
+ if len(pk_cols) == 1:
66
+ return pk_cols[0].key
67
+ return tuple(col.key for col in pk_cols)
68
+
69
+
70
+ def auto_label(name: str) -> str:
71
+ """Auto-generate a human-readable label from a field name.
72
+
73
+ Examples:
74
+ "category_id" → "Category"
75
+ "is_active" → "Is Active"
76
+ "created_at" → "Created At"
77
+ "skuCode" → "Sku Code"
78
+ """
79
+ label = name
80
+ if label.endswith("_id"):
81
+ label = label[:-3]
82
+ label = re.sub(r"([A-Z])", r" \1", label)
83
+ return label.replace("_", " ").strip().title()
84
+
85
+
86
+ def is_required(col: ColumnMeta) -> bool:
87
+ """Determine if a column is required (NOT NULL with no default).
88
+
89
+ A column is required if:
90
+ - It is NOT NULL
91
+ - It has no Python default
92
+ - It has no server_default (DB-side default)
93
+ - It is NOT a primary key (PKs are handled separately)
94
+ """
95
+ return (
96
+ not col.nullable
97
+ and col.default is None
98
+ and col.server_default is None
99
+ and not col.primary_key
100
+ )
101
+
102
+
103
+ def model_display_name(obj: Any) -> str:
104
+ """Return a human-readable label for an ORM object.
105
+
106
+ Uses the model's ``__str__`` if it has a custom implementation.
107
+ Falls back to ``ClassName:pk`` when ``__str__`` is the default
108
+ ``object.__str__``.
109
+ """
110
+ if type(obj).__str__ is not object.__str__:
111
+ return str(obj)
112
+ pk = getattr(obj, "id", None)
113
+ return (
114
+ f"{type(obj).__name__}:{pk}" if pk is not None else type(obj).__name__
115
+ )