openadmin 0.2.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 (37) hide show
  1. openadmin/fastapi/__init__.py +11 -0
  2. openadmin/fastapi/admin_page.py +526 -0
  3. openadmin/fastapi/admin_panel.py +89 -0
  4. openadmin/fastapi/types/__init__.py +36 -0
  5. openadmin/fastapi/types/action.py +20 -0
  6. openadmin/fastapi/types/area_chart.py +19 -0
  7. openadmin/fastapi/types/bar_chart.py +19 -0
  8. openadmin/fastapi/types/form.py +20 -0
  9. openadmin/fastapi/types/line_chart.py +19 -0
  10. openadmin/fastapi/types/markdown.py +19 -0
  11. openadmin/fastapi/types/page_protocol.py +12 -0
  12. openadmin/fastapi/types/pie_chart.py +19 -0
  13. openadmin/fastapi/types/section.py +15 -0
  14. openadmin/fastapi/types/stat.py +19 -0
  15. openadmin/fastapi/types/table.py +19 -0
  16. openadmin/fastapi/utils.py +167 -0
  17. openadmin/spec/__init__.py +41 -0
  18. openadmin/spec/components/__init__.py +35 -0
  19. openadmin/spec/components/action.py +22 -0
  20. openadmin/spec/components/area_chart.py +19 -0
  21. openadmin/spec/components/bar_chart.py +19 -0
  22. openadmin/spec/components/form.py +22 -0
  23. openadmin/spec/components/http_methods.py +7 -0
  24. openadmin/spec/components/line_chart.py +19 -0
  25. openadmin/spec/components/markdown.py +19 -0
  26. openadmin/spec/components/pie_chart.py +19 -0
  27. openadmin/spec/components/property.py +17 -0
  28. openadmin/spec/components/property_type.py +18 -0
  29. openadmin/spec/components/stat.py +19 -0
  30. openadmin/spec/components/table.py +21 -0
  31. openadmin/spec/page.py +15 -0
  32. openadmin/spec/section.py +15 -0
  33. openadmin/spec/spec.py +16 -0
  34. openadmin-0.2.0.dist-info/METADATA +304 -0
  35. openadmin-0.2.0.dist-info/RECORD +37 -0
  36. openadmin-0.2.0.dist-info/WHEEL +4 -0
  37. openadmin-0.2.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,11 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ from .admin_page import AdminPage
