imbi-api 2.12.2__tar.gz → 2.13.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. {imbi_api-2.12.2 → imbi_api-2.13.1}/PKG-INFO +2 -2
  2. {imbi_api-2.12.2 → imbi_api-2.13.1}/pyproject.toml +2 -2
  3. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/app.py +1 -0
  4. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/seed.py +13 -0
  5. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/__init__.py +2 -0
  6. imbi_api-2.13.1/src/imbi_api/endpoints/maintenance.py +218 -0
  7. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_analysis.py +4 -4
  8. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_configuration.py +6 -1
  9. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/lifespans.py +34 -0
  10. imbi_api-2.13.1/src/imbi_api/maintenance/__init__.py +9 -0
  11. imbi_api-2.13.1/src/imbi_api/maintenance/operations.py +241 -0
  12. imbi_api-2.13.1/src/imbi_api/maintenance/registry.py +110 -0
  13. imbi_api-2.13.1/src/imbi_api/maintenance/state.py +346 -0
  14. imbi_api-2.13.1/src/imbi_api/maintenance/worker.py +147 -0
  15. imbi_api-2.13.1/tests/endpoints/test_maintenance.py +198 -0
  16. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_configuration.py +81 -0
  17. imbi_api-2.13.1/tests/test_maintenance_operations.py +237 -0
  18. imbi_api-2.13.1/tests/test_maintenance_state.py +239 -0
  19. imbi_api-2.13.1/tests/test_maintenance_worker.py +191 -0
  20. {imbi_api-2.12.2 → imbi_api-2.13.1}/uv.lock +5 -5
  21. {imbi_api-2.12.2 → imbi_api-2.13.1}/.github/workflows/deploy.yaml +0 -0
  22. {imbi_api-2.12.2 → imbi_api-2.13.1}/.github/workflows/docs.yaml +0 -0
  23. {imbi_api-2.12.2 → imbi_api-2.13.1}/.github/workflows/testing.yaml +0 -0
  24. {imbi_api-2.12.2 → imbi_api-2.13.1}/.gitignore +0 -0
  25. {imbi_api-2.12.2 → imbi_api-2.13.1}/.pre-commit-config.yaml +0 -0
  26. {imbi_api-2.12.2 → imbi_api-2.13.1}/.python-version +0 -0
  27. {imbi_api-2.12.2 → imbi_api-2.13.1}/CLAUDE.md +0 -0
  28. {imbi_api-2.12.2 → imbi_api-2.13.1}/LICENSE +0 -0
  29. {imbi_api-2.12.2 → imbi_api-2.13.1}/README.md +0 -0
  30. {imbi_api-2.12.2 → imbi_api-2.13.1}/coderabbit.yaml +0 -0
  31. {imbi_api-2.12.2 → imbi_api-2.13.1}/compose.yaml +0 -0
  32. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0001-record-architecture-decisions.md +0 -0
  33. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
  34. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0003-email-sending-architecture.md +0 -0
  35. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
  36. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
  37. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
  38. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0007-relationship-blueprints.md +0 -0
  39. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0008-plugin-system-architecture.md +0 -0
  40. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
  41. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0010-identity-plugin-architecture.md +0 -0
  42. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0011-graph-based-project-scoring.md +0 -0
  43. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
  44. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0013-deployment-plugin-type.md +0 -0
  45. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
  46. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
  47. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr/0016-delegated-execution-for-scheduled-jobs.md +0 -0
  48. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/adr.md +0 -0
  49. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/configuration.md +0 -0
  50. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/index.md +0 -0
  51. {imbi_api-2.12.2 → imbi_api-2.13.1}/docs/restore-backup.md +0 -0
  52. {imbi_api-2.12.2 → imbi_api-2.13.1}/justfile +0 -0
  53. {imbi_api-2.12.2 → imbi_api-2.13.1}/mkdocs.yml +0 -0
  54. {imbi_api-2.12.2 → imbi_api-2.13.1}/mypy.ini +0 -0
  55. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/__init__.py +0 -0
  56. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/__init__.py +0 -0
  57. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/authorization_codes.py +0 -0
  58. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/local_auth.py +0 -0
  59. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/login_providers.py +0 -0
  60. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/membership.py +0 -0
  61. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/models.py +0 -0
  62. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/oauth.py +0 -0
  63. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/oauth_clients.py +0 -0
  64. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/password.py +0 -0
  65. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/permissions.py +0 -0
  66. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/sessions.py +0 -0
  67. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/tokens.py +0 -0
  68. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/auth/totp.py +0 -0
  69. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/backfill_embeddings.py +0 -0
  70. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/blueprint_attributes.py +0 -0
  71. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/blueprint_compliance.py +0 -0
  72. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/commit_sync/__init__.py +0 -0
  73. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/commit_sync/queue.py +0 -0
  74. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/commit_sync/service.py +0 -0
  75. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/domain/__init__.py +0 -0
  76. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/domain/models.py +0 -0
  77. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/domain/scoring.py +0 -0
  78. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/__init__.py +0 -0
  79. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/client.py +0 -0
  80. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/dependencies.py +0 -0
  81. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/models.py +0 -0
  82. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/base.html +0 -0
  83. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/base.txt +0 -0
  84. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/password_reset.html +0 -0
  85. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/password_reset.txt +0 -0
  86. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/welcome.html +0 -0
  87. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates/welcome.txt +0 -0
  88. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/email/templates.py +0 -0
  89. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/_credentials.py +0 -0
  90. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/_helpers.py +0 -0
  91. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/_json_fields.py +0 -0
  92. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/_pagination.py +0 -0
  93. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/_request_urls.py +0 -0
  94. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/admin.py +0 -0
  95. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/admin_plugins.py +0 -0
  96. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/api_keys.py +0 -0
  97. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/auth.py +0 -0
  98. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/auth_providers.py +0 -0
  99. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/blueprints.py +0 -0
  100. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/client_credentials.py +0 -0
  101. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/comments.py +0 -0
  102. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/dashboard.py +0 -0
  103. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/document_templates.py +0 -0
  104. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/documents.py +0 -0
  105. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/environments.py +0 -0
  106. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/events.py +0 -0
  107. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/graph_query.py +0 -0
  108. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/integrations.py +0 -0
  109. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/link_definitions.py +0 -0
  110. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/local_auth.py +0 -0
  111. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/mcp_servers.py +0 -0
  112. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/mfa.py +0 -0
  113. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
  114. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/operations_log.py +0 -0
  115. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/organizations.py +0 -0
  116. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/plugin_edges.py +0 -0
  117. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/plugin_entities.py +0 -0
  118. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/plugins.py +0 -0
  119. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_commit_sync.py +0 -0
  120. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_deployments.py +0 -0
  121. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_incidents.py +0 -0
  122. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_integrations.py +0 -0
  123. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_lifecycle.py +0 -0
  124. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_logs.py +0 -0
  125. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_plugins.py +0 -0
  126. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_pr_sync.py +0 -0
  127. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/project_types.py +0 -0
  128. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/projects.py +0 -0
  129. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/pull_requests.py +0 -0
  130. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/releases.py +0 -0
  131. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/roles.py +0 -0
  132. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
  133. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/scoring.py +0 -0
  134. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/scoring_policies.py +0 -0
  135. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/search.py +0 -0
  136. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/service_accounts.py +0 -0
  137. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/status.py +0 -0
  138. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/tags.py +0 -0
  139. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/teams.py +0 -0
  140. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/uploads.py +0 -0
  141. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/user_activity.py +0 -0
  142. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/users.py +0 -0
  143. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/endpoints/webhooks.py +0 -0
  144. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/entrypoint.py +0 -0
  145. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/graph_sql.py +0 -0
  146. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/__init__.py +0 -0
  147. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/attribution.py +0 -0
  148. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/endpoints.py +0 -0
  149. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/errors.py +0 -0
  150. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/flows.py +0 -0
  151. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/host_integration.py +0 -0
  152. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/models.py +0 -0
  153. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/repository.py +0 -0
  154. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/resolution.py +0 -0
  155. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/state.py +0 -0
  156. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/identity/sweeper.py +0 -0
  157. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/llm/__init__.py +0 -0
  158. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/llm/dependencies.py +0 -0
  159. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/mcp_test.py +0 -0
  160. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/middleware/__init__.py +0 -0
  161. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/middleware/rate_limit.py +0 -0
  162. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/models.py +0 -0
  163. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/openapi.py +0 -0
  164. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/patch.py +0 -0
  165. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/__init__.py +0 -0
  166. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/assignment_writer.py +0 -0
  167. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/assignments.py +0 -0
  168. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/credentials.py +0 -0
  169. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/installer.py +0 -0
  170. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/lifecycle.py +0 -0
  171. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/lifecycle_dispatch.py +0 -0
  172. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/reload.py +0 -0
  173. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/resolution.py +0 -0
  174. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/plugins/schemas.py +0 -0
  175. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/pr_sync/__init__.py +0 -0
  176. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/pr_sync/queue.py +0 -0
  177. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/pr_sync/service.py +0 -0
  178. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/prompts/__init__.py +0 -0
  179. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/prompts/release_notes_system.md +0 -0
  180. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/py.typed +0 -0
  181. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/relationships.py +0 -0
  182. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/sbom.py +0 -0
  183. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/scoring/__init__.py +0 -0
  184. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/scoring/queue.py +0 -0
  185. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/settings.py +0 -0
  186. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/storage/__init__.py +0 -0
  187. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/storage/client.py +0 -0
  188. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/storage/dependencies.py +0 -0
  189. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/storage/thumbnails.py +0 -0
  190. {imbi_api-2.12.2 → imbi_api-2.13.1}/src/imbi_api/storage/validation.py +0 -0
  191. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/__init__.py +0 -0
  192. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/__init__.py +0 -0
  193. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_api_key_auth.py +0 -0
  194. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_authentication.py +0 -0
  195. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_authorization.py +0 -0
  196. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_encryption.py +0 -0
  197. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_login_providers.py +0 -0
  198. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_membership.py +0 -0
  199. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_oauth.py +0 -0
  200. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_oauth_server.py +0 -0
  201. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_permissions.py +0 -0
  202. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_seed.py +0 -0
  203. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_sessions.py +0 -0
  204. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/auth/test_totp.py +0 -0
  205. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/clickhouse/__init__.py +0 -0
  206. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/clickhouse/test_client.py +0 -0
  207. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/clickhouse/test_init.py +0 -0
  208. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/clickhouse/test_privacy.py +0 -0
  209. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/__init__.py +0 -0
  210. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/test_client.py +0 -0
  211. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/test_init.py +0 -0
  212. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/test_integration.py +0 -0
  213. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/test_models.py +0 -0
  214. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/email/test_templates.py +0 -0
  215. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/__init__.py +0 -0
  216. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_admin.py +0 -0
  217. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_admin_plugins.py +0 -0
  218. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_api_keys.py +0 -0
  219. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_auth.py +0 -0
  220. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_auth_providers.py +0 -0
  221. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_blueprints.py +0 -0
  222. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_client_credentials.py +0 -0
  223. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_comments.py +0 -0
  224. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_credentials.py +0 -0
  225. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_dashboard.py +0 -0
  226. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_document_templates.py +0 -0
  227. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_documents.py +0 -0
  228. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_environments.py +0 -0
  229. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_events.py +0 -0
  230. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_graph_query.py +0 -0
  231. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_init.py +0 -0
  232. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_integrations.py +0 -0
  233. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_json_fields.py +0 -0
  234. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_link_definitions.py +0 -0
  235. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_local_auth.py +0 -0
  236. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_mcp_servers.py +0 -0
  237. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_mfa.py +0 -0
  238. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_operations_log.py +0 -0
  239. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_organizations.py +0 -0
  240. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_pagination.py +0 -0
  241. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_plugin_label_validation.py +0 -0
  242. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_plugins.py +0 -0
  243. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_analysis.py +0 -0
  244. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_commit_sync.py +0 -0
  245. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_deployments.py +0 -0
  246. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_incidents.py +0 -0
  247. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_integrations.py +0 -0
  248. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_lifecycle.py +0 -0
  249. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_logs.py +0 -0
  250. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_plugins.py +0 -0
  251. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_response_surfacing.py +0 -0
  252. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_project_types.py +0 -0
  253. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_projects.py +0 -0
  254. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_projects_helpers.py +0 -0
  255. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_pull_requests.py +0 -0
  256. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_releases.py +0 -0
  257. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_request_urls.py +0 -0
  258. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_roles.py +0 -0
  259. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_sa_api_keys.py +0 -0
  260. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_scoring.py +0 -0
  261. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_scoring_policies.py +0 -0
  262. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_search.py +0 -0
  263. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_service_accounts.py +0 -0
  264. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_service_writeback.py +0 -0
  265. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_status.py +0 -0
  266. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_tags.py +0 -0
  267. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_teams.py +0 -0
  268. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_uploads.py +0 -0
  269. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_user_activity.py +0 -0
  270. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_users.py +0 -0
  271. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/endpoints/test_webhooks.py +0 -0
  272. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/fixtures/sbom/npm-realistic.json +0 -0
  273. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  274. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/fixtures/sbom/tiny.json +0 -0
  275. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/__init__.py +0 -0
  276. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_endpoints.py +0 -0
  277. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_errors.py +0 -0
  278. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_flows.py +0 -0
  279. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_host_integration.py +0 -0
  280. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_repository.py +0 -0
  281. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_resolution.py +0 -0
  282. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_state.py +0 -0
  283. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/identity/test_sweeper.py +0 -0
  284. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/middleware/test_rate_limit.py +0 -0
  285. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/storage/__init__.py +0 -0
  286. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/storage/test_client.py +0 -0
  287. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/storage/test_init.py +0 -0
  288. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/storage/test_thumbnails.py +0 -0
  289. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/storage/test_validation.py +0 -0
  290. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/support.py +0 -0
  291. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_app.py +0 -0
  292. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_assignment_writer.py +0 -0
  293. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_backfill_embeddings.py +0 -0
  294. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_blueprint_attributes.py +0 -0
  295. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_blueprint_compliance.py +0 -0
  296. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_blueprints.py +0 -0
  297. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_commit_sync_queue.py +0 -0
  298. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_commit_sync_service.py +0 -0
  299. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_entrypoint.py +0 -0
  300. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_graph_sql.py +0 -0
  301. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_identity_attribution.py +0 -0
  302. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_init.py +0 -0
  303. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_installer.py +0 -0
  304. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_lifecycle_dispatch.py +0 -0
  305. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_lifespans.py +0 -0
  306. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_models.py +0 -0
  307. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_openapi.py +0 -0
  308. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_patch.py +0 -0
  309. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_plugin_schemas.py +0 -0
  310. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_plugins.py +0 -0
  311. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_plugins_assignments.py +0 -0
  312. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_plugins_credentials.py +0 -0
  313. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_pr_sync_queue.py +0 -0
  314. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_pr_sync_service.py +0 -0
  315. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_sbom.py +0 -0
  316. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_scoring_queue.py +0 -0
  317. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_scoring_triggers.py +0 -0
  318. {imbi_api-2.12.2 → imbi_api-2.13.1}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-api
