openadmin-dev 0.6.3__tar.gz → 0.7.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. openadmin_dev-0.7.4/Makefile +45 -0
  2. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/PKG-INFO +47 -21
  3. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/README.md +46 -20
  4. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/analytics.py +26 -7
  5. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/authors.py +14 -4
  6. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/genres.py +23 -3
  7. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/health.py +18 -3
  8. openadmin_dev-0.7.4/examples/admin/weekdays.py +87 -0
  9. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/lib/database.py +1 -1
  10. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/lib/models.py +10 -10
  11. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/main.py +2 -0
  12. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/admin_page.py +32 -2
  13. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/admin_panel.py +4 -5
  14. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/component.py +11 -4
  15. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/pie_chart.py +8 -0
  16. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/section.py +1 -1
  17. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/stat.py +2 -0
  18. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/__init__.py +19 -5
  19. openadmin_dev-0.7.4/openadmin/spec/action.py +23 -0
  20. openadmin_dev-0.7.4/openadmin/spec/colors.py +59 -0
  21. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/component.py +13 -6
  22. openadmin_dev-0.7.4/openadmin/spec/form.py +23 -0
  23. openadmin_dev-0.6.3/openadmin/spec/pie_chart.py → openadmin_dev-0.7.4/openadmin/spec/markdown.py +2 -2
  24. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/page.py +1 -2
  25. openadmin_dev-0.7.4/openadmin/spec/pie_chart.py +53 -0
  26. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/property.py +1 -2
  27. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/section.py +1 -2
  28. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/spec.py +1 -2
  29. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/stat.py +8 -1
  30. openadmin_dev-0.7.4/openadmin/spec/table.py +22 -0
  31. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/pyproject.toml +9 -2
  32. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/uv.lock +259 -1
  33. openadmin_dev-0.6.3/Makefile +0 -45
  34. openadmin_dev-0.6.3/openadmin/spec/action.py +0 -23
  35. openadmin_dev-0.6.3/openadmin/spec/form.py +0 -23
  36. openadmin_dev-0.6.3/openadmin/spec/markdown.py +0 -20
  37. openadmin_dev-0.6.3/openadmin/spec/table.py +0 -22
  38. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/ISSUE_TEMPLATE/bug.md +0 -0
  39. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/ISSUE_TEMPLATE/chore.md +0 -0
  40. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/ISSUE_TEMPLATE/feature.md +0 -0
  41. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/branch-name/action.yml +0 -0
  42. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/dependency-cves/action.yml +0 -0
  43. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/format/action.yml +0 -0
  44. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/license/action.yml +0 -0
  45. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/lint/action.yml +0 -0
  46. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/security/action.yml +0 -0
  47. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/spell-check/action.yml +0 -0
  48. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/test/action.yml +0 -0
  49. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/typecheck/action.yml +0 -0
  50. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/actions/unused-code/action.yml +0 -0
  51. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/workflows/ci.yml +0 -0
  52. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.github/workflows/publish.yml +0 -0
  53. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.gitignore +0 -0
  54. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/.python-version +0 -0
  55. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/LICENSE +0 -0
  56. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/LICENSES/AGPL-3.0-or-later.txt +0 -0
  57. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/REUSE.toml +0 -0
  58. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/docs/assets/logo.png +0 -0
  59. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/__init__.py +0 -0
  60. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/__init__.py +0 -0
  61. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/books.py +0 -0
  62. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/overview.py +0 -0
  63. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/publishers.py +0 -0
  64. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/reports.py +0 -0
  65. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/admin/tags.py +0 -0
  66. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/lib/__init__.py +0 -0
  67. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/lib/lifespan.py +0 -0
  68. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/examples/lib/seed.py +0 -0
  69. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/__init__.py +0 -0
  70. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/action.py +0 -0
  71. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/area_chart.py +0 -0
  72. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/bar_chart.py +0 -0
  73. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/counters.py +0 -0
  74. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/deps.py +0 -0
  75. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/form.py +0 -0
  76. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/line_chart.py +0 -0
  77. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/markdown.py +0 -0
  78. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/page_protocol.py +0 -0
  79. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/pagination_params.py +0 -0
  80. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/table.py +0 -0
  81. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/fastapi/utils.py +0 -0
  82. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/area_chart.py +0 -0
  83. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/bar_chart.py +0 -0
  84. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/http_methods.py +0 -0
  85. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/icons.py +0 -0
  86. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/line_chart.py +0 -0
  87. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/openadmin/spec/property_type.py +0 -0
  88. {openadmin_dev-0.6.3 → openadmin_dev-0.7.4}/tests/test_hello.py +0 -0
