imbi-api 2.9.3__tar.gz → 2.11.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 (303) hide show
  1. {imbi_api-2.9.3 → imbi_api-2.11.0}/PKG-INFO +8 -7
  2. {imbi_api-2.9.3 → imbi_api-2.11.0}/pyproject.toml +8 -7
  3. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/backfill_embeddings.py +5 -2
  4. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/commit_sync/queue.py +73 -0
  5. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/commit_sync/service.py +4 -0
  6. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/domain/models.py +48 -1
  7. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/domain/scoring.py +8 -1
  8. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/_helpers.py +33 -13
  9. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/admin_plugins.py +4 -0
  10. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/comments.py +35 -17
  11. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/dashboard.py +18 -7
  12. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/document_templates.py +31 -0
  13. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/documents.py +470 -106
  14. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/events.py +114 -12
  15. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/organizations.py +30 -1
  16. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_deployments.py +667 -37
  17. imbi_api-2.11.0/src/imbi_api/endpoints/project_incidents.py +127 -0
  18. imbi_api-2.11.0/src/imbi_api/endpoints/project_lifecycle.py +77 -0
  19. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_plugins.py +9 -1
  20. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/projects.py +187 -42
  21. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/releases.py +88 -1
  22. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/scoring_policies.py +2 -0
  23. imbi_api-2.11.0/src/imbi_api/endpoints/search.py +287 -0
  24. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/third_party_services.py +93 -9
  25. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/users.py +28 -0
  26. imbi_api-2.11.0/src/imbi_api/identity/attribution.py +130 -0
  27. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/models.py +2 -1
  28. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/assignments.py +8 -0
  29. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/lifecycle_dispatch.py +2 -0
  30. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/scoring/queue.py +12 -6
  31. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_document_templates.py +82 -0
  32. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_documents.py +316 -143
  33. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_events.py +122 -2
  34. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_deployments.py +518 -0
  35. imbi_api-2.11.0/tests/endpoints/test_project_incidents.py +241 -0
  36. imbi_api-2.11.0/tests/endpoints/test_project_lifecycle.py +110 -0
  37. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_projects.py +66 -4
  38. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_projects_helpers.py +5 -5
  39. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_releases.py +109 -0
  40. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_scoring_policies.py +46 -0
  41. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_search.py +188 -1
  42. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_service_writeback.py +55 -0
  43. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_third_party_services.py +59 -0
  44. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_users.py +33 -12
  45. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_commit_sync_queue.py +75 -0
  46. imbi_api-2.11.0/tests/test_identity_attribution.py +158 -0
  47. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_lifecycle_dispatch.py +9 -0
  48. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_scoring_triggers.py +23 -0
  49. imbi_api-2.11.0/uv.lock +3226 -0
  50. imbi_api-2.9.3/src/imbi_api/endpoints/search.py +0 -169
  51. imbi_api-2.9.3/uv.lock +0 -3209
  52. {imbi_api-2.9.3 → imbi_api-2.11.0}/.github/workflows/deploy.yaml +0 -0
  53. {imbi_api-2.9.3 → imbi_api-2.11.0}/.github/workflows/docs.yaml +0 -0
  54. {imbi_api-2.9.3 → imbi_api-2.11.0}/.github/workflows/testing.yaml +0 -0
  55. {imbi_api-2.9.3 → imbi_api-2.11.0}/.gitignore +0 -0
  56. {imbi_api-2.9.3 → imbi_api-2.11.0}/.pre-commit-config.yaml +0 -0
  57. {imbi_api-2.9.3 → imbi_api-2.11.0}/.python-version +0 -0
  58. {imbi_api-2.9.3 → imbi_api-2.11.0}/CLAUDE.md +0 -0
  59. {imbi_api-2.9.3 → imbi_api-2.11.0}/CODE_REVIEW_PUNCHLIST.md +0 -0
  60. {imbi_api-2.9.3 → imbi_api-2.11.0}/LICENSE +0 -0
  61. {imbi_api-2.9.3 → imbi_api-2.11.0}/README.md +0 -0
  62. {imbi_api-2.9.3 → imbi_api-2.11.0}/coderabbit.yaml +0 -0
  63. {imbi_api-2.9.3 → imbi_api-2.11.0}/compose.yaml +0 -0
  64. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0001-record-architecture-decisions.md +0 -0
  65. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
  66. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0003-email-sending-architecture.md +0 -0
  67. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
  68. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
  69. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
  70. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0007-relationship-blueprints.md +0 -0
  71. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0008-plugin-system-architecture.md +0 -0
  72. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
  73. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0010-identity-plugin-architecture.md +0 -0
  74. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0011-graph-based-project-scoring.md +0 -0
  75. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
  76. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0013-deployment-plugin-type.md +0 -0
  77. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
  78. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
  79. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/adr.md +0 -0
  80. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/configuration.md +0 -0
  81. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/index.md +0 -0
  82. {imbi_api-2.9.3 → imbi_api-2.11.0}/docs/restore-backup.md +0 -0
  83. {imbi_api-2.9.3 → imbi_api-2.11.0}/justfile +0 -0
  84. {imbi_api-2.9.3 → imbi_api-2.11.0}/mkdocs.yml +0 -0
  85. {imbi_api-2.9.3 → imbi_api-2.11.0}/mypy.ini +0 -0
  86. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/__init__.py +0 -0
  87. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/app.py +0 -0
  88. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/__init__.py +0 -0
  89. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/authorization_codes.py +0 -0
  90. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/local_auth.py +0 -0
  91. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/login_providers.py +0 -0
  92. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/membership.py +0 -0
  93. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/models.py +0 -0
  94. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/oauth.py +0 -0
  95. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/oauth_clients.py +0 -0
  96. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/password.py +0 -0
  97. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/permissions.py +0 -0
  98. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/seed.py +0 -0
  99. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/sessions.py +0 -0
  100. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/tokens.py +0 -0
  101. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/auth/totp.py +0 -0
  102. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/blueprint_attributes.py +0 -0
  103. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/commit_sync/__init__.py +0 -0
  104. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/domain/__init__.py +0 -0
  105. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/__init__.py +0 -0
  106. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/client.py +0 -0
  107. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/dependencies.py +0 -0
  108. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/models.py +0 -0
  109. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/base.html +0 -0
  110. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/base.txt +0 -0
  111. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/password_reset.html +0 -0
  112. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/password_reset.txt +0 -0
  113. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/welcome.html +0 -0
  114. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates/welcome.txt +0 -0
  115. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/email/templates.py +0 -0
  116. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/__init__.py +0 -0
  117. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/_credentials.py +0 -0
  118. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/_json_fields.py +0 -0
  119. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/_pagination.py +0 -0
  120. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/_request_urls.py +0 -0
  121. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/admin.py +0 -0
  122. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/api_keys.py +0 -0
  123. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/auth.py +0 -0
  124. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/auth_providers.py +0 -0
  125. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/blueprints.py +0 -0
  126. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/client_credentials.py +0 -0
  127. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/environments.py +0 -0
  128. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/graph_query.py +0 -0
  129. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/identity_plugins.py +0 -0
  130. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/link_definitions.py +0 -0
  131. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/local_auth.py +0 -0
  132. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/mcp_servers.py +0 -0
  133. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/mfa.py +0 -0
  134. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
  135. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/operations_log.py +0 -0
  136. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/plugin_edges.py +0 -0
  137. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/plugin_entities.py +0 -0
  138. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/plugins.py +0 -0
  139. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_analysis.py +0 -0
  140. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_commit_sync.py +0 -0
  141. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_configuration.py +0 -0
  142. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_logs.py +0 -0
  143. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_type_plugins.py +0 -0
  144. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/project_types.py +0 -0
  145. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/pull_requests.py +0 -0
  146. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/roles.py +0 -0
  147. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
  148. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/scoring.py +0 -0
  149. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/service_accounts.py +0 -0
  150. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/service_plugins.py +0 -0
  151. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/status.py +0 -0
  152. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/tags.py +0 -0
  153. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/teams.py +0 -0
  154. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/uploads.py +0 -0
  155. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/user_activity.py +0 -0
  156. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/endpoints/webhooks.py +0 -0
  157. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/entrypoint.py +0 -0
  158. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/graph_sql.py +0 -0
  159. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/__init__.py +0 -0
  160. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/endpoints.py +0 -0
  161. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/errors.py +0 -0
  162. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/flows.py +0 -0
  163. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/host_integration.py +0 -0
  164. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/models.py +0 -0
  165. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/repository.py +0 -0
  166. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/resolution.py +0 -0
  167. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/state.py +0 -0
  168. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/identity/sweeper.py +0 -0
  169. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/lifespans.py +0 -0
  170. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/llm/__init__.py +0 -0
  171. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/llm/dependencies.py +0 -0
  172. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/mcp_test.py +0 -0
  173. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/middleware/__init__.py +0 -0
  174. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/middleware/rate_limit.py +0 -0
  175. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/openapi.py +0 -0
  176. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/patch.py +0 -0
  177. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/__init__.py +0 -0
  178. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/assignment_writer.py +0 -0
  179. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/credentials.py +0 -0
  180. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/installer.py +0 -0
  181. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/lifecycle.py +0 -0
  182. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/reload.py +0 -0
  183. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/resolution.py +0 -0
  184. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/plugins/schemas.py +0 -0
  185. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/prompts/__init__.py +0 -0
  186. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/prompts/release_notes_system.md +0 -0
  187. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/py.typed +0 -0
  188. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/relationships.py +0 -0
  189. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/sbom.py +0 -0
  190. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/scoring/__init__.py +0 -0
  191. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/settings.py +0 -0
  192. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/storage/__init__.py +0 -0
  193. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/storage/client.py +0 -0
  194. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/storage/dependencies.py +0 -0
  195. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/storage/thumbnails.py +0 -0
  196. {imbi_api-2.9.3 → imbi_api-2.11.0}/src/imbi_api/storage/validation.py +0 -0
  197. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/__init__.py +0 -0
  198. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/__init__.py +0 -0
  199. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_api_key_auth.py +0 -0
  200. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_authentication.py +0 -0
  201. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_authorization.py +0 -0
  202. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_encryption.py +0 -0
  203. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_login_providers.py +0 -0
  204. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_membership.py +0 -0
  205. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_oauth.py +0 -0
  206. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_oauth_server.py +0 -0
  207. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_permissions.py +0 -0
  208. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_seed.py +0 -0
  209. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_sessions.py +0 -0
  210. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/auth/test_totp.py +0 -0
  211. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/clickhouse/__init__.py +0 -0
  212. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/clickhouse/test_client.py +0 -0
  213. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/clickhouse/test_init.py +0 -0
  214. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/clickhouse/test_privacy.py +0 -0
  215. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/__init__.py +0 -0
  216. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/test_client.py +0 -0
  217. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/test_init.py +0 -0
  218. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/test_integration.py +0 -0
  219. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/test_models.py +0 -0
  220. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/email/test_templates.py +0 -0
  221. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/__init__.py +0 -0
  222. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_admin.py +0 -0
  223. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_admin_plugins.py +0 -0
  224. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_api_keys.py +0 -0
  225. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_auth.py +0 -0
  226. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_auth_providers.py +0 -0
  227. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_blueprints.py +0 -0
  228. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_client_credentials.py +0 -0
  229. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_comments.py +0 -0
  230. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_credentials.py +0 -0
  231. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_dashboard.py +0 -0
  232. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_environments.py +0 -0
  233. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_graph_query.py +0 -0
  234. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_init.py +0 -0
  235. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_json_fields.py +0 -0
  236. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_link_definitions.py +0 -0
  237. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_local_auth.py +0 -0
  238. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_mcp_servers.py +0 -0
  239. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_mfa.py +0 -0
  240. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_operations_log.py +0 -0
  241. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_organizations.py +0 -0
  242. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_pagination.py +0 -0
  243. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
  244. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_plugins.py +0 -0
  245. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_analysis.py +0 -0
  246. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_commit_sync.py +0 -0
  247. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_configuration.py +0 -0
  248. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_logs.py +0 -0
  249. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_plugins.py +0 -0
  250. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
  251. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_type_plugins.py +0 -0
  252. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_project_types.py +0 -0
  253. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_pull_requests.py +0 -0
  254. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_request_urls.py +0 -0
  255. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_roles.py +0 -0
  256. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_sa_api_keys.py +0 -0
  257. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_scoring.py +0 -0
  258. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_service_accounts.py +0 -0
  259. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_service_plugins.py +0 -0
  260. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_status.py +0 -0
  261. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_tags.py +0 -0
  262. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_teams.py +0 -0
  263. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_uploads.py +0 -0
  264. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_user_activity.py +0 -0
  265. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/endpoints/test_webhooks.py +0 -0
  266. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
  267. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  268. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/fixtures/sbom/tiny.json +0 -0
  269. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/__init__.py +0 -0
  270. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_endpoints.py +0 -0
  271. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_errors.py +0 -0
  272. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_flows.py +0 -0
  273. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_host_integration.py +0 -0
  274. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_repository.py +0 -0
  275. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_resolution.py +0 -0
  276. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_state.py +0 -0
  277. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/identity/test_sweeper.py +0 -0
  278. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/middleware/test_rate_limit.py +0 -0
  279. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/storage/__init__.py +0 -0
  280. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/storage/test_client.py +0 -0
  281. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/storage/test_init.py +0 -0
  282. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/storage/test_thumbnails.py +0 -0
  283. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/storage/test_validation.py +0 -0
  284. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/support.py +0 -0
  285. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_app.py +0 -0
  286. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_assignment_writer.py +0 -0
  287. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_backfill_embeddings.py +0 -0
  288. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_blueprint_attributes.py +0 -0
  289. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_blueprints.py +0 -0
  290. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_commit_sync_service.py +0 -0
  291. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_entrypoint.py +0 -0
  292. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_graph_sql.py +0 -0
  293. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_init.py +0 -0
  294. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_installer.py +0 -0
  295. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_lifespans.py +0 -0
  296. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_models.py +0 -0
  297. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_openapi.py +0 -0
  298. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_patch.py +0 -0
  299. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_plugin_schemas.py +0 -0
  300. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_plugins.py +0 -0
  301. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_sbom.py +0 -0
  302. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_scoring_queue.py +0 -0
  303. {imbi_api-2.9.3 → imbi_api-2.11.0}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-api