3
- Version: 2.12.2
3
+ Version: 2.13.1
4
4
  Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
5
5
  Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
6
6
  License: BSD-3-Clause
@@ -20,7 +20,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
20
20
  Requires-Dist: fastapi
21
21
  Requires-Dist: filetype
22
22
  Requires-Dist: httpx<1,>=0.28.1
23
- Requires-Dist: imbi-common[databases,llm,sentry,server]==2.12.2
23
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.13.0
24
24
  Requires-Dist: jinja2
25
25
  Requires-Dist: jsonpatch>=1.33
26
26
  Requires-Dist: mcp>=1.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.12.2"
3
+ version = "2.13.1"
4
4
  description = "Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -28,7 +28,7 @@ dependencies = [
28
28
  "fastapi",
29
29
  "filetype",
30
30
  "httpx>=0.28.1,<1",
31
- "imbi-common[databases,llm,sentry,server]==2.12.2",
31
+ "imbi-common[databases,llm,sentry,server]==2.13.0",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -27,6 +27,7 @@ def create_app() -> fastapi.FastAPI:
27
27
  lifespans.score_worker_hook,
28
28
  lifespans.commit_sync_worker_hook,
29
29
  lifespans.pr_sync_worker_hook,
30
+ lifespans.maintenance_worker_hook,
30
31
  lifespans.identity_refresh_hook,
