imbi-api 2.36.0__tar.gz → 2.37.0__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 (341) hide show
  1. {imbi_api-2.36.0 → imbi_api-2.37.0}/PKG-INFO +2 -2
  2. {imbi_api-2.36.0 → imbi_api-2.37.0}/pyproject.toml +2 -2
  3. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_helpers.py +150 -1
  4. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_analysis.py +22 -1
  5. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_deployments.py +33 -17
  6. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/projects.py +25 -0
  7. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/operations.py +62 -12
  8. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/registry.py +16 -0
  9. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/lifecycle_dispatch.py +24 -0
  10. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_analysis.py +85 -0
  11. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_deployments.py +41 -0
  12. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_projects.py +40 -0
  13. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_service_writeback.py +107 -0
  14. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_lifecycle_dispatch.py +69 -0
  15. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_operations.py +60 -0
  16. {imbi_api-2.36.0 → imbi_api-2.37.0}/.gitignore +0 -0
  17. {imbi_api-2.36.0 → imbi_api-2.37.0}/README.md +0 -0
  18. {imbi_api-2.36.0 → imbi_api-2.37.0}/moon.yml +0 -0
  19. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/README.md +0 -0
  20. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/__init__.py +0 -0
  21. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/app.py +0 -0
  22. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/__init__.py +0 -0
  23. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/authorization_codes.py +0 -0
  24. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/autonomous.py +0 -0
  25. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/internal_services.py +0 -0
  26. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/local_auth.py +0 -0
  27. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/login_providers.py +0 -0
  28. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/membership.py +0 -0
  29. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/models.py +0 -0
  30. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/oauth.py +0 -0
  31. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/oauth_clients.py +0 -0
  32. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/password.py +0 -0
  33. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/permissions.py +0 -0
  34. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/principals.py +0 -0
  35. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/seed.py +0 -0
  36. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/sessions.py +0 -0
  37. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/tokens.py +0 -0
  38. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/totp.py +0 -0
  39. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/backfill_embeddings.py +0 -0
  40. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/blueprint_attributes.py +0 -0
  41. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/blueprint_compliance.py +0 -0
  42. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_pushed_at_repair.py +0 -0
  43. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/__init__.py +0 -0
  44. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/queue.py +0 -0
  45. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/service.py +0 -0
  46. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/component_facts.py +0 -0
  47. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_status_repair.py +0 -0
  48. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sweeper.py +0 -0
  49. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
  50. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/queue.py +0 -0
  51. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/service.py +0 -0
  52. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/documents/__init__.py +0 -0
  53. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/documents/read_sweeper.py +0 -0
  54. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/__init__.py +0 -0
  55. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/models.py +0 -0
  56. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/scoring.py +0 -0
  57. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/drift.py +0 -0
  58. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/__init__.py +0 -0
  59. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/client.py +0 -0
  60. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/dependencies.py +0 -0
  61. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/models.py +0 -0
  62. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/base.html +0 -0
  63. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/base.txt +0 -0
  64. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/password_reset.html +0 -0
  65. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
  66. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/welcome.html +0 -0
  67. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/welcome.txt +0 -0
  68. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates.py +0 -0
  69. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/__init__.py +0 -0
  70. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_credentials.py +0 -0
  71. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_events.py +0 -0
  72. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_history.py +0 -0
  73. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
  74. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
  75. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
  76. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_pagination.py +0 -0
  77. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
  78. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_search_index.py +0 -0
  79. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/admin.py +0 -0
  80. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
  81. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/ai_models.py +0 -0
  82. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
  83. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/api_keys.py +0 -0
  84. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/auth.py +0 -0
  85. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
  86. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/blueprints.py +0 -0
  87. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
  88. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/comments.py +0 -0
  89. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/components.py +0 -0
  90. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/dashboard.py +0 -0
  91. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
  92. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_likes.py +0 -0
  93. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_presence.py +0 -0
  94. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_templates.py +0 -0
  95. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_versions.py +0 -0
  96. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/documents.py +0 -0
  97. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/environments.py +0 -0
  98. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/events.py +0 -0
  99. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/graph_query.py +0 -0
  100. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
  101. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/integrations.py +0 -0
  102. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
  103. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/local_auth.py +0 -0
  104. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/maintenance.py +0 -0
  105. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
  106. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/mfa.py +0 -0
  107. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
  108. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/operations_log.py +0 -0
  109. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/organizations.py +0 -0
  110. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
  111. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
  112. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugins.py +0 -0
  113. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
  114. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
  115. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
  116. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
  117. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
  118. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_logs.py +0 -0
  119. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
  120. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
  121. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_types.py +0 -0
  122. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/pull_requests.py +0 -0
  123. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/releases.py +0 -0
  124. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/roles.py +0 -0
  125. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
  126. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/scoring.py +0 -0
  127. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
  128. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/search.py +0 -0
  129. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
  130. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/status.py +0 -0
  131. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/tags.py +0 -0
  132. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/teams.py +0 -0
  133. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/uploads.py +0 -0
  134. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/user_activity.py +0 -0
  135. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/users.py +0 -0
  136. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/webhooks.py +0 -0
  137. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/entrypoint.py +0 -0
  138. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/graph_sql.py +0 -0
  139. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/__init__.py +0 -0
  140. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/attribution.py +0 -0
  141. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/endpoints.py +0 -0
  142. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/errors.py +0 -0
  143. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/flows.py +0 -0
  144. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/host_integration.py +0 -0
  145. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/models.py +0 -0
  146. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/repository.py +0 -0
  147. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/resolution.py +0 -0
  148. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/state.py +0 -0
  149. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/sweeper.py +0 -0
  150. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/lifespans.py +0 -0
  151. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/llm/__init__.py +0 -0
  152. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/llm/dependencies.py +0 -0
  153. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/__init__.py +0 -0
  154. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/log.py +0 -0
  155. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/state.py +0 -0
  156. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/worker.py +0 -0
  157. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/mcp_test.py +0 -0
  158. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/middleware/__init__.py +0 -0
  159. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/middleware/rate_limit.py +0 -0
  160. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/models.py +0 -0
  161. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/openapi.py +0 -0
  162. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/orphan_releases.py +0 -0
  163. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/__init__.py +0 -0
  164. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
  165. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/assignments.py +0 -0
  166. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/credentials.py +0 -0
  167. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/installer.py +0 -0
  168. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/lifecycle.py +0 -0
  169. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/reload.py +0 -0
  170. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/resolution.py +0 -0
  171. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/schemas.py +0 -0
  172. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/__init__.py +0 -0
  173. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/queue.py +0 -0
  174. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/service.py +0 -0
  175. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/prompts/__init__.py +0 -0
  176. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
  177. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/py.typed +0 -0
  178. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/relationships.py +0 -0
  179. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/__init__.py +0 -0
  180. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/queue.py +0 -0
  181. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/service.py +0 -0
  182. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/sbom.py +0 -0
  183. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/scoring/__init__.py +0 -0
  184. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/scoring/queue.py +0 -0
  185. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/settings.py +0 -0
  186. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/__init__.py +0 -0
  187. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/client.py +0 -0
  188. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/dependencies.py +0 -0
  189. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/thumbnails.py +0 -0
  190. {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/validation.py +0 -0
  191. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/__init__.py +0 -0
  192. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/__init__.py +0 -0
  193. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_authentication.py +0 -0
  194. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_authorization.py +0 -0
  195. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_encryption.py +0 -0
  196. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_internal_services.py +0 -0
  197. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_login_providers.py +0 -0
  198. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_membership.py +0 -0
  199. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_oauth.py +0 -0
  200. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_oauth_server.py +0 -0
  201. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_permissions.py +0 -0
  202. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_seed.py +0 -0
  203. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_sessions.py +0 -0
  204. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_totp.py +0 -0
  205. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/__init__.py +0 -0
  206. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_client.py +0 -0
  207. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_init.py +0 -0
  208. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_privacy.py +0 -0
  209. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/__init__.py +0 -0
  210. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_audit_clickhouse.py +0 -0
  211. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_client.py +0 -0
  212. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_init.py +0 -0
  213. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_integration.py +0 -0
  214. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_models.py +0 -0
  215. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_templates.py +0 -0
  216. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/__init__.py +0 -0
  217. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_admin.py +0 -0
  218. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_admin_plugins.py +0 -0
  219. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_ai_models.py +0 -0
  220. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_ai_providers.py +0 -0
  221. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_api_keys.py +0 -0
  222. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_auth.py +0 -0
  223. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_auth_providers.py +0 -0
  224. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_blueprints.py +0 -0
  225. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_client_credentials.py +0 -0
  226. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_comments.py +0 -0
  227. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_components.py +0 -0
  228. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_credentials.py +0 -0
  229. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_dashboard.py +0 -0
  230. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_analytics.py +0 -0
  231. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_likes.py +0 -0
  232. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_presence.py +0 -0
  233. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_reads.py +0 -0
  234. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_reads_sql.py +0 -0
  235. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_templates.py +0 -0
  236. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_versions.py +0 -0
  237. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_documents.py +0 -0
  238. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_environments.py +0 -0
  239. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_events.py +0 -0
  240. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_graph_query.py +0 -0
  241. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_iggy_connectors.py +0 -0
  242. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_init.py +0 -0
  243. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_integrations.py +0 -0
  244. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_json_fields.py +0 -0
  245. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_link_definitions.py +0 -0
  246. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_local_auth.py +0 -0
  247. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_maintenance.py +0 -0
  248. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_mcp_servers.py +0 -0
  249. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_mfa.py +0 -0
  250. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_operations_log.py +0 -0
  251. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_organizations.py +0 -0
  252. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_pagination.py +0 -0
  253. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
  254. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_plugins.py +0 -0
  255. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_commit_sync.py +0 -0
  256. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_configuration.py +0 -0
  257. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
  258. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_incidents.py +0 -0
  259. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_integrations.py +0 -0
  260. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_lifecycle.py +0 -0
  261. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_logs.py +0 -0
  262. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_plugins.py +0 -0
  263. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
  264. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_types.py +0 -0
  265. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_projects_helpers.py +0 -0
  266. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_pull_requests.py +0 -0
  267. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_releases.py +0 -0
  268. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_request_urls.py +0 -0
  269. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_roles.py +0 -0
  270. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_sa_api_keys.py +0 -0
  271. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_scoring.py +0 -0
  272. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_scoring_policies.py +0 -0
  273. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_search.py +0 -0
  274. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_search_index.py +0 -0
  275. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_service_accounts.py +0 -0
  276. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_status.py +0 -0
  277. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_tags.py +0 -0
  278. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_teams.py +0 -0
  279. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_uploads.py +0 -0
  280. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_user_activity.py +0 -0
  281. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_users.py +0 -0
  282. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_webhooks.py +0 -0
  283. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
  284. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  285. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/tiny.json +0 -0
  286. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/__init__.py +0 -0
  287. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_endpoints.py +0 -0
  288. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_errors.py +0 -0
  289. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_flows.py +0 -0
  290. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_host_integration.py +0 -0
  291. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_repository.py +0 -0
  292. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_resolution.py +0 -0
  293. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_state.py +0 -0
  294. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_sweeper.py +0 -0
  295. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/middleware/test_rate_limit.py +0 -0
  296. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/__init__.py +0 -0
  297. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_client.py +0 -0
  298. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_init.py +0 -0
  299. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_thumbnails.py +0 -0
  300. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_validation.py +0 -0
  301. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/support.py +0 -0
  302. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_app.py +0 -0
  303. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_assignment_writer.py +0 -0
  304. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_backfill_embeddings.py +0 -0
  305. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprint_attributes.py +0 -0
  306. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprint_compliance.py +0 -0
  307. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprints.py +0 -0
  308. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_pushed_at_repair.py +0 -0
  309. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_sync_queue.py +0 -0
  310. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_sync_service.py +0 -0
  311. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_component_facts.py +0 -0
  312. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_status_repair.py +0 -0
  313. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sweeper.py +0 -0
  314. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sync_queue.py +0 -0
  315. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sync_service.py +0 -0
  316. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_drift.py +0 -0
  317. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_entrypoint.py +0 -0
  318. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_graph_sql.py +0 -0
  319. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_identity_attribution.py +0 -0
  320. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_init.py +0 -0
  321. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_installer.py +0 -0
  322. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_lifespans.py +0 -0
  323. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_log.py +0 -0
  324. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_state.py +0 -0
  325. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_worker.py +0 -0
  326. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_models.py +0 -0
  327. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_openapi.py +0 -0
  328. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_orphan_releases.py +0 -0
  329. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugin_schemas.py +0 -0
  330. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins.py +0 -0
  331. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins_assignments.py +0 -0
  332. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins_credentials.py +0 -0
  333. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pr_sync_queue.py +0 -0
  334. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pr_sync_service.py +0 -0
  335. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_projects_drift_ranges.py +0 -0
  336. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pull_requests_pending_deploy.py +0 -0
  337. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_release_promote_service.py +0 -0
  338. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_sbom.py +0 -0
  339. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_scoring_queue.py +0 -0
  340. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_scoring_triggers.py +0 -0
  341. {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imbi-api
3
- Version: 2.36.0
3
+ Version: 2.37.0
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-Expression: BSD-3-Clause
@@ -19,7 +19,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
19
19
  Requires-Dist: fastapi
20
20
  Requires-Dist: filetype
21
21
  Requires-Dist: httpx<1,>=0.28.1
22
- Requires-Dist: imbi-common[databases,llm,sentry,server]==2.36.0
22
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.37.0
23
23
  Requires-Dist: jinja2
24
24
  Requires-Dist: jsonpatch>=1.33
25
25
  Requires-Dist: mcp>=1.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.36.0"
3
+ version = "2.37.0"
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.36.0",
31
+ "imbi-common[databases,llm,sentry,server]==2.37.0",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -14,7 +14,11 @@ import psycopg
14
14
  from imbi.common import graph
15
15
  from imbi.common import models as common_models
16
16
  from imbi.common import patch as json_patch
17
- from imbi.common.plugins.base import PluginContext, ServiceConnection
17
+ from imbi.common.plugins.base import (
18
+ PluginContext,
19
+ ProjectDependency,
20
+ ServiceConnection,
21
+ )
18
22
 
19
23
  LOGGER = logging.getLogger(__name__)
20
24
 
@@ -523,6 +527,151 @@ async def lookup_project_exists_in(
523
527
  return connections
524
528
 
525
529
 
530
+ _DEPENDENCY_QUERIES: dict[str, typing.LiteralString] = {
531
+ 'outbound': (
532
+ 'MATCH (p:Project {{id: {project_id}}})-[:DEPENDS_ON]->(n:Project) '
533
+ 'OPTIONAL MATCH (n)-[ei:EXISTS_IN]->(i:Integration) '
534
+ 'RETURN n.id AS project_id, n.slug AS project_slug, '
535
+ 'i.slug AS integration_slug, ei.identifier AS identifier, '
536
+ 'ei.canonical_url AS canonical_url'
537
+ ),
538
+ 'inbound': (
539
+ 'MATCH (p:Project {{id: {project_id}}})<-[:DEPENDS_ON]-(n:Project) '
540
+ 'OPTIONAL MATCH (n)-[ei:EXISTS_IN]->(i:Integration) '
541
+ 'RETURN n.id AS project_id, n.slug AS project_slug, '
542
+ 'i.slug AS integration_slug, ei.identifier AS identifier, '
543
+ 'ei.canonical_url AS canonical_url'
544
+ ),
545
+ }
546
+
547
+
548
+ async def lookup_project_dependencies(
549
+ db: graph.Graph,
550
+ project_id: str,
551
+ ) -> list[ProjectDependency] | None:
552
+ """Return the project's ``DEPENDS_ON`` neighbours in both directions.
553
+
554
+ Each :class:`ProjectDependency` carries the neighbour's own
555
+ ``EXISTS_IN`` connections. Populated onto
556
+ :attr:`PluginContext.dependencies`. Returns ``None`` on lookup
557
+ failure, so callers can tell "no dependencies" from "unknown".
558
+ When the result is ``None``, the host must not wire
559
+ :attr:`PluginContext.resolve_linked_identifiers`: without a
560
+ resolver, capabilities do not remove remote state.
561
+ """
562
+ columns = [
563
+ 'project_id',
564
+ 'project_slug',
565
+ 'integration_slug',
566
+ 'identifier',
567
+ 'canonical_url',
568
+ ]
569
+ dependencies: list[ProjectDependency] = []
570
+ for direction, query in _DEPENDENCY_QUERIES.items():
571
+ try:
572
+ records = await db.execute(
573
+ query, {'project_id': project_id}, columns
574
+ )
575
+ except Exception: # noqa: BLE001
576
+ LOGGER.warning('Project dependency lookup failed', exc_info=True)
577
+ return None
578
+ by_id: dict[str, ProjectDependency] = {}
579
+ for r in records:
580
+ neighbour_id = graph.parse_agtype(r.get('project_id'))
581
+ if not neighbour_id:
582
+ continue
583
+ dep = by_id.get(str(neighbour_id))
584
+ if dep is None:
585
+ dep = ProjectDependency(
586
+ direction=typing.cast(
587
+ typing.Literal['inbound', 'outbound'], direction
588
+ ),
589
+ project_id=str(neighbour_id),
590
+ project_slug=str(
591
+ graph.parse_agtype(r.get('project_slug')) or ''
592
+ ),
593
+ )
594
+ by_id[dep.project_id] = dep
595
+ slug = graph.parse_agtype(r.get('integration_slug'))
596
+ identifier = graph.parse_agtype(r.get('identifier'))
597
+ if not slug or not identifier:
598
+ continue
599
+ canonical_url = graph.parse_agtype(r.get('canonical_url'))
600
+ dep.service_connections.append(
601
+ ServiceConnection(
602
+ integration_slug=str(slug),
603
+ identifier=str(identifier),
604
+ canonical_url=(
605
+ None if canonical_url is None else str(canonical_url)
606
+ ),
607
+ )
608
+ )
609
+ dependencies.extend(by_id.values())
610
+ return dependencies
611
+
612
+
613
+ async def lookup_linked_identifiers(
614
+ db: graph.Graph,
615
+ org_slug: str,
616
+ integration_slug: str,
617
+ identifiers: list[str],
618
+ ) -> set[str]:
619
+ """Return the ``identifiers`` bound to a project in ``org_slug``.
620
+
621
+ Matches ``EXISTS_IN`` edges to the ``integration_slug`` Integration
622
+ from any project owned by a team in the organization. Returns an
623
+ empty set on lookup failure, so a caller that removes remote state
624
+ only for linked identifiers fails safe.
625
+ """
626
+ if not identifiers:
627
+ return set()
628
+ query: typing.LiteralString = (
629
+ 'MATCH (:Organization {{slug: {org_slug}}})<-[:BELONGS_TO]-(:Team)'
630
+ '<-[:OWNED_BY]-(:Project)-[ei:EXISTS_IN]->'
631
+ '(:Integration {{slug: {integration_slug}}}) '
632
+ 'WHERE ei.identifier IN {identifiers} '
633
+ 'RETURN DISTINCT ei.identifier AS identifier'
634
+ )
635
+ try:
636
+ records = await db.execute(
637
+ query,
638
+ {
639
+ 'org_slug': org_slug,
640
+ 'integration_slug': integration_slug,
641
+ 'identifiers': identifiers,
642
+ },
643
+ ['identifier'],
644
+ )
645
+ except Exception: # noqa: BLE001
646
+ LOGGER.debug('Linked identifier lookup failed', exc_info=True)
647
+ return set()
648
+ return {
649
+ str(value)
650
+ for r in records
651
+ if (value := graph.parse_agtype(r.get('identifier')))
652
+ }
653
+
654
+
655
+ def linked_identifier_resolver(
656
+ db: graph.Graph, org_slug: str, integration_slug: str | None
657
+ ) -> (
658
+ collections.abc.Callable[[list[str]], collections.abc.Awaitable[set[str]]]
659
+ | None
660
+ ):
661
+ """Build the :attr:`PluginContext.resolve_linked_identifiers` hook.
662
+
663
+ Returns ``None`` for a capability not bound to an Integration.
664
+ """
665
+ if not integration_slug:
666
+ return None
667
+ slug = integration_slug
668
+
669
+ async def _resolve(identifiers: list[str]) -> set[str]:
670
+ return await lookup_linked_identifiers(db, org_slug, slug, identifiers)
671
+
672
+ return _resolve
673
+
674
+
526
675
  async def persist_service_writeback(
527
676
  db: graph.Graph, ctx: PluginContext
528
677
  ) -> None:
@@ -29,6 +29,8 @@ from imbi.api.blueprint_compliance import (
29
29
  remediate_blueprint,
30
30
  )
31
31
  from imbi.api.endpoints._helpers import (
32
+ linked_identifier_resolver,
33
+ lookup_project_dependencies,
32
34
  lookup_project_exists_in,
33
35
  lookup_project_links,
34
36
  lookup_project_slugs,
@@ -110,6 +112,7 @@ async def _build_context(
110
112
  project_links = await lookup_project_links(db, project_id)
111
113
  project_type_slugs = await lookup_project_type_slugs(db, project_id)
112
114
  service_connections = await lookup_project_exists_in(db, project_id)
115
+ dependencies = await lookup_project_dependencies(db, project_id)
113
116
  return PluginContext(
114
117
  project_id=project_id,
115
118
  project_slug=project_slug,
@@ -122,6 +125,16 @@ async def _build_context(
122
125
  project_type_slugs=project_type_slugs,
123
126
  integration_slug=resolved.integration_slug,
124
127
  service_connections=service_connections,
128
+ dependencies=dependencies or [],
129
+ # No resolver when the dependency lookup failed, so a fix does
130
+ # not remove remote state on partial data.
131
+ resolve_linked_identifiers=(
132
+ None
133
+ if dependencies is None
134
+ else linked_identifier_resolver(
135
+ db, org_slug, resolved.integration_slug
136
+ )
137
+ ),
125
138
  )
126
139
 
127
140
 
@@ -669,6 +682,7 @@ async def remediate_all_for_project(
669
682
  org_slug: str,
670
683
  project_id: str,
671
684
  auth: permissions.AuthContext,
685
+ sweepable_only: bool = False,
672
686
  ) -> RemediateAllResponse | None:
673
687
  """Apply every fixable finding in a project's report (best-effort).
674
688
 
@@ -678,7 +692,10 @@ async def remediate_all_for_project(
678
692
  re-run once at the end so the returned report reflects every fix.
679
693
 
680
694
  Shared by the per-project endpoint and the ``remediate`` maintenance
681
- sweep, so both apply findings identically.
695
+ sweep, so both apply findings identically. ``sweepable_only``
696
+ restricts the pass to offers marked
697
+ :attr:`RemediationOffer.sweepable` (the ``sync-fixes`` sweep) and
698
+ skips the closing re-analysis when nothing was applied.
682
699
  """
683
700
  report = await _fetch_report(db, project_id)
684
701
  if report is None:
@@ -692,6 +709,8 @@ async def remediate_all_for_project(
692
709
  for finding in report.results:
693
710
  if finding.remediation is None:
694
711
  continue
712
+ if sweepable_only and not finding.remediation.sweepable:
713
+ continue
695
714
  try:
696
715
  result = await _remediate_one(
697
716
  db,
@@ -724,6 +743,8 @@ async def remediate_all_for_project(
724
743
  result=result,
725
744
  )
726
745
  )
746
+ if sweepable_only and not outcomes:
747
+ return RemediateAllResponse(outcomes=outcomes, report=report)
727
748
  fresh = await run_and_persist(db, org_slug, project_id, auth)
728
749
  return RemediateAllResponse(outcomes=outcomes, report=fresh)
729
750
 
@@ -5263,23 +5263,39 @@ async def complete_promote_build(
5263
5263
  resolved.plugin_slug,
5264
5264
  run.run_id,
5265
5265
  )
5266
- appended = await append_deployment_event(
5267
- db,
5268
- org_slug=org_slug,
5269
- project_id=project_id,
5270
- release_id=release_id,
5271
- env_slug=to_environment,
5272
- status='in_progress',
5273
- note=f'via {resolved.plugin_slug}',
5274
- external_run_id=str(run.run_id) if run.run_id else None,
5275
- external_run_url=run.run_url,
5276
- # The person who pressed promote, not the worker that got here.
5277
- performed_by=requested_by or None,
5278
- credential=ctx.credential_note,
5279
- # Stamps ``origin`` on the node, which is what lets a reclaimed
5280
- # job recognize its own in-flight deployment.
5281
- source='promote',
5282
- )
5266
+ # The deployment is dispatched and will run whatever happens next, so
5267
+ # recording it is best-effort from here on. Raising would abandon the
5268
+ # promote before the audit row below is written and before the watcher
5269
+ # closes the deployment -- the two writes that attribute it to
5270
+ # ``requested_by`` -- leaving the webhook's anonymous event as the only
5271
+ # record and "Deployed by" blank. The watcher's close re-creates the
5272
+ # node on the same ``external_run_id`` if this write is lost.
5273
+ try:
5274
+ appended = await append_deployment_event(
5275
+ db,
5276
+ org_slug=org_slug,
5277
+ project_id=project_id,
5278
+ release_id=release_id,
5279
+ env_slug=to_environment,
5280
+ status='in_progress',
5281
+ note=f'via {resolved.plugin_slug}',
5282
+ external_run_id=str(run.run_id) if run.run_id else None,
5283
+ external_run_url=run.run_url,
5284
+ # The person who pressed promote, not the worker that got here.
5285
+ performed_by=requested_by or None,
5286
+ credential=ctx.credential_note,
5287
+ # Stamps ``origin`` on the node, which is what lets a reclaimed
5288
+ # job recognize its own in-flight deployment.
5289
+ source='promote',
5290
+ )
5291
+ except Exception:
5292
+ LOGGER.exception(
5293
+ 'release-promote could not record the dispatched deployment '
5294
+ 'for project %s release %s',
5295
+ project_id,
5296
+ release_id,
5297
+ )
5298
+ appended = None
5283
5299
  if isinstance(appended, str):
5284
5300
  LOGGER.warning(
5285
5301
  'release-promote could not record the deployment event for '
@@ -2918,6 +2918,29 @@ async def list_project_relationships(
2918
2918
  )
2919
2919
 
2920
2920
 
2921
+ async def _dispatch_dependencies_changed(
2922
+ db: graph.Pool,
2923
+ org_slug: str,
2924
+ project_id: str,
2925
+ auth: permissions.AuthContext,
2926
+ ) -> None:
2927
+ """Tell lifecycle plugins the source project's dependencies changed.
2928
+
2929
+ Only the source is dispatched: its plugin reconciles the dependency
2930
+ in both directions, so the target needs no second call. Failures
2931
+ are logged and never undo the Imbi-side edge change.
2932
+ """
2933
+ try:
2934
+ await dispatch_lifecycle(
2935
+ db, project_id, org_slug, 'dependencies_changed', auth
2936
+ )
2937
+ except Exception:
2938
+ LOGGER.exception(
2939
+ 'Lifecycle dependencies_changed dispatch failed for project %s',
2940
+ project_id,
2941
+ )
2942
+
2943
+
2921
2944
  @projects_router.post(
2922
2945
  '/{project_id}/relationships/{target_id}',
2923
2946
  status_code=204,
@@ -2975,6 +2998,7 @@ async def create_project_relationship(
2975
2998
  f'Project {project_id!r} or target {target_id!r} not found'
2976
2999
  ),
2977
3000
  )
3001
+ await _dispatch_dependencies_changed(db, org_slug, project_id, auth)
2978
3002
  # The source project gained a dependency, which can change a
2979
3003
  # condition-policy score that reads its neighbours. No-op unless a
2980
3004
  # condition policy exists.
@@ -3034,6 +3058,7 @@ async def delete_project_relationship(
3034
3058
  f'Relationship from {project_id!r} to {target_id!r} not found'
3035
3059
  ),
3036
3060
  )
3061
+ await _dispatch_dependencies_changed(db, org_slug, project_id, auth)
3037
3062
  # The source project lost a dependency; re-score it so a condition
3038
3063
  # policy reading its neighbours reflects the change. No-op unless a
3039
3064
  # condition policy exists.
@@ -31,6 +31,9 @@ from imbi.common import clickhouse, graph, iggy
31
31
  from imbi.common import models as common_models
32
32
  from imbi.common.plugins.errors import PluginRateLimited
33
33
 
34
+ if typing.TYPE_CHECKING:
35
+ from imbi.api.endpoints.project_analysis import RemediateOutcome
36
+
34
37
  LOGGER = logging.getLogger(__name__)
35
38
 
36
39
  #: ``requested_by`` / ``principal_name`` recorded on work this runs.
@@ -144,32 +147,79 @@ async def execute_remediate(
144
147
  return _skip(
145
148
  ctx, 'remediate', 'No persisted report, or no fixable findings.'
146
149
  )
147
- # One row per remediation that did not work: which finding, which
148
- # plugin, and what it said. Successful fixes stay as a count -- a
149
- # project with forty findings would otherwise write forty rows to
150
- # say nothing an operator will read.
151
- for outcome in response.outcomes:
150
+ return _record_remediations(ctx, 'remediate', response.outcomes)
151
+
152
+
153
+ async def execute_sync_fixes(
154
+ db: graph.Graph,
155
+ client: valkey.Valkey,
156
+ project_id: str,
157
+ *,
158
+ ctx: log.MaintenanceContext,
159
+ ) -> ExecuteOutcome:
160
+ """Analyze one project, then apply only its sweepable fixes.
161
+
162
+ Unlike ``remediate`` this does not trust a stored report: it runs
163
+ the analysis first so drift that appeared since the last run is
164
+ found, then applies the findings whose offer is marked
165
+ :attr:`~imbi.common.plugins.base.RemediationOffer.sweepable` --
166
+ idempotent fixes that bring a remote back in line with Imbi, never
167
+ ones that create or delete a remote resource.
168
+ """
169
+ from imbi.api.endpoints import project_analysis
170
+
171
+ org_slug = await _org_slug_for(db, project_id)
172
+ if org_slug is None:
173
+ return _skip(ctx, 'no-organization', _NO_ORG)
174
+ auth = _system_auth()
175
+ await project_analysis.run_and_persist(db, org_slug, project_id, auth)
176
+ response = await project_analysis.remediate_all_for_project(
177
+ db,
178
+ org_slug=org_slug,
179
+ project_id=project_id,
180
+ auth=auth,
181
+ sweepable_only=True,
182
+ )
183
+ if response is None or not response.outcomes:
184
+ return _skip(ctx, 'sync-fixes', 'No sweepable findings.')
185
+ return _record_remediations(ctx, 'sync-fixes', response.outcomes)
186
+
187
+
188
+ def _record_remediations(
189
+ ctx: log.MaintenanceContext,
190
+ action: str,
191
+ outcomes: list[RemediateOutcome],
192
+ ) -> ExecuteOutcome:
193
+ """Log a remediation pass and classify the item.
194
+
195
+ One row per remediation that did not work: which finding, which
196
+ plugin, and what it said. Successful fixes stay as a count -- a
197
+ project with forty findings would otherwise write forty rows to say
198
+ nothing an operator will read. Raises
199
+ :class:`MaintenanceItemFailed` when any remediation failed.
200
+ """
201
+ for outcome in outcomes:
152
202
  if outcome.result.status == 'failed':
153
203
  ctx.log.record(
154
204
  'failed',
155
- 'remediate',
205
+ action,
156
206
  outcome.result.message,
157
207
  finding=outcome.slug,
158
208
  plugin=outcome.plugin_id,
159
209
  )
160
- failed = sum(1 for o in response.outcomes if o.result.status == 'failed')
161
- fixed = sum(1 for o in response.outcomes if o.result.status == 'fixed')
210
+ failed = sum(1 for o in outcomes if o.result.status == 'failed')
211
+ fixed = sum(1 for o in outcomes if o.result.status == 'fixed')
162
212
  ctx.log.record(
163
213
  'failed' if failed else 'succeeded',
164
- 'remediate',
165
- f'{fixed} of {len(response.outcomes)} findings fixed.',
214
+ action,
215
+ f'{fixed} of {len(outcomes)} findings fixed.',
166
216
  fixed=fixed,
167
217
  failed=failed,
168
- total=len(response.outcomes),
218
+ total=len(outcomes),
169
219
  )
170
220
  if failed:
171
221
  raise MaintenanceItemFailed(
172
- f'{failed} of {len(response.outcomes)} remediations failed; '
222
+ f'{failed} of {len(outcomes)} remediations failed; '
173
223
  'see server logs for details.'
174
224
  )
175
225
  return 'succeeded'
@@ -22,6 +22,7 @@ from imbi.common import graph
22
22
  MaintenanceSlug = typing.Literal[
23
23
  'run-analysis',
24
24
  'remediate',
25
+ 'sync-fixes',
25
26
  'rescore',
26
27
  'deployment-resync',
27
28
  'deployment-sweep',
@@ -104,6 +105,21 @@ OPERATIONS: dict[MaintenanceSlug, OperationDefinition] = {
104
105
  enumerate=operations.enumerate_all_projects,
105
106
  execute=operations.execute_remediate,
106
107
  ),
108
+ OperationDefinition(
109
+ slug='sync-fixes',
110
+ label='Apply Sync Fixes',
111
+ description=(
112
+ 'Run the Project Doctor analysis for every project, then '
113
+ 'apply only the fixes a plugin marks safe to sweep -- '
114
+ 'e.g. repointing a PagerDuty escalation policy or syncing '
115
+ 'PagerDuty service dependencies. Never creates or deletes '
116
+ 'a remote resource. Projects with no such findings are '
117
+ 'skipped.'
118
+ ),
119
+ pause_key=None,
120
+ enumerate=operations.enumerate_all_projects,
121
+ execute=operations.execute_sync_fixes,
122
+ ),
107
123
  OperationDefinition(
108
124
  slug='rescore',
109
125
  label='Recompute Scores',
@@ -39,6 +39,7 @@ from imbi.common.plugins.base import (
39
39
  LifecycleResult,
40
40
  LinkWriteback,
41
41
  PluginContext,
42
+ ProjectDependency,
42
43
  ServiceConnection,
43
44
  ServiceWriteback,
44
45
  )
@@ -60,6 +61,7 @@ LifecycleEvent = typing.Literal[
60
61
  'unarchived',
61
62
  'deleted',
62
63
  'relocated',
64
+ 'dependencies_changed',
63
65
  ]
64
66
 
65
67
  #: Map of lifecycle events to the matching ``LifecycleCapability`` hook
@@ -72,6 +74,7 @@ _EVENT_METHOD: dict[LifecycleEvent, str] = {
72
74
  'unarchived': 'on_project_unarchived',
73
75
  'deleted': 'on_project_deleted',
74
76
  'relocated': 'on_project_relocated',
77
+ 'dependencies_changed': 'on_project_dependencies_changed',
75
78
  }
76
79
 
77
80
 
@@ -137,6 +140,10 @@ class LifecycleContextBundle:
137
140
  )
138
141
  project_name: str | None = None
139
142
  project_description: str | None = None
143
+ # ``None`` when the dependency lookup failed.
144
+ dependencies: list[ProjectDependency] | None = dataclasses.field(
145
+ default_factory=list
146
+ )
140
147
 
141
148
 
142
149
  async def build_lifecycle_context_bundle(
@@ -148,6 +155,7 @@ async def build_lifecycle_context_bundle(
148
155
  write, and pass it as ``bundle=`` to :func:`dispatch_lifecycle`.
149
156
  """
150
157
  from imbi.api.endpoints._helpers import (
158
+ lookup_project_dependencies,
151
159
  lookup_project_exists_in,
152
160
  lookup_project_links,
153
161
  lookup_project_name_description,
@@ -163,12 +171,14 @@ async def build_lifecycle_context_bundle(
163
171
  project_type_slugs,
164
172
  service_connections,
165
173
  (project_name, project_description),
174
+ dependencies,
166
175
  ) = await asyncio.gather(
167
176
  lookup_project_slugs(db, project_id),
168
177
  lookup_project_links(db, project_id),
169
178
  lookup_project_type_slugs(db, project_id),
170
179
  lookup_project_exists_in(db, project_id),
171
180
  lookup_project_name_description(db, project_id),
181
+ lookup_project_dependencies(db, project_id),
172
182
  )
173
183
  return LifecycleContextBundle(
174
184
  project_slug=project_slug,
@@ -178,6 +188,7 @@ async def build_lifecycle_context_bundle(
178
188
  service_connections=service_connections,
179
189
  project_name=project_name,
180
190
  project_description=project_description,
191
+ dependencies=dependencies,
181
192
  )
182
193
 
183
194
 
@@ -225,6 +236,9 @@ async def dispatch_lifecycle(
225
236
  :class:`PluginContext` for plugins that need a before/after view
226
237
  (``'updated'`` / ``'relocated'``).
227
238
  """
239
+ # Lazy import to avoid the endpoints/_helpers <-> this-module cycle.
240
+ from imbi.api.endpoints._helpers import linked_identifier_resolver
241
+
228
242
  if resolved_list is None:
229
243
  resolved_list = await resolve_all_capabilities(
230
244
  db, project_id, 'lifecycle'
@@ -277,6 +291,16 @@ async def dispatch_lifecycle(
277
291
  integration_options=resolved.integration_options,
278
292
  capability_options=resolved.capability_options,
279
293
  service_connections=bundle.service_connections,
294
+ dependencies=bundle.dependencies or [],
295
+ # No resolver when the dependency lookup failed, so a
296
+ # capability does not remove remote state on partial data.
297
+ resolve_linked_identifiers=(
298
+ None
299
+ if bundle.dependencies is None
300
+ else linked_identifier_resolver(
301
+ db, org_slug, resolved.integration_slug
302
+ )
303
+ ),
280
304
  )
281
305
  invocation = await _invoke_one(db, ctx, resolved, event, auth)
282
306
  results.append(invocation)