3
- Version: 2.9.3
3
+ Version: 2.11.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: 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.9.2
23
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.11.0
24
24
  Requires-Dist: jinja2
25
25
  Requires-Dist: jsonpatch>=1.33
26
26
  Requires-Dist: mcp>=1.26.0
@@ -43,11 +43,12 @@ Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'otel'
43
43
  Requires-Dist: opentelemetry-instrumentation-httpx; extra == 'otel'
44
44
  Requires-Dist: opentelemetry-instrumentation-jinja2; extra == 'otel'
45
45
  Provides-Extra: plugins
46
- Requires-Dist: imbi-plugin-aws>=1.0.0; extra == 'plugins'
47
- Requires-Dist: imbi-plugin-github>=1.0.0; extra == 'plugins'
48
- Requires-Dist: imbi-plugin-logzio>=1.0.0; extra == 'plugins'
49
- Requires-Dist: imbi-plugin-oidc>=1.0.0; extra == 'plugins'
50
- Requires-Dist: imbi-plugin-sonarqube>=0.0.0; extra == 'plugins'
46
+ Requires-Dist: imbi-plugin-aws>=2.1.0; extra == 'plugins'
47
+ Requires-Dist: imbi-plugin-github>=2.1.0; extra == 'plugins'
48
+ Requires-Dist: imbi-plugin-logzio>=2.1.0; extra == 'plugins'
49
+ Requires-Dist: imbi-plugin-oidc>=2.1.0; extra == 'plugins'
50
+ Requires-Dist: imbi-plugin-pagerduty>=2.1.0; extra == 'plugins'
51
+ Requires-Dist: imbi-plugin-sonarqube>=2.1.0; extra == 'plugins'
51
52
  Provides-Extra: sentry