@@ -0,0 +1,45 @@
1
+ fix/license:
2
+ @ uv run reuse download --all
3
+ @ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised openadmin/
4
+ @ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised tests/
5
+ @ uv run reuse annotate --license AGPL-3.0-or-later --copyright "OpenAdmin" --recursive --skip-unrecognised examples/
6
+
7
+ fix/format:
8
+ @ uv run ruff format .
9
+
10
+ fix/lint:
11
+ @ uv run ruff check --fix .
12
+
13
+ fix: fix/license fix/format fix/lint
14
+
15
+ check/format:
16
+ @ uv run ruff format --check .
17
+
18
+ check/lint:
19
+ @ uv run ruff check .
20
+
21
+ check/typing:
22
+ @ uv run pyright .
23
+
24
+ check/cves:
25
+ @ uv audit --preview-features audit
26
+
27
+ check/security:
28
+ @ uv run bandit -r openadmin -q
29
+
30
+ check/unused:
31
+ @ uv run vulture openadmin --min-confidence 80
32
+
33
+ check/spell:
34
+ @ uv run codespell .
35
+
36
+ check/license:
37
+ @ uv run reuse lint
38
+
39
+ check/test:
40
+ @ uv run pytest
41
+
42
+ check: check/format check/lint check/typing check/cves check/security check/unused check/spell check/license check/test
43
+
44
+ dev/run:
45
+ @ PYTHONPATH=. uv run fastapi dev examples/main.py --host 0.0.0.0 --port $${PORT:-8000} --reload
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openadmin-dev
3
- Version: 0.6.3
3
+ Version: 0.7.4
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/openadmin-team/openadmin-py
6
6
  Project-URL: Issues, https://github.com/openadmin-team/openadmin-py/issues
@@ -60,16 +60,21 @@ admin = AdminPanel()
60
60
  # Define a page
61
61
  page = AdminPage("Dashboard")
62
62
 
63
+
63
64
  @page.stat("Total Users")
64
65
  async def total_users() -> Stat:
65
66
  return Stat(value=1_024)
66
67
 
68
+
67
69
  @page.table("Recent Users")
68
70
  async def recent_users() -> Table:
69
- return Table(data=[
70
- {"id": 1, "name": "Alice", "role": "admin"},
71
- {"id": 2, "name": "Bob", "role": "viewer"},
72
- ])
71
+ return Table(
72
+ data=[
73
+ {"id": 1, "name": "Alice", "role": "admin"},
74
+ {"id": 2, "name": "Bob", "role": "viewer"},
75
+ ]
76
+ )
77
+
73
78
 
74
79
  # Register the page and mount the panel
75
80
  admin.include_page(page)
@@ -87,6 +92,7 @@ Display a single numeric or boolean value.
87
92
  ```python
88
93
  from openadmin.types import Stat
89
94
 
95
+
90
96
  @page.stat("Active Sessions")
91
97
  async def active_sessions() -> Stat:
92
98
  return Stat(value=42)
@@ -100,6 +106,7 @@ Paginated tables with optional search. Use the built-in dependency types to rece
100
106
  from openadmin.fastapi import PaginationParamsDep, SearchQueryDep
101
107
  from openadmin.types import Table
102
108
 
109
+
103
110
  @page.table("Users")
104
111
  async def users_table(
105
112
  pagination: PaginationParamsDep,
@@ -117,18 +124,23 @@ Attach per-row action buttons by including `__actions__` in each row:
117
124
  ```python
118
125
  from openadmin.types import Action, Table, TableRow
119
126
 
127
+
120
128
  @page.table("Users")
121
129
  async def users_table() -> Table:
122
- return Table(data=[
123
- TableRow(
124
- id=1,
125
- name="Alice",
126
- __actions__=[
127
- Action(color="danger", method="DELETE", url="/users/1", body=None),
128
- Action(color="info", method="POST", url="/users/1/reset", body=None),
129
- ],
130
- )
131
- ])
130
+ return Table(
131
+ data=[
132
+ TableRow(
133
+ id=1,
134
+ name="Alice",
135
+ __actions__=[
136
+ Action(color="danger", method="DELETE", url="/users/1", body=None),
137
+ Action(
138
+ color="info", method="POST", url="/users/1/reset", body=None
139
+ ),
140
+ ],
141
+ )
142
+ ]
143
+ )
132
144
  ```
133
145
 
134
146
  ### Charts
@@ -138,6 +150,7 @@ All chart types share the same structure: a `data` list of dicts and a `config`
138
150
  ```python
139
151
  from openadmin.types import BarChart, PieChart
140
152
 
153
+
141
154
  @page.bar_chart("Sales by Region", "Total sales per region this quarter")
142
155
  async def sales_chart() -> BarChart:
143
156
  return BarChart(
@@ -148,11 +161,12 @@ async def sales_chart() -> BarChart:
148
161
  config={"sales": {"label": "Sales", "color": "#6366f1"}},
149
162
  )
150
163
 
164
+
151
165
  @page.pie_chart("User Roles", "Breakdown of user roles")
152
166
  async def roles_chart() -> PieChart:
153
167
  return PieChart(
154
168
  data=[
155
- {"segment": "Admin", "count": 5},
169
+ {"segment": "Admin", "count": 5},
156
170
  {"segment": "Editor", "count": 20},
157
171
  {"segment": "Viewer", "count": 75},
158
172
  ],
@@ -178,10 +192,12 @@ Forms collect user input and submit it to your endpoint. Mark a form hidden if i
178
192
  ```python
179
193
  from pydantic import BaseModel
180
194
 
195
+
181
196
  class InvitePayload(BaseModel):
182
197
  email: str
183
198
  role: str
184
199
 
200
+
185
201
  @page.form_post("Invite User", "Send an invitation email to a new user")
186
202
  async def invite_user(payload: InvitePayload):
187
203
  # send invite...
@@ -209,27 +225,37 @@ from openadmin.types import Stat, Table, BarChart
209
225
 
210
226
  page = AdminPage("Library")
211
227
 
228
+
212
229
  @page.stat("Total Books")
213
230
  async def total_books(session: AsyncSessionDep) -> Stat:
214
231
  result = await session.execute(select(func.count()).select_from(Book))
215
232
  return Stat(value=result.scalar_one())
216
233
 
234
+
217
235
  @page.table("Books")
218
236
  async def books_table(
219
237
  session: AsyncSessionDep,
220
238
  pagination: PaginationParamsDep,
221
239
  search: SearchQueryDep,
222
240
  ) -> Table:
223
- stmt = select(Book).offset(pagination.page * pagination.per_page).limit(pagination.per_page)
241
+ stmt = (
242
+ select(Book)
243
+ .offset(pagination.page * pagination.per_page)
244
+ .limit(pagination.per_page)
245
+ )
224
246
  books = (await session.execute(stmt)).scalars().all()
225
247
  return Table(data=[{"title": b.title, "year": b.published_year} for b in books])
226
248
 
249
+
227
250
  @page.bar_chart("Books per Genre", "Number of books in each genre")
228
251
  async def books_per_genre(session: AsyncSessionDep) -> BarChart:
229
- rows = (await session.execute(
230
- select(Genre.name, func.count().label("books"))
231
- .join(BookToGenre).group_by(Genre.id)
232
- )).all()
252
+ rows = (
253
+ await session.execute(
254
+ select(Genre.name, func.count().label("books"))
255
+ .join(BookToGenre)
256
+ .group_by(Genre.id)
257
+ )
258
+ ).all()
233
259
  return BarChart(
234
260
  data=[{"genre": name, "books": count} for name, count in rows],
235
261
  config={"books": {"label": "Books", "color": "#6366f1"}},
@@ -45,16 +45,21 @@ admin = AdminPanel()
45
45
  # Define a page
46
46
  page = AdminPage("Dashboard")
47
47
 
48
+
48
49
  @page.stat("Total Users")
49
50
  async def total_users() -> Stat:
50
51
  return Stat(value=1_024)
51
52
 
53
+
52
54
  @page.table("Recent Users")
53
55
  async def recent_users() -> Table:
54
- return Table(data=[
55
- {"id": 1, "name": "Alice", "role": "admin"},
56
- {"id": 2, "name": "Bob", "role": "viewer"},
57
- ])
56
+ return Table(
57
+ data=[
58
+ {"id": 1, "name": "Alice", "role": "admin"},
59
+ {"id": 2, "name": "Bob", "role": "viewer"},
60
+ ]
61
+ )
62
+
58
63
 
59
64
  # Register the page and mount the panel
60
65
  admin.include_page(page)
@@ -72,6 +77,7 @@ Display a single numeric or boolean value.
72
77
  ```python
73
78
  from openadmin.types import Stat
74
79
 
80
+
75
81
  @page.stat("Active Sessions")
76
82
  async def active_sessions() -> Stat:
77
83
  return Stat(value=42)
@@ -85,6 +91,7 @@ Paginated tables with optional search. Use the built-in dependency types to rece
85
91
  from openadmin.fastapi import PaginationParamsDep, SearchQueryDep
86
92
  from openadmin.types import Table
87
93
 
94
+
88
95
  @page.table("Users")
89
96
  async def users_table(
90
97
  pagination: PaginationParamsDep,
@@ -102,18 +109,23 @@ Attach per-row action buttons by including `__actions__` in each row:
102
109
  ```python
103
110
  from openadmin.types import Action, Table, TableRow
104
111
 
112
+
105
113
  @page.table("Users")
106
114
  async def users_table() -> Table:
107
- return Table(data=[
108
- TableRow(
109
- id=1,
110
- name="Alice",
111
- __actions__=[
112
- Action(color="danger", method="DELETE", url="/users/1", body=None),
113
- Action(color="info", method="POST", url="/users/1/reset", body=None),
114
- ],
115
- )
116
- ])
115
+ return Table(
116
+ data=[
117
+ TableRow(
118
+ id=1,
119
+ name="Alice",
120
+ __actions__=[
121
+ Action(color="danger", method="DELETE", url="/users/1", body=None),
122
+ Action(
123
+ color="info", method="POST", url="/users/1/reset", body=None
124
+ ),
125
+ ],
126
+ )
127
+ ]
128
+ )
117
129
  ```
118
130
 
119
131
  ### Charts
@@ -123,6 +135,7 @@ All chart types share the same structure: a `data` list of dicts and a `config`
123
135
  ```python
124
136
  from openadmin.types import BarChart, PieChart
125
137
 
138
+
126
139
  @page.bar_chart("Sales by Region", "Total sales per region this quarter")
127
140
  async def sales_chart() -> BarChart:
128
141
  return BarChart(
@@ -133,11 +146,12 @@ async def sales_chart() -> BarChart:
133
146
  config={"sales": {"label": "Sales", "color": "#6366f1"}},
134
147
  )
135
148
 
149
+
136
150
  @page.pie_chart("User Roles", "Breakdown of user roles")
137
151
  async def roles_chart() -> PieChart:
138
152
  return PieChart(
139
153
  data=[
140
- {"segment": "Admin", "count": 5},
154
+ {"segment": "Admin", "count": 5},
141
155
  {"segment": "Editor", "count": 20},
142
156
  {"segment": "Viewer", "count": 75},
143
157
  ],
@@ -163,10 +177,12 @@ Forms collect user input and submit it to your endpoint. Mark a form hidden if i
163
177
  ```python
164
178
  from pydantic import BaseModel
165
179
 
180
+
166
181
  class InvitePayload(BaseModel):
167
182
  email: str
168
183
  role: str
169
184
 
185
+
170
186
  @page.form_post("Invite User", "Send an invitation email to a new user")
171
187
  async def invite_user(payload: InvitePayload):
172
188
  # send invite...
@@ -194,27 +210,37 @@ from openadmin.types import Stat, Table, BarChart
194
210
 
195
211
  page = AdminPage("Library")
196
212
 
213
+
197
214
  @page.stat("Total Books")
198
215
  async def total_books(session: AsyncSessionDep) -> Stat:
199
216
  result = await session.execute(select(func.count()).select_from(Book))
200
217
  return Stat(value=result.scalar_one())
201
218
 
219
+
202
220
  @page.table("Books")
203
221
  async def books_table(
204
222
  session: AsyncSessionDep,
205
223
  pagination: PaginationParamsDep,
206
224
  search: SearchQueryDep,
207
225
  ) -> Table:
208
- stmt = select(Book).offset(pagination.page * pagination.per_page).limit(pagination.per_page)
226
+ stmt = (
227
+ select(Book)
228
+ .offset(pagination.page * pagination.per_page)
229
+ .limit(pagination.per_page)
230
+ )
209
231
  books = (await session.execute(stmt)).scalars().all()
210
232
  return Table(data=[{"title": b.title, "year": b.published_year} for b in books])
211
233
 
234
+
212
235
  @page.bar_chart("Books per Genre", "Number of books in each genre")
213
236
  async def books_per_genre(session: AsyncSessionDep) -> BarChart:
214
- rows = (await session.execute(
215
- select(Genre.name, func.count().label("books"))
216
- .join(BookToGenre).group_by(Genre.id)
217
- )).all()
237
+ rows = (
238
+ await session.execute(
239
+ select(Genre.name, func.count().label("books"))
240
+ .join(BookToGenre)
241
+ .group_by(Genre.id)
242
+ )
243
+ ).all()
218
244
  return BarChart(
219
245
  data=[{"genre": name, "books": count} for name, count in rows],
220
246
  config={"books": {"label": "Books", "color": "#6366f1"}},
@@ -4,6 +4,7 @@
4
4
 
5
5
  from sqlalchemy import func, select
6
6
 
7
+ from openadmin import spec
7
8
  from openadmin.fastapi import AdminPage
8
9
 
9
10
  from ..lib import models
@@ -16,27 +17,45 @@ page = AdminPage(
16
17
  )
17
18
 
18
19
 
19
- @page.stat("Earliest Publication")
20
- async def get_earliest_publication(session: AsyncSessionDep) -> int | str:
20
+ @page.stat(
21
+ "Earliest Publication",
22
+ icon="sun",
23
+ description="This is earliest publications for the last quorder",
24
+ color="red",
25
+ )
26
+ async def get_earliest_publication(session: AsyncSessionDep) -> spec.Stat:
21
27
  result = await session.execute(
22
28
  select(func.min(models.Book.published_year)).where(
23
29
  models.Book.published_year.isnot(None)
24
30
  )
25
31
  )
26
- return result.scalar_one_or_none() or "N/A"
32
+ return result.scalar_one_or_none()
27
33
 
28
34
 
29
- @page.stat("Latest Publication")
30
- async def get_latest_publication(session: AsyncSessionDep) -> int | str:
35
+ @page.stat(
36
+ "Latest Publication",
37
+ description="Most recent publication year found in the catalog",
38
+ )
39
+ async def get_latest_publication(session: AsyncSessionDep) -> spec.Stat:
31
40
  result = await session.execute(
32
41
  select(func.max(models.Book.published_year)).where(
33
42
  models.Book.published_year.isnot(None)
34
43
  )
35
44
  )
36
- return result.scalar_one_or_none() or "N/A"
37
45
 
46
+ return {
47
+ "value": result.scalar_one_or_none() or "N/A",
48
+ "color": "blue",
49
+ "icon": "clock",
50
+ }
38
51
 
39
- @page.stat("Books with Summary")
52
+
53
+ @page.stat(
54
+ "Books with Summary",
55
+ icon="book-check",
56
+ color="teal",
57
+ description="Number of books that include a summary",
58
+ )
40
59
  async def get_books_with_summary(session: AsyncSessionDep) -> int:
41
60
  result = await session.execute(
42
61
  select(func.count(models.Book.id)).where(models.Book.summary.isnot(None))
@@ -19,13 +19,18 @@ page = AdminPage(
19
19
  )
20
20
 
21
21
 
22
- @page.stat("Total Authors")
22
+ @page.stat("Total Authors", description="Total number of authors in the catalog")
23
23
  async def get_total_authors(session: AsyncSessionDep) -> spec.StatResponse:
24
24
  result = await session.execute(select(func.count(models.Author.id)))
25
- return {"value": result.scalar_one(), "icon": "sun"}
25
+ return {"value": result.scalar_one(), "icon": "users", "color": "indigo"}
26
26
 
27
27
 
28
- @page.stat("Authors with Bio")
28
+ @page.stat(
29
+ "Authors with Bio",
30
+ icon="user-check",
31
+ color="emerald",
32
+ description="Authors that have a biography on file",
33
+ )
29
34
  async def get_authors_with_bio(session: AsyncSessionDep) -> int:
30
35
  result = await session.execute(
31
36
  select(func.count(models.Author.id)).where(models.Author.bio.isnot(None))
@@ -33,7 +38,12 @@ async def get_authors_with_bio(session: AsyncSessionDep) -> int:
33
38
  return result.scalar_one()
34
39
 
35
40
 
36
- @page.stat("Avg Books per Author")
41
+ @page.stat(
42
+ "Avg Books per Author",
43
+ icon="library",
44
+ color="amber",
45
+ description="Average number of books published per author",
46
+ )
37
47
  async def get_avg_books_per_author(session: AsyncSessionDep) -> float:
38
48
  subq = (
39
49
  select(func.count(models.Book.id).label("cnt"))
@@ -4,6 +4,7 @@
4
4
 
5
5
  from sqlalchemy import func, select
6
6
 
7
+ from openadmin import spec
7
8
  from openadmin.fastapi import AdminPage
8
9
  from openadmin.fastapi.deps import PageDep
9
10
 
@@ -59,8 +60,25 @@ async def get_all_genres(session: AsyncSessionDep, pagination: PageDep):
59
60
  ]
60
61
 
61
62
 
62
- @page.pie_chart("Genre Distribution", description="Share of books across all genres")
63
- async def get_genre_distribution(session: AsyncSessionDep):
63
+ @page.pie_chart(
64
+ "Genre Distribution 1",
65
+ description="Share of books across all genres",
66
+ name_key="name",
67
+ value_key="count",
68
+ )
69
+ async def get_genre_distribution_1(session: AsyncSessionDep):
70
+ stmt = (
71
+ select(models.Genre, func.count(models.BookToGenre.book_id).label("count"))
72
+ .join(models.BookToGenre, models.BookToGenre.genre_id == models.Genre.id)
73
+ .group_by(models.Genre.id)
74
+ .order_by(func.count(models.BookToGenre.book_id).desc())
75
+ )
76
+ result = await session.execute(stmt)
77
+ return [{**g, "count": count} for g, count in result.all()]
78
+
79
+
80
+ @page.pie_chart("Genre Distribution 2", description="Share of books across all genres")
81
+ async def get_genre_distribution_2(session: AsyncSessionDep) -> spec.PieChart:
64
82
  stmt = (
65
83
  select(models.Genre.name, func.count(models.BookToGenre.book_id).label("count"))
66
84
  .join(models.BookToGenre, models.BookToGenre.genre_id == models.Genre.id)
@@ -68,7 +86,9 @@ async def get_genre_distribution(session: AsyncSessionDep):
68
86
  .order_by(func.count(models.BookToGenre.book_id).desc())
69
87
  )
70
88
  result = await session.execute(stmt)
71
- return [{"label": row.name, "value": row.count} for row in result]
89
+ return {
90
+ "data": [{"name": row.name, "value": row.count} for row in result],
91
+ }
72
92
 
73
93
 
74
94
  @page.bar_chart("Books per Genre", description="Absolute book count for each genre")
@@ -9,17 +9,32 @@ page = AdminPage(
9
9
  )
10
10
 
11
11
 
12
- @page.stat("API Uptime (%)")
12
+ @page.stat(
13
+ "API Uptime (%)",
14
+ icon="check-circle",
15
+ color="green",
16
+ description="Percentage of successful API requests over the last 30 days",
17
+ )
13
18
  async def get_api_uptime() -> float:
14
19
  return 99.97
15
20
 
16
21
 
17
- @page.stat("Avg Response Time (ms)")
22
+ @page.stat(
23
+ "Avg Response Time (ms)",
24
+ icon="gauge",
25
+ color="blue",
26
+ description="Average API response time across all endpoints",
27
+ )
18
28
  async def get_avg_response_time() -> int:
19
29
  return 84
20
30
 
21
31
 
22
- @page.stat("Active Connections")
32
+ @page.stat(
33
+ "Active Connections",
34
+ icon="plug",
35
+ color="violet",
36
+ description="Current number of open client connections",
37
+ )
23
38
  async def get_active_connections() -> int:
24
39
  return 12
25
40
 
@@ -0,0 +1,87 @@
1
+ # SPDX-FileCopyrightText: 2026 OpenAdmin
2
+ #
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+
5
+
6
+ from typing import Any
7
+
8
+ from openadmin import fastapi, spec
9
+
10
+ WEEKDAYS: list[dict[str, Any]] = [
11
+ {"day": "Monday", "books": 5},
12
+ {"day": "Tuesday", "books": 3},
13
+ {"day": "Wednesday", "books": 7},
14
+ {"day": "Thursday", "books": 9},
15
+ {"day": "Friday", "books": 10},
16
+ ]
17
+
18
+ page = fastapi.AdminPage(
19
+ "Week Days",
20
+ icon="calendar",
21
+ )
22
+
23
+
24
+ @page.pie_chart(
25
+ "How many books I have read 1",
26
+ description="Books read per weekday, week 1",
27
+ name_key="day",
28
+ value_key="books",
29
+ config={
30
+ item["day"]: {
31
+ "name": item["day"],
32
+ "color": spec.COLORS[num % len(spec.COLORS)],
33
+ }
34
+ for num, item in enumerate(WEEKDAYS)
35
+ },
36
+ color="violet",
37
+ icon="book-open",
38
+ caption="5 day streak",
39
+ caption_description="Reading every day this work week",
40
+ caption_icon="flame",
41
+ )
42
+ def get_books_1() -> spec.PieChart:
43
+ return WEEKDAYS
44
+
45
+
46
+ @page.pie_chart(
47
+ "How many books I have read 2",
48
+ description="Books read per weekday, week 2",
49
+ name_key="day",
50
+ value_key="books",
51
+ caption="3 day streak",
52
+ caption_description="Reading every day this work week",
53
+ caption_icon="flame",
54
+ )
55
+ def get_books_2() -> spec.PieChart:
56
+ return {
57
+ "data": WEEKDAYS,
58
+ "color": "amber",
59
+ "icon": "a-arrow-up",
60
+ "config": {
61
+ item["day"]: {
62
+ "name": item["day"],
63
+ "color": spec.COLORS[num % len(spec.COLORS)],
64
+ }
65
+ for num, item in enumerate(WEEKDAYS)
66
+ },
67
+ }
68
+
69
+
70
+ @page.pie_chart(
71
+ "How many books I have read 3",
72
+ description="Books read per weekday, all pie chart options set",
73
+ color="violet",
74
+ icon="book-open",
75
+ caption="5 day streak",
76
+ caption_description="Reading every day this work week",
77
+ caption_icon="flame",
78
+ config={
79
+ item["day"]: {
80
+ "name": item["day"],
81
+ "color": spec.COLORS[num % len(spec.COLORS)],
82
+ }
83
+ for num, item in enumerate(WEEKDAYS)
84
+ },
85
+ )
86
+ def get_books_3() -> spec.PieChart:
87
+ return [{"name": day["day"], "value": day["value"]} for day in WEEKDAYS]
@@ -34,7 +34,7 @@ async def lifespan():
34
34
  await engine.dispose()
35
35
 
36
36
 
37
- @lru_cache()
37
+ @lru_cache
38
38
  def get_async_engine() -> AsyncEngine:
39
39
  return create_async_engine(f"sqlite+aiosqlite:///{DB_PATH}")
40
40