31
32
  ),
32
33
  version=version,
@@ -378,6 +378,19 @@ STANDARD_PERMISSIONS: list[tuple[str, str, str, str]] = [
378
378
  'plugins:manage',
379
379
  'Install and uninstall plugins',
380
380
  ),
381
+ # Global maintenance operations
382
+ (
383
+ 'admin:maintenance:read',
384
+ 'admin',
385
+ 'maintenance:read',
386
+ 'View global maintenance operations',
387
+ ),
388
+ (
389
+ 'admin:maintenance:manage',
390
+ 'admin',
391
+ 'maintenance:manage',
392
+ 'Run and cancel global maintenance operations',
393
+ ),
381
394
  # Project plugin access
382
395
  (
383
396
  'project:configuration:read',
@@ -16,6 +16,7 @@ from .dashboard import dashboard_router
16
16
  from .events import events_router
17
17
  from .graph_query import graph_query_router
18
18
  from .local_auth import local_auth_router
19
+ from .maintenance import maintenance_router
19
20
  from .mcp_servers import mcp_servers_router
20
21
  from .mfa import mfa_router
21
22
  from .oauth_metadata import oauth_metadata_router
@@ -46,6 +47,7 @@ prefixed_routers: list[fastapi.APIRouter] = [
46
47
  events_router,
47
48
  graph_query_router,
48
49
  local_auth_router,
50
+ maintenance_router,
49
51
  mcp_servers_router,
50
52
  me_identities_router,
51
53
  mfa_router,
@@ -0,0 +1,218 @@
1
+ """Global maintenance operation endpoints (admin Maintenance page).
2
+
3
+ The operation list is registry-driven (:mod:`imbi_api.maintenance`):
4
+ the UI renders whatever this returns, so adding an operation to the
5
+ registry is all that is required for a new button to appear.
6
+ """
7
+
8
+ import datetime
9
+ import logging
10
+ import typing
11
+
12
+ import fastapi
13
+ import pydantic
14
+ from imbi_common import graph
15
+
16
+ from imbi_api.auth import permissions
17
+ from imbi_api.maintenance import (
18
+ OPERATIONS,
19
+ MaintenanceSlug,
20
+ OperationDefinition,
21
+ state,
22
+ )
23
+ from imbi_api.scoring import OptionalValkeyClient
24
+
25
+ LOGGER = logging.getLogger(__name__)
26
+
27
+ maintenance_router = fastapi.APIRouter(
28
+ prefix='/maintenance', tags=['Admin: Maintenance']
29
+ )
30
+
31
+ RequireRead = typing.Annotated[
32
+ permissions.AuthContext,
33
+ fastapi.Depends(
34
+ permissions.require_permission('admin:maintenance:read'),
35
+ ),
36
+ ]
37
+ RequireManage = typing.Annotated[
38
+ permissions.AuthContext,
39
+ fastapi.Depends(
40
+ permissions.require_permission('admin:maintenance:manage'),
41
+ ),
42
+ ]
43
+
44
+
45
+ class MaintenanceProgress(pydantic.BaseModel):
46
+ """Counters for an operation's current or last run."""
47
+
48
+ total: int = 0
49
+ remaining: int = 0
50
+ in_flight: int = 0
51
+ succeeded: int = 0
52
+ failed: int = 0
53
+ skipped: int = 0
54
+
55
+
56
+ class MaintenanceOperation(pydantic.BaseModel):
57
+ """One registry operation merged with its run state."""
58
+
59
+ slug: str
60
+ label: str
61
+ description: str
62
+ running: bool
63
+ state: state.RunState
64
+ progress: MaintenanceProgress | None = None
65
+ started_at: datetime.datetime | None = None
66
+ started_by: str | None = None
67
+ completed_at: datetime.datetime | None = None
68
+ #: Per-project failure detail; only populated on the per-operation
69
+ #: GET to keep the list response small.
70
+ failures: dict[str, str] | None = None
71
+
72
+
73
+ class MaintenanceRunResponse(pydantic.BaseModel):
74
+ """Acknowledgement that a run was started."""
75
+
76
+ run_id: str
77
+ total: int
78
+
79
+
80
+ def _to_operation(
81
+ definition: OperationDefinition,
82
+ status: state.RunStatus,
83
+ failures: dict[str, str] | None = None,
84
+ ) -> MaintenanceOperation:
85
+ progress: MaintenanceProgress | None = None
86
+ if status.state != 'idle':
87
+ progress = MaintenanceProgress(
88
+ total=status.total,
89
+ remaining=status.remaining,
90
+ in_flight=status.in_flight,
91
+ succeeded=status.succeeded,
92
+ failed=status.failed,
93
+ skipped=status.skipped,
94
+ )
95
+ return MaintenanceOperation(
96
+ slug=definition.slug,
97
+ label=definition.label,
98
+ description=definition.description,
99
+ running=status.state == 'running',
100
+ state=status.state,
101
+ progress=progress,
102
+ started_at=status.started_at,
103
+ started_by=status.started_by,
104
+ completed_at=status.completed_at,
105
+ failures=failures,
106
+ )
107
+
108
+
109
+ def _definition_or_404(slug: str) -> OperationDefinition:
110
+ definition = OPERATIONS.get(typing.cast('MaintenanceSlug', slug))
111
+ if definition is None:
112
+ raise fastapi.HTTPException(
113
+ status_code=404, detail=f'Unknown maintenance operation {slug!r}'
114
+ )
115
+ return definition
116
+
117
+
118
+ @maintenance_router.get('/operations')
119
+ async def list_maintenance_operations(
120
+ auth: RequireRead,
121
+ client: OptionalValkeyClient,
122
+ ) -> list[MaintenanceOperation]:
123
+ """The operation registry merged with each operation's run state."""
124
+ _ = auth
125
+ if client is None:
126
+ raise fastapi.HTTPException(
127
+ status_code=503,
128
+ detail='Maintenance state is unavailable '
129
+ '(Valkey is not connected).',
130
+ )
131
+ results: list[MaintenanceOperation] = []
132
+ for definition in OPERATIONS.values():
133
+ status = await state.read_status(client, definition.slug)
134
+ results.append(_to_operation(definition, status))
135
+ return results
136
+
137
+
138
+ @maintenance_router.get('/operations/{slug}')
139
+ async def get_maintenance_operation(
140
+ slug: str,
141
+ auth: RequireRead,
142
+ client: OptionalValkeyClient,
143
+ ) -> MaintenanceOperation:
144
+ """One operation's run state, including per-project failures."""
145
+ _ = auth
146
+ definition = _definition_or_404(slug)
147
+ if client is None:
148
+ raise fastapi.HTTPException(
149
+ status_code=503,
150
+ detail='Maintenance state is unavailable '
151
+ '(Valkey is not connected).',
152
+ )
153
+ status = await state.read_status(client, definition.slug)
154
+ failures = await state.read_failures(client, definition.slug)
155
+ return _to_operation(definition, status, failures or None)
156
+
157
+
158
+ @maintenance_router.post('/operations/{slug}/run', status_code=202)
159
+ async def run_maintenance_operation(
160
+ slug: str,
161
+ auth: RequireManage,
162
+ db: graph.Pool,
163
+ client: OptionalValkeyClient,
164
+ ) -> MaintenanceRunResponse:
165
+ """Start a global run of the operation across all projects."""
166
+ definition = _definition_or_404(slug)
167
+ if client is None:
168
+ raise fastapi.HTTPException(
169
+ status_code=503,
170
+ detail='Queueing is unavailable (Valkey is not connected).',
171
+ )
172
+ project_ids = await definition.enumerate(db)
173
+ status = await state.start_run(
174
+ client, definition.slug, project_ids, auth.principal_name
175
+ )
176
+ if status is None:
177
+ raise fastapi.HTTPException(
178
+ status_code=409,
179
+ detail=f'{definition.label} is already running.',
180
+ )
181
+ LOGGER.info(
182
+ 'maintenance %s run %s started by %s (%d projects)',
183
+ definition.slug,
184
+ status.run_id,
185
+ auth.principal_name,
186
+ status.total,
187
+ )
188
+ return MaintenanceRunResponse(
189
+ run_id=status.run_id or '', total=status.total
190
+ )
191
+
192
+
193
+ @maintenance_router.post('/operations/{slug}/cancel')
194
+ async def cancel_maintenance_operation(
195
+ slug: str,
196
+ auth: RequireManage,
197
+ client: OptionalValkeyClient,
198
+ ) -> MaintenanceOperation:
199
+ """Cancel the operation's in-progress run."""
200
+ definition = _definition_or_404(slug)
201
+ if client is None:
202
+ raise fastapi.HTTPException(
203
+ status_code=503,
204
+ detail='Queueing is unavailable (Valkey is not connected).',
205
+ )
206
+ cancelled = await state.cancel_run(client, definition.slug)
207
+ if not cancelled:
208
+ raise fastapi.HTTPException(
209
+ status_code=409,
210
+ detail=f'{definition.label} is not running.',
211
+ )
212
+ LOGGER.info(
213
+ 'maintenance %s run cancelled by %s',
214
+ definition.slug,
215
+ auth.principal_name,
216
+ )
217
+ status = await state.read_status(client, definition.slug)
218
+ return _to_operation(definition, status)
@@ -499,7 +499,7 @@ async def _collect_results(
499
499
  return results
500
500
 
501
501
 
502
- async def _run_and_persist(
502
+ async def run_and_persist(
503
503
  db: graph.Graph,
504
504
  org_slug: str,
505
505
  project_id: str,
@@ -526,7 +526,7 @@ async def run_project_analysis(
526
526
  fastapi.Depends(permissions.require_permission('project:write')),
527
527
  ],
528
528
  ) -> AnalysisReport:
529
- return await _run_and_persist(db, org_slug, project_id, auth)
529
+ return await run_and_persist(db, org_slug, project_id, auth)
530
530
 
531
531
 
532
532
  class RemediateRequest(pydantic.BaseModel):
@@ -659,7 +659,7 @@ async def remediate_project_finding(
659
659
  result = await _remediate_one(
660
660
  db, org_slug, project_id, body.plugin_id, body.remediation_id, auth
661
661
  )
662
- report = await _run_and_persist(db, org_slug, project_id, auth)
662
+ report = await run_and_persist(db, org_slug, project_id, auth)
663
663
  return RemediateResponse(result=result, report=report)
664
664
 
665
665
 
@@ -728,5 +728,5 @@ async def remediate_all_project_findings(
728
728
  result=result,
729
729
  )
730
730
  )
731
- fresh = await _run_and_persist(db, org_slug, project_id, auth)
731
+ fresh = await run_and_persist(db, org_slug, project_id, auth)
732
732
  return RemediateAllResponse(outcomes=outcomes, report=fresh)
@@ -19,7 +19,10 @@ from imbi_common.plugins.base import (
19
19
 
20
20
  from imbi_api.auth import permissions
21
21
  from imbi_api.domain import models
22
- from imbi_api.endpoints._helpers import lookup_project_slugs
22
+ from imbi_api.endpoints._helpers import (
23
+ lookup_project_slugs,
24
+ lookup_project_type_slugs,
25
+ )
23
26
  from imbi_api.identity.host_integration import call_with_identity_retry
24
27
  from imbi_api.plugins import call_with_timeout
25
28
  from imbi_api.plugins.resolution import ResolvedCapability, resolve_capability
@@ -97,12 +100,14 @@ async def _resolve_and_prepare(
97
100
  db, project_id, 'configuration', source
98
101
  )
99
102
  project_slug, team_slug = await lookup_project_slugs(db, project_id)
103
+ project_type_slugs = await lookup_project_type_slugs(db, project_id)
100
104
  ctx = PluginContext(
101
105
  project_id=project_id,
102
106
  project_slug=project_slug,
103
107
  org_slug=org_slug,
104
108
  team_slug=team_slug,
105
109
  environment=environment,
110
+ project_type_slugs=project_type_slugs,
106
111
  assignment_options=resolved.capability_options,
107
112
  integration_options=resolved.integration_options,
108
113
  capability_options=resolved.capability_options,
@@ -18,6 +18,7 @@ from imbi_api.commit_sync import queue as commit_sync_queue
18
18
  from imbi_api.email.client import EmailClient
19
19
  from imbi_api.email.templates import TemplateManager
20
20
  from imbi_api.identity import sweeper as identity_sweeper
21
+ from imbi_api.maintenance import worker as maintenance_worker
21
22
  from imbi_api.plugins import lifecycle as plugin_lifecycle
22
23
  from imbi_api.pr_sync import queue as pr_sync_queue
23
24
  from imbi_api.scoring import queue as score_queue
@@ -199,6 +200,39 @@ async def pr_sync_worker_hook() -> abc.AsyncGenerator[None]:
199
200
  )
200
201
 
201
202
 
203
+ @contextlib.asynccontextmanager
204
+ async def maintenance_worker_hook() -> abc.AsyncGenerator[None]:
205
+ """Run the global maintenance operation consumer loop."""
206
+ try:
207
+ client = valkey.get_client()
208
+ except RuntimeError:
209
+ LOGGER.warning('Valkey unavailable; maintenance worker not started')
210
+ yield None
211
+ return
212
+ if _graph is None:
213
+ LOGGER.warning('Graph not ready; maintenance worker not started')
214
+ yield None
215
+ return
216
+ stop = asyncio.Event()
217
+ LOGGER.info('Maintenance worker starting')
218
+ consumer_task = asyncio.create_task(
219
+ maintenance_worker.run_worker(client, _graph, stop=stop)
220
+ )
221
+ try:
222
+ yield None
223
+ finally:
224
+ stop.set()
225
+ consumer_task.cancel()
226
+ try:
227
+ await consumer_task
228
+ except asyncio.CancelledError:
229
+ pass
230
+ except Exception: # noqa: BLE001
231
+ LOGGER.warning(
232
+ 'Maintenance worker task exited with error', exc_info=True
233
+ )
234
+
235
+
202
236
  @contextlib.asynccontextmanager
203
237
  async def score_worker_hook() -> abc.AsyncGenerator[None]:
204
238
  """Run the score-recompute consumer and the daily tick."""
@@ -0,0 +1,9 @@
1
+ """Global maintenance operations (admin Maintenance page)."""
2
+
3
+ from imbi_api.maintenance.registry import (
4
+ OPERATIONS,
5
+ MaintenanceSlug,
6
+ OperationDefinition,
7
+ )
8
+
9
+ __all__ = ['OPERATIONS', 'MaintenanceSlug', 'OperationDefinition']
@@ -0,0 +1,241 @@
1
+ """Per-project execute functions for global maintenance operations.
2
+
3
+ Each ``execute_*`` runs one project's slice of a global run by reusing
4
+ the same service code the per-project Doctor endpoints call. Outcomes:
5
+
6
+ - return ``'succeeded'`` / ``'skipped'`` (skipped means the operation
7
+ does not apply -- e.g. no integration provides the capability);
8
+ - raise :class:`MaintenanceItemFailed` with a user-safe message for
9
+ recordable failures (raw detail belongs in logs only);
10
+ - let :class:`~imbi_common.plugins.errors.PluginRateLimited` propagate
11
+ so the worker can requeue the project and pause the operation.
12
+
13
+ Endpoint modules are imported inside function bodies (the
14
+ ``commit_sync.service`` pattern) so this module never pulls the
15
+ endpoints package at import time.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import functools
21
+ import logging
22
+ import typing
23
+
24
+ import fastapi
25
+ from imbi_common import graph
26
+ from imbi_common.plugins.errors import PluginRateLimited
27
+ from valkey import asyncio as valkey
28
+
29
+ from imbi_api import models
30
+ from imbi_api.auth import permissions
31
+ from imbi_api.scoring import queue as score_queue
32
+
33
+ LOGGER = logging.getLogger(__name__)
34
+
35
+ #: ``requested_by`` / ``principal_name`` recorded on work this runs.
36
+ REQUESTED_BY = 'maintenance'
37
+
38
+ ExecuteOutcome = typing.Literal['succeeded', 'skipped']
39
+
40
+ _ORG_SLUG_QUERY: typing.LiteralString = (
41
+ 'MATCH (p:Project {{id: {project_id}}})-[:OWNED_BY]->(:Team)'
42
+ '-[:BELONGS_TO]->(o:Organization) RETURN o.slug AS slug'
43
+ )
44
+
45
+
46
+ class MaintenanceItemFailed(Exception):
47
+ """One project's operation failed; the message is user-safe."""
48
+
49
+
50
+ @functools.cache
51
+ def _system_auth() -> permissions.AuthContext:
52
+ """Synthetic principal for background maintenance work.
53
+
54
+ Never persisted; exists so service functions that record
55
+ ``principal_name`` attribute the work to ``'maintenance'``.
56
+ """
57
+ return permissions.AuthContext(
58
+ auth_method='client_credentials',
59
+ service_account=models.ServiceAccount(
60
+ slug=REQUESTED_BY, display_name='Imbi Maintenance'
61
+ ),
62
+ )
63
+
64
+
65
+ async def enumerate_all_projects(db: graph.Graph) -> list[str]:
66
+ """Every project id -- maintenance operations self-classify
67
+ inapplicable projects as skipped rather than pre-filtering (which
68
+ would cost a capability resolution per project up front)."""
69
+ return await score_queue.all_project_ids(db)
70
+
71
+
72
+ async def _org_slug_for(db: graph.Graph, project_id: str) -> str | None:
73
+ rows = await db.execute(
74
+ _ORG_SLUG_QUERY, {'project_id': project_id}, ['slug']
75
+ )
76
+ if not rows:
77
+ return None
78
+ value = graph.parse_agtype(rows[0].get('slug'))
79
+ return str(value) if value else None
80
+
81
+
82
+ async def execute_analysis(
83
+ db: graph.Graph, client: valkey.Valkey, project_id: str
84
+ ) -> ExecuteOutcome:
85
+ """Run the Doctor analysis and persist the report.
86
+
87
+ Per-plugin errors already surface as synthetic ``fail`` findings
88
+ inside the report, so an exception here is infrastructural.
89
+ """
90
+ from imbi_api.endpoints import project_analysis
91
+
92
+ org_slug = await _org_slug_for(db, project_id)
93
+ if org_slug is None:
94
+ return 'skipped'
95
+ await project_analysis.run_and_persist(
96
+ db, org_slug, project_id, _system_auth()
97
+ )
98
+ return 'succeeded'
99
+
100
+
101
+ async def execute_commit_sync(
102
+ db: graph.Graph, client: valkey.Valkey, project_id: str
103
+ ) -> ExecuteOutcome:
104
+ """Full commit/tag backfill, mirroring the queue consumer's status
105
+ transitions so the per-project Doctor status stays truthful."""
106
+ from imbi_api.commit_sync import service
107
+
108
+ org_slug = await _org_slug_for(db, project_id)
109
+ if org_slug is None:
110
+ return 'skipped'
111
+ await service.set_status(
112
+ db, project_id, status='running', requested_by=REQUESTED_BY
113
+ )
114
+ try:
115
+ commits, tags = await service.run_sync(db, org_slug, project_id)
116
+ except service.CommitSyncUnavailable as exc:
117
+ await service.set_status(
118
+ db,
119
+ project_id,
120
+ status='failed',
121
+ requested_by=REQUESTED_BY,
122
+ error=str(exc),
123
+ )
124
+ return 'skipped'
125
+ except PluginRateLimited:
126
+ # Leave the project requeue-able; the worker pauses the op.
127
+ await service.set_status(
128
+ db, project_id, status='queued', requested_by=REQUESTED_BY
129
+ )
130
+ raise
131
+ except Exception as exc:
132
+ LOGGER.exception('maintenance commit-sync failed for %s', project_id)
133
+ message = 'Commit sync failed. See server logs for details.'
134
+ await service.set_status(
135
+ db,
136
+ project_id,
137
+ status='failed',
138
+ requested_by=REQUESTED_BY,
139
+ error=message,
140
+ )
141
+ raise MaintenanceItemFailed(message) from exc
142
+ await service.set_status(
143
+ db,
144
+ project_id,
145
+ status='success',
146
+ requested_by=REQUESTED_BY,
147
+ commits=commits,
148
+ tags=tags,
149
+ )
150
+ return 'succeeded'
151
+
152
+
153
+ async def execute_pr_sync(
154
+ db: graph.Graph, client: valkey.Valkey, project_id: str
155
+ ) -> ExecuteOutcome:
156
+ """Full PR-history backfill; same shape as commit sync."""
157
+ from imbi_api.pr_sync import service
158
+
159
+ org_slug = await _org_slug_for(db, project_id)
160
+ if org_slug is None:
161
+ return 'skipped'
162
+ await service.set_status(
163
+ db, project_id, status='running', requested_by=REQUESTED_BY
164
+ )
165
+ try:
166
+ prs = await service.run_sync(db, org_slug, project_id)
167
+ except service.PRSyncUnavailable as exc:
168
+ await service.set_status(
169
+ db,
170
+ project_id,
171
+ status='failed',
172
+ requested_by=REQUESTED_BY,
173
+ error=str(exc),
174
+ )
175
+ return 'skipped'
176
+ except PluginRateLimited:
177
+ # Leave the project requeue-able; the worker pauses the op.
178
+ await service.set_status(
179
+ db, project_id, status='queued', requested_by=REQUESTED_BY
180
+ )
181
+ raise
182
+ except Exception as exc:
183
+ LOGGER.exception('maintenance pr-sync failed for %s', project_id)
184
+ message = 'PR sync failed. See server logs for details.'
185
+ await service.set_status(
186
+ db,
187
+ project_id,
188
+ status='failed',
189
+ requested_by=REQUESTED_BY,
190
+ error=message,
191
+ )
192
+ raise MaintenanceItemFailed(message) from exc
193
+ await service.set_status(
194
+ db,
195
+ project_id,
196
+ status='success',
197
+ requested_by=REQUESTED_BY,
198
+ prs=prs,
199
+ )
200
+ return 'succeeded'
201
+
202
+
203
+ async def execute_deployment_resync(
204
+ db: graph.Graph, client: valkey.Valkey, project_id: str
205
+ ) -> ExecuteOutcome:
206
+ """Backfill recent remote deployments via the deployment plugin."""
207
+ from imbi_api.endpoints import project_deployments
208
+
209
+ org_slug = await _org_slug_for(db, project_id)
210
+ if org_slug is None:
211
+ return 'skipped'
212
+ try:
213
+ await project_deployments.resync_for_project(
214
+ db,
215
+ org_slug=org_slug,
216
+ project_id=project_id,
217
+ auth=_system_auth(),
218
+ limit=1,
219
+ )
220
+ except fastapi.HTTPException as exc:
221
+ # 404: no deployment capability bound; 400: the plugin doesn't
222
+ # support deployment sync. Neither is a failure of this run.
223
+ if exc.status_code in (400, 404):
224
+ return 'skipped'
225
+ raise MaintenanceItemFailed(str(exc.detail)) from exc
226
+ return 'succeeded'
227
+
228
+
229
+ async def execute_rescore(
230
+ db: graph.Graph, client: valkey.Valkey, project_id: str
231
+ ) -> ExecuteOutcome:
232
+ """Enqueue a score recompute onto the existing scoring stream.
233
+
234
+ Succeeded means enqueued -- the scoring workers do the computation
235
+ with their own debounce/DLQ/history handling. Skipped means the
236
+ project was debounced (a recompute is already queued).
237
+ """
238
+ enqueued = await score_queue.enqueue_recompute(
239
+ client, project_id, 'bulk_rescore', REQUESTED_BY
240
+ )
241
+ return 'succeeded' if enqueued else 'skipped'