python-platform 0.1.1__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 (344) hide show
  1. python_platform/__init__.py +445 -0
  2. python_platform/application/__init__.py +6 -0
  3. python_platform/application/build_spec.py +37 -0
  4. python_platform/application/builder.py +260 -0
  5. python_platform/application/composition.py +219 -0
  6. python_platform/application/runtime.py +546 -0
  7. python_platform/application/runtime_hooks.py +327 -0
  8. python_platform/application/state.py +216 -0
  9. python_platform/application_services/__init__.py +54 -0
  10. python_platform/application_services/catalog.py +500 -0
  11. python_platform/application_services/contracts.py +188 -0
  12. python_platform/application_services/dispatcher.py +423 -0
  13. python_platform/application_services/errors.py +89 -0
  14. python_platform/application_services/execution.py +199 -0
  15. python_platform/application_services/interceptors.py +44 -0
  16. python_platform/application_services/invocation.py +427 -0
  17. python_platform/application_services/policies.py +46 -0
  18. python_platform/application_services/seeding.py +72 -0
  19. python_platform/application_services/signature.py +49 -0
  20. python_platform/application_services/validation.py +84 -0
  21. python_platform/auditing/__init__.py +12 -0
  22. python_platform/auditing/contracts.py +39 -0
  23. python_platform/auditing/control.py +48 -0
  24. python_platform/auditing/sqlalchemy/__init__.py +6 -0
  25. python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
  26. python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_platform/auditing/sqlalchemy/models.py +31 -0
  28. python_platform/auditing/sqlalchemy/module.py +32 -0
  29. python_platform/auditing/sqlalchemy/store.py +63 -0
  30. python_platform/authorization/__init__.py +38 -0
  31. python_platform/authorization/catalog.py +63 -0
  32. python_platform/authorization/contracts.py +241 -0
  33. python_platform/authorization/definition_discovery.py +81 -0
  34. python_platform/authorization/definitions.py +43 -0
  35. python_platform/authorization/errors.py +40 -0
  36. python_platform/background_execution/__init__.py +15 -0
  37. python_platform/background_execution/application.py +41 -0
  38. python_platform/background_execution/child.py +123 -0
  39. python_platform/background_execution/contracts.py +39 -0
  40. python_platform/background_execution/lifecycle.py +22 -0
  41. python_platform/background_execution/local.py +127 -0
  42. python_platform/background_execution/locks.py +31 -0
  43. python_platform/background_execution/management.py +17 -0
  44. python_platform/background_execution/module.py +10 -0
  45. python_platform/background_execution/permissions.py +14 -0
  46. python_platform/background_execution/processes.py +331 -0
  47. python_platform/background_jobs/__init__.py +31 -0
  48. python_platform/background_jobs/catalog.py +287 -0
  49. python_platform/background_jobs/contracts.py +202 -0
  50. python_platform/background_jobs/errors.py +28 -0
  51. python_platform/background_jobs/execution.py +201 -0
  52. python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  53. python_platform/background_jobs/pgqueuer/__init__.py +6 -0
  54. python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
  55. python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
  56. python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  57. python_platform/background_jobs/pgqueuer/module.py +102 -0
  58. python_platform/background_jobs/pgqueuer/options.py +70 -0
  59. python_platform/background_jobs/pgqueuer/runtime.py +479 -0
  60. python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  61. python_platform/background_jobs/pgqueuer/supervision.py +201 -0
  62. python_platform/background_workers/__init__.py +22 -0
  63. python_platform/background_workers/catalog.py +96 -0
  64. python_platform/background_workers/contracts.py +124 -0
  65. python_platform/background_workers/errors.py +20 -0
  66. python_platform/background_workers/execution.py +133 -0
  67. python_platform/background_workers/runtime.py +203 -0
  68. python_platform/caching/__init__.py +11 -0
  69. python_platform/caching/catalog.py +73 -0
  70. python_platform/caching/contracts.py +73 -0
  71. python_platform/caching/errors.py +20 -0
  72. python_platform/cli/__init__.py +87 -0
  73. python_platform/cli/errors.py +5 -0
  74. python_platform/cli/project.py +69 -0
  75. python_platform/cli/runtime.py +68 -0
  76. python_platform/configuration/__init__.py +21 -0
  77. python_platform/configuration/composition.py +65 -0
  78. python_platform/configuration/contracts.py +68 -0
  79. python_platform/configuration/dotenv_source.py +51 -0
  80. python_platform/configuration/environment_source.py +38 -0
  81. python_platform/configuration/immutability.py +124 -0
  82. python_platform/configuration/input_shape.py +58 -0
  83. python_platform/configuration/merge.py +103 -0
  84. python_platform/configuration/root.py +215 -0
  85. python_platform/configuration/sources.py +519 -0
  86. python_platform/configuration/values.py +427 -0
  87. python_platform/configuration/yaml_source.py +51 -0
  88. python_platform/developer_kit/__init__.py +1 -0
  89. python_platform/developer_kit/generation.py +182 -0
  90. python_platform/developer_kit/project_metadata.py +45 -0
  91. python_platform/developer_kit/source.py +53 -0
  92. python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
  93. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  94. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  95. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  96. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  97. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
  98. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
  99. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
  100. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
  101. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
  102. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  103. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  104. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
  105. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  106. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  107. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
  108. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
  109. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  110. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  111. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  112. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  113. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  114. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  115. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  116. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
  117. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
  118. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
  119. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  120. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  121. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
  122. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
  123. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
  124. python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  125. python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
  126. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
  127. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
  128. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  129. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  130. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  131. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
  132. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
  133. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  134. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
  135. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  136. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  137. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
  138. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
  139. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  140. python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
  141. python_platform/developer_kit/wiring.py +85 -0
  142. python_platform/diagnostics/__init__.py +23 -0
  143. python_platform/diagnostics/journal.py +174 -0
  144. python_platform/diagnostics/model.py +83 -0
  145. python_platform/diagnostics/source.py +25 -0
  146. python_platform/distributed_lock/__init__.py +6 -0
  147. python_platform/distributed_lock/contracts.py +20 -0
  148. python_platform/distributed_lock/options.py +19 -0
  149. python_platform/domain/__init__.py +13 -0
  150. python_platform/domain/aggregates.py +64 -0
  151. python_platform/domain/entities.py +33 -0
  152. python_platform/domain/errors.py +12 -0
  153. python_platform/domain/value_objects.py +31 -0
  154. python_platform/errors/__init__.py +75 -0
  155. python_platform/errors/base.py +13 -0
  156. python_platform/errors/business.py +53 -0
  157. python_platform/errors/configuration.py +119 -0
  158. python_platform/errors/diagnostics.py +12 -0
  159. python_platform/errors/lifecycle.py +164 -0
  160. python_platform/errors/modularity.py +100 -0
  161. python_platform/errors/services.py +100 -0
  162. python_platform/events/__init__.py +25 -0
  163. python_platform/events/aggregate.py +39 -0
  164. python_platform/events/catalog.py +136 -0
  165. python_platform/events/contracts.py +39 -0
  166. python_platform/events/contribution.py +128 -0
  167. python_platform/events/discovery.py +105 -0
  168. python_platform/events/errors.py +48 -0
  169. python_platform/events/runtime.py +192 -0
  170. python_platform/fastapi/__init__.py +49 -0
  171. python_platform/fastapi/action.py +175 -0
  172. python_platform/fastapi/adapter.py +381 -0
  173. python_platform/fastapi/application_services.py +647 -0
  174. python_platform/fastapi/background.py +42 -0
  175. python_platform/fastapi/contracts.py +278 -0
  176. python_platform/fastapi/errors.py +32 -0
  177. python_platform/fastapi/filters.py +111 -0
  178. python_platform/fastapi/health.py +44 -0
  179. python_platform/fastapi/http.py +55 -0
  180. python_platform/fastapi/http_router.py +212 -0
  181. python_platform/fastapi/manual_action.py +105 -0
  182. python_platform/fastapi/middleware.py +93 -0
  183. python_platform/fastapi/parameters.py +97 -0
  184. python_platform/fastapi/realtime/__init__.py +13 -0
  185. python_platform/fastapi/realtime/authentication.py +181 -0
  186. python_platform/fastapi/realtime/connection.py +166 -0
  187. python_platform/fastapi/realtime/module.py +41 -0
  188. python_platform/fastapi/realtime/options.py +50 -0
  189. python_platform/fastapi/realtime/runtime.py +423 -0
  190. python_platform/fastapi/request_context.py +244 -0
  191. python_platform/fastapi/route_integrity.py +88 -0
  192. python_platform/fastapi/routing.py +410 -0
  193. python_platform/fastapi/server.py +181 -0
  194. python_platform/fastapi/settings.py +40 -0
  195. python_platform/fastapi/tracing.py +62 -0
  196. python_platform/fastapi/transfer.py +128 -0
  197. python_platform/fastapi/upload_limits.py +63 -0
  198. python_platform/fastapi/uploads.py +94 -0
  199. python_platform/hosted_services/__init__.py +29 -0
  200. python_platform/hosted_services/bridge.py +420 -0
  201. python_platform/hosted_services/catalog.py +259 -0
  202. python_platform/hosted_services/contracts.py +55 -0
  203. python_platform/hosted_services/errors.py +53 -0
  204. python_platform/hosted_services/options.py +17 -0
  205. python_platform/hosted_services/runtime.py +267 -0
  206. python_platform/hosted_services/state.py +40 -0
  207. python_platform/hosting/__init__.py +4 -0
  208. python_platform/hosting/instance.py +40 -0
  209. python_platform/identity/__init__.py +73 -0
  210. python_platform/identity/application.py +287 -0
  211. python_platform/identity/contracts.py +283 -0
  212. python_platform/identity/errors.py +28 -0
  213. python_platform/identity/http_api.py +90 -0
  214. python_platform/identity/module.py +46 -0
  215. python_platform/identity/passwords.py +34 -0
  216. python_platform/identity/permissions.py +14 -0
  217. python_platform/identity/services.py +108 -0
  218. python_platform/identity/sqlalchemy/__init__.py +5 -0
  219. python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
  220. python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  221. python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
  222. python_platform/identity/sqlalchemy/models.py +85 -0
  223. python_platform/identity/sqlalchemy/module.py +62 -0
  224. python_platform/identity/sqlalchemy/stores.py +526 -0
  225. python_platform/identity/tokens.py +106 -0
  226. python_platform/invocation/__init__.py +3 -0
  227. python_platform/invocation/callables.py +171 -0
  228. python_platform/invocation/contracts.py +33 -0
  229. python_platform/invocation/entries.py +54 -0
  230. python_platform/invocation/function_runtime.py +48 -0
  231. python_platform/invocation/interception.py +184 -0
  232. python_platform/lifecycle/__init__.py +25 -0
  233. python_platform/lifecycle/composition.py +35 -0
  234. python_platform/lifecycle/context.py +31 -0
  235. python_platform/lifecycle/runtime.py +43 -0
  236. python_platform/lifecycle/state.py +20 -0
  237. python_platform/modularity/__init__.py +15 -0
  238. python_platform/modularity/contracts.py +90 -0
  239. python_platform/modularity/discovery.py +143 -0
  240. python_platform/modularity/graph.py +334 -0
  241. python_platform/modularity/registry.py +100 -0
  242. python_platform/modularity/selection.py +16 -0
  243. python_platform/notifications/__init__.py +19 -0
  244. python_platform/notifications/catalog.py +46 -0
  245. python_platform/notifications/contracts.py +107 -0
  246. python_platform/observability/__init__.py +5 -0
  247. python_platform/observability/context.py +43 -0
  248. python_platform/observability/export.py +73 -0
  249. python_platform/observability/formatting.py +89 -0
  250. python_platform/observability/logging.py +118 -0
  251. python_platform/observability/options.py +45 -0
  252. python_platform/observability/tracing.py +80 -0
  253. python_platform/options/__init__.py +19 -0
  254. python_platform/options/aliases.py +331 -0
  255. python_platform/options/contribution.py +62 -0
  256. python_platform/options/immutability.py +48 -0
  257. python_platform/options/input_keys.py +214 -0
  258. python_platform/options/issues.py +335 -0
  259. python_platform/options/models.py +114 -0
  260. python_platform/options/registry.py +280 -0
  261. python_platform/options/schema.py +488 -0
  262. python_platform/options/validation.py +120 -0
  263. python_platform/py.typed +0 -0
  264. python_platform/realtime/__init__.py +12 -0
  265. python_platform/realtime/contracts.py +28 -0
  266. python_platform/realtime/diagnostics.py +37 -0
  267. python_platform/realtime/messages.py +113 -0
  268. python_platform/redis/__init__.py +6 -0
  269. python_platform/redis/distributed_lock.py +212 -0
  270. python_platform/redis/lease_lock.py +36 -0
  271. python_platform/redis/module.py +50 -0
  272. python_platform/redis/notification_runtime.py +182 -0
  273. python_platform/redis/notifications.py +25 -0
  274. python_platform/redis/options.py +35 -0
  275. python_platform/redis/runtime.py +116 -0
  276. python_platform/services/__init__.py +39 -0
  277. python_platform/services/application_bindings.py +221 -0
  278. python_platform/services/arbitration.py +381 -0
  279. python_platform/services/binding.py +102 -0
  280. python_platform/services/contribution.py +438 -0
  281. python_platform/services/convention.py +292 -0
  282. python_platform/services/convention_contracts.py +94 -0
  283. python_platform/services/exposure.py +23 -0
  284. python_platform/services/framework_provider.py +178 -0
  285. python_platform/services/native_graph.py +172 -0
  286. python_platform/services/provider.py +204 -0
  287. python_platform/services/registration.py +101 -0
  288. python_platform/services/runtime.py +298 -0
  289. python_platform/settings/__init__.py +30 -0
  290. python_platform/settings/application.py +95 -0
  291. python_platform/settings/binding.py +33 -0
  292. python_platform/settings/catalog.py +121 -0
  293. python_platform/settings/changes.py +14 -0
  294. python_platform/settings/contracts.py +115 -0
  295. python_platform/settings/definition_discovery.py +71 -0
  296. python_platform/settings/definitions.py +41 -0
  297. python_platform/settings/errors.py +22 -0
  298. python_platform/settings/handlers.py +43 -0
  299. python_platform/settings/management.py +57 -0
  300. python_platform/settings/manager.py +89 -0
  301. python_platform/settings/module.py +7 -0
  302. python_platform/settings/notifications.py +39 -0
  303. python_platform/settings/permissions.py +14 -0
  304. python_platform/settings/provider.py +74 -0
  305. python_platform/settings/refresh.py +140 -0
  306. python_platform/settings/refresh_module.py +45 -0
  307. python_platform/settings/sqlalchemy/__init__.py +5 -0
  308. python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
  309. python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  310. python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  311. python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
  312. python_platform/settings/sqlalchemy/models.py +36 -0
  313. python_platform/settings/sqlalchemy/module.py +36 -0
  314. python_platform/settings/sqlalchemy/store.py +86 -0
  315. python_platform/settings/store.py +16 -0
  316. python_platform/settings/values.py +39 -0
  317. python_platform/sqlalchemy/__init__.py +51 -0
  318. python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
  319. python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
  320. python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  321. python_platform/sqlalchemy/auditing.py +48 -0
  322. python_platform/sqlalchemy/errors.py +85 -0
  323. python_platform/sqlalchemy/metadata.py +538 -0
  324. python_platform/sqlalchemy/migration.py +346 -0
  325. python_platform/sqlalchemy/module.py +48 -0
  326. python_platform/sqlalchemy/module_migration.py +148 -0
  327. python_platform/sqlalchemy/options.py +66 -0
  328. python_platform/sqlalchemy/repository.py +33 -0
  329. python_platform/sqlalchemy/runtime.py +85 -0
  330. python_platform/sqlalchemy/session_provider.py +45 -0
  331. python_platform/sqlalchemy/unit_of_work.py +97 -0
  332. python_platform/testing/__init__.py +5 -0
  333. python_platform/testing/runtime.py +112 -0
  334. python_platform/unit_of_work/__init__.py +14 -0
  335. python_platform/unit_of_work/contracts.py +223 -0
  336. python_platform/unit_of_work/errors.py +27 -0
  337. python_platform/unit_of_work/manager.py +210 -0
  338. python_platform/unit_of_work/options.py +92 -0
  339. python_platform-0.1.1.dist-info/METADATA +122 -0
  340. python_platform-0.1.1.dist-info/RECORD +344 -0
  341. python_platform-0.1.1.dist-info/WHEEL +4 -0
  342. python_platform-0.1.1.dist-info/entry_points.txt +9 -0
  343. python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
  344. python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,287 @@