52
53
  Requires-Dist: sentry-sdk; extra == 'sentry'
53
54
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.9.3"
3
+ version = "2.11.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.9.2",
31
+ "imbi-common[databases,llm,sentry,server]==2.11.0",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -58,11 +58,12 @@ otel = [
58
58
  "opentelemetry-instrumentation-jinja2",
59
59
  ]
60
60
  plugins = [
61
- "imbi-plugin-aws>=1.0.0",
62
- "imbi-plugin-github>=1.0.0",
63
- "imbi-plugin-logzio>=1.0.0",
64
- "imbi-plugin-oidc>=1.0.0",
65
- "imbi-plugin-sonarqube>=0.0.0",
61
+ "imbi-plugin-aws>=2.1.0",
62
+ "imbi-plugin-github>=2.1.0",
63
+ "imbi-plugin-logzio>=2.1.0",
64
+ "imbi-plugin-oidc>=2.1.0",
65
+ "imbi-plugin-pagerduty>=2.1.0",
66
+ "imbi-plugin-sonarqube>=2.1.0"
66
67
  ]
67
68
  sentry = ["sentry-sdk"]
68
69
 
@@ -45,10 +45,13 @@ _NODE_TYPES: list[type[models.Node]] = [
45
45
  ]
46
46
 
47
47
  # GraphModel types that have Embeddable fields but are not Node subclasses.
