imbi-api 2.1.0__tar.gz → 2.5.2__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 (244) hide show
  1. {imbi_api-2.1.0 → imbi_api-2.5.2}/PKG-INFO +3 -2
  2. imbi_api-2.5.2/docs/restore-backup.md +117 -0
  3. {imbi_api-2.1.0 → imbi_api-2.5.2}/mkdocs.yml +1 -0
  4. {imbi_api-2.1.0 → imbi_api-2.5.2}/pyproject.toml +41 -36
  5. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/app.py +2 -1
  6. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/permissions.py +89 -10
  7. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/seed.py +10 -0
  8. imbi_api-2.5.2/src/imbi_api/backfill_embeddings.py +78 -0
  9. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/models.py +17 -2
  10. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/scoring.py +29 -4
  11. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/__init__.py +1 -1
  12. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/admin_plugins.py +9 -0
  13. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/environments.py +8 -0
  14. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/operations_log.py +83 -1
  15. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/organizations.py +14 -0
  16. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_deployments.py +788 -223
  17. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_plugins.py +4 -0
  18. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_type_plugins.py +4 -0
  19. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/projects.py +325 -20
  20. imbi_api-2.5.2/src/imbi_api/endpoints/pull_requests.py +236 -0
  21. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/releases.py +333 -119
  22. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/scoring_policies.py +138 -92
  23. imbi_api-2.5.2/src/imbi_api/endpoints/search.py +171 -0
  24. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/service_plugins.py +2 -2
  25. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/third_party_services.py +177 -1
  26. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/user_activity.py +8 -4
  27. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/repository.py +31 -4
  28. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/lifespans.py +15 -9
  29. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/assignments.py +27 -3
  30. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/resolution.py +49 -0
  31. imbi_api-2.5.2/src/imbi_api/prompts/release_notes_system.md +73 -0
  32. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/scoring/__init__.py +1 -1
  33. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/scoring/queue.py +98 -6
  34. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_environments.py +51 -0
  35. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_operations_log.py +43 -0
  36. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_deployments.py +496 -58
  37. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_projects.py +88 -0
  38. imbi_api-2.5.2/tests/endpoints/test_projects_helpers.py +305 -0
  39. imbi_api-2.5.2/tests/endpoints/test_pull_requests.py +181 -0
  40. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_releases.py +360 -59
  41. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_scoring_policies.py +6 -8
  42. imbi_api-2.5.2/tests/endpoints/test_search.py +410 -0
  43. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_third_party_services.py +141 -0
  44. imbi_api-2.5.2/tests/storage/__init__.py +0 -0
  45. imbi_api-2.5.2/tests/test_backfill_embeddings.py +81 -0
  46. imbi_api-2.5.2/tests/test_graph_sql.py +46 -0
  47. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_scoring_queue.py +134 -1
  48. {imbi_api-2.1.0 → imbi_api-2.5.2}/uv.lock +80 -54
  49. {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/deploy.yaml +0 -0
  50. {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/docs.yaml +0 -0
  51. {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/testing.yaml +0 -0
  52. {imbi_api-2.1.0 → imbi_api-2.5.2}/.gitignore +0 -0
  53. {imbi_api-2.1.0 → imbi_api-2.5.2}/.pre-commit-config.yaml +0 -0
  54. {imbi_api-2.1.0 → imbi_api-2.5.2}/.python-version +0 -0
  55. {imbi_api-2.1.0 → imbi_api-2.5.2}/CLAUDE.md +0 -0
  56. {imbi_api-2.1.0 → imbi_api-2.5.2}/LICENSE +0 -0
  57. {imbi_api-2.1.0 → imbi_api-2.5.2}/README.md +0 -0
  58. {imbi_api-2.1.0 → imbi_api-2.5.2}/coderabbit.yaml +0 -0
  59. {imbi_api-2.1.0 → imbi_api-2.5.2}/compose.yaml +0 -0
  60. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
  61. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
  62. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0003-email-sending-architecture.md +0 -0
  63. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
  64. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
  65. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
  66. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0007-relationship-blueprints.md +0 -0
  67. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
  68. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
  69. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
  70. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
  71. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
  72. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
  73. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
  74. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr.md +0 -0
  75. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/configuration.md +0 -0
  76. {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/index.md +0 -0
  77. {imbi_api-2.1.0 → imbi_api-2.5.2}/justfile +0 -0
  78. {imbi_api-2.1.0 → imbi_api-2.5.2}/mypy.ini +0 -0
  79. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/__init__.py +0 -0
  80. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/__init__.py +0 -0
  81. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/local_auth.py +0 -0
  82. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/login_providers.py +0 -0
  83. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/membership.py +0 -0
  84. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/models.py +0 -0
  85. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/oauth.py +0 -0
  86. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/password.py +0 -0
  87. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/sessions.py +0 -0
  88. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/tokens.py +0 -0
  89. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/__init__.py +0 -0
  90. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/__init__.py +0 -0
  91. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/client.py +0 -0
  92. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/dependencies.py +0 -0
  93. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/models.py +0 -0
  94. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/base.html +0 -0
  95. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/base.txt +0 -0
  96. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/password_reset.html +0 -0
  97. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
  98. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/welcome.html +0 -0
  99. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/welcome.txt +0 -0
  100. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates.py +0 -0
  101. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/_helpers.py +0 -0
  102. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/admin.py +0 -0
  103. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/api_keys.py +0 -0
  104. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/auth.py +0 -0
  105. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/auth_providers.py +0 -0
  106. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/blueprints.py +0 -0
  107. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
  108. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/document_templates.py +0 -0
  109. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/documents.py +0 -0
  110. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/events.py +0 -0
  111. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/identity_plugins.py +0 -0
  112. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
  113. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/local_auth.py +0 -0
  114. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/mfa.py +0 -0
  115. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
  116. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
  117. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_configuration.py +0 -0
  118. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_logs.py +0 -0
  119. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_types.py +0 -0
  120. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/roles.py +0 -0
  121. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
  122. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/scoring.py +0 -0
  123. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
  124. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/status.py +0 -0
  125. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/tags.py +0 -0
  126. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/teams.py +0 -0
  127. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/uploads.py +0 -0
  128. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/users.py +0 -0
  129. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/webhooks.py +0 -0
  130. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/entrypoint.py +0 -0
  131. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/graph_sql.py +0 -0
  132. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/__init__.py +0 -0
  133. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/endpoints.py +0 -0
  134. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/errors.py +0 -0
  135. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/flows.py +0 -0
  136. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/host_integration.py +0 -0
  137. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/models.py +0 -0
  138. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/resolution.py +0 -0
  139. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/state.py +0 -0
  140. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/sweeper.py +0 -0
  141. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/llm/__init__.py +0 -0
  142. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/llm/dependencies.py +0 -0
  143. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/middleware/__init__.py +0 -0
  144. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/middleware/rate_limit.py +0 -0
  145. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/models.py +0 -0
  146. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/openapi.py +0 -0
  147. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/patch.py +0 -0
  148. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/__init__.py +0 -0
  149. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/credentials.py +0 -0
  150. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/installer.py +0 -0
  151. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/lifecycle.py +0 -0
  152. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/lifecycle_dispatch.py +0 -0
  153. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/reload.py +0 -0
  154. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/schemas.py +0 -0
  155. {imbi_api-2.1.0/tests → imbi_api-2.5.2/src/imbi_api/prompts}/__init__.py +0 -0
  156. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/py.typed +0 -0
  157. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/relationships.py +0 -0
  158. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/settings.py +0 -0
  159. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/__init__.py +0 -0
  160. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/client.py +0 -0
  161. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/dependencies.py +0 -0
  162. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/thumbnails.py +0 -0
  163. {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/validation.py +0 -0
  164. {imbi_api-2.1.0/tests/endpoints → imbi_api-2.5.2/tests}/__init__.py +0 -0
  165. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/__init__.py +0 -0
  166. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_api_key_auth.py +0 -0
  167. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_authentication.py +0 -0
  168. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_authorization.py +0 -0
  169. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_encryption.py +0 -0
  170. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_login_providers.py +0 -0
  171. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_membership.py +0 -0
  172. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_oauth.py +0 -0
  173. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_permissions.py +0 -0
  174. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_seed.py +0 -0
  175. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_sessions.py +0 -0
  176. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/__init__.py +0 -0
  177. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_client.py +0 -0
  178. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_init.py +0 -0
  179. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_privacy.py +0 -0
  180. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/__init__.py +0 -0
  181. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_client.py +0 -0
  182. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_init.py +0 -0
  183. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_integration.py +0 -0
  184. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_models.py +0 -0
  185. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_templates.py +0 -0
  186. {imbi_api-2.1.0/tests/storage → imbi_api-2.5.2/tests/endpoints}/__init__.py +0 -0
  187. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_admin.py +0 -0
  188. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_admin_plugins.py +0 -0
  189. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_api_keys.py +0 -0
  190. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_auth.py +0 -0
  191. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_auth_providers.py +0 -0
  192. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_blueprints.py +0 -0
  193. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_client_credentials.py +0 -0
  194. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_document_templates.py +0 -0
  195. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_documents.py +0 -0
  196. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_events.py +0 -0
  197. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_init.py +0 -0
  198. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_link_definitions.py +0 -0
  199. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_local_auth.py +0 -0
  200. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_mfa.py +0 -0
  201. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_organizations.py +0 -0
  202. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_configuration.py +0 -0
  203. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_logs.py +0 -0
  204. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_plugins.py +0 -0
  205. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_type_plugins.py +0 -0
  206. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_types.py +0 -0
  207. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_roles.py +0 -0
  208. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_sa_api_keys.py +0 -0
  209. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_scoring.py +0 -0
  210. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_service_accounts.py +0 -0
  211. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_service_plugins.py +0 -0
  212. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_status.py +0 -0
  213. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_tags.py +0 -0
  214. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_teams.py +0 -0
  215. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_uploads.py +0 -0
  216. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_user_activity.py +0 -0
  217. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_users.py +0 -0
  218. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_webhooks.py +0 -0
  219. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/__init__.py +0 -0
  220. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_endpoints.py +0 -0
  221. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_errors.py +0 -0
  222. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_flows.py +0 -0
  223. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_host_integration.py +0 -0
  224. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_repository.py +0 -0
  225. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_resolution.py +0 -0
  226. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_state.py +0 -0
  227. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_sweeper.py +0 -0
  228. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/middleware/test_rate_limit.py +0 -0
  229. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_client.py +0 -0
  230. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_init.py +0 -0
  231. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_thumbnails.py +0 -0
  232. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_validation.py +0 -0
  233. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_app.py +0 -0
  234. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_blueprints.py +0 -0
  235. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_entrypoint.py +0 -0
  236. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_init.py +0 -0
  237. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_lifecycle_dispatch.py +0 -0
  238. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_lifespans.py +0 -0
  239. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_models.py +0 -0
  240. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_openapi.py +0 -0
  241. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_patch.py +0 -0
  242. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_plugins.py +0 -0
  243. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_scoring_triggers.py +0 -0
  244. {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-api
3
- Version: 2.1.0
3
+ Version: 2.5.2
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
@@ -19,7 +19,7 @@ Requires-Dist: authlib>=1.6.11
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,server]>=0.11.0
22
+ Requires-Dist: imbi-common[databases,llm,sentry,server]>=2.5.5
23
23
  Requires-Dist: jinja2
24
24
  Requires-Dist: jsonpatch>=1.33
25
25
  Requires-Dist: nanoid>=2.0.0
@@ -29,6 +29,7 @@ Requires-Dist: pydantic[email]
29
29
  Requires-Dist: pyotp>=2.9.0
30
30
  Requires-Dist: python-multipart>=0.0.26
31
31
  Requires-Dist: qrcode>=8.0
32
+ Requires-Dist: sentry-sdk>=2.48.0
32
33
  Requires-Dist: slowapi==0.1.9
33
34
  Requires-Dist: typer
34
35
  Requires-Dist: uvicorn
@@ -0,0 +1,117 @@
1
+ # How to Restore an Imbi Database Backup
2
+
3
+ Restore a `pg_dump` backup of the Imbi database, including the Apache AGE graph.
4
+
5
+ **Prerequisites**: AGE extension installed on the target PostgreSQL instance, target DB created and empty.
6
+
7
+ ---
8
+
9
+ ## 1. Prepare the Target Database
10
+
11
+ AGE must be installed before restore. Connect to the target DB and run:
12
+
13
+ ```sql
14
+ CREATE EXTENSION age;
15
+ LOAD 'age';
16
+ SET search_path = ag_catalog, "$user", public;
17
+ ```
18
+
19
+ ## 2. Restore the Dump
20
+
21
+ ```bash
22
+ pg_restore -d imbi -Fc --no-owner --role=imbi imbi.dump
23
+ ```
24
+
25
+ Or for plain SQL:
26
+
27
+ ```bash
28
+ psql -d imbi -f imbi.sql
29
+ ```
30
+
31
+ ## 3. Verify Graph Catalog Integrity
32
+
33
+ After restore, confirm all graph labels are registered:
34
+
35
+ ```sql
36
+ -- Get the graph OID
37
+ SELECT graphid FROM ag_catalog.ag_graph WHERE name = 'imbi';
38
+
39
+ -- List registered labels (note the graphid from above)
40
+ SELECT name, kind FROM ag_catalog.ag_label WHERE graph = <graphid> ORDER BY name;
41
+ ```
42
+
43
+ ## 4. Find Unregistered Labels
44
+
45
+ Physical label tables may exist without `ag_catalog.ag_label` entries. This causes
46
+ `DuplicateTable` errors when the application first accesses those labels.
47
+
48
+ ```sql
49
+ SELECT tablename
50
+ FROM pg_tables
51
+ WHERE schemaname = 'imbi'
52
+ AND tablename NOT IN (
53
+ SELECT name FROM ag_catalog.ag_label WHERE graph = <graphid>
54
+ )
55
+ ORDER BY tablename;
56
+ ```
57
+
58
+ If this query returns rows, proceed to step 5. If empty, the restore is complete.
59
+
60
+ ## 5. Register Missing Labels
61
+
62
+ For each missing label, determine its kind: vertex (`v`) or edge (`e`). In Imbi,
63
+ edge labels are conventionally `ALL_CAPS` and vertex labels are `PascalCase`.
64
+
65
+ Run this once, substituting the correct list and your `graphid`:
66
+
67
+ ```sql
68
+ INSERT INTO ag_catalog.ag_label (name, graph, id, kind, relation, seq_name)
69
+ SELECT
70
+ name,
71
+ <graphid>,
72
+ (SELECT max(id) FROM ag_catalog.ag_label WHERE graph = <graphid>)
73
+ + row_number() OVER (ORDER BY name),
74
+ CASE WHEN name ~ '^[A-Z][a-z]' THEN 'v' ELSE 'e' END,
75
+ 'imbi."' || name || '"',
76
+ name || '_id_seq'
77
+ FROM unnest(ARRAY[
78
+ -- paste the tablename list from step 4 here
79
+ ]) AS name;
80
+ ```
81
+
82
+ ## 6. Verify
83
+
84
+ ```sql
85
+ -- All physical tables should now have catalog entries
86
+ SELECT tablename
87
+ FROM pg_tables
88
+ WHERE schemaname = 'imbi'
89
+ AND tablename NOT IN (
90
+ SELECT name FROM ag_catalog.ag_label WHERE graph = <graphid>
91
+ );
92
+ -- Expected: 0 rows
93
+ ```
94
+
95
+ Restart the application and confirm no `DuplicateTable` errors occur.
96
+
97
+ ---
98
+
99
+ ## Troubleshooting
100
+
101
+ **`DuplicateTable: relation "X" already exists`**
102
+ The label table exists but is not in `ag_catalog.ag_label`. Run steps 4 and 5.
103
+
104
+ **`ag_catalog` functions not found**
105
+ The `age` extension is not loaded for the session. Run:
106
+
107
+ ```sql
108
+ LOAD 'age';
109
+ SET search_path = ag_catalog, "$user", public;
110
+ ```
111
+
112
+ **Wrong `kind` assigned to a label**
113
+ Update the catalog entry directly:
114
+
115
+ ```sql
116
+ UPDATE ag_catalog.ag_label SET kind = 'e' WHERE name = 'LABEL_NAME' AND graph = <graphid>;
117
+ ```
@@ -63,6 +63,7 @@ markdown_extensions:
63
63
  nav:
64
64
  - Home: index.md
65
65
  - Configuration: configuration.md
66
+ - Restore Backup: restore-backup.md
66
67
  - Architecture Decision Records:
67
68
  - Overview: adr.md
68
69
  - 0001 Record Architecture Decisions: adr/0001-record-architecture-decisions.md
@@ -1,10 +1,6 @@
1
- [build-system]
2
- requires = ["build", "hatchling", "wheel"]
3
- build-backend = "hatchling.build"
4
-
5
1
  [project]
6
2
  name = "imbi-api"
7
- version = "2.1.0"
3
+ version = "2.5.2"
8
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."
9
5
  readme = "README.md"
10
6
  requires-python = ">=3.14"
@@ -31,7 +27,7 @@ dependencies = [
31
27
  "fastapi",
32
28
  "filetype",
33
29
  "httpx>=0.28.1,<1",
34
- "imbi-common[databases,llm,server]>=0.11.0",
30
+ "imbi-common[databases,llm,sentry,server]>=2.5.5",
35
31
  "jinja2",
36
32
  "jsonpatch>=1.33",
37
33
  "nanoid>=2.0.0",
@@ -40,6 +36,7 @@ dependencies = [
40
36
  "pyotp>=2.9.0",
41
37
  "python-multipart>=0.0.26",
42
38
  "qrcode>=8.0",
39
+ "sentry-sdk>=2.48.0",
43
40
  "slowapi==0.1.9", # Pin: alpha quality, API may change
44
41
  "typer",
45
42
  "uvicorn",
@@ -47,6 +44,9 @@ dependencies = [
47
44
  ]
48
45
  license-file = "LICENSE"
49
46
 
47
+ [project.scripts]
48
+ imbi-api = "imbi_api.entrypoint:main"
49
+
50
50
  [project.optional-dependencies]
51
51
  otel = [
52
52
  "opentelemetry-distro",
@@ -57,8 +57,36 @@ otel = [
57
57
  ]
58
58
  sentry = ["sentry-sdk"]
59
59
 
60
- [project.scripts]
61
- imbi-api = "imbi_api.entrypoint:main"
60
+ [dependency-groups]
61
+ dev = [
62
+ "basedpyright",
63
+ "build",
64
+ "coverage[toml]",
65
+ "httpx",
66
+ "mypy",
67
+ "pre-commit",
68
+ "pytest",
69
+ "pytest-cov",
70
+ "ruff",
71
+ "watchfiles"
72
+ ]
73
+ docs = [
74
+ "griffe-pydantic",
75
+ "mkdocs>=1.5,<2",
76
+ "mkdocs-material>9.5,<10",
77
+ "mkdocstrings-python-xref>=1.6,<2",
78
+ "mkdocstrings[python]"
79
+ ]
80
+ plugins = [
81
+ "imbi-plugin-aws>=1.0.0",
82
+ "imbi-plugin-github>=1.0.0",
83
+ "imbi-plugin-logzio>=1.0.0",
84
+ "imbi-plugin-oidc>=1.0.0",
85
+ ]
86
+
87
+ [build-system]
88
+ requires = ["build", "hatchling", "wheel"]
89
+ build-backend = "hatchling.build"
62
90
 
63
91
  [tool.coverage.html]
64
92
  directory = "build/coverage"
@@ -126,6 +154,7 @@ reportAttributeAccessIssue = "none"
126
154
  reportCallIssue = "none"
127
155
  reportMissingParameterType = "none"
128
156
  reportMissingTypeArgument = "none"
157
+ reportMissingTypeStubs = "none"
129
158
  reportOptionalMemberAccess = "none"
130
159
  reportOptionalSubscript = "none"
131
160
  reportPrivateUsage = "none"
@@ -138,12 +167,12 @@ reportUnusedFunction = "none"
138
167
  extraPaths = ["src", "."]
139
168
 
140
169
  [tool.pytest.ini_options]
141
- testpaths = ["tests"]
142
170
  addopts = [
143
171
  "--cov=src/imbi_api",
144
172
  "--cov-report=xml:build/coverage.xml",
145
173
  "--cov-report=term",
146
174
  ]
175
+ testpaths = ["tests"]
147
176
 
148
177
  [tool.ruff]
149
178
  line-length = 79
@@ -191,33 +220,9 @@ max-complexity = 15
191
220
  [tool.ruff.lint.per-file-ignores]
192
221
  "tests/**/*.py" = ["S"]
193
222
 
194
- [dependency-groups]
195
- dev = [
196
- "basedpyright",
197
- "build",
198
- "coverage[toml]",
199
- "httpx",
200
- "mypy",
201
- "pre-commit",
202
- "pytest",
203
- "pytest-cov",
204
- "ruff",
205
- "watchfiles"
206
- ]
207
- docs = [
208
- "griffe-pydantic",
209
- "mkdocs>=1.5,<2",
210
- "mkdocs-material>9.5,<10",
211
- "mkdocstrings-python-xref>=1.6,<2",
212
- "mkdocstrings[python]"
213
- ]
214
- plugins = [
215
- "imbi-plugin-aws>=1.0.0",
216
- "imbi-plugin-github>=1.0.0",
217
- "imbi-plugin-logzio>=1.0.0",
218
- "imbi-plugin-oidc>=1.0.0",
219
- ]
220
-
221
223
  [[tool.uv.index]]
222
224
  url = "https://pypi.org/simple"
223
225
  default = true
226
+
227
+ [tool.uv.sources]
228
+ imbi-plugin-github = { path = "../imbi-plugin-github", editable = true }
@@ -3,7 +3,7 @@ import logging
3
3
  import fastapi
4
4
  from fastapi import responses
5
5
  from fastapi.middleware import cors
6
- from imbi_common import access_log, graph, lifespan, valkey
6
+ from imbi_common import access_log, graph, lifespan, sentry, valkey
7
7
  from imbi_common.plugins.errors import PluginCredentialsMissing
8
8
  from uvicorn.middleware import proxy_headers
9
9
 
@@ -17,6 +17,7 @@ def create_app() -> fastapi.FastAPI:
17
17
  app = fastapi.FastAPI(
18
18
  title='Imbi',
19
19
  lifespan=lifespan.Lifespan(
20
+ sentry.sentry_lifespan,
20
21
  lifespans.clickhouse_hook,
21
22
  graph.graph_lifespan,
22
23
  lifespans.email_hook,
@@ -21,6 +21,13 @@ LOGGER = logging.getLogger(__name__)
21
21
  oauth2_scheme = security.HTTPBearer(auto_error=False)
22
22
 
23
23
 
24
+ class IdentityInfo(pydantic.BaseModel):
25
+ """A single third-party identity connection for an authenticated user."""
26
+
27
+ plugin_slug: str
28
+ subject: str
29
+
30
+
24
31
  class AuthContext(pydantic.BaseModel):
25
32
  """Authentication context for the current request."""
26
33
 
@@ -29,6 +36,14 @@ class AuthContext(pydantic.BaseModel):
29
36
  session_id: str | None = None
30
37
  auth_method: typing.Literal['jwt', 'api_key', 'client_credentials']
31
38
  permissions: set[str] = pydantic.Field(default_factory=set)
39
+ identities: list[IdentityInfo] = pydantic.Field(default_factory=list)
40
+
41
+ def identity_for(self, plugin_slug: str) -> str | None:
42
+ """Return the subject for the given plugin_slug, or None."""
43
+ for i in self.identities:
44
+ if i.plugin_slug == plugin_slug:
45
+ return i.subject
46
+ return None
32
47
 
33
48
  @property
34
49
  def principal_name(self) -> str:
@@ -134,6 +149,47 @@ async def load_principal_permissions(
134
149
  return set()
135
150
 
136
151
 
152
+ async def _load_user_identities(
153
+ db: graph.Graph,
154
+ user_id: str,
155
+ ) -> list[IdentityInfo]:
156
+ """Return active third-party identity connections for the user.
157
+
158
+ Identity enrichment is auxiliary — authentication has already
159
+ succeeded by the time this runs. A failure here (DB hiccup,
160
+ AGType parse error) must not turn a valid token into a 5xx, so
161
+ all exceptions are caught, logged, and treated as "no identities".
162
+ """
163
+ query: typing.LiteralString = """
164
+ MATCH (u:User {{id: {user_id}}})-[:HAS_IDENTITY]->(c:IdentityConnection)
165
+ WHERE c.status = 'active'
166
+ OPTIONAL MATCH (c)-[:USES_PLUGIN]->(p:Plugin)
167
+ RETURN p.plugin_slug AS plugin_slug, c.subject AS subject
168
+ """
169
+ try:
170
+ records = await db.execute(
171
+ query,
172
+ {'user_id': user_id},
173
+ ['plugin_slug', 'subject'],
174
+ )
175
+ result: list[IdentityInfo] = []
176
+ for row in records:
177
+ plugin_slug = graph.parse_agtype(row['plugin_slug'])
178
+ subject = graph.parse_agtype(row['subject'])
179
+ if plugin_slug and subject:
180
+ result.append(
181
+ IdentityInfo(plugin_slug=plugin_slug, subject=subject)
182
+ )
183
+ return result
184
+ except Exception: # noqa: BLE001
185
+ LOGGER.warning(
186
+ 'Failed to load identity connections for user_id=%s',
187
+ user_id,
188
+ exc_info=True,
189
+ )
190
+ return []
191
+
192
+
137
193
  async def authenticate_jwt(
138
194
  db: graph.Graph,
139
195
  token: str,
@@ -237,15 +293,44 @@ async def authenticate_jwt(
237
293
 
238
294
  # Load permissions
239
295
  perms = await load_principal_permissions(db, 'User', 'email', subject)
296
+ identities = await _load_user_identities(db, user.id)
240
297
 
241
298
  return AuthContext(
242
299
  user=user,
243
300
  session_id=jti,
244
301
  auth_method='jwt',
245
302
  permissions=perms,
303
+ identities=identities,
246
304
  )
247
305
 
248
306
 
307
+ async def _stamp_api_key_last_used(db: graph.Graph, key_id: str) -> None:
308
+ """Set last_used on the APIKey node identified by key_id.
309
+
310
+ AGE's cypher() wrapper requires a RETURN clause to properly
311
+ finalize a write operation — omitting it causes an internal
312
+ "Entity failed to be updated" error. Failures are logged and
313
+ swallowed: the stamp is best-effort and must not fail authentication.
314
+ """
315
+ now = datetime.datetime.now(datetime.UTC).isoformat()
316
+ query: typing.LiteralString = (
317
+ 'MATCH (k:APIKey {{key_id: {key_id}}})'
318
+ ' SET k.last_used = {now} RETURN k'
319
+ )
320
+ try:
321
+ await db.execute(
322
+ query,
323
+ {'key_id': key_id, 'now': now},
324
+ columns=['k'],
325
+ )
326
+ except Exception: # noqa: BLE001
327
+ LOGGER.warning(
328
+ 'Failed to stamp last_used for API key %s',
329
+ key_id,
330
+ exc_info=True,
331
+ )
332
+
333
+
249
334
  async def authenticate_api_key(
250
335
  db: graph.Graph,
251
336
  key: str,
@@ -349,11 +434,7 @@ async def authenticate_api_key(
349
434
  filtered = all_perms.intersection(set(scopes)) if scopes else all_perms
350
435
 
351
436
  # Update last_used only after all validation passes
352
- now = datetime.datetime.now(datetime.UTC).isoformat()
353
- update_query = (
354
- 'MATCH (k:APIKey {{key_id: {key_id}}}) SET k.last_used = {now}'
355
- )
356
- await db.execute(update_query, {'key_id': key_id, 'now': now})
437
+ await _stamp_api_key_last_used(db, key_id)
357
438
 
358
439
  return AuthContext(
359
440
  service_account=sa,
@@ -372,19 +453,17 @@ async def authenticate_api_key(
372
453
  db, 'User', 'email', user.email
373
454
  )
374
455
  filtered = all_perms.intersection(set(scopes)) if scopes else all_perms
456
+ identities = await _load_user_identities(db, user.id)
375
457
 
376
458
  # Update last_used only after all validation passes
377
- now = datetime.datetime.now(datetime.UTC).isoformat()
378
- update_query = (
379
- 'MATCH (k:APIKey {{key_id: {key_id}}}) SET k.last_used = {now}'
380
- )
381
- await db.execute(update_query, {'key_id': key_id, 'now': now})
459
+ await _stamp_api_key_last_used(db, key_id)
382
460
 
383
461
  return AuthContext(
384
462
  user=user,
385
463
  session_id=key_id,
386
464
  auth_method='api_key',
387
465
  permissions=filtered,
466
+ identities=identities,
388
467
  )
389
468
 
390
469
 
@@ -365,6 +365,13 @@ STANDARD_PERMISSIONS: list[tuple[str, str, str, str]] = [
365
365
  'deployment:write',
366
366
  'Trigger deployments and promotions via plugins',
367
367
  ),
368
+ # Vector search
369
+ (
370
+ 'search:read',
371
+ 'search',
372
+ 'read',
373
+ 'Search nodes by semantic similarity',
374
+ ),
368
375
  # Identity plugin connections
369
376
  (
370
377
  'me:identities:manage',
@@ -436,6 +443,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
436
443
  'document:delete',
437
444
  'document_template:read',
438
445
  'me:identities:manage',
446
+ 'search:read',
439
447
  ],
440
448
  False,
441
449
  ),
@@ -456,6 +464,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
456
464
  'project:read',
457
465
  'project_type:read',
458
466
  'role:read',
467
+ 'search:read',
459
468
  'tag:read',
460
469
  'team:read',
461
470
  'third_party_service:read',
@@ -478,6 +487,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
478
487
  'project:read',
479
488
  'project_type:read',
480
489
  'organization:read',
490
+ 'search:read',
481
491
  'team:read',
482
492
  'third_party_service:read',
483
493
  'user:read',
@@ -0,0 +1,78 @@
1
+ """One-shot backfill: generate embeddings for all existing nodes.
2
+
3
+ Run with:
4
+ uv run python -m imbi_api.backfill_embeddings
5
+
6
+ Iterates every embeddable node type and calls _auto_embed for each
7
+ instance that doesn't already have embeddings (or re-embeds if the
8
+ stored vectors are stale).
9
+ """
10
+
11
+ import asyncio
12
+ import logging
13
+
14
+ from imbi_common import graph, models
15
+ from imbi_common.graph.client import (
16
+ _embeddable_fields, # type: ignore[attr-defined]
17
+ )
18
+
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s %(levelname)-8s %(name)s: %(message)s',
22
+ )
23
+ LOGGER = logging.getLogger('backfill_embeddings')
24
+
25
+ # Node subclasses — _auto_embed is called normally.
26
+ _NODE_TYPES: list[type[models.Node]] = [
27
+ models.Organization,
28
+ models.Blueprint,
29
+ models.Team,
30
+ models.Environment,
31
+ models.ProjectType,
32
+ models.ThirdPartyService,
33
+ models.Tag,
34
+ models.LinkDefinition,
35
+ models.DocumentTemplate,
36
+ models.Project,
37
+ ]
38
+
39
+ # GraphModel types that have Embeddable fields but are not Node subclasses.
40
+ # match() still works via model_construct fallback; _auto_embed accepts any
41
+ # GraphModel even though its type annotation says Node.
42
+ _GRAPH_MODEL_TYPES: list[type[models.GraphModel]] = [
43
+ models.Document,
44
+ ]
45
+
46
+
47
+ async def _embed_type(
48
+ db: graph.Graph,
49
+ node_type: type[models.GraphModel],
50
+ ) -> int:
51
+ nodes = await db.match(node_type) # type: ignore[arg-type]
52
+ count = 0
53
+ for node in nodes:
54
+ if not _embeddable_fields(node): # type: ignore[arg-type]
55
+ continue
56
+ await db._auto_embed(node) # type: ignore[arg-type]
57
+ count += 1
58
+ return count
59
+
60
+
61
+ async def run() -> None:
62
+ db = graph.Graph()
63
+ await db.open()
64
+ try:
65
+ total = 0
66
+ for node_type in [*_NODE_TYPES, *_GRAPH_MODEL_TYPES]:
67
+ label = node_type.__name__
68
+ LOGGER.info('Embedding %s nodes...', label)
69
+ n = await _embed_type(db, node_type)
70
+ LOGGER.info(' %s: %d nodes embedded', label, n)
71
+ total += n
72
+ LOGGER.info('Done. Total nodes embedded: %d', total)
73
+ finally:
74
+ await db.close()
75
+
76
+
77
+ if __name__ == '__main__':
78
+ asyncio.run(run())
@@ -858,10 +858,17 @@ class PluginAssignmentCreate(pydantic.BaseModel):
858
858
  """Request model for assigning a plugin to a project-type or project."""
859
859
 
860
860
  plugin_id: str
861
- tab: typing.Literal['configuration', 'logs', 'deployment']
861
+ tab: typing.Literal['configuration', 'logs', 'deployment', 'lifecycle']
862
862
  default: bool = False
863
863
  options: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
864
864
  identity_plugin_id: str | None = None
865
+ # Per-env workflow-input overrides keyed by env slug. Merged into
866
+ # the deploy / promote dispatch payload at trigger time. Sibling
867
+ # to ``options`` (which is plugin-level) because the axis is the
868
+ # *environment* being deployed into, not the plugin's own config.
869
+ env_payloads: dict[str, dict[str, typing.Any]] = pydantic.Field(
870
+ default_factory=dict,
871
+ )
865
872
 
866
873
 
867
874
  class PluginAssignmentResponse(pydantic.BaseModel):
@@ -870,17 +877,25 @@ class PluginAssignmentResponse(pydantic.BaseModel):
870
877
  plugin_id: str
871
878
  plugin_slug: str
872
879
  label: str
873
- tab: typing.Literal['configuration', 'logs', 'deployment']
880
+ tab: typing.Literal['configuration', 'logs', 'deployment', 'lifecycle']
874
881
  default: bool
875
882
  options: dict[str, typing.Any] = {}
876
883
  source: typing.Literal['project', 'project_type', 'merged'] = (
877
884
  'project_type'
878
885
  )
879
886
  identity_plugin_id: str | None = None
887
+ env_payloads: dict[str, dict[str, typing.Any]] = pydantic.Field(
888
+ default_factory=dict,
889
+ )
880
890
  # Pulled from the plugin manifest so the UI can hide affordances
881
891
  # (e.g. the histogram strip on the Logs tab) for plugins whose
882
892
  # underlying API does not support them.
883
893
  supports_histogram: bool = False
894
+ # Set on deployment plugins that implement
895
+ # ``list_recent_deployments`` -- gates the "Resync deployments"
896
+ # card on the project settings page (and the admin TPS page) so
897
+ # the UI doesn't surface a button that the API will 400 on.
898
+ supports_deployment_sync: bool = False
884
899
 
885
900
 
886
901
  # -- Configuration / Logs response models ---------------------------------