1
+ """Identity Application Services:认证、refresh family 与管理员业务编排。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime, timedelta
6
+ from uuid import UUID, uuid4
7
+
8
+ from ..application_services import ApplicationService, DataSeedContributor, application_service
9
+ from ..authorization import (
10
+ CurrentUser,
11
+ PermissionCatalog,
12
+ PermissionName,
13
+ allow_anonymous,
14
+ authorize,
15
+ )
16
+ from ..options import Options
17
+ from .contracts import (
18
+ IDENTITY_ROLES_MANAGE,
19
+ IDENTITY_USERS_MANAGE,
20
+ AuthenticationApplicationService,
21
+ CreateRoleCommand,
22
+ CreateUserCommand,
23
+ CurrentUserView,
24
+ IdentityLoginStore,
25
+ IdentityManagementApplicationService,
26
+ IdentityOptions,
27
+ IdentityRepository,
28
+ IdentityRole,
29
+ IdentityUser,
30
+ LoginCommand,
31
+ RefreshRotationStatus,
32
+ RefreshSession,
33
+ RefreshSessionStore,
34
+ RefreshTokenCommand,
35
+ RevokeTokenCommand,
36
+ RoleView,
37
+ SetRolePermissionsCommand,
38
+ SetUserRolesCommand,
39
+ TokenPair,
40
+ UserView,
41
+ _normalize_name,
42
+ )
43
+ from .errors import (
44
+ IdentityConflictError,
45
+ IdentityResourceNotFoundError,
46
+ InvalidCredentialsError,
47
+ )
48
+ from .passwords import PasswordHasher
49
+ from .tokens import (
50
+ ACCESS_TOKEN_LIFETIME,
51
+ BEARER_TOKEN_TYPE,
52
+ JwtTokenService,
53
+ hash_refresh_token,
54
+ new_refresh_token,
55
+ )
56
+
57
+ FAILED_ATTEMPT_LIMIT = 5
58
+ LOCKOUT_DURATION = timedelta(minutes=15)
59
+
60
+
61
+ @authorize()
62
+ @application_service(provides=AuthenticationApplicationService)
63
+ class _AuthenticationApplicationService(ApplicationService):
64
+ def __init__(
65
+ self,
66
+ login_store: IdentityLoginStore,
67
+ sessions: RefreshSessionStore,
68
+ passwords: PasswordHasher,
69
+ tokens: JwtTokenService,
70
+ current_user: CurrentUser,
71
+ ) -> None:
72
+ self._login_store = login_store
73
+ self._sessions = sessions
74
+ self._passwords = passwords
75
+ self._tokens = tokens
76
+ self._current_user = current_user
77
+
78
+ @allow_anonymous
79
+ async def login(self, command: LoginCommand) -> TokenPair:
80
+ now = datetime.now(UTC)
81
+ user = await self._login_store.find_user(_normalize_name(command.username, role="username"))
82
+ supplied_password = command.password.get_secret_value()
83
+ verified, updated_hash = await self._passwords.verify(
84
+ supplied_password,
85
+ user.password_hash if user is not None else None,
86
+ )
87
+ if user is None:
88
+ raise InvalidCredentialsError
89
+ if (
90
+ not user.is_active
91
+ or (user.lockout_end is not None and user.lockout_end > now)
92
+ or not verified
93
+ ):
94
+ if user.is_active and (user.lockout_end is None or user.lockout_end <= now):
95
+ await self._login_store.record_failure(
96
+ user.user_id,
97
+ now=now,
98
+ attempt_limit=FAILED_ATTEMPT_LIMIT,
99
+ lockout_duration_seconds=int(LOCKOUT_DURATION.total_seconds()),
100
+ )
101
+ raise InvalidCredentialsError
102
+
103
+ refresh_token = new_refresh_token()
104
+ session = _new_session(user.user_id, refresh_token, now=now)
105
+ completed = await self._login_store.complete_success(
106
+ user.user_id,
107
+ expected_password_hash=user.password_hash,
108
+ updated_password_hash=updated_hash,
109
+ session=session,
110
+ now=now,
111
+ )
112
+ if not completed:
113
+ raise InvalidCredentialsError
114
+ return self._token_pair(session, refresh_token, now=now)
115
+
116
+ @allow_anonymous
117
+ async def refresh(self, command: RefreshTokenCommand) -> TokenPair:
118
+ now = datetime.now(UTC)
119
+ refresh_token = new_refresh_token()
120
+ result = await self._sessions.rotate(
121
+ hash_refresh_token(command.refresh_token.get_secret_value()),
122
+ replacement_id=uuid4(),
123
+ replacement_hash=hash_refresh_token(refresh_token),
124
+ now=now,
125
+ )
126
+ if result.status is not RefreshRotationStatus.ROTATED or result.session is None:
127
+ raise InvalidCredentialsError
128
+ return self._token_pair(result.session, refresh_token, now=now)
129
+
130
+ @allow_anonymous
131
+ async def revoke(self, command: RevokeTokenCommand) -> None:
132
+ await self._sessions.revoke(
133
+ hash_refresh_token(command.refresh_token.get_secret_value()),
134
+ now=datetime.now(UTC),
135
+ )
136
+
137
+ async def me(self) -> CurrentUserView:
138
+ if (
139
+ self._current_user.user_id is None
140
+ or self._current_user.session_id is None
141
+ or self._current_user.username is None
142
+ ):
143
+ # Invocation authorization normally rejects first;此 guard 保持 implementation 完整。
144
+ raise InvalidCredentialsError
145
+ return CurrentUserView(
146
+ user_id=self._current_user.user_id,
147
+ session_id=self._current_user.session_id,
148
+ username=self._current_user.username,
149
+ )
150
+
151
+ def _token_pair(
152
+ self,
153
+ session: RefreshSession,
154
+ refresh_token: str,
155
+ *,
156
+ now: datetime,
157
+ ) -> TokenPair:
158
+ return TokenPair(
159
+ access_token=self._tokens.issue(session.user_id, session.session_id, now=now),
160
+ refresh_token=refresh_token,
161
+ token_type=BEARER_TOKEN_TYPE,
162
+ expires_in=int(ACCESS_TOKEN_LIFETIME.total_seconds()),
163
+ )
164
+
165
+
166
+ @application_service(provides=IdentityManagementApplicationService)
167
+ class _IdentityManagementApplicationService(ApplicationService):
168
+ def __init__(
169
+ self,
170
+ repository: IdentityRepository,
171
+ passwords: PasswordHasher,
172
+ permissions: PermissionCatalog,
173
+ ) -> None:
174
+ self._repository = repository
175
+ self._passwords = passwords
176
+ self._permissions = permissions
177
+
178
+ @authorize(IDENTITY_USERS_MANAGE)
179
+ async def create_user(self, command: CreateUserCommand) -> UserView:
180
+ normalized = _normalize_name(command.username, role="username")
181
+ if await self._repository.find_user_by_normalized_username(normalized) is not None:
182
+ raise IdentityConflictError(resource="user")
183
+ user = IdentityUser(
184
+ user_id=uuid4(),
185
+ username=command.username.strip(),
186
+ normalized_username=normalized,
187
+ password_hash=await self._passwords.hash(command.password.get_secret_value()),
188
+ is_active=True,
189
+ failed_access_count=0,
190
+ lockout_end=None,
191
+ permission_version=0,
192
+ )
193
+ await self._repository.create_user(user)
194
+ return UserView(user.user_id, user.username, user.is_active, ())
195
+
196
+ @authorize(IDENTITY_USERS_MANAGE)
197
+ async def get_users(self) -> tuple[UserView, ...]:
198
+ return await self._repository.list_users()
199
+
200
+ @authorize(IDENTITY_ROLES_MANAGE)
201
+ async def create_role(self, command: CreateRoleCommand) -> RoleView:
202
+ normalized = _normalize_name(command.name, role="role name")
203
+ if await self._repository.find_role_by_normalized_name(normalized) is not None:
204
+ raise IdentityConflictError(resource="role")
205
+ role = IdentityRole(uuid4(), command.name.strip(), normalized, 0)
206
+ await self._repository.create_role(role)
207
+ return RoleView(role.role_id, role.name, ())
208
+
209
+ @authorize(IDENTITY_ROLES_MANAGE)
210
+ async def get_roles(self) -> tuple[RoleView, ...]:
211
+ return await self._repository.list_roles()
212
+
213
+ @authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
214
+ async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None:
215
+ role_ids = tuple(dict.fromkeys(command.role_ids))
216
+ await self._repository.set_user_roles(user_id, role_ids)
217
+
218
+ @authorize(IDENTITY_ROLES_MANAGE)
219
+ async def set_role_permissions(self, role_id: UUID, command: SetRolePermissionsCommand) -> None:
220
+ names = tuple(PermissionName(item) for item in command.permissions)
221
+ if len(set(names)) != len(names):
222
+ raise IdentityConflictError(resource="role permission")
223
+ unknown = tuple(item for item in names if item not in self._permissions)
224
+ if unknown:
225
+ raise IdentityResourceNotFoundError(resource="permission")
226
+ await self._repository.set_role_permissions(
227
+ role_id,
228
+ tuple(item.value for item in names),
229
+ )
230
+
231
+
232
+ @application_service()
233
+ class IdentityAdminSeedContributor(DataSeedContributor):
234
+ def __init__(
235
+ self,
236
+ repository: IdentityRepository,
237
+ passwords: PasswordHasher,
238
+ permissions: PermissionCatalog,
239
+ options: Options[IdentityOptions],
240
+ ) -> None:
241
+ self._repository = repository
242
+ self._passwords = passwords
243
+ self._permissions = permissions
244
+ self._options = options.value
245
+
246
+ async def seed(self) -> None:
247
+ username = self._options.seed_admin_username
248
+ password = self._options.seed_admin_password
249
+ if username is None or password is None:
250
+ return
251
+ normalized_username = _normalize_name(username, role="seed administrator username")
252
+ user = await self._repository.find_user_by_normalized_username(normalized_username)
253
+ if user is None:
254
+ user = IdentityUser(
255
+ uuid4(),
256
+ username.strip(),
257
+ normalized_username,
258
+ await self._passwords.hash(password.get_secret_value()),
259
+ True,
260
+ 0,
261
+ None,
262
+ 0,
263
+ )
264
+ await self._repository.create_user(user)
265
+ normalized_role = "administrator"
266
+ role = await self._repository.find_role_by_normalized_name(normalized_role)
267
+ if role is None:
268
+ role = IdentityRole(uuid4(), "Administrator", normalized_role, 0)
269
+ await self._repository.create_role(role)
270
+ await self._repository.set_role_permissions(
271
+ role.role_id,
272
+ tuple(item.name.value for item in self._permissions.definitions),
273
+ )
274
+ await self._repository.set_user_roles(user.user_id, (role.role_id,))
275
+
276
+
277
+ def _new_session(user_id: UUID, refresh_token: str, *, now: datetime) -> RefreshSession:
278
+ family_id = uuid4()
279
+ return RefreshSession(
280
+ session_id=uuid4(),
281
+ user_id=user_id,
282
+ family_id=family_id,
283
+ token_hash=hash_refresh_token(refresh_token),
284
+ created_at=now,
285
+ family_created_at=now,
286
+ last_used_at=now,
287
+ )
@@ -0,0 +1,283 @@
1
+ """Identity 的 HTTP/Application contracts、持久化值与 Host keyring Options。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from typing import Annotated, Protocol
10
+ from uuid import UUID
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
13
+
14
+ from ..application_services import ApplicationServiceContract
15
+ from ..authorization import (
16
+ CurrentUser,
17
+ PermissionName,
18
+ RolePermissionVersion,
19
+ )
20
+ from ..options import BaseOptions
21
+
22
+ IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
23
+ IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
24
+
25
+
26
+ class IdentityOptions(BaseOptions):
27
+ issuer: str
28
+ audience: str
29
+ active_kid: str
30
+ keys: Mapping[str, SecretStr]
31
+ seed_admin_username: str | None = None
32
+ seed_admin_password: SecretStr | None = None
33
+
34
+ @field_validator("issuer", "audience", "active_kid")
35
+ @classmethod
36
+ def _validate_identifier(cls, value: str) -> str:
37
+ if not value or value != value.strip():
38
+ raise ValueError("JWT identifier must be a non-empty trimmed string")
39
+ return value
40
+
41
+ @field_validator("keys")
42
+ @classmethod
43
+ def _validate_keys(cls, value: Mapping[str, SecretStr]) -> Mapping[str, SecretStr]:
44
+ if not value:
45
+ raise ValueError("JWT keyring must contain at least one key")
46
+ for kid, secret in value.items():
47
+ if not kid or kid != kid.strip():
48
+ raise ValueError("JWT kid must be a non-empty trimmed string")
49
+ if len(secret.get_secret_value().encode("utf-8")) < 32:
50
+ raise ValueError("HS256 keys must contain at least 256 bits")
51
+ return value
52
+
53
+ @model_validator(mode="after")
54
+ def _validate_active_and_seed(self) -> IdentityOptions:
55
+ if self.active_kid not in self.keys:
56
+ raise ValueError("active_kid must reference the JWT keyring")
57
+ if (self.seed_admin_username is None) != (self.seed_admin_password is None):
58
+ raise ValueError("seed administrator username and password must be configured together")
59
+ if self.seed_admin_username is not None:
60
+ _normalize_name(self.seed_admin_username, role="seed administrator username")
61
+ return self
62
+
63
+
64
+ class LoginCommand(BaseModel):
65
+ model_config = ConfigDict(frozen=True)
66
+
67
+ username: Annotated[str, Field(min_length=1, max_length=120)]
68
+ password: SecretStr
69
+
70
+
71
+ class RefreshTokenCommand(BaseModel):
72
+ model_config = ConfigDict(frozen=True)
73
+
74
+ refresh_token: SecretStr
75
+
76
+
77
+ class RevokeTokenCommand(BaseModel):
78
+ model_config = ConfigDict(frozen=True)
79
+
80
+ refresh_token: SecretStr
81
+
82
+
83
+ class CreateUserCommand(BaseModel):
84
+ model_config = ConfigDict(frozen=True)
85
+
86
+ username: Annotated[str, Field(min_length=1, max_length=120)]
87
+ password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
88
+
89
+
90
+ class CreateRoleCommand(BaseModel):
91
+ model_config = ConfigDict(frozen=True)
92
+
93
+ name: Annotated[str, Field(min_length=1, max_length=120)]
94
+
95
+
96
+ class SetUserRolesCommand(BaseModel):
97
+ model_config = ConfigDict(frozen=True)
98
+
99
+ role_ids: tuple[UUID, ...]
100
+
101
+
102
+ class SetRolePermissionsCommand(BaseModel):
103
+ model_config = ConfigDict(frozen=True)
104
+
105
+ permissions: tuple[str, ...]
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class TokenPair:
110
+ access_token: str
111
+ refresh_token: str
112
+ token_type: str
113
+ expires_in: int
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class UserView:
118
+ user_id: UUID
119
+ username: str
120
+ is_active: bool
121
+ role_ids: tuple[UUID, ...]
122
+
123
+
124
+ @dataclass(frozen=True, slots=True)
125
+ class RoleView:
126
+ role_id: UUID
127
+ name: str
128
+ permissions: tuple[str, ...]
129
+
130
+
131
+ @dataclass(frozen=True, slots=True)
132
+ class CurrentUserView:
133
+ user_id: UUID
134
+ session_id: UUID
135
+ username: str
136
+
137
+
138
+ @dataclass(frozen=True, slots=True)
139
+ class IdentityUser:
140
+ user_id: UUID
141
+ username: str
142
+ normalized_username: str
143
+ password_hash: str
144
+ is_active: bool
145
+ failed_access_count: int
146
+ lockout_end: datetime | None
147
+ permission_version: int
148
+
149
+
150
+ @dataclass(frozen=True, slots=True)
151
+ class IdentityRole:
152
+ role_id: UUID
153
+ name: str
154
+ normalized_name: str
155
+ permission_version: int
156
+
157
+
158
+ @dataclass(frozen=True, slots=True)
159
+ class RefreshSession:
160
+ session_id: UUID
161
+ user_id: UUID
162
+ family_id: UUID
163
+ token_hash: str
164
+ created_at: datetime
165
+ family_created_at: datetime
166
+ last_used_at: datetime
167
+ revoked_at: datetime | None = None
168
+ replaced_by_session_id: UUID | None = None
169
+
170
+
171
+ class RefreshRotationStatus(Enum):
172
+ ROTATED = "rotated"
173
+ NOT_FOUND = "not_found"
174
+ REUSED = "reused"
175
+ EXPIRED = "expired"
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class RefreshRotationResult:
180
+ status: RefreshRotationStatus
181
+ session: RefreshSession | None
182
+
183
+
184
+ class IdentityRepository(Protocol):
185
+ async def find_user_by_normalized_username(
186
+ self, normalized_username: str
187
+ ) -> IdentityUser | None: ...
188
+
189
+ async def create_user(self, user: IdentityUser) -> None: ...
190
+
191
+ async def list_users(self) -> tuple[UserView, ...]: ...
192
+
193
+ async def create_role(self, role: IdentityRole) -> None: ...
194
+
195
+ async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
196
+
197
+ async def list_roles(self) -> tuple[RoleView, ...]: ...
198
+
199
+ async def set_user_roles(self, user_id: UUID, role_ids: tuple[UUID, ...]) -> None: ...
200
+
201
+ async def set_role_permissions(self, role_id: UUID, permissions: tuple[str, ...]) -> None: ...
202
+
203
+
204
+ class IdentityAccessStore(Protocol):
205
+ async def load_current_user(
206
+ self,
207
+ user_id: UUID,
208
+ session_id: UUID,
209
+ now: datetime,
210
+ ) -> CurrentUser | None: ...
211
+
212
+ async def permissions_for_roles(
213
+ self, role_versions: tuple[RolePermissionVersion, ...]
214
+ ) -> frozenset[str]: ...
215
+
216
+
217
+ class IdentityLoginStore(Protocol):
218
+ async def find_user(self, normalized_username: str) -> IdentityUser | None: ...
219
+
220
+ async def record_failure(
221
+ self,
222
+ user_id: UUID,
223
+ *,
224
+ now: datetime,
225
+ attempt_limit: int,
226
+ lockout_duration_seconds: int,
227
+ ) -> None: ...
228
+
229
+ async def complete_success(
230
+ self,
231
+ user_id: UUID,
232
+ *,
233
+ expected_password_hash: str,
234
+ updated_password_hash: str | None,
235
+ session: RefreshSession,
236
+ now: datetime,
237
+ ) -> bool: ...
238
+
239
+
240
+ class RefreshSessionStore(Protocol):
241
+ async def rotate(
242
+ self,
243
+ token_hash: str,
244
+ *,
245
+ replacement_id: UUID,
246
+ replacement_hash: str,
247
+ now: datetime,
248
+ ) -> RefreshRotationResult: ...
249
+
250
+ async def revoke(self, token_hash: str, *, now: datetime) -> None: ...
251
+
252
+
253
+ class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
254
+ async def login(self, command: LoginCommand) -> TokenPair: ...
255
+
256
+ async def refresh(self, command: RefreshTokenCommand) -> TokenPair: ...
257
+
258
+ async def revoke(self, command: RevokeTokenCommand) -> None: ...
259
+
260
+ async def me(self) -> CurrentUserView: ...
261
+
262
+
263
+ class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
264
+ async def create_user(self, command: CreateUserCommand) -> UserView: ...
265
+
266
+ async def get_users(self) -> tuple[UserView, ...]: ...
267
+
268
+ async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
269
+
270
+ async def get_roles(self) -> tuple[RoleView, ...]: ...
271
+
272
+ async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None: ...
273
+
274
+ async def set_role_permissions(
275
+ self, role_id: UUID, command: SetRolePermissionsCommand
276
+ ) -> None: ...
277
+
278
+
279
+ def _normalize_name(value: str, *, role: str) -> str:
280
+ normalized = value.strip().casefold()
281
+ if not normalized or len(normalized) > 120:
282
+ raise ValueError(f"{role} must contain between 1 and 120 characters")
283
+ return normalized
@@ -0,0 +1,28 @@
1
+ """Identity 对外只暴露静态错误类型,不泄漏凭据、Token 或数据库文本。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..authorization import UnauthenticatedError
6
+ from ..errors.base import FrameworkError, ResourceConflictError, ResourceNotFoundError
7
+
8
+
9
+ class IdentityDefinitionError(FrameworkError):
10
+ def __init__(self, *, reason: str) -> None:
11
+ self.reason = reason
12
+ super().__init__(f"Invalid Identity configuration: {reason}")
13
+
14
+
15
+ class InvalidCredentialsError(UnauthenticatedError):
16
+ """用户名不存在、密码错误、锁定和 Token 无效共享同一公开失败。"""
17
+
18
+
19
+ class IdentityConflictError(ResourceConflictError):
20
+ def __init__(self, *, resource: str) -> None:
21
+ self.resource = resource
22
+ super().__init__(f"Identity resource {resource!r} already exists")
23
+
24
+
25
+ class IdentityResourceNotFoundError(ResourceNotFoundError):
26
+ def __init__(self, *, resource: str) -> None:
27
+ self.resource = resource
28
+ super().__init__(f"Identity resource {resource!r} was not found")
@@ -0,0 +1,90 @@
1
+ """Identity Application Services 的标准 HTTP 路径。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..fastapi import ApplicationServiceRouteOverride, exposes_application_services
6
+ from ..modularity import AppModule
7
+ from .contracts import AuthenticationApplicationService, IdentityManagementApplicationService
8
+ from .module import IdentityModule
9
+
10
+
11
+ @exposes_application_services(
12
+ IdentityModule,
13
+ root_path="identity",
14
+ overrides=(
15
+ ApplicationServiceRouteOverride(
16
+ AuthenticationApplicationService,
17
+ method=AuthenticationApplicationService.login,
18
+ path="/api/auth/login",
19
+ verb="POST",
20
+ operation_id="identity_login",
21
+ ),
22
+ ApplicationServiceRouteOverride(
23
+ AuthenticationApplicationService,
24
+ method=AuthenticationApplicationService.refresh,
25
+ path="/api/auth/refresh",
26
+ verb="POST",
27
+ operation_id="identity_refresh",
28
+ ),
29
+ ApplicationServiceRouteOverride(
30
+ AuthenticationApplicationService,
31
+ method=AuthenticationApplicationService.revoke,
32
+ path="/api/auth/revoke",
33
+ verb="POST",
34
+ operation_id="identity_revoke",
35
+ ),
36
+ ApplicationServiceRouteOverride(
37
+ AuthenticationApplicationService,
38
+ method=AuthenticationApplicationService.me,
39
+ path="/api/auth/me",
40
+ verb="GET",
41
+ operation_id="identity_me",
42
+ ),
43
+ ApplicationServiceRouteOverride(
44
+ IdentityManagementApplicationService,
45
+ method=IdentityManagementApplicationService.create_user,
46
+ path="/api/identity/users",
47
+ verb="POST",
48
+ status_code=201,
49
+ operation_id="identity_create_user",
50
+ ),
51
+ ApplicationServiceRouteOverride(
52
+ IdentityManagementApplicationService,
53
+ method=IdentityManagementApplicationService.get_users,
54
+ path="/api/identity/users",
55
+ verb="GET",
56
+ operation_id="identity_get_users",
57
+ ),
58
+ ApplicationServiceRouteOverride(
59
+ IdentityManagementApplicationService,
60
+ method=IdentityManagementApplicationService.create_role,
61
+ path="/api/identity/roles",
62
+ verb="POST",
63
+ status_code=201,
64
+ operation_id="identity_create_role",
65
+ ),
66
+ ApplicationServiceRouteOverride(
67
+ IdentityManagementApplicationService,
68
+ method=IdentityManagementApplicationService.get_roles,
69
+ path="/api/identity/roles",
70
+ verb="GET",
71
+ operation_id="identity_get_roles",
72
+ ),
73
+ ApplicationServiceRouteOverride(
74
+ IdentityManagementApplicationService,
75
+ method=IdentityManagementApplicationService.set_user_roles,
76
+ path="/api/identity/users/{user_id}/roles",
77
+ verb="PUT",
78
+ operation_id="identity_set_user_roles",
79
+ ),
80
+ ApplicationServiceRouteOverride(
81
+ IdentityManagementApplicationService,
82
+ method=IdentityManagementApplicationService.set_role_permissions,
83
+ path="/api/identity/roles/{role_id}/permissions",
84
+ verb="PUT",
85
+ operation_id="identity_set_role_permissions",
86
+ ),
87
+ ),
88
+ )
89
+ class IdentityHttpApiModule(AppModule):
90
+ dependencies = (IdentityModule,)