48
- # match() still works via model_construct fallback; _auto_embed accepts any
49
- # GraphModel even though its type annotation says Node.
48
+ # match() still works via model_construct fallback; _auto_embed embeds any
49
+ # GraphModel that declares Embeddable fields.
50
50
  _GRAPH_MODEL_TYPES: list[type[models.GraphModel]] = [
51
51
  models.Document,
52
+ models.Release,
53
+ models.Comment,
54
+ models.Component,
52
55
  ]
53
56
 
54
57
  _DEFAULT_CONCURRENCY = 4
@@ -13,10 +13,12 @@ import asyncio
13
13
  import logging
14
14
  import os
15
15
  import socket
16
+ import time
16
17
  import typing
17
18
  from collections import abc
18
19
 
19
20
  from imbi_common import graph
21
+ from imbi_common.plugins.errors import PluginRateLimited
20
22
  from valkey import asyncio as valkey
21
23
 
22
24
  from imbi_api.commit_sync.service import (
@@ -33,6 +35,18 @@ DEBOUNCE_PREFIX = 'imbi:commit-sync:debounce'
33
35
  DEBOUNCE_SECONDS = 10
34
36
  MAX_DELIVERIES = 3
35
37
  CLAIM_IDLE_MS = 120_000
38
+ # A GitHub rate-limit reset can be an hour out -- farther than the
39
+ # plugin's per-call wait cap -- so the plugin raises PluginRateLimited
40
+ # instead of failing the job. The worker records the resume time here and
41
+ # every worker honors it before reading, so one exhausted token pauses the
42
+ # whole consumer (the backfill runs against a single token) until GitHub
43
+ # resumes, rather than burning MAX_DELIVERIES and dead-lettering the work.
44
+ PAUSE_KEY = 'imbi:commit-sync:paused-until'
45
+ # Longest single sleep while paused; bounded so a *stop* signal is honored
46
+ # promptly and a long reset is re-checked rather than slept through blind.
47
+ PAUSE_POLL_CAP_SECONDS = 30
48
+ # Cushion on the pause key's TTL so it outlives the resume time it carries.
49
+ PAUSE_KEY_BUFFER_SECONDS = 5
36
50
 
37
51
  LOGGER = logging.getLogger(__name__)
38
52
 
@@ -106,6 +120,20 @@ async def _process_message(db: graph.Graph, fields: dict[str, str]) -> None:
106
120
  error=str(exc),
107
121
  )
108
122
  return
123
+ except PluginRateLimited:
124
+ # Transient: GitHub's rate limit is exhausted. Keep the job queued
125
+ # (the consumer pauses until the reset and retries) instead of
126
+ # marking it failed -- the failure path would dead-letter it before
127
+ # the limit clears. _handle_entries sets the pause from the
128
+ # re-raised exception's retry_at.
129
+ LOGGER.warning(
130
+ 'commit-sync rate-limited for %s; left queued until GitHub resets',
131
+ project_id,
132
+ )
133
+ await set_status(
134
+ db, project_id, status='queued', requested_by=requested_by
135
+ )
136
+ raise
109
137
  except Exception:
110
138
  # Persist a generic, user-safe message; the polling endpoint
111
139
  # exposes this status, so keep raw exception detail in logs only.
@@ -145,6 +173,30 @@ def _decode_fields(
145
173
  }
146
174
 
147
175
 
