oldman 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 (517) hide show
  1. oldman/__init__.py +14 -0
  2. oldman/apps/__init__.py +23 -0
  3. oldman/apps/admin/__init__.py +15 -0
  4. oldman/apps/admin/apps.py +19 -0
  5. oldman/apps/admin/commands.py +94 -0
  6. oldman/apps/admin/crud.py +94 -0
  7. oldman/apps/admin/forms.py +323 -0
  8. oldman/apps/admin/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
  9. oldman/apps/admin/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  10. oldman/apps/admin/model_admin.py +524 -0
  11. oldman/apps/admin/permissions.py +42 -0
  12. oldman/apps/admin/runtime.py +121 -0
  13. oldman/apps/admin/settings.py +17 -0
  14. oldman/apps/admin/site.py +1345 -0
  15. oldman/apps/admin/static/oldman/admin/.vite/manifest.json +212 -0
  16. oldman/apps/admin/static/oldman/admin/assets/autocomplete-jPdFW4Z7.js +1 -0
  17. oldman/apps/admin/static/oldman/admin/assets/date-time-picker-DXKOYDM_.js +5 -0
  18. oldman/apps/admin/static/oldman/admin/assets/date-time-picker-XdZfF2Hc.css +1 -0
  19. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-300-normal-C46oWILc.woff2 +0 -0
  20. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-300-normal-CsNfKS1n.woff +0 -0
  21. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-400-normal-BwCSEQnW.woff +0 -0
  22. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-400-normal-CW0RaeGs.woff2 +0 -0
  23. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-500-normal-B9HHJjqV.woff2 +0 -0
  24. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-500-normal-Dr3UlScf.woff +0 -0
  25. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-600-normal-Aqo67rzb.woff2 +0 -0
  26. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-600-normal-BmdmIIQ2.woff +0 -0
  27. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-700-normal-CUSSCpQX.woff +0 -0
  28. oldman/apps/admin/static/oldman/admin/assets/dm-sans-latin-700-normal-DvUfVpUG.woff2 +0 -0
  29. oldman/apps/admin/static/oldman/admin/assets/dropdown-DQmEUd1r.js +1 -0
  30. oldman/apps/admin/static/oldman/admin/assets/form-DZC6BBLO.js +1 -0
  31. oldman/apps/admin/static/oldman/admin/assets/form-validator-Gq06sYfE.js +1 -0
  32. oldman/apps/admin/static/oldman/admin/assets/language-switcher-CjE-89Lp.js +1 -0
  33. oldman/apps/admin/static/oldman/admin/assets/main-BVK9XJmx.css +7 -0
  34. oldman/apps/admin/static/oldman/admin/assets/main-BcU_G9Qv.js +116 -0
  35. oldman/apps/admin/static/oldman/admin/assets/modal-C9DpqkgN.js +1 -0
  36. oldman/apps/admin/static/oldman/admin/assets/preloader-C4Cw6JCy.js +1 -0
  37. oldman/apps/admin/static/oldman/admin/assets/select-79I-Au9m.js +1 -0
  38. oldman/apps/admin/static/oldman/admin/assets/select-BnyRxOjF.css +1 -0
  39. oldman/apps/admin/static/oldman/admin/assets/sidebar-menu-DqAbkKBH.js +1 -0
  40. oldman/apps/admin/static/oldman/admin/assets/table-B0PQSRm0.js +1 -0
  41. oldman/apps/admin/static/oldman/admin/assets/table-filter-form-zfFJ-D_E.js +1 -0
  42. oldman/apps/admin/static/oldman/admin/assets/table-json-BQldyVm2.js +1 -0
  43. oldman/apps/admin/static/oldman/admin/assets/turbo.es2017-esm-C9IpioHW.js +29 -0
  44. oldman/apps/admin/staticfiles.py +75 -0
  45. oldman/apps/admin/table.py +194 -0
  46. oldman/apps/admin/template.py +26 -0
  47. oldman/apps/admin/templates/.gitkeep +1 -0
  48. oldman/apps/admin/templates/admin/base.html +82 -0
  49. oldman/apps/admin/templates/admin/index.html +32 -0
  50. oldman/apps/admin/templates/admin/login.html +60 -0
  51. oldman/apps/admin/templates/admin/model/confirm_delete.html +27 -0
  52. oldman/apps/admin/templates/admin/model/delete_modal_form.html +18 -0
  53. oldman/apps/admin/templates/admin/model/form.html +35 -0
  54. oldman/apps/admin/templates/admin/model/list.html +71 -0
  55. oldman/apps/admin/templates/admin/model/status_modal_form.html +19 -0
  56. oldman/apps/admin/templates/admin/partials/language_switcher.html +5 -0
  57. oldman/apps/admin/templates/admin/partials/topbar.html +21 -0
  58. oldman/apps/admin/templates/admin/user_notifications.html +12 -0
  59. oldman/apps/admin/templates/admin/user_session.html +7 -0
  60. oldman/apps/admin/users.py +56 -0
  61. oldman/apps/config.py +121 -0
  62. oldman/apps/registry.py +490 -0
  63. oldman/auth/__init__.py +34 -0
  64. oldman/auth/apps.py +19 -0
  65. oldman/auth/base.py +138 -0
  66. oldman/auth/contracts.py +244 -0
  67. oldman/auth/migrations/__init__.py +0 -0
  68. oldman/auth/migrations/b7c396a46832_create_oldman_user.py +80 -0
  69. oldman/auth/models.py +10 -0
  70. oldman/auth/registry.py +44 -0
  71. oldman/auth/security.py +42 -0
  72. oldman/auth/services.py +224 -0
  73. oldman/auth/settings.py +28 -0
  74. oldman/cache/__init__.py +17 -0
  75. oldman/cache/backends/__init__.py +4 -0
  76. oldman/cache/backends/memory.py +120 -0
  77. oldman/cache/backends/redis.py +263 -0
  78. oldman/cache/base.py +367 -0
  79. oldman/cache/exceptions.py +34 -0
  80. oldman/cache/images.py +420 -0
  81. oldman/cache/two_level.py +55 -0
  82. oldman/cache/utils.py +270 -0
  83. oldman/cli/__init__.py +32 -0
  84. oldman/cli/__main__.py +6 -0
  85. oldman/cli/_main.py +1038 -0
  86. oldman/cli/_service_discovery.py +14 -0
  87. oldman/cli/babel.cfg +5 -0
  88. oldman/cli/commands.py +68 -0
  89. oldman/cli/database.py +257 -0
  90. oldman/cli/discovery.py +5 -0
  91. oldman/cli/fixtures.py +60 -0
  92. oldman/cli/i18n_commands.py +49 -0
  93. oldman/cli/locales/README.md +29 -0
  94. oldman/cli/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
  95. oldman/cli/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  96. oldman/cli/localization.py +302 -0
  97. oldman/cli/scaffold.py +331 -0
  98. oldman/cli/service.py +198 -0
  99. oldman/cli/settings.py +70 -0
  100. oldman/cli/shell.py +37 -0
  101. oldman/cli/staticfiles.py +59 -0
  102. oldman/compat/__init__.py +7 -0
  103. oldman/compat/django/__init__.py +7 -0
  104. oldman/compat/django/cache.py +108 -0
  105. oldman/compat/django/serializers.py +34 -0
  106. oldman/conf/__init__.py +79 -0
  107. oldman/conf/base.py +156 -0
  108. oldman/conf/constants.py +157 -0
  109. oldman/conf/containers.py +275 -0
  110. oldman/conf/manager.py +574 -0
  111. oldman/conf/schemas.py +866 -0
  112. oldman/contrib/__init__.py +7 -0
  113. oldman/contrib/http/__init__.py +37 -0
  114. oldman/contrib/http/_logging.py +43 -0
  115. oldman/contrib/http/backends/__init__.py +14 -0
  116. oldman/contrib/http/backends/aiohttp.py +356 -0
  117. oldman/contrib/http/backends/curl.py +455 -0
  118. oldman/contrib/http/backends/httpx.py +199 -0
  119. oldman/contrib/http/base.py +91 -0
  120. oldman/contrib/http/contants.py +50 -0
  121. oldman/contrib/http/exceptions.py +63 -0
  122. oldman/contrib/http/multi_client.py +554 -0
  123. oldman/contrib/http/reconnect_stream.py +559 -0
  124. oldman/contrib/http/response.py +702 -0
  125. oldman/contrib/http/schemas.py +29 -0
  126. oldman/contrib/proxy/__init__.py +19 -0
  127. oldman/contrib/proxy/base.py +1177 -0
  128. oldman/contrib/proxy/encrypt.py +568 -0
  129. oldman/contrib/proxy/simple.py +724 -0
  130. oldman/db/__init__.py +31 -0
  131. oldman/db/fixtures.py +584 -0
  132. oldman/db/migrations/__init__.py +65 -0
  133. oldman/db/migrations/alembic.py +287 -0
  134. oldman/db/migrations/autogenerate.py +1505 -0
  135. oldman/db/migrations/commands.py +1070 -0
  136. oldman/db/migrations/interaction.py +120 -0
  137. oldman/db/migrations/metadata.py +196 -0
  138. oldman/db/migrations/project.py +296 -0
  139. oldman/db/migrations/revisions.py +652 -0
  140. oldman/db/migrations/state.py +410 -0
  141. oldman/db/migrations/templates/env.py +67 -0
  142. oldman/db/migrations/templates/script.py.mako +27 -0
  143. oldman/db/models.py +250 -0
  144. oldman/db/schemas.py +236 -0
  145. oldman/db/services.py +5 -0
  146. oldman/db/session.py +365 -0
  147. oldman/db/sqlalchemy/__init__.py +7 -0
  148. oldman/db/sqlalchemy/cache.py +469 -0
  149. oldman/db/sqlalchemy/log.py +134 -0
  150. oldman/db/sqlalchemy/models.py +246 -0
  151. oldman/db/sqlalchemy/services.py +18 -0
  152. oldman/db/sqlalchemy/session.py +22 -0
  153. oldman/db/sqlalchemy/utils.py +64 -0
  154. oldman/i18n/__init__.py +46 -0
  155. oldman/i18n/catalogs.py +47 -0
  156. oldman/i18n/commands.py +205 -0
  157. oldman/i18n/data/oldman_web_messages.json +639 -0
  158. oldman/i18n/framework-babel.cfg +24 -0
  159. oldman/i18n/frontend.py +398 -0
  160. oldman/i18n/profiles.py +35 -0
  161. oldman/i18n/registry.py +183 -0
  162. oldman/i18n/serialization.py +165 -0
  163. oldman/i18n/translations.py +109 -0
  164. oldman/i18n/utils.py +55 -0
  165. oldman/licenses/aiocache-BSD-3-Clause.txt +12 -0
  166. oldman/licenses/flag-icons-LICENSE.txt +26 -0
  167. oldman/licenses/nats-py-Apache-2.0.txt +201 -0
  168. oldman/logging/__init__.py +25 -0
  169. oldman/logging/config.py +196 -0
  170. oldman/logging/formatters.py +216 -0
  171. oldman/logging/handlers.py +195 -0
  172. oldman/logging/rotation.py +229 -0
  173. oldman/logging/runtime.py +402 -0
  174. oldman/processes/__init__.py +28 -0
  175. oldman/processes/_subprocess_supervisor.py +262 -0
  176. oldman/processes/executor.py +602 -0
  177. oldman/processes/subprocess.py +568 -0
  178. oldman/providers/__init__.py +7 -0
  179. oldman/providers/nats/__init__.py +13 -0
  180. oldman/providers/nats/_compat.py +197 -0
  181. oldman/providers/nats/bus.py +32 -0
  182. oldman/providers/nats/config.py +45 -0
  183. oldman/providers/nats/connection.py +524 -0
  184. oldman/providers/nats/serializers.py +195 -0
  185. oldman/providers/redis/__init__.py +17 -0
  186. oldman/providers/redis/client.py +206 -0
  187. oldman/providers/redis/redis.py +266 -0
  188. oldman/py.typed +1 -0
  189. oldman/runtime/__init__.py +81 -0
  190. oldman/runtime/_taskiq_process.py +166 -0
  191. oldman/runtime/base.py +605 -0
  192. oldman/runtime/bootstrap.py +91 -0
  193. oldman/runtime/discovery.py +212 -0
  194. oldman/runtime/simple.py +218 -0
  195. oldman/runtime/taskiq.py +316 -0
  196. oldman/runtime/web.py +421 -0
  197. oldman/scaffolds/__init__.py +1 -0
  198. oldman/scaffolds/app/api_app/.gitkeep +1 -0
  199. oldman/scaffolds/app/api_app/apps/__app_slug__/__init__.py.tpl +1 -0
  200. oldman/scaffolds/app/api_app/apps/__app_slug__/apps.py.tpl +16 -0
  201. oldman/scaffolds/app/api_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
  202. oldman/scaffolds/app/api_app/apps/__app_slug__/models.py.tpl +1 -0
  203. oldman/scaffolds/app/api_app/apps/__app_slug__/views.py.tpl +44 -0
  204. oldman/scaffolds/app/dashboard_app/.gitkeep +1 -0
  205. oldman/scaffolds/app/dashboard_app/apps/__app_slug__/__init__.py.tpl +1 -0
  206. oldman/scaffolds/app/dashboard_app/apps/__app_slug__/apps.py.tpl +16 -0
  207. oldman/scaffolds/app/dashboard_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
  208. oldman/scaffolds/app/dashboard_app/apps/__app_slug__/models.py.tpl +1 -0
  209. oldman/scaffolds/app/dashboard_app/apps/__app_slug__/views.py.tpl +36 -0
  210. oldman/scaffolds/app/dashboard_app/frontend/src/pages/__app_slug__.ts.tpl +13 -0
  211. oldman/scaffolds/app/dashboard_app/templates/__app_slug__/index.html.tpl +12 -0
  212. oldman/scaffolds/app/service_app/.gitkeep +1 -0
  213. oldman/scaffolds/app/service_app/apps/__app_slug__/__init__.py.tpl +1 -0
  214. oldman/scaffolds/app/service_app/apps/__app_slug__/apps.py.tpl +16 -0
  215. oldman/scaffolds/app/service_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
  216. oldman/scaffolds/app/service_app/apps/__app_slug__/services.py.tpl +1 -0
  217. oldman/scaffolds/app/web_app/.gitkeep +1 -0
  218. oldman/scaffolds/app/web_app/apps/__app_slug__/__init__.py.tpl +1 -0
  219. oldman/scaffolds/app/web_app/apps/__app_slug__/apps.py.tpl +16 -0
  220. oldman/scaffolds/app/web_app/apps/__app_slug__/migrations/__init__.py.tpl +1 -0
  221. oldman/scaffolds/app/web_app/apps/__app_slug__/models.py.tpl +1 -0
  222. oldman/scaffolds/app/web_app/apps/__app_slug__/views.py.tpl +13 -0
  223. oldman/scaffolds/app/web_app/templates/__app_slug__/index.html.tpl +7 -0
  224. oldman/scaffolds/project/api_service/.gitignore.tpl +4 -0
  225. oldman/scaffolds/project/api_service/.gitkeep +1 -0
  226. oldman/scaffolds/project/api_service/README.md.tpl +47 -0
  227. oldman/scaffolds/project/api_service/apps/__init__.py.tpl +1 -0
  228. oldman/scaffolds/project/api_service/babel.cfg.tpl +28 -0
  229. oldman/scaffolds/project/api_service/config/__init__.py.tpl +1 -0
  230. oldman/scaffolds/project/api_service/config/schemas.py.tpl +9 -0
  231. oldman/scaffolds/project/api_service/config/settings.py.tpl +10 -0
  232. oldman/scaffolds/project/api_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
  233. oldman/scaffolds/project/api_service/locales/.gitkeep +1 -0
  234. oldman/scaffolds/project/api_service/pyproject.toml.tpl +14 -0
  235. oldman/scaffolds/project/api_service/run.sh.tpl +14 -0
  236. oldman/scaffolds/project/api_service/services/__init__.py.tpl +1 -0
  237. oldman/scaffolds/project/api_service/services/{{ service_name }}.py.tpl +28 -0
  238. oldman/scaffolds/project/app_service/.gitignore.tpl +4 -0
  239. oldman/scaffolds/project/app_service/.gitkeep +1 -0
  240. oldman/scaffolds/project/app_service/README.md.tpl +30 -0
  241. oldman/scaffolds/project/app_service/apps/__init__.py.tpl +1 -0
  242. oldman/scaffolds/project/app_service/config/__init__.py.tpl +1 -0
  243. oldman/scaffolds/project/app_service/config/schemas.py.tpl +9 -0
  244. oldman/scaffolds/project/app_service/config/settings.py.tpl +10 -0
  245. oldman/scaffolds/project/app_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
  246. oldman/scaffolds/project/app_service/pyproject.toml.tpl +14 -0
  247. oldman/scaffolds/project/app_service/run.sh.tpl +14 -0
  248. oldman/scaffolds/project/app_service/services/__init__.py.tpl +1 -0
  249. oldman/scaffolds/project/app_service/services/{{ service_name }}.py.tpl +18 -0
  250. oldman/scaffolds/project/cli_app/.gitkeep +1 -0
  251. oldman/scaffolds/project/cli_app/README.md.tpl +16 -0
  252. oldman/scaffolds/project/cli_app/main.py.tpl +13 -0
  253. oldman/scaffolds/project/cli_app/pyproject.toml.tpl +14 -0
  254. oldman/scaffolds/project/dashboard/.gitignore.tpl +7 -0
  255. oldman/scaffolds/project/dashboard/.gitkeep +1 -0
  256. oldman/scaffolds/project/dashboard/README.md.tpl +100 -0
  257. oldman/scaffolds/project/dashboard/apps/__init__.py.tpl +1 -0
  258. oldman/scaffolds/project/dashboard/babel.cfg.tpl +28 -0
  259. oldman/scaffolds/project/dashboard/config/__init__.py.tpl +1 -0
  260. oldman/scaffolds/project/dashboard/config/schemas.py.tpl +9 -0
  261. oldman/scaffolds/project/dashboard/config/settings.py.tpl +10 -0
  262. oldman/scaffolds/project/dashboard/data/{{ service_name }}_settings.yaml.tpl +3 -0
  263. oldman/scaffolds/project/dashboard/frontend/index.html.tpl +2 -0
  264. oldman/scaffolds/project/dashboard/frontend/package.json.tpl +26 -0
  265. oldman/scaffolds/project/dashboard/frontend/src/app.css.tpl +13 -0
  266. oldman/scaffolds/project/dashboard/frontend/src/generated/icons.css.tpl +18 -0
  267. oldman/scaffolds/project/dashboard/frontend/src/main.ts.tpl +89 -0
  268. oldman/scaffolds/project/dashboard/frontend/tsconfig.json.tpl +11 -0
  269. oldman/scaffolds/project/dashboard/frontend/vite.config.ts.tpl +20 -0
  270. oldman/scaffolds/project/dashboard/locales/.gitkeep +1 -0
  271. oldman/scaffolds/project/dashboard/pyproject.toml.tpl +14 -0
  272. oldman/scaffolds/project/dashboard/run.sh.tpl +14 -0
  273. oldman/scaffolds/project/dashboard/scripts/build_frontend_i18n.py.tpl +119 -0
  274. oldman/scaffolds/project/dashboard/services/__init__.py.tpl +1 -0
  275. oldman/scaffolds/project/dashboard/services/{{ service_name }}.py.tpl +125 -0
  276. oldman/scaffolds/project/dashboard/static/.oldman_keep.tpl +1 -0
  277. oldman/scaffolds/project/dashboard/templates/base.html.tpl +44 -0
  278. oldman/scaffolds/project/dashboard/templates/errors/403.html.tpl +1 -0
  279. oldman/scaffolds/project/dashboard/templates/errors/404.html.tpl +1 -0
  280. oldman/scaffolds/project/dashboard/templates/errors/500.html.tpl +1 -0
  281. oldman/scaffolds/project/dashboard/templates/errors/default.html.tpl +1 -0
  282. oldman/scaffolds/project/web_service/.gitignore.tpl +5 -0
  283. oldman/scaffolds/project/web_service/.gitkeep +1 -0
  284. oldman/scaffolds/project/web_service/README.md.tpl +40 -0
  285. oldman/scaffolds/project/web_service/apps/__init__.py.tpl +1 -0
  286. oldman/scaffolds/project/web_service/babel.cfg.tpl +28 -0
  287. oldman/scaffolds/project/web_service/config/__init__.py.tpl +1 -0
  288. oldman/scaffolds/project/web_service/config/schemas.py.tpl +9 -0
  289. oldman/scaffolds/project/web_service/config/settings.py.tpl +10 -0
  290. oldman/scaffolds/project/web_service/data/{{ service_name }}_settings.yaml.tpl +3 -0
  291. oldman/scaffolds/project/web_service/locales/.gitkeep +1 -0
  292. oldman/scaffolds/project/web_service/pyproject.toml.tpl +14 -0
  293. oldman/scaffolds/project/web_service/run.sh.tpl +14 -0
  294. oldman/scaffolds/project/web_service/services/__init__.py.tpl +1 -0
  295. oldman/scaffolds/project/web_service/services/{{ service_name }}.py.tpl +39 -0
  296. oldman/scaffolds/project/web_service/static/.oldman_keep.tpl +1 -0
  297. oldman/scaffolds/project/web_service/templates/base.html.tpl +11 -0
  298. oldman/scaffolds/service/simple/services/{{ service_name }}.py.tpl +18 -0
  299. oldman/scaffolds/service/taskiq_scheduler/services/{{ service_name }}.py.tpl +7 -0
  300. oldman/scaffolds/service/taskiq_worker/services/{{ service_name }}.py.tpl +7 -0
  301. oldman/scaffolds/service/web/services/{{ service_name }}.py.tpl +39 -0
  302. oldman/security/__init__.py +17 -0
  303. oldman/security/jwt.py +155 -0
  304. oldman/security/rate_limiter/__init__.py +8 -0
  305. oldman/security/rate_limiter/token_bucket.py +72 -0
  306. oldman/serializers/__init__.py +31 -0
  307. oldman/serializers/base.py +226 -0
  308. oldman/serializers/cache.py +115 -0
  309. oldman/storage/__init__.py +46 -0
  310. oldman/storage/_web.py +89 -0
  311. oldman/storage/backends/__init__.py +4 -0
  312. oldman/storage/backends/filesystem.py +540 -0
  313. oldman/storage/backends/memory.py +98 -0
  314. oldman/storage/base.py +166 -0
  315. oldman/storage/exceptions.py +38 -0
  316. oldman/storage/lifecycle.py +261 -0
  317. oldman/storage/models.py +106 -0
  318. oldman/storage/registry.py +198 -0
  319. oldman/storage/streams.py +50 -0
  320. oldman/tasks/__init__.py +23 -0
  321. oldman/tasks/base.py +52 -0
  322. oldman/tasks/communication.py +100 -0
  323. oldman/tasks/distributed/__init__.py +13 -0
  324. oldman/tasks/distributed/broker.py +353 -0
  325. oldman/tasks/distributed/middleware.py +40 -0
  326. oldman/tasks/distributed/receiver.py +26 -0
  327. oldman/tasks/distributed/results.py +67 -0
  328. oldman/tasks/distributed/scheduler.py +116 -0
  329. oldman/tasks/distributed/worker.py +144 -0
  330. oldman/tasks/manager.py +367 -0
  331. oldman/tasks/messages.py +39 -0
  332. oldman/tasks/queue.py +94 -0
  333. oldman/tasks/simple.py +326 -0
  334. oldman/tasks/worker.py +159 -0
  335. oldman/utils/__init__.py +7 -0
  336. oldman/utils/collections_utils.py +233 -0
  337. oldman/utils/console.py +10 -0
  338. oldman/utils/date.py +86 -0
  339. oldman/utils/decorators.py +79 -0
  340. oldman/utils/files.py +38 -0
  341. oldman/utils/hash.py +192 -0
  342. oldman/utils/hash_ids.py +61 -0
  343. oldman/utils/headers.py +41 -0
  344. oldman/utils/http.py +113 -0
  345. oldman/utils/loop_utls.py +179 -0
  346. oldman/utils/singleton.py +160 -0
  347. oldman/utils/strings_utils.py +285 -0
  348. oldman/utils/thread.py +210 -0
  349. oldman/utils/timezone.py +59 -0
  350. oldman/version.py +10 -0
  351. oldman/web/__init__.py +54 -0
  352. oldman/web/api/__init__.py +27 -0
  353. oldman/web/api/actions.py +70 -0
  354. oldman/web/api/enums.py +40 -0
  355. oldman/web/api/responses.py +29 -0
  356. oldman/web/auth/__init__.py +35 -0
  357. oldman/web/auth/decorators.py +229 -0
  358. oldman/web/auth/forms.py +60 -0
  359. oldman/web/auth/session.py +40 -0
  360. oldman/web/auth/user_session.py +231 -0
  361. oldman/web/components/__init__.py +7 -0
  362. oldman/web/components/charts/__init__.py +21 -0
  363. oldman/web/components/charts/config.py +26 -0
  364. oldman/web/components/charts/renderers.py +50 -0
  365. oldman/web/components/charts/request.py +41 -0
  366. oldman/web/components/charts/results.py +49 -0
  367. oldman/web/components/charts/sqlalchemy.py +35 -0
  368. oldman/web/components/charts/views.py +133 -0
  369. oldman/web/components/forms/__init__.py +70 -0
  370. oldman/web/components/forms/base.py +493 -0
  371. oldman/web/components/forms/choices.py +54 -0
  372. oldman/web/components/forms/fields.py +395 -0
  373. oldman/web/components/forms/layouts.py +69 -0
  374. oldman/web/components/forms/models.py +367 -0
  375. oldman/web/components/forms/renderers.py +592 -0
  376. oldman/web/components/forms/tailwind.py +31 -0
  377. oldman/web/components/forms/widgets.py +391 -0
  378. oldman/web/components/selects/__init__.py +23 -0
  379. oldman/web/components/selects/choices.py +51 -0
  380. oldman/web/components/selects/providers.py +299 -0
  381. oldman/web/components/selects/registry.py +41 -0
  382. oldman/web/components/selects/signing.py +81 -0
  383. oldman/web/components/selects/views.py +76 -0
  384. oldman/web/components/tables/__init__.py +22 -0
  385. oldman/web/components/tables/columns.py +123 -0
  386. oldman/web/components/tables/legacy.py +207 -0
  387. oldman/web/components/tables/renderers.py +314 -0
  388. oldman/web/components/tables/request.py +34 -0
  389. oldman/web/components/tables/results.py +23 -0
  390. oldman/web/components/tables/views.py +659 -0
  391. oldman/web/errors.py +61 -0
  392. oldman/web/exceptions.py +5 -0
  393. oldman/web/html.py +53 -0
  394. oldman/web/http.py +154 -0
  395. oldman/web/i18n/__init__.py +15 -0
  396. oldman/web/i18n/assets.py +34 -0
  397. oldman/web/i18n/request.py +55 -0
  398. oldman/web/i18n/translation.py +223 -0
  399. oldman/web/messages/__init__.py +35 -0
  400. oldman/web/messages/_cookie_storage.py +396 -0
  401. oldman/web/messages/_template.py +42 -0
  402. oldman/web/messages/actions.py +20 -0
  403. oldman/web/messages/flash.py +173 -0
  404. oldman/web/messages/notifications/__init__.py +38 -0
  405. oldman/web/messages/notifications/apps.py +17 -0
  406. oldman/web/messages/notifications/locales/zh_Hans/LC_MESSAGES/messages.mo +0 -0
  407. oldman/web/messages/notifications/locales/zh_Hant/LC_MESSAGES/messages.mo +0 -0
  408. oldman/web/messages/notifications/migrations/4858aab957ee_create_oldman_notification.py +52 -0
  409. oldman/web/messages/notifications/migrations/__init__.py +0 -0
  410. oldman/web/messages/notifications/models.py +74 -0
  411. oldman/web/messages/notifications/payloads.py +122 -0
  412. oldman/web/messages/notifications/rendering.py +228 -0
  413. oldman/web/messages/notifications/runtime.py +242 -0
  414. oldman/web/messages/notifications/service.py +556 -0
  415. oldman/web/middlewares/__init__.py +5 -0
  416. oldman/web/middlewares/_legacy_i18n.py +7 -0
  417. oldman/web/middlewares/context.py +17 -0
  418. oldman/web/middlewares/i18n.py +80 -0
  419. oldman/web/middlewares/session.py +7 -0
  420. oldman/web/package_data.py +18 -0
  421. oldman/web/request.py +27 -0
  422. oldman/web/response.py +161 -0
  423. oldman/web/routing.py +69 -0
  424. oldman/web/security/__init__.py +27 -0
  425. oldman/web/security/csrf/__init__.py +12 -0
  426. oldman/web/security/csrf/csrf_extension.py +61 -0
  427. oldman/web/security/csrf/decorators.py +91 -0
  428. oldman/web/security/csrf/manager.py +243 -0
  429. oldman/web/security/decryptors.py +62 -0
  430. oldman/web/security/fingerprint.py +107 -0
  431. oldman/web/security/keys.py +52 -0
  432. oldman/web/security/rate_limiter/__init__.py +13 -0
  433. oldman/web/security/rate_limiter/base.py +13 -0
  434. oldman/web/security/rate_limiter/fingerprint.py +118 -0
  435. oldman/web/security/rate_limiter/fixed_window.py +42 -0
  436. oldman/web/security/rate_limiter/lua.py +123 -0
  437. oldman/web/session/__init__.py +182 -0
  438. oldman/web/session/base.py +693 -0
  439. oldman/web/sse/__init__.py +27 -0
  440. oldman/web/sse/connection.py +208 -0
  441. oldman/web/sse/events.py +75 -0
  442. oldman/web/sse/extension.py +438 -0
  443. oldman/web/sse/messages.py +39 -0
  444. oldman/web/sse/pool.py +83 -0
  445. oldman/web/sse/publisher.py +163 -0
  446. oldman/web/sse/stream.py +111 -0
  447. oldman/web/static/oldman/images/flags/cn.svg +11 -0
  448. oldman/web/static/oldman/images/flags/tw.svg +32 -0
  449. oldman/web/static/oldman/images/flags/us.svg +171 -0
  450. oldman/web/staticfiles/__init__.py +40 -0
  451. oldman/web/staticfiles/bundles.py +261 -0
  452. oldman/web/staticfiles/collector.py +655 -0
  453. oldman/web/staticfiles/finders.py +107 -0
  454. oldman/web/staticfiles/urls.py +47 -0
  455. oldman/web/template/__init__.py +143 -0
  456. oldman/web/template/_legacy_init.py +7 -0
  457. oldman/web/template/i18n_extension.py +149 -0
  458. oldman/web/template/render.py +38 -0
  459. oldman/web/templates/.gitkeep +1 -0
  460. oldman/web/templates/oldman/auth/partials/account_controls.html +28 -0
  461. oldman/web/templates/oldman/auth/partials/password_form.html +20 -0
  462. oldman/web/templates/oldman/auth/user_session_content.html +135 -0
  463. oldman/web/templates/oldman/charts/default/shell.html +8 -0
  464. oldman/web/templates/oldman/dashboard/base.html +41 -0
  465. oldman/web/templates/oldman/dashboard/components/modal.html +62 -0
  466. oldman/web/templates/oldman/dashboard/components/modal_fragment.html +14 -0
  467. oldman/web/templates/oldman/dashboard/errors/403.html +10 -0
  468. oldman/web/templates/oldman/dashboard/errors/404.html +10 -0
  469. oldman/web/templates/oldman/dashboard/errors/500.html +10 -0
  470. oldman/web/templates/oldman/dashboard/errors/_base.html +39 -0
  471. oldman/web/templates/oldman/dashboard/errors/default.html +10 -0
  472. oldman/web/templates/oldman/dashboard/partials/back_to_top.html +3 -0
  473. oldman/web/templates/oldman/dashboard/partials/language_switcher.html +40 -0
  474. oldman/web/templates/oldman/dashboard/partials/preloader.html +3 -0
  475. oldman/web/templates/oldman/dashboard/partials/preloader_critical_css.html +33 -0
  476. oldman/web/templates/oldman/dashboard/partials/shell.html +34 -0
  477. oldman/web/templates/oldman/dashboard/partials/topbar.html +131 -0
  478. oldman/web/templates/oldman/errors/403.html +10 -0
  479. oldman/web/templates/oldman/errors/404.html +10 -0
  480. oldman/web/templates/oldman/errors/500.html +10 -0
  481. oldman/web/templates/oldman/errors/_base.html +32 -0
  482. oldman/web/templates/oldman/errors/default.html +10 -0
  483. oldman/web/templates/oldman/forms/default/actions.html +5 -0
  484. oldman/web/templates/oldman/forms/default/boolean_field.html +2 -0
  485. oldman/web/templates/oldman/forms/default/field.html +6 -0
  486. oldman/web/templates/oldman/forms/default/field_errors.html +1 -0
  487. oldman/web/templates/oldman/forms/default/fields.html +3 -0
  488. oldman/web/templates/oldman/forms/default/form.html +9 -0
  489. oldman/web/templates/oldman/forms/default/help.html +1 -0
  490. oldman/web/templates/oldman/forms/default/json_list_field.html +42 -0
  491. oldman/web/templates/oldman/forms/default/message.html +1 -0
  492. oldman/web/templates/oldman/forms/default/multi_step.html +22 -0
  493. oldman/web/templates/oldman/forms/default/password_field.html +9 -0
  494. oldman/web/templates/oldman/forms/default/radio_field.html +8 -0
  495. oldman/web/templates/oldman/forms/default/status.html +1 -0
  496. oldman/web/templates/oldman/forms/default/widgets/ajax_autocomplete.html +7 -0
  497. oldman/web/templates/oldman/forms/default/widgets/ajax_select.html +5 -0
  498. oldman/web/templates/oldman/forms/default/widgets/color_picker.html +5 -0
  499. oldman/web/templates/oldman/forms/default/widgets/input_spinner.html +5 -0
  500. oldman/web/templates/oldman/forms/default/widgets/rich_text.html +4 -0
  501. oldman/web/templates/oldman/messages/flash_message.html +13 -0
  502. oldman/web/templates/oldman/messages/notifications/center_content.html +99 -0
  503. oldman/web/templates/oldman/messages/notifications/topbar_fragment.html +24 -0
  504. oldman/web/templates/oldman/tables/default/cell.html +1 -0
  505. oldman/web/templates/oldman/tables/default/error.html +1 -0
  506. oldman/web/templates/oldman/tables/default/fragment.html +39 -0
  507. oldman/web/templates/oldman/tables/default/head.html +19 -0
  508. oldman/web/templates/oldman/tables/default/pagination.html +11 -0
  509. oldman/web/templates/oldman/tables/default/row.html +6 -0
  510. oldman/web/templates/oldman/tables/default/shell.html +15 -0
  511. oldman/web/templates/oldman/tables/default/summary.html +5 -0
  512. oldman/web/websocket/__init__.py +46 -0
  513. oldman-0.1.0.dist-info/METADATA +157 -0
  514. oldman-0.1.0.dist-info/RECORD +517 -0
  515. oldman-0.1.0.dist-info/WHEEL +4 -0
  516. oldman-0.1.0.dist-info/entry_points.txt +2 -0
  517. oldman-0.1.0.dist-info/licenses/LICENSE +21 -0