6
+ from .admin_panel import AdminPanel
7
+
8
+ __all__ = [
9
+ "AdminPage",
10
+ "AdminPanel",
11
+ ]
@@ -0,0 +1,526 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ import re
6
+ import uuid
7
+ from collections.abc import Callable
8
+
9
+ from fastapi import APIRouter, FastAPI
10
+ from openadmin import spec
11
+
12
+ from . import types
13
+ from .utils import extract_params
14
+
15
+ _SPECIAL_CHARS_RE = re.compile(r"[^a-zA-Z0-9\s]")
16
+
17
+
18
+ class AdminPage:
19
+ def __init__(
20
+ self,
21
+ name: str,
22
+ *,
23
+ description: str | None = None,
24
+ ) -> None:
25
+ self.name = name
26
+ self.description = description
27
+ self.state: list[types.Component] = []
28
+ self.router = APIRouter(prefix=f"/{name.lower().replace(' ', '-')}")
29
+ self.key_repeat_count: dict[str, int] = {}
30
+
31
+ def get_page_spec(self, app: FastAPI) -> spec.Page:
32
+ components: list[spec.Component] = []
33
+
34
+ for item in self.state:
35
+ url = app.url_path_for(item.function_name)
36
+ query, body, form = (
37
+ extract_params(item.func) if item.func else (None, None, None)
38
+ )
39
+
40
+ if isinstance(item, types.Stat):
41
+ components.append(
42
+ spec.Stat(
43
+ type="stat",
44
+ name=item.name,
45
+ description=item.description,
46
+ method=item.method,
47
+ url=url,
48
+ query=query,
49
+ )
50
+ )
51
+ elif isinstance(item, types.Table):
52
+ components.append(
53
+ spec.Table(
54
+ type="table",
55
+ name=item.name,
56
+ description=item.description,
57
+ method=item.method,
58
+ url=url,
59
+ query=query,
60
+ body=body,
61
+ form=form,
62
+ )
63
+ )
64
+ elif isinstance(item, types.AreaChart):
65
+ components.append(
66
+ spec.AreaChart(
67
+ type="area-chart",
68
+ name=item.name,
69
+ description=item.description,
70
+ method=item.method,
71
+ url=url,
72
+ query=query,
73
+ )
74
+ )
75
+ elif isinstance(item, types.BarChart):
76
+ components.append(
77
+ spec.BarChart(
78
+ type="bar-chart",
79
+ name=item.name,
80
+ description=item.description,
81
+ method=item.method,
82
+ url=url,
83
+ query=query,
84
+ )
85
+ )
86
+ elif isinstance(item, types.LineChart):
87
+ components.append(
88
+ spec.LineChart(
89
+ type="line-chart",
90
+ name=item.name,
91
+ description=item.description,
92
+ method=item.method,
93
+ url=url,
94
+ query=query,
95
+ )
96
+ )
97
+ elif isinstance(item, types.PieChart):
98
+ components.append(
99
+ spec.PieChart(
100
+ type="pie-chart",
101
+ name=item.name,
102
+ description=item.description,
103
+ method=item.method,
104
+ url=url,
105
+ query=query,
106
+ )
107
+ )
108
+ elif isinstance(item, types.Action):
109
+ components.append(
110
+ spec.Action(
111
+ type="action",
112
+ name=item.name,
113
+ description=item.description,
114
+ method=item.method,
115
+ url=url,
116
+ is_hidden=item.is_hidden,
117
+ query=query,
118
+ body=body,
119
+ form=form,
120
+ )
121
+ )
122
+ elif isinstance(item, types.Form):
123
+ components.append(
124
+ spec.Form(
125
+ type="form",
126
+ name=item.name,
127
+ description=item.description,
128
+ method=item.method,
129
+ url=url,
130
+ is_hiden=item.is_hiden,
131
+ query=query,
132
+ body=body,
133
+ form=form,
134
+ )
135
+ )
136
+ elif isinstance(item, types.Markdown):
137
+ components.append(
138
+ spec.Markdown(
139
+ type="markdown",
140
+ name=item.name,
141
+ description=item.description,
142
+ method=item.method,
143
+ url=url,
144
+ query=query,
145
+ )
146
+ )
147
+
148
+ return spec.Page(
149
+ name=self.name,
150
+ description=self.description,
151
+ components=components,
152
+ )
153
+
154
+ def _wrap_user_handler(self, item: types.Component, fastapi_decorator) -> Callable:
155
+ def decorator(func: Callable) -> Callable:
156
+ item.func = func
157
+ return fastapi_decorator(func)
158
+
159
+ return decorator
160
+
161
+ def __get_kebab_and_unique_name(self, name: str) -> tuple[str, str]:
162
+ kebab_name = _SPECIAL_CHARS_RE.sub("", name).lower().replace(" ", "-")
163
+
164
+ if kebab_name in self.key_repeat_count:
165
+ number = self.key_repeat_count[kebab_name]
166
+ self.key_repeat_count[kebab_name] += 1
167
+ kebab_name = f"{kebab_name}-{number}"
168
+ else:
169
+ self.key_repeat_count[kebab_name] = 1
170
+
171
+ return kebab_name, f"{kebab_name}-{uuid.uuid4()}"
172
+
173
+ def table(
174
+ self,
175
+ name: str,
176
+ *,
177
+ description: str | None = None,
178
+ ):
179
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
180
+
181
+ item = types.Table(
182
+ function_name=unique_name, method="get", name=name, description=description
183
+ )
184
+ self.state.append(item)
185
+ return self._wrap_user_handler(
186
+ item,
187
+ self.router.get(
188
+ f"/table/{kebab_name}", name=unique_name, description=description
189
+ ),
190
+ )
191
+
192
+ def stat(
193
+ self,
194
+ name: str,
195
+ *,
196
+ description: str | None = None,
197
+ ):
198
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
199
+
200
+ item = types.Stat(
201
+ function_name=unique_name, method="get", name=name, description=description
202
+ )
203
+ self.state.append(item)
204
+ return self._wrap_user_handler(
205
+ item,
206
+ self.router.get(
207
+ f"/stat/{kebab_name}", name=unique_name, description=description
208
+ ),
209
+ )
210
+
211
+ def markdown(
212
+ self,
213
+ name: str,
214
+ *,
215
+ description: str | None = None,
216
+ ):
217
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
218
+
219
+ item = types.Markdown(
220
+ function_name=unique_name,
221
+ method="get",
222
+ name=name,
223
+ description=description,
224
+ )
225
+ self.state.append(item)
226
+
227
+ return self._wrap_user_handler(
228
+ item,
229
+ self.router.get(
230
+ f"/markdown/{kebab_name}",
231
+ name=unique_name,
232
+ description=description,
233
+ ),
234
+ )
235
+
236
+ def action_post(
237
+ self,
238
+ name: str,
239
+ *,
240
+ description: str | None = None,
241
+ is_hiden: bool = False,
242
+ ):
243
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
244
+
245
+ item = types.Action(
246
+ function_name=unique_name,
247
+ method="post",
248
+ name=name,
249
+ description=description,
250
+ is_hidden=is_hiden,
251
+ )
252
+ self.state.append(item)
253
+ return self._wrap_user_handler(
254
+ item,
255
+ self.router.post(
256
+ f"/action/{kebab_name}", name=unique_name, description=description
257
+ ),
258
+ )
259
+
260
+ def action_get(
261
+ self,
262
+ name: str,
263
+ *,
264
+ description: str | None = None,
265
+ is_hiden: bool = False,
266
+ ):
267
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
268
+
269
+ item = types.Action(
270
+ function_name=unique_name,
271
+ method="get",
272
+ name=name,
273
+ description=description,
274
+ is_hidden=is_hiden,
275
+ )
276
+ self.state.append(item)
277
+ return self._wrap_user_handler(
278
+ item,
279
+ self.router.get(
280
+ f"/action/{kebab_name}", name=unique_name, description=description
281
+ ),
282
+ )
283
+
284
+ def action_put(
285
+ self,
286
+ name: str,
287
+ *,
288
+ description: str | None = None,
289
+ is_hiden: bool = False,
290
+ ):
291
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
292
+
293
+ item = types.Action(
294
+ function_name=unique_name,
295
+ method="put",
296
+ name=name,
297
+ description=description,
298
+ is_hidden=is_hiden,
299
+ )
300
+ self.state.append(item)
301
+ return self._wrap_user_handler(
302
+ item,
303
+ self.router.put(
304
+ f"/action/{kebab_name}", name=unique_name, description=description
305
+ ),
306
+ )
307
+
308
+ def action_patch(
309
+ self,
310
+ name: str,
311
+ *,
312
+ description: str | None = None,
313
+ is_hiden: bool = False,
314
+ ):
315
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
316
+
317
+ item = types.Action(
318
+ function_name=unique_name,
319
+ method="patch",
320
+ name=name,
321
+ description=description,
322
+ is_hidden=is_hiden,
323
+ )
324
+ self.state.append(item)
325
+ return self._wrap_user_handler(
326
+ item,
327
+ self.router.patch(
328
+ f"/action/{kebab_name}", name=unique_name, description=description
329
+ ),
330
+ )
331
+
332
+ def action_delete(
333
+ self,
334
+ name: str,
335
+ *,
336
+ description: str | None = None,
337
+ is_hiden: bool = False,
338
+ ):
339
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
340
+
341
+ item = types.Action(
342
+ function_name=unique_name,
343
+ method="delete",
344
+ name=name,
345
+ description=description,
346
+ is_hidden=is_hiden,
347
+ )
348
+ self.state.append(item)
349
+ return self._wrap_user_handler(
350
+ item,
351
+ self.router.delete(
352
+ f"/action/{kebab_name}", name=unique_name, description=description
353
+ ),
354
+ )
355
+
356
+ def form_post(
357
+ self,
358
+ name: str,
359
+ *,
360
+ description: str | None = None,
361
+ is_hiden: bool = False,
362
+ ):
363
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
364
+
365
+ item = types.Form(
366
+ function_name=unique_name,
367
+ method="post",
368
+ name=name,
369
+ description=description,
370
+ is_hiden=is_hiden,
371
+ )
372
+ self.state.append(item)
373
+ return self._wrap_user_handler(
374
+ item,
375
+ self.router.post(
376
+ f"/form/{kebab_name}", name=unique_name, description=description
377
+ ),
378
+ )
379
+
380
+ def form_put(
381
+ self,
382
+ name: str,
383
+ *,
384
+ description: str | None = None,
385
+ is_hiden: bool = False,
386
+ ):
387
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
388
+
389
+ item = types.Form(
390
+ function_name=unique_name,
391
+ method="put",
392
+ name=name,
393
+ description=description,
394
+ is_hiden=is_hiden,
395
+ )
396
+ self.state.append(item)
397
+ return self._wrap_user_handler(
398
+ item,
399
+ self.router.put(
400
+ f"/form/{kebab_name}", name=unique_name, description=description
401
+ ),
402
+ )
403
+
404
+ def form_patch(
405
+ self,
406
+ name: str,
407
+ *,
408
+ description: str | None = None,
409
+ is_hiden: bool = False,
410
+ ):
411
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
412
+
413
+ item = types.Form(
414
+ function_name=unique_name,
415
+ method="patch",
416
+ name=name,
417
+ description=description,
418
+ is_hiden=is_hiden,
419
+ )
420
+ self.state.append(item)
421
+ return self._wrap_user_handler(
422
+ item,
423
+ self.router.patch(
424
+ f"/form/{kebab_name}", name=unique_name, description=description
425
+ ),
426
+ )
427
+
428
+ def form_delete(
429
+ self,
430
+ name: str,
431
+ *,
432
+ description: str | None = None,
433
+ is_hiden: bool = False,
434
+ ):
435
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
436
+
437
+ item = types.Form(
438
+ function_name=unique_name,
439
+ method="delete",
440
+ name=name,
441
+ description=description,
442
+ is_hiden=is_hiden,
443
+ )
444
+ self.state.append(item)
445
+ return self._wrap_user_handler(
446
+ item,
447
+ self.router.delete(
448
+ f"/form/{kebab_name}", name=unique_name, description=description
449
+ ),
450
+ )
451
+
452
+ def area_chart(
453
+ self,
454
+ name: str,
455
+ *,
456
+ description: str | None = None,
457
+ ):
458
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
459
+
460
+ item = types.AreaChart(
461
+ function_name=unique_name, method="get", name=name, description=description
462
+ )
463
+ self.state.append(item)
464
+ return self._wrap_user_handler(
465
+ item,
466
+ self.router.get(
467
+ f"/area-chart/{kebab_name}", name=unique_name, description=description
468
+ ),
469
+ )
470
+
471
+ def bar_chart(
472
+ self,
473
+ name: str,
474
+ *,
475
+ description: str | None = None,
476
+ ):
477
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
478
+
479
+ item = types.BarChart(
480
+ function_name=unique_name, method="get", name=name, description=description
481
+ )
482
+ self.state.append(item)
483
+ return self._wrap_user_handler(
484
+ item,
485
+ self.router.get(
486
+ f"/bar-chart/{kebab_name}", name=unique_name, description=description
487
+ ),
488
+ )
489
+
490
+ def line_chart(
491
+ self,
492
+ name: str,
493
+ *,
494
+ description: str | None = None,
495
+ ):
496
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
497
+
498
+ item = types.LineChart(
499
+ function_name=unique_name, method="get", name=name, description=description
500
+ )
501
+ self.state.append(item)
502
+ return self._wrap_user_handler(
503
+ item,
504
+ self.router.get(
505
+ f"/line-chart/{kebab_name}", name=unique_name, description=description
506
+ ),
507
+ )
508
+
509
+ def pie_chart(
510
+ self,
511
+ name: str,
512
+ *,
513
+ description: str | None = None,
514
+ ):
515
+ kebab_name, unique_name = self.__get_kebab_and_unique_name(name)
516
+
517
+ item = types.PieChart(
518
+ function_name=unique_name, method="get", name=name, description=description
519
+ )
520
+ self.state.append(item)
521
+ return self._wrap_user_handler(
522
+ item,
523
+ self.router.get(
524
+ f"/pie-chart/{kebab_name}", name=unique_name, description=description
525
+ ),
526
+ )
@@ -0,0 +1,89 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ from typing import Dict, List
6
+
7
+ from fastapi import FastAPI, HTTPException, status
8
+ from openadmin import spec
9
+
10
+ from . import types
11
+ from .admin_page import AdminPage
12
+
13
+
14
+ class AdminPanel:
15
+ def __init__(self, name: str, *, description: str | None = None) -> None:
16
+ self.version = "1.0.0"
17
+ self.name = name
18
+ self.description = description
19
+ self.state: List[types.Section] = []
20
+ self.app = FastAPI()
21
+ self.key_repeat_count: Dict[str, int] = {}
22
+ self.__init_spec_route(self.app)
23
+ self.root: FastAPI | None = None
24
+
25
+ def get_panel_spec(self, app: FastAPI) -> spec.Spec:
26
+ sections: List[spec.Section] = []
27
+
28
+ for section in self.state:
29
+ sections.append(
30
+ spec.Section(
31
+ name=section.name,
32
+ description=section.description,
33
+ pages=[p.get_page_spec(app) for p in section.pages],
34
+ )
35
+ )
36
+
37
+ return spec.Spec(
38
+ version=self.version,
39
+ name=self.name,
40
+ description=self.description,
41
+ sections=sections,
42
+ )
43
+
44
+ def section(
45
+ self,
46
+ name: str,
47
+ *,
48
+ description: str | None = None,
49
+ pages: List[AdminPage],
50
+ ) -> None:
51
+ kebab_name = name.lower().replace(" ", "-")
52
+
53
+ if kebab_name in self.key_repeat_count:
54
+ number = self.key_repeat_count[kebab_name]
55
+ kebab_name = f"{kebab_name}-{number}"
56
+ self.key_repeat_count[kebab_name] += 1
57
+ else:
58
+ self.key_repeat_count[kebab_name] = 1
59
+
60
+ self.state.append(
61
+ types.Section(
62
+ name=name,
63
+ description=description,
64
+ pages=pages,
65
+ )
66
+ )
67
+
68
+ for page in pages:
69
+ self.app.include_router(
70
+ prefix=f"/{kebab_name}", router=page.router, tags=[name]
71
+ )
72
+
73
+ def mount_to(self, root: FastAPI) -> None:
74
+ self.root = root
75
+ root.mount("/openadmin", self.app)
76
+
77
+ def __init_spec_route(self, app: FastAPI) -> None:
78
+ @app.get(
79
+ "/spec.json",
80
+ response_model=spec.Spec,
81
+ )
82
+ async def _():
83
+ if not self.root:
84
+ raise HTTPException(
85
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
86
+ detail="Admin panel should be mounted to root, user admin_panel.mount_to(app)",
87
+ )
88
+
89
+ return self.get_panel_spec(self.root)
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ from typing import Union
6
+
7
+ from .action import Action
8
+ from .area_chart import AreaChart
9
+ from .bar_chart import BarChart
10
+ from .form import Form
11
+ from .line_chart import LineChart
12
+ from .markdown import Markdown
13
+ from .page_protocol import PageProtocol
14
+ from .pie_chart import PieChart
15
+ from .section import Section
16
+ from .stat import Stat
17
+ from .table import Table
18
+
19
+ type Component = Union[
20
+ Stat, Table, AreaChart, BarChart, LineChart, PieChart, Action, Form, Markdown
21
+ ]
22
+
23
+ __all__ = [
24
+ "PageProtocol",
25
+ "Section",
26
+ "Stat",
27
+ "Table",
28
+ "Action",
29
+ "AreaChart",
30
+ "BarChart",
31
+ "Form",
32
+ "LineChart",
33
+ "PieChart",
34
+ "Component",
35
+ "Markdown",
36
+ ]
@@ -0,0 +1,20 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ from collections.abc import Callable
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from openadmin import spec
10
+
11
+
12
+ class Action(BaseModel):
13
+ model_config = ConfigDict(arbitrary_types_allowed=True)
14
+
15
+ function_name: str
16
+ name: str
17
+ description: str | None
18
+ method: spec.HttpMethod
19
+ is_hidden: bool
20
+ func: Callable | None = None
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+ from collections.abc import Callable
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from openadmin import spec
10
+
11
+
12
+ class AreaChart(BaseModel):
13
+ model_config = ConfigDict(arbitrary_types_allowed=True)
14
+
15
+ function_name: str
16
+ name: str
17
+ description: str | None
18
+ method: spec.HttpMethod
19
+ func: Callable | None = None