176
+ async def _paused_remaining(client: valkey.Valkey) -> float:
177
+ """Seconds left on the global rate-limit pause, ``0.0`` when clear."""
178
+ try:
179
+ raw = await client.get(PAUSE_KEY)
180
+ except Exception: # noqa: BLE001
181
+ return 0.0
182
+ if raw is None:
183
+ return 0.0
184
+ try:
185
+ until = float(raw.decode() if isinstance(raw, bytes) else raw)
186
+ except ValueError:
187
+ return 0.0
188
+ return max(0.0, until - time.time())
189
+
190
+
191
+ async def _pause_until(client: valkey.Valkey, retry_at: float) -> None:
192
+ """Record the resume time so every worker backs off until *retry_at*."""
193
+ ttl = max(1, int(retry_at - time.time()) + PAUSE_KEY_BUFFER_SECONDS)
194
+ try:
195
+ await client.set(PAUSE_KEY, str(retry_at), ex=ttl)
196
+ except Exception:
197
+ LOGGER.exception('failed to set commit-sync pause marker')
198
+
199
+
148
200
  async def _claim_stale(
149
201
  client: valkey.Valkey,
150
202
  consumer: str,
@@ -215,6 +267,19 @@ async def _handle_entries(
215
267
  continue
216
268
  try:
217
269
  await _process_message(db, fields)
270
+ except PluginRateLimited as exc:
271
+ # Don't ack, don't dead-letter: leave the job pending and pause
272
+ # every worker until GitHub resets. Stop the batch -- its
273
+ # siblings hit the same token -- and let the next reclaim drain
274
+ # it once the pause clears.
275
+ await _pause_until(client, exc.retry_at)
276
+ LOGGER.warning(
277
+ 'commit-sync paused ~%.0fs (GitHub rate limit); %d job(s) '
278
+ 'left queued',
279
+ max(0.0, exc.retry_at - time.time()),
280
+ len(entries),
281
+ )
282
+ return
218
283
  except Exception:
219
284
  LOGGER.exception('commit-sync failed for %s', fields)
220
285
  continue
@@ -236,6 +301,10 @@ async def consume_commit_sync(
236
301
  await ensure_group(client)
237
302
  LOGGER.info('Commit-sync consumer loop running (consumer=%s)', consumer)
238
303
  while stop is None or not stop.is_set():
304
+ paused = await _paused_remaining(client)
305
+ if paused > 0:
306
+ await asyncio.sleep(min(paused, PAUSE_POLL_CAP_SECONDS))
307
+ continue
239
308
  stale = await _claim_stale(client, consumer)
240
309
  if stale:
241
310
  try:
@@ -244,6 +313,10 @@ async def consume_commit_sync(
244
313
  LOGGER.exception('commit-sync stale-entry handling failed')
245
314
  await asyncio.sleep(1)
246
315
  continue
316
+ # Stale handling may have just paused us; don't read new work into
317
+ # a guaranteed re-throttle -- loop back and honor the pause.
318
+ if await _paused_remaining(client) > 0:
319
+ continue
247
320
  try:
248
321
  response = await client.xreadgroup(
249
322
  GROUP,
@@ -25,6 +25,7 @@ from imbi_common.plugins.base import PluginContext, ServicePlugin
25
25
  from imbi_common.plugins.errors import PluginNotFoundError
26
26
  from imbi_common.plugins.registry import RegistryEntry, get_plugin
27
27
 
28
+ from imbi_api.identity import attribution
28
29
  from imbi_api.plugins import parse_options
29
30
  from imbi_api.plugins.credentials import get_plugin_credentials
30
31
 
@@ -174,6 +175,9 @@ async def _build_context(
174
175
  project_type_slugs=project_type_slugs,
175
176
  third_party_service_slug=resolved.tps_slug or None,
176
177
  service_connections=service_connections,
178
+ resolve_user_by_identity=attribution.make_user_resolver(
179
+ db, resolved.service_plugins
180
+ ),
177
181
  )
178
182
 
179
183
 
@@ -55,6 +55,7 @@ __all__ = [
55
55
  'ServiceApplicationSecrets',
56
56
  'Session',
57
57
  'TOTPSecret',
58
+ 'TeamMembership',
58
59
  'ThirdPartyService',
59
60
  'ThirdPartyServiceCreate',
60
61
  'ThirdPartyServiceResponse',
@@ -224,6 +225,15 @@ class OrgMembership(pydantic.BaseModel):
224
225
  role: str
225
226
 
226
227
 
228
+ class TeamMembership(pydantic.BaseModel):
229
+ """Team membership for API responses."""
230
+
231
+ model_config = pydantic.ConfigDict(extra='ignore')
232
+ team_name: str
233
+ team_slug: str
234
+ organization_slug: str
235
+
236
+
227
237
  class Role(models.Node): # type: ignore[misc]
228
238
  """Role for grouping permissions."""
229
239
 
@@ -445,12 +455,14 @@ class CurrentUserResponse(UserResponse):
445
455
  """Response model for the authenticated user's own profile.
446
456
 
447
457
  Extends ``UserResponse`` with the caller's effective permission set
448
- so the UI can gate features in a single request. Only exposed via
458
+ and team memberships so the UI can gate features and offer
459
+ membership-scoped filters in a single request. Only exposed via
449
460
  ``GET /users/me`` — the per-email endpoint deliberately omits
450
461
  permissions to avoid disclosing one user's grants to another.
451
462
  """
452
463
 
453
464
  permissions: list[str] = []
465
+ teams: list[TeamMembership] = []
454
466
 
455
467
 
456
468
  class PasswordChangeRequest(pydantic.BaseModel):
@@ -919,6 +931,41 @@ class PluginAssignmentResponse(pydantic.BaseModel):
919
931
  # card on the project settings page (and the admin TPS page) so
920
932
  # the UI doesn't surface a button that the API will 400 on.
921
933
  supports_deployment_sync: bool = False
934
+ # Set on lifecycle plugins whose ``on_project_updated`` is a safe
935
+ # upsert -- gates the "Sync lifecycle" controls on the project
936
+ # settings page (and the admin TPS page), which push current Imbi
937
+ # state to the remote on demand.
938
+ supports_lifecycle_sync: bool = False
939
+ # Parent third-party service (``ThirdPartyService -[:HAS_PLUGIN]->
940
+ # Plugin``) so the UI can show which service powers the tab.
941
+ service_name: str | None = None
942
+ service_icon: str | None = None
943
+
944
+
945
+ class LifecycleSyncError(pydantic.BaseModel):
946
+ """One non-fatal failure encountered during a lifecycle sync."""
947
+
948
+ project_id: str
949
+ detail: str
950
+
951
+
952
+ class LifecycleSyncSummary(pydantic.BaseModel):
953
+ """Aggregate outcome of a lifecycle push-sync run.
954
+
955
+ A sync re-dispatches each project's ``on_project_updated`` -- an
956
+ upsert that creates the remote when missing and updates it
957
+ otherwise -- so the per-project outcome is the plugin's
958
+ ``LifecycleResult.status``: ``synced`` counts ``ok`` results,
959
+ ``skipped`` counts ``skipped`` (e.g. an unmapped team), and
960
+ ``failed`` counts ``failed`` results plus dispatches that raised
961
+ (each captured in ``errors``).
962
+ """
963
+
964
+ projects: int = 0
965
+ synced: int = 0
966
+ skipped: int = 0
967
+ failed: int = 0
968
+ errors: list[LifecycleSyncError] = []
922
969
 
923
970
 
924
971
  # -- Configuration / Logs response models ---------------------------------
@@ -58,7 +58,12 @@ class PolicyCreate(pydantic.BaseModel):
58
58
  slug: str
59
59
  description: str | None = None
60
60
  category: typing.Literal[
61
- 'attribute', 'presence', 'link_presence', 'age', 'analysis_result'
61
+ 'attribute',
62
+ 'presence',
63
+ 'link_presence',
64
+ 'age',
65
+ 'analysis_result',
66
+ 'deployment_status',
62
67
  ] = 'attribute'
63
68
  weight: int = pydantic.Field(ge=0, le=100)
64
69
  enabled: bool = True
@@ -67,6 +72,7 @@ class PolicyCreate(pydantic.BaseModel):
67
72
  attribute_name: str | None = None
68
73
  link_slug: str | None = None
69
74
  result_slug: str | None = None
75
+ environment_slug: str | None = None
70
76
  present_score: int | None = pydantic.Field(default=None, ge=0, le=100)
71
77
  missing_score: int | None = pydantic.Field(default=None, ge=0, le=100)
72
78
  value_score_map: dict[str, int] | None = None
@@ -85,6 +91,7 @@ class PolicyUpdate(pydantic.BaseModel):
85
91
  attribute_name: str | None = None
86
92
  link_slug: str | None = None
87
93
  result_slug: str | None = None
94
+ environment_slug: str | None = None
88
95
  weight: int | None = pydantic.Field(default=None, ge=0, le=100)
89
96
  enabled: bool | None = None
90
97
  priority: int | None = None
@@ -405,6 +405,7 @@ async def persist_service_writeback(
405
405
  slug,
406
406
  writeback.identifier,
407
407
  writeback.canonical_url,
408
+ writeback.webhook_secret_enc,
408
409
  )
409
410
  await merge_project_links(
410
411
  db, ctx.project_id, add=writeback.dashboard_links
@@ -432,9 +433,26 @@ async def _merge_exists_in(
432
433
  tps_slug: str,
433
434
  identifier: str,
434
435
  canonical_url: str,
436
+ webhook_secret_enc: str | None = None,
435
437
  ) -> None:
436
- """Upsert the ``EXISTS_IN`` edge for ``(project, service)``."""
437
- query: typing.LiteralString = """
438
+ """Upsert the ``EXISTS_IN`` edge for ``(project, service)``.
439
+
440
+ ``webhook_secret_enc`` is an opaque, already-encrypted secret (the
441
+ plugin encrypts it; this never decrypts or re-encrypts it) stored on
442
+ the edge alongside ``identifier`` for a gateway to read back at
443
+ webhook-delivery time. When ``None`` the ``SET`` omits it, so an
444
+ existing edge's secret is left untouched.
445
+ """
446
+ # Conditionally append the secret to the SET clause so a writeback
447
+ # without one cannot null out a previously-stored secret. Both
448
+ # branches are string literals, keeping the query a ``LiteralString``.
449
+ secret_set: typing.LiteralString = (
450
+ ',\n ei.webhook_secret_enc = {webhook_secret_enc}'
451
+ if webhook_secret_enc is not None
452
+ else ''
453
+ )
454
+ query: typing.LiteralString = (
455
+ """
438
456
  MATCH (p:Project {{id: {project_id}}})
439
457
  -[:OWNED_BY]->(:Team)
440
458
  -[:BELONGS_TO]->(o:Organization {{slug: {org_slug}}})
@@ -442,20 +460,22 @@ async def _merge_exists_in(
442
460
  -[:BELONGS_TO]->(o)
443
461
  MERGE (p)-[ei:EXISTS_IN]->(tps)
444
462
  SET ei.identifier = {identifier},
445
- ei.canonical_url = {canonical_url}
463
+ ei.canonical_url = {canonical_url}"""
464
+ + secret_set
465
+ + """
446
466
  RETURN ei.identifier AS identifier
447
467
  """
448
- await db.execute(
449
- query,
450
- {
451
- 'org_slug': org_slug,
452
- 'project_id': project_id,
453
- 'tps_slug': tps_slug,
454
- 'identifier': identifier,
455
- 'canonical_url': canonical_url,
456
- },
457
- ['identifier'],
458
468
  )
469
+ params: dict[str, typing.Any] = {
470
+ 'org_slug': org_slug,
471
+ 'project_id': project_id,
472
+ 'tps_slug': tps_slug,
473
+ 'identifier': identifier,
474
+ 'canonical_url': canonical_url,
475
+ }
476
+ if webhook_secret_enc is not None:
477
+ params['webhook_secret_enc'] = webhook_secret_enc
478
+ await db.execute(query, params, ['identifier'])
459
479
 
460
480
 
461
481
  async def _delete_exists_in(
@@ -163,6 +163,9 @@ def _serialize(
163
163
  'supports_deployment_sync': bool(
164
164
  getattr(entry.manifest, 'supports_deployment_sync', False)
165
165
  ),
166
+ 'supports_lifecycle_sync': bool(
167
+ getattr(entry.manifest, 'supports_lifecycle_sync', False)
168
+ ),
166
169
  'options': [o.model_dump() for o in entry.manifest.options],
167
170
  'credentials': [c.model_dump() for c in entry.manifest.credentials],
168
171
  'vertex_labels': [
@@ -198,6 +201,7 @@ def _placeholder(package_name: str) -> dict[str, typing.Any]:
198
201
  'docs_url': None,
199
202
  'supported_tabs': [],
200
203
  'supports_deployment_sync': False,
204
+ 'supports_lifecycle_sync': False,
201
205
  'options': [],
202
206
  'credentials': [],
203
207
  'vertex_labels': [],
@@ -1,6 +1,6 @@
1
- """Page-level threaded comments on project documents.
1
+ """Page-level threaded comments on documents.
2
2
 
3
- Comments hang off a project ``Document`` via a ``CommentThread`` vertex
3
+ Comments hang off a ``Document`` via a ``CommentThread`` vertex
4
4
  (``ON_DOCUMENT`` edge) containing one or more ``Comment`` vertices
5
5
  (``IN_THREAD`` edge). The root comment is the oldest comment in a thread;
6
6
  replies follow in ``created_at`` order.
@@ -39,7 +39,7 @@ _COMMENT_EVENT_TYPE: typing.LiteralString = 'document-comment'
39
39
 
40
40
  async def _emit_comment_event(
41
41
  *,
42
- project_id: str,
42
+ project_id: str | None,
43
43
  document_id: str,
44
44
  thread_id: str,
45
45
  comment_id: str,
@@ -59,7 +59,7 @@ async def _emit_comment_event(
59
59
  [
60
60
  [
61
61
  nanoid.generate(),
62
- project_id,
62
+ project_id or '',
63
63
  datetime.datetime.now(datetime.UTC),
64
64
  _COMMENT_EVENT_TYPE,
65
65
  'internal',
@@ -265,22 +265,40 @@ _COMMENTS_TAIL: typing.LiteralString = """
265
265
  RETURN t, d, comments
266
266
  """
267
267
 
268
- # Document-ownership join used by every query to scope to project -> org.
268
+ # Document-ownership join used by every query to scope the document to
269
+ # the organization through whichever vertex it is attached to (project,
270
+ # project type, or user). When ``{project_id}`` is non-null the document
271
+ # must additionally be attached to that project — this keeps the
272
+ # project-scoped comment routes from resolving another project's
273
+ # document.
269
274
  _DOC_JOIN: typing.LiteralString = """
270
275
  MATCH (d:Document {{id: {document_id}}})
271
- -[:ATTACHED_TO]->(:Project {{id: {project_id}}})
276
+ OPTIONAL MATCH (d)-[:ATTACHED_TO]->(p:Project)
272
277
  -[:OWNED_BY]->(:Team)
273
278
  -[:BELONGS_TO]->(:Organization {{slug: {org_slug}}})
279
+ OPTIONAL MATCH (d)-[:ATTACHED_TO]->(pt:ProjectType)
280
+ -[:BELONGS_TO]->(:Organization {{slug: {org_slug}}})
281
+ OPTIONAL MATCH (d)-[:ATTACHED_TO]->(u:User)
282
+ -[:MEMBER_OF]->(:Organization {{slug: {org_slug}}})
283
+ WITH d, p, pt, u
284
+ WHERE ({project_id} IS NULL
285
+ AND (p IS NOT NULL OR pt IS NOT NULL OR u IS NOT NULL))
286
+ OR (p IS NOT NULL AND p.id = {project_id})
287
+ WITH DISTINCT d
274
288
  """
275
289
 
276
290
 
277
291
  async def _verify_document(
278
292
  db: graph.Pool,
279
293
  org_slug: str,
280
- project_id: str,
294
+ project_id: str | None,
281
295
  document_id: str,
282
296
  ) -> None:
283
- """Raise 404 unless the document belongs to the project -> org."""
297
+ """Raise 404 unless the document belongs to the org.
298
+
299
+ When ``project_id`` is provided the document must be attached to
300
+ that project.
301
+ """
284
302
  query: str = _DOC_JOIN + 'RETURN d.id AS id'
285
303
  records = await db.execute(
286
304
  query,
@@ -301,7 +319,7 @@ async def _verify_document(
301
319
  async def _fetch_thread(
302
320
  db: graph.Pool,
303
321
  org_slug: str,
304
- project_id: str,
322
+ project_id: str | None,
305
323
  document_id: str,
306
324
  thread_id: str,
307
325
  ) -> dict[str, typing.Any] | None:
@@ -332,7 +350,7 @@ async def _fetch_thread(
332
350
  async def _fetch_comment(
333
351
  db: graph.Pool,
334
352
  org_slug: str,
335
- project_id: str,
353
+ project_id: str | None,
336
354
  document_id: str,
337
355
  thread_id: str,
338
356
  comment_id: str,
@@ -366,13 +384,13 @@ async def _fetch_comment(
366
384
  @comments_router.get('', response_model=CommentThreadListResponse)
367
385
  async def list_comment_threads(
368
386
  org_slug: str,
369
- project_id: str,
370
387
  document_id: str,
371
388
  db: graph.Pool,
372
389
  _auth: typing.Annotated[
373
390
  permissions.AuthContext,
374
391
  fastapi.Depends(permissions.require_permission('document:read')),
375
392
  ],
393
+ project_id: str | None = None,
376
394
  ) -> dict[str, typing.Any]:
377
395
  """List every comment thread on a document, comments oldest-first."""
378
396
  await _verify_document(db, org_slug, project_id, document_id)
@@ -402,7 +420,6 @@ async def list_comment_threads(
402
420
  )
403
421
  async def create_comment_thread(
404
422
  org_slug: str,
405
- project_id: str,
406
423
  document_id: str,
407
424
  data: CommentThreadCreate,
408
425
  db: graph.Pool,
@@ -410,6 +427,7 @@ async def create_comment_thread(
410
427
  permissions.AuthContext,
411
428
  fastapi.Depends(permissions.require_permission('comment:create')),
412
429
  ],
430
+ project_id: str | None = None,
413
431
  ) -> dict[str, typing.Any]:
414
432
  """Create a thread, its ``ON_DOCUMENT`` edge, and the root comment."""
415
433
  await _verify_document(db, org_slug, project_id, document_id)
@@ -511,7 +529,6 @@ async def create_comment_thread(
511
529
  )
512
530
  async def create_reply(
513
531
  org_slug: str,
514
- project_id: str,
515
532
  document_id: str,
516
533
  thread_id: str,
517
534
  data: CommentBodyCreate,
@@ -520,6 +537,7 @@ async def create_reply(
520
537
  permissions.AuthContext,
521
538
  fastapi.Depends(permissions.require_permission('comment:create')),
522
539
  ],
540
+ project_id: str | None = None,
523
541
  ) -> dict[str, typing.Any]:
524
542
  """Add a reply comment to an existing thread."""
525
543
  now = datetime.datetime.now(datetime.UTC)
@@ -582,7 +600,6 @@ async def create_reply(
582
600
  @comments_router.patch('/{thread_id}', response_model=CommentThreadResponse)
583
601
  async def patch_comment_thread(
584
602
  org_slug: str,
585
- project_id: str,
586
603
  document_id: str,
587
604
  thread_id: str,
588
605
  operations: list[json_patch.PatchOperation],
@@ -591,6 +608,7 @@ async def patch_comment_thread(
591
608
  permissions.AuthContext,
592
609
  fastapi.Depends(permissions.require_permission('comment:write')),
593
610
  ],
611
+ project_id: str | None = None,
594
612
  ) -> dict[str, typing.Any]:
595
613
  """Resolve or reopen a thread via JSON Patch (only ``/resolved``)."""
596
614
  existing = await _fetch_thread(
@@ -659,7 +677,6 @@ async def patch_comment_thread(
659
677
  )
660
678
  async def patch_comment(
661
679
  org_slug: str,
662
- project_id: str,
663
680
  document_id: str,
664
681
  thread_id: str,
665
682
  comment_id: str,
@@ -669,6 +686,7 @@ async def patch_comment(
669
686
  permissions.AuthContext,
670
687
  fastapi.Depends(permissions.require_permission('comment:write')),
671
688
  ],
689
+ project_id: str | None = None,
672
690
  ) -> dict[str, typing.Any]:
673
691
  """Edit a comment via JSON Patch (only ``/body`` and ``/mentions``).
674
692
 
@@ -747,7 +765,6 @@ async def patch_comment(
747
765
  )
748
766
  async def acknowledge_comment(
749
767
  org_slug: str,
750
- project_id: str,
751
768
  document_id: str,
752
769
  thread_id: str,
753
770
  comment_id: str,
@@ -756,6 +773,7 @@ async def acknowledge_comment(
756
773
  permissions.AuthContext,
757
774
  fastapi.Depends(permissions.require_permission('comment:write')),
758
775
  ],
776
+ project_id: str | None = None,
759
777
  ) -> dict[str, typing.Any]:
760
778
  """Toggle the principal in the comment's ``acknowledged_by`` array."""
761
779
  existing = await _fetch_comment(
@@ -807,7 +825,6 @@ async def acknowledge_comment(
807
825
  @comments_router.delete('/{thread_id}/comments/{comment_id}', status_code=204)
808
826
  async def delete_comment(
809
827
  org_slug: str,
810
- project_id: str,
811
828
  document_id: str,
812
829
  thread_id: str,
813
830
  comment_id: str,
@@ -816,6 +833,7 @@ async def delete_comment(
816
833
  permissions.AuthContext,
817
834
  fastapi.Depends(permissions.require_permission('comment:delete')),
818
835
  ],
836
+ project_id: str | None = None,
819
837
  ) -> None:
820
838
  """Delete a comment. Author-only. Deletes the thread if it was the
821
839
  root and no other comments remain.
@@ -37,8 +37,11 @@ class DatastoreStatus(pydantic.BaseModel):
37
37
  status: typing.Literal['ok', 'error']
38
38
  latency_ms: float | None = None
39
39
  # Bytes on disk (Postgres/ClickHouse) or resident memory (Valkey,
40
- # which is in-memory). ``None`` when the size probe failed.
40
+ # which is in-memory). ``None`` when the size probe failed. For
41
+ # ClickHouse this is the Imbi application database; ``total_bytes``
42
+ # carries the whole-server footprint (incl. ``system.*``).
41
43
  size_bytes: int | None = None
44
+ total_bytes: int | None = None
42
45
  detail: str | None = None
43
46
 
44
47
 
@@ -134,13 +137,19 @@ async def _check_postgres(db: graph.Graph) -> DatastoreStatus:
134
137
 
135
138
 
136
139
  async def _check_clickhouse() -> DatastoreStatus:
137
- """Probe ClickHouse and read total on-disk size of active parts."""
140
+ """Probe ClickHouse for the app-database and whole-server disk size.
141
+
142
+ ``size`` is the active-part footprint of the connected (Imbi)
143
+ database; ``total`` spans every database, including ``system.*``.
144
+ """
138
145
  start = time.perf_counter()
139
146
  try:
140
147
  rows = await asyncio.wait_for(
141
148
  clickhouse.query(
142
- 'SELECT sum(bytes_on_disk) AS size FROM system.parts'
143
- " WHERE active AND database != 'system'"
149
+ 'SELECT'
150
+ ' sumIf(bytes_on_disk, database = currentDatabase()) AS size,'
151
+ ' sum(bytes_on_disk) AS total'
152
+ ' FROM system.parts WHERE active'
144
153
  ),
145
154
  _CHECK_TIMEOUT,
146
155
  )
@@ -152,14 +161,16 @@ async def _check_clickhouse() -> DatastoreStatus:
152
161
  status='error',
153
162
  detail=str(err),
154
163
  )
155
- raw = rows[0].get('size') if rows else None
156
- size = int(raw) if raw is not None else None
164
+ row = rows[0] if rows else {}
165
+ size_raw = row.get('size')
166
+ total_raw = row.get('total')
157
167
  return DatastoreStatus(
158
168
  name='ClickHouse',
159
169
  role='Timeseries data',
160
170
  status='ok',
161
171
  latency_ms=_ms(start),
162
- size_bytes=size,
172
+ size_bytes=int(size_raw) if size_raw is not None else None,
173
+ total_bytes=int(total_raw) if total_raw is not None else None,
163
174
  )
164
175
 
165
176