oldman/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ @author:alex
3
+ @date:2023/12/29
4
+ @time:上午1:23
5
+ """
6
+
7
+ __author__ = "alex"
8
+
9
+ from oldman.runtime.bootstrap import (
10
+ ServiceBootstrapContext,
11
+ bootstrap_service,
12
+ )
13
+
14
+ __all__ = ["ServiceBootstrapContext", "bootstrap_service"]
@@ -0,0 +1,23 @@
1
+ """
2
+ @author:alex
3
+ @date:2026/7/14
4
+ @time:18:44
5
+ """
6
+
7
+ __author__ = "alex"
8
+
9
+ from oldman.apps.config import (
10
+ AppConfig,
11
+ AppNotInstalledError,
12
+ AppSettingsNotDefinedError,
13
+ AppSettingsNotReadyError,
14
+ )
15
+ from oldman.apps.registry import AppRegistry
16
+
17
+ __all__ = [
18
+ "AppConfig",
19
+ "AppNotInstalledError",
20
+ "AppRegistry",
21
+ "AppSettingsNotDefinedError",
22
+ "AppSettingsNotReadyError",
23
+ ]
@@ -0,0 +1,15 @@
1
+ """Built-in Admin application."""
2
+
3
+ from oldman.apps.admin.model_admin import AdminUserModelAdmin, ModelAdmin
4
+ from oldman.apps.admin.runtime import install_admin
5
+ from oldman.apps.admin.site import AdminSite, site
6
+ from oldman.apps.admin.users import AdminUserManagementError
7
+
8
+ __all__ = [
9
+ "AdminSite",
10
+ "AdminUserManagementError",
11
+ "AdminUserModelAdmin",
12
+ "ModelAdmin",
13
+ "install_admin",
14
+ "site",
15
+ ]
@@ -0,0 +1,19 @@
1
+ """Installable Admin application metadata."""
2
+
3
+ from oldman.apps import AppConfig
4
+ from oldman.apps.admin.settings import AdminSettings
5
+ from oldman.i18n import gettext_lazy as _
6
+
7
+
8
+ class AdminAppConfig(AppConfig[AdminSettings]):
9
+ """Describe the built-in Admin application and its settings owner."""
10
+
11
+ label = "admin"
12
+ display_name = _("Administration")
13
+ icon = "ri-admin-line"
14
+ settings_model = AdminSettings
15
+
16
+
17
+ app = AdminAppConfig()
18
+
19
+ __all__ = ["AdminAppConfig", "app"]
@@ -0,0 +1,94 @@
1
+ """User administration commands provided by the built-in Admin app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from oldman.auth import (
8
+ change_user_password,
9
+ ensure_superuser,
10
+ get_user_by_username,
11
+ user_identity,
12
+ )
13
+ from oldman.cli import Command
14
+ from oldman.i18n import gettext
15
+ from oldman.i18n import gettext_lazy as _
16
+
17
+
18
+ def _username(value: str | None) -> str:
19
+ """Return one non-empty username from an option or interactive prompt."""
20
+ username = (value if value is not None else typer.prompt(gettext("Username"))).strip()
21
+ if not username:
22
+ raise ValueError(gettext("Username cannot be empty."))
23
+ return username
24
+
25
+
26
+ def _prompt_password() -> str:
27
+ """Read one password without exposing it in command arguments or terminal output."""
28
+ return str(
29
+ typer.prompt(
30
+ gettext("Password"),
31
+ hide_input=True,
32
+ confirmation_prompt=gettext("Confirm Password"),
33
+ )
34
+ )
35
+
36
+
37
+ class CreateSuperuser(Command):
38
+ """Create one new active staff superuser."""
39
+
40
+ name = "createsuperuser"
41
+ help = _("Create Superuser")
42
+
43
+ async def handle(
44
+ self,
45
+ username: str | None = None,
46
+ email: str | None = None,
47
+ ) -> str:
48
+ """Prompt for missing credentials and create one configured User."""
49
+ selected_username = _username(username)
50
+ if await get_user_by_username(selected_username) is not None:
51
+ raise ValueError(gettext("Username already exists"))
52
+
53
+ selected_email = (
54
+ email
55
+ if email is not None
56
+ else str(
57
+ typer.prompt(
58
+ gettext("Email"),
59
+ default="",
60
+ show_default=False,
61
+ )
62
+ )
63
+ )
64
+ await ensure_superuser(
65
+ selected_username,
66
+ _prompt_password(),
67
+ selected_email.strip() or None,
68
+ )
69
+ return gettext("User created")
70
+
71
+
72
+ class ChangePassword(Command):
73
+ """Change one existing configured User password."""
74
+
75
+ name = "changepassword"
76
+ help = _("Change Password")
77
+
78
+ async def handle(self, username: str) -> str:
79
+ """Find the selected User and replace its password."""
80
+ selected_username = _username(username)
81
+ user = await get_user_by_username(selected_username)
82
+ if user is None:
83
+ raise ValueError(gettext("User not found."))
84
+
85
+ changed = await change_user_password(
86
+ user_identity(user),
87
+ _prompt_password(),
88
+ )
89
+ if changed is None:
90
+ raise ValueError(gettext("User not found."))
91
+ return gettext("Password changed")
92
+
93
+
94
+ __all__ = ["ChangePassword", "CreateSuperuser"]
@@ -0,0 +1,94 @@
1
+ """Admin model identity coercion helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid as uuid_pkg
6
+ from decimal import Decimal
7
+ from typing import Any
8
+
9
+ from sqlalchemy import Integer
10
+ from sqlalchemy.sql.schema import Column
11
+
12
+
13
+ def coerce_value(column: Column[Any], value: Any) -> Any:
14
+ """Coerce a form or identity value through one mapped column's Python type."""
15
+ value = first_value(value)
16
+ if value is None:
17
+ return None
18
+ if value == "" and column.nullable:
19
+ return None
20
+ try:
21
+ python_type = column.type.python_type
22
+ except (AttributeError, NotImplementedError):
23
+ return value
24
+ if isinstance(value, python_type):
25
+ return value
26
+ if python_type is bool:
27
+ normalized = str(value).strip().lower()
28
+ if normalized in {"1", "true", "yes", "on"}:
29
+ return True
30
+ if normalized in {"0", "false", "no", "off"}:
31
+ return False
32
+ raise ValueError(f"Invalid boolean value: {value!r}")
33
+ try:
34
+ if python_type is Decimal:
35
+ return Decimal(str(value))
36
+ if python_type is uuid_pkg.UUID:
37
+ return uuid_pkg.UUID(str(value))
38
+ return python_type(value)
39
+ except (ArithmeticError, TypeError, ValueError) as exc:
40
+ raise ValueError(f"Invalid {python_type.__name__} value: {value!r}") from exc
41
+
42
+
43
+ def first_value(value: Any) -> Any:
44
+ """Return a scalar value from Sanic form mappings."""
45
+ if isinstance(value, (list, tuple)):
46
+ return value[0] if value else None
47
+ return value
48
+
49
+
50
+ def explicit_primary_key_column(mapper: Any, *, dialect: Any = None) -> Any | None:
51
+ """Return a key that the active SQLAlchemy dialect cannot generate."""
52
+ primary_key = list(mapper.primary_key)
53
+ if len(primary_key) != 1:
54
+ raise ValueError("Admin models require exactly one primary key column")
55
+ column = primary_key[0]
56
+ if column.default is not None or column.server_default is not None or column.identity is not None:
57
+ return None
58
+ if not isinstance(column.type, Integer) or column.autoincrement not in {True, "auto", "ignore_fk"}:
59
+ return column
60
+ if getattr(column.table, "_autoincrement_column", None) is not column:
61
+ return column
62
+
63
+ if dialect is None:
64
+ # With no bound session, keep the framework's portable Integer default
65
+ # while requiring dialect-sensitive subclasses such as BigInteger.
66
+ return None if type(column.type) is Integer else column
67
+
68
+ dialect_name = str(getattr(dialect, "name", "")).lower()
69
+ if dialect_name == "sqlite":
70
+ # SQLite generates rowids only when the dialect renders the declared
71
+ # type as the exact token INTEGER (including Integer with_variant()).
72
+ rendered_type = str(column.type.compile(dialect=dialect)).strip().upper()
73
+ if rendered_type == "INTEGER":
74
+ return None
75
+ return column
76
+ if dialect_name in {"cockroachdb", "mariadb", "mssql", "mysql", "postgresql"}:
77
+ return None
78
+ return column
79
+
80
+
81
+ def session_dialect(session: Any) -> Any | None:
82
+ """Return the SQLAlchemy dialect bound to a sync or async session."""
83
+ if session is None:
84
+ return None
85
+ bind = None
86
+ get_bind = getattr(session, "get_bind", None)
87
+ if callable(get_bind):
88
+ try:
89
+ bind = get_bind()
90
+ except (AttributeError, NotImplementedError):
91
+ bind = None
92
+ if bind is None:
93
+ bind = getattr(session, "bind", None)
94
+ return getattr(bind, "dialect", None)
@@ -0,0 +1,323 @@
1
+ """Forms for the built-in Admin user manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import cache
6
+ from typing import Any, cast
7
+
8
+ from sqlalchemy import inspect as sqlalchemy_inspect
9
+ from sqlalchemy import select
10
+ from wtforms import (
11
+ BooleanField,
12
+ DateTimeLocalField,
13
+ PasswordField,
14
+ SelectField,
15
+ StringField,
16
+ )
17
+ from wtforms.validators import DataRequired, InputRequired, Length, Optional, Regexp
18
+
19
+ from oldman.apps.admin.crud import explicit_primary_key_column, session_dialect
20
+ from oldman.apps.admin.users import (
21
+ admin_user_identity_matches,
22
+ change_admin_user_password,
23
+ )
24
+ from oldman.i18n import gettext, gettext_lazy
25
+ from oldman.web.auth.forms import PASSWORD_MESSAGE, PASSWORD_PATTERN
26
+ from oldman.web.components.forms import (
27
+ DateTimePickerWidget,
28
+ FieldLayout,
29
+ TailwindForm,
30
+ TailwindModelForm,
31
+ TailwindTableFilterForm,
32
+ )
33
+ from oldman.web.components.forms.models import model_field_for_column
34
+
35
+ BOOLEAN_FILTER_CHOICES = (
36
+ ("", cast(str, gettext_lazy("All"))),
37
+ ("true", cast(str, gettext_lazy("Yes"))),
38
+ ("false", cast(str, gettext_lazy("No"))),
39
+ )
40
+ EDITABLE_USER_FIELDS = ("username", "email", "display_name", "is_active", "is_staff", "is_superuser")
41
+
42
+
43
+ class AdminLoginForm(TailwindForm):
44
+ """Collect the built-in Admin login credentials."""
45
+
46
+ username = StringField(
47
+ cast(str, gettext_lazy("Username")),
48
+ id="username",
49
+ validators=[DataRequired()],
50
+ render_kw={
51
+ "required": True,
52
+ "autocomplete": "username",
53
+ "placeholder": gettext_lazy("Enter username"),
54
+ "autofocus": True,
55
+ },
56
+ )
57
+ password = PasswordField(
58
+ cast(str, gettext_lazy("Password")),
59
+ id="password-input",
60
+ validators=[DataRequired()],
61
+ render_kw={
62
+ "required": True,
63
+ "autocomplete": "current-password",
64
+ "placeholder": gettext_lazy("Enter password"),
65
+ },
66
+ )
67
+
68
+
69
+ class AdminUserFilterForm(TailwindTableFilterForm):
70
+ """Filter the configured Admin user table."""
71
+
72
+ q = StringField(
73
+ cast(str, gettext_lazy("Search")),
74
+ render_kw={"type": "search", "placeholder": gettext_lazy("Search username, email, display name")},
75
+ )
76
+ is_active = SelectField(cast(str, gettext_lazy("Active")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
77
+ is_staff = SelectField(cast(str, gettext_lazy("Staff")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
78
+ is_superuser = SelectField(cast(str, gettext_lazy("Superuser")), choices=BOOLEAN_FILTER_CHOICES, validators=[Optional()])
79
+ last_login_from = DateTimeLocalField(
80
+ cast(str, gettext_lazy("Last Login From")),
81
+ format="%Y-%m-%dT%H:%M",
82
+ validators=[Optional()],
83
+ widget=DateTimePickerWidget(date_format="Y-m-d\\TH:i"),
84
+ )
85
+ last_login_to = DateTimeLocalField(
86
+ cast(str, gettext_lazy("Last Login To")),
87
+ format="%Y-%m-%dT%H:%M",
88
+ validators=[Optional()],
89
+ widget=DateTimePickerWidget(date_format="Y-m-d\\TH:i"),
90
+ )
91
+
92
+ field_layout = (
93
+ FieldLayout("q", "lg:col-span-4 md:col-span-6"),
94
+ FieldLayout("is_active", "lg:col-span-2 md:col-span-6"),
95
+ FieldLayout("is_staff", "lg:col-span-2 md:col-span-6"),
96
+ FieldLayout("is_superuser", "lg:col-span-2 md:col-span-6"),
97
+ FieldLayout("last_login_from", "lg:col-span-2 md:col-span-6"),
98
+ FieldLayout("last_login_to", "lg:col-span-2 md:col-span-6"),
99
+ )
100
+
101
+
102
+ class AdminUserModelForm(TailwindModelForm):
103
+ """Shared validation and persistence for configured Admin user models."""
104
+
105
+ current_user_id: int | None = None
106
+ confirm_password: Any
107
+ email: Any
108
+ password: Any
109
+ username: Any
110
+
111
+ async def clean_username(self) -> str:
112
+ """Validate username uniqueness before a database constraint can fail."""
113
+ username = str(self.username.data or "").strip()
114
+ if self.session is None or not username:
115
+ return username
116
+ result = await self.session.execute(select(self.model).where(self.model.username == username))
117
+ existing = result.scalar_one_or_none()
118
+ if existing is not None and not self.is_same_instance(existing):
119
+ self.add_error("username", gettext("Username already exists"))
120
+ return username
121
+
122
+ async def clean_email(self) -> str | None:
123
+ """Validate optional email uniqueness before persistence."""
124
+ email = str(self.email.data or "").strip()
125
+ if not email:
126
+ return None
127
+ if self.session is None:
128
+ return email
129
+ result = await self.session.execute(select(self.model).where(self.model.email == email))
130
+ existing = result.scalar_one_or_none()
131
+ if existing is not None and not self.is_same_instance(existing):
132
+ self.add_error("email", gettext("Email already exists"))
133
+ return email
134
+
135
+ async def clean_explicit_primary_key(self) -> Any:
136
+ """Validate a user-supplied primary key before database persistence."""
137
+ primary_key = list(self.mapper.primary_key)[0]
138
+ field_name = primary_key.key
139
+ value = self._fields[field_name].data
140
+ if self.session is None or value is None or value == "":
141
+ return value
142
+ result = await self.session.execute(select(self.model).where(getattr(self.model, field_name) == value))
143
+ existing = result.scalar_one_or_none()
144
+ if existing is not None and not self.is_same_instance(existing):
145
+ self.add_error(
146
+ field_name,
147
+ gettext("%(field)s already exists", field=self._fields[field_name].label.text),
148
+ )
149
+ return value
150
+
151
+ async def clean(self) -> None:
152
+ """Protect the current user and validate password confirmation."""
153
+ await super().clean()
154
+ if self.instance is not None and admin_user_identity_matches(self.instance, self.current_user_id):
155
+ if self.cleaned_data.get("is_active") is False:
156
+ self.add_error("is_active", gettext("cannot disable current user"))
157
+ if self.cleaned_data.get("is_staff") is False:
158
+ self.add_error("is_staff", gettext("cannot remove current user staff access"))
159
+ if self.cleaned_data.get("is_superuser") is False:
160
+ self.add_error("is_superuser", gettext("cannot remove current user superuser access"))
161
+ if "confirm_password" in self._fields and self.password.data != self.confirm_password.data:
162
+ self.add_error("confirm_password", gettext("Passwords do not match"))
163
+
164
+ async def save(self, *, commit: bool = False, session: Any = None) -> Any:
165
+ """Persist profile fields and hash the create-form password."""
166
+ user = await super().save(commit=False)
167
+ if bool(getattr(user, "is_superuser", False)):
168
+ user.is_staff = True
169
+ if "password" in self._fields:
170
+ change_admin_user_password(user, str(self.password.data or ""))
171
+ if commit:
172
+ active_session = session or self.session
173
+ if active_session is None:
174
+ raise ValueError("save(commit=True) requires a session")
175
+ active_session.add(user)
176
+ flush_result = active_session.flush()
177
+ import inspect
178
+
179
+ if inspect.isawaitable(flush_result):
180
+ await flush_result
181
+ return user
182
+
183
+ def is_same_instance(self, other: Any) -> bool:
184
+ """Compare rows without assuming the primary key is named ``id``."""
185
+ if self.instance is None:
186
+ return False
187
+ try:
188
+ primary_key = list(self.mapper.primary_key)
189
+ return len(primary_key) == 1 and getattr(other, primary_key[0].key) == getattr(self.instance, primary_key[0].key)
190
+ except (AttributeError, TypeError):
191
+ return other is self.instance
192
+
193
+ @property
194
+ def mapper(self):
195
+ """Return the mapped model metadata used by identity comparisons."""
196
+ from sqlalchemy import inspect as sqlalchemy_inspect
197
+
198
+ return sqlalchemy_inspect(self.model)
199
+
200
+
201
+ @cache
202
+ def admin_user_edit_form_class(user_model: type[Any]) -> type[AdminUserModelForm]:
203
+ """Build a shared ModelForm bound to the configured user model."""
204
+ return _admin_user_model_form_class(user_model, create=False, include_explicit_primary_key=False)
205
+
206
+
207
+ def admin_user_create_form_class(user_model: type[Any], *, session: Any = None, dialect: Any = None) -> type[AdminUserModelForm]:
208
+ """Build a password-aware ModelForm bound to the configured user model."""
209
+ mapper = sqlalchemy_inspect(user_model)
210
+ resolved_dialect = dialect or session_dialect(session)
211
+ include_primary_key = explicit_primary_key_column(mapper, dialect=resolved_dialect) is not None
212
+ return _admin_user_model_form_class(
213
+ user_model,
214
+ create=True,
215
+ include_explicit_primary_key=include_primary_key,
216
+ )
217
+
218
+
219
+ @cache
220
+ def _admin_user_model_form_class(
221
+ user_model: type[Any],
222
+ *,
223
+ create: bool,
224
+ include_explicit_primary_key: bool,
225
+ ) -> type[AdminUserModelForm]:
226
+ mapper = sqlalchemy_inspect(user_model)
227
+ model_fields = list(EDITABLE_USER_FIELDS)
228
+ field_layout: list[FieldLayout] = [
229
+ FieldLayout("username", "md:col-span-6"),
230
+ FieldLayout("email", "md:col-span-6"),
231
+ FieldLayout("display_name", "md:col-span-6"),
232
+ ]
233
+ fields: dict[str, Any] = {
234
+ "username": StringField(
235
+ cast(str, gettext_lazy("Username")),
236
+ validators=[DataRequired(), Length(max=150)],
237
+ render_kw={"required": True, "maxlength": 150},
238
+ ),
239
+ "email": StringField(
240
+ cast(str, gettext_lazy("Email")),
241
+ validators=[Optional(), Length(max=254)],
242
+ render_kw={"maxlength": 254},
243
+ ),
244
+ "display_name": StringField(
245
+ cast(str, gettext_lazy("Display Name")),
246
+ validators=[Optional(), Length(max=150)],
247
+ render_kw={"maxlength": 150},
248
+ ),
249
+ "is_active": BooleanField(cast(str, gettext_lazy("Active"))),
250
+ "is_staff": BooleanField(cast(str, gettext_lazy("Staff"))),
251
+ "is_superuser": BooleanField(cast(str, gettext_lazy("Superuser"))),
252
+ }
253
+ primary_key = list(mapper.primary_key)[0]
254
+ if not create and primary_key.key in model_fields:
255
+ # Session identity is backed by the persisted primary key. Editing it
256
+ # would orphan the active session and bypass the source self-protection
257
+ # rules, so custom protocol models may only collect it during create.
258
+ model_fields.remove(primary_key.key)
259
+ fields.pop(primary_key.key, None)
260
+ field_layout = [item for item in field_layout if item.name != primary_key.key]
261
+ if create:
262
+ if include_explicit_primary_key:
263
+ field_name = primary_key.key
264
+ if field_name not in fields:
265
+ fields[field_name] = model_field_for_column(primary_key, mapper=mapper)
266
+ model_fields.insert(0, field_name)
267
+ field_layout.insert(0, FieldLayout(field_name, "md:col-span-6"))
268
+ validators = list(fields[field_name].kwargs.get("validators", []))
269
+ if fields[field_name].field_class is not BooleanField and not any(
270
+ isinstance(validator, (DataRequired, InputRequired)) for validator in validators
271
+ ):
272
+ fields[field_name].kwargs["validators"] = [InputRequired(), *validators]
273
+ if not hasattr(AdminUserModelForm, f"clean_{field_name}"):
274
+ fields[f"clean_{field_name}"] = AdminUserModelForm.clean_explicit_primary_key
275
+ fields.update(
276
+ password=PasswordField(
277
+ cast(str, gettext_lazy("Password")),
278
+ validators=[
279
+ DataRequired(),
280
+ Length(min=8, max=128),
281
+ Regexp(PASSWORD_PATTERN, message=PASSWORD_MESSAGE),
282
+ ],
283
+ render_kw={
284
+ "required": True,
285
+ "minlength": 8,
286
+ "maxlength": 128,
287
+ "pattern": PASSWORD_PATTERN,
288
+ "autocomplete": "new-password",
289
+ },
290
+ ),
291
+ confirm_password=PasswordField(
292
+ cast(str, gettext_lazy("Confirm Password")),
293
+ validators=[DataRequired(), Length(min=8, max=128)],
294
+ render_kw={"required": True, "minlength": 8, "maxlength": 128, "autocomplete": "new-password"},
295
+ ),
296
+ )
297
+ field_layout.extend(
298
+ (
299
+ FieldLayout("password", "md:col-span-6"),
300
+ FieldLayout("confirm_password", "md:col-span-6"),
301
+ )
302
+ )
303
+ field_layout.extend(
304
+ (
305
+ FieldLayout("is_active", "md:col-span-4"),
306
+ FieldLayout("is_staff", "md:col-span-4"),
307
+ FieldLayout("is_superuser", "md:col-span-4"),
308
+ )
309
+ )
310
+ fields["field_layout"] = tuple(field_layout)
311
+ fields["Meta"] = type("Meta", (), {"model": user_model, "fields": model_fields})
312
+ suffix = "CreateForm" if create else "EditForm"
313
+ return type(f"{user_model.__name__}{suffix}", (AdminUserModelForm,), fields)
314
+
315
+
316
+ __all__ = [
317
+ "AdminLoginForm",
318
+ "AdminUserFilterForm",
319
+ "PASSWORD_MESSAGE",
320
+ "PASSWORD_PATTERN",
321
+ "admin_user_create_form_class",
322
+ "admin_user_edit_form_class",
323
+ ]