imbi-api 2.35.0__tar.gz → 2.36.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. {imbi_api-2.35.0 → imbi_api-2.36.0}/.gitignore +8 -0
  2. {imbi_api-2.35.0 → imbi_api-2.36.0}/PKG-INFO +2 -2
  3. {imbi_api-2.35.0 → imbi_api-2.36.0}/pyproject.toml +2 -2
  4. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/client.py +7 -2
  5. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/dashboard.py +48 -0
  6. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/operations.py +3 -3
  7. imbi_api-2.36.0/tests/email/test_audit_clickhouse.py +101 -0
  8. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_client.py +31 -1
  9. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_dashboard.py +70 -1
  10. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_iggy_connectors.py +3 -1
  11. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_deployments.py +8 -5
  12. {imbi_api-2.35.0 → imbi_api-2.36.0}/README.md +0 -0
  13. {imbi_api-2.35.0 → imbi_api-2.36.0}/moon.yml +0 -0
  14. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/README.md +0 -0
  15. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/__init__.py +0 -0
  16. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/app.py +0 -0
  17. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/__init__.py +0 -0
  18. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/authorization_codes.py +0 -0
  19. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/autonomous.py +0 -0
  20. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/internal_services.py +0 -0
  21. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/local_auth.py +0 -0
  22. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/login_providers.py +0 -0
  23. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/membership.py +0 -0
  24. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/models.py +0 -0
  25. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/oauth.py +0 -0
  26. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/oauth_clients.py +0 -0
  27. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/password.py +0 -0
  28. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/permissions.py +0 -0
  29. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/principals.py +0 -0
  30. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/seed.py +0 -0
  31. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/sessions.py +0 -0
  32. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/tokens.py +0 -0
  33. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/totp.py +0 -0
  34. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/backfill_embeddings.py +0 -0
  35. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/blueprint_attributes.py +0 -0
  36. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/blueprint_compliance.py +0 -0
  37. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_pushed_at_repair.py +0 -0
  38. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/__init__.py +0 -0
  39. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/queue.py +0 -0
  40. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/service.py +0 -0
  41. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/component_facts.py +0 -0
  42. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_status_repair.py +0 -0
  43. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sweeper.py +0 -0
  44. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
  45. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/queue.py +0 -0
  46. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/service.py +0 -0
  47. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/documents/__init__.py +0 -0
  48. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/documents/read_sweeper.py +0 -0
  49. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/__init__.py +0 -0
  50. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/models.py +0 -0
  51. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/scoring.py +0 -0
  52. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/drift.py +0 -0
  53. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/__init__.py +0 -0
  54. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/dependencies.py +0 -0
  55. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/models.py +0 -0
  56. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/base.html +0 -0
  57. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/base.txt +0 -0
  58. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/password_reset.html +0 -0
  59. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
  60. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/welcome.html +0 -0
  61. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/welcome.txt +0 -0
  62. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates.py +0 -0
  63. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/__init__.py +0 -0
  64. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_credentials.py +0 -0
  65. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_events.py +0 -0
  66. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_history.py +0 -0
  67. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
  68. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
  69. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_helpers.py +0 -0
  70. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
  71. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_pagination.py +0 -0
  72. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
  73. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_search_index.py +0 -0
  74. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/admin.py +0 -0
  75. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
  76. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/ai_models.py +0 -0
  77. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
  78. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/api_keys.py +0 -0
  79. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/auth.py +0 -0
  80. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
  81. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/blueprints.py +0 -0
  82. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
  83. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/comments.py +0 -0
  84. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/components.py +0 -0
  85. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
  86. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_likes.py +0 -0
  87. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_presence.py +0 -0
  88. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_templates.py +0 -0
  89. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_versions.py +0 -0
  90. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/documents.py +0 -0
  91. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/environments.py +0 -0
  92. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/events.py +0 -0
  93. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/graph_query.py +0 -0
  94. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
  95. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/integrations.py +0 -0
  96. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
  97. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/local_auth.py +0 -0
  98. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/maintenance.py +0 -0
  99. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
  100. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/mfa.py +0 -0
  101. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
  102. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/operations_log.py +0 -0
  103. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/organizations.py +0 -0
  104. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
  105. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
  106. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugins.py +0 -0
  107. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_analysis.py +0 -0
  108. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
  109. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
  110. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_deployments.py +0 -0
  111. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
  112. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
  113. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
  114. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_logs.py +0 -0
  115. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
  116. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
  117. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_types.py +0 -0
  118. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/projects.py +0 -0
  119. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/pull_requests.py +0 -0
  120. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/releases.py +0 -0
  121. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/roles.py +0 -0
  122. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
  123. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/scoring.py +0 -0
  124. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
  125. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/search.py +0 -0
  126. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
  127. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/status.py +0 -0
  128. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/tags.py +0 -0
  129. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/teams.py +0 -0
  130. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/uploads.py +0 -0
  131. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/user_activity.py +0 -0
  132. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/users.py +0 -0
  133. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/webhooks.py +0 -0
  134. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/entrypoint.py +0 -0
  135. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/graph_sql.py +0 -0
  136. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/__init__.py +0 -0
  137. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/attribution.py +0 -0
  138. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/endpoints.py +0 -0
  139. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/errors.py +0 -0
  140. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/flows.py +0 -0
  141. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/host_integration.py +0 -0
  142. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/models.py +0 -0
  143. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/repository.py +0 -0
  144. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/resolution.py +0 -0
  145. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/state.py +0 -0
  146. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/sweeper.py +0 -0
  147. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/lifespans.py +0 -0
  148. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/llm/__init__.py +0 -0
  149. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/llm/dependencies.py +0 -0
  150. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/__init__.py +0 -0
  151. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/log.py +0 -0
  152. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/registry.py +0 -0
  153. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/state.py +0 -0
  154. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/worker.py +0 -0
  155. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/mcp_test.py +0 -0
  156. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/middleware/__init__.py +0 -0
  157. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/middleware/rate_limit.py +0 -0
  158. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/models.py +0 -0
  159. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/openapi.py +0 -0
  160. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/orphan_releases.py +0 -0
  161. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/__init__.py +0 -0
  162. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
  163. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/assignments.py +0 -0
  164. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/credentials.py +0 -0
  165. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/installer.py +0 -0
  166. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/lifecycle.py +0 -0
  167. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
  168. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/reload.py +0 -0
  169. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/resolution.py +0 -0
  170. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/schemas.py +0 -0
  171. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/__init__.py +0 -0
  172. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/queue.py +0 -0
  173. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/service.py +0 -0
  174. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/prompts/__init__.py +0 -0
  175. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
  176. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/py.typed +0 -0
  177. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/relationships.py +0 -0
  178. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/__init__.py +0 -0
  179. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/queue.py +0 -0
  180. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/service.py +0 -0
  181. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/sbom.py +0 -0
  182. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/scoring/__init__.py +0 -0
  183. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/scoring/queue.py +0 -0
  184. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/settings.py +0 -0
  185. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/__init__.py +0 -0
  186. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/client.py +0 -0
  187. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/dependencies.py +0 -0
  188. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/thumbnails.py +0 -0
  189. {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/validation.py +0 -0
  190. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/__init__.py +0 -0
  191. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/__init__.py +0 -0
  192. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_authentication.py +0 -0
  193. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_authorization.py +0 -0
  194. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_encryption.py +0 -0
  195. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_internal_services.py +0 -0
  196. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_login_providers.py +0 -0
  197. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_membership.py +0 -0
  198. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_oauth.py +0 -0
  199. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_oauth_server.py +0 -0
  200. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_permissions.py +0 -0
  201. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_seed.py +0 -0
  202. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_sessions.py +0 -0
  203. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_totp.py +0 -0
  204. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/__init__.py +0 -0
  205. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_client.py +0 -0
  206. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_init.py +0 -0
  207. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_privacy.py +0 -0
  208. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/__init__.py +0 -0
  209. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_init.py +0 -0
  210. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_integration.py +0 -0
  211. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_models.py +0 -0
  212. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_templates.py +0 -0
  213. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/__init__.py +0 -0
  214. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_admin.py +0 -0
  215. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_admin_plugins.py +0 -0
  216. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_ai_models.py +0 -0
  217. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_ai_providers.py +0 -0
  218. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_api_keys.py +0 -0
  219. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_auth.py +0 -0
  220. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_auth_providers.py +0 -0
  221. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_blueprints.py +0 -0
  222. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_client_credentials.py +0 -0
  223. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_comments.py +0 -0
  224. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_components.py +0 -0
  225. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_credentials.py +0 -0
  226. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_analytics.py +0 -0
  227. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_likes.py +0 -0
  228. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_presence.py +0 -0
  229. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_reads.py +0 -0
  230. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_reads_sql.py +0 -0
  231. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_templates.py +0 -0
  232. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_versions.py +0 -0
  233. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_documents.py +0 -0
  234. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_environments.py +0 -0
  235. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_events.py +0 -0
  236. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_graph_query.py +0 -0
  237. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_init.py +0 -0
  238. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_integrations.py +0 -0
  239. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_json_fields.py +0 -0
  240. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_link_definitions.py +0 -0
  241. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_local_auth.py +0 -0
  242. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_maintenance.py +0 -0
  243. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_mcp_servers.py +0 -0
  244. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_mfa.py +0 -0
  245. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_operations_log.py +0 -0
  246. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_organizations.py +0 -0
  247. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_pagination.py +0 -0
  248. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
  249. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_plugins.py +0 -0
  250. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_analysis.py +0 -0
  251. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_commit_sync.py +0 -0
  252. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_configuration.py +0 -0
  253. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
  254. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_incidents.py +0 -0
  255. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_integrations.py +0 -0
  256. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_lifecycle.py +0 -0
  257. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_logs.py +0 -0
  258. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_plugins.py +0 -0
  259. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
  260. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_types.py +0 -0
  261. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_projects.py +0 -0
  262. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_projects_helpers.py +0 -0
  263. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_pull_requests.py +0 -0
  264. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_releases.py +0 -0
  265. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_request_urls.py +0 -0
  266. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_roles.py +0 -0
  267. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_sa_api_keys.py +0 -0
  268. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_scoring.py +0 -0
  269. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_scoring_policies.py +0 -0
  270. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_search.py +0 -0
  271. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_search_index.py +0 -0
  272. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_service_accounts.py +0 -0
  273. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_service_writeback.py +0 -0
  274. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_status.py +0 -0
  275. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_tags.py +0 -0
  276. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_teams.py +0 -0
  277. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_uploads.py +0 -0
  278. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_user_activity.py +0 -0
  279. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_users.py +0 -0
  280. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_webhooks.py +0 -0
  281. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
  282. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  283. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/tiny.json +0 -0
  284. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/__init__.py +0 -0
  285. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_endpoints.py +0 -0
  286. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_errors.py +0 -0
  287. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_flows.py +0 -0
  288. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_host_integration.py +0 -0
  289. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_repository.py +0 -0
  290. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_resolution.py +0 -0
  291. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_state.py +0 -0
  292. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_sweeper.py +0 -0
  293. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/middleware/test_rate_limit.py +0 -0
  294. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/__init__.py +0 -0
  295. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_client.py +0 -0
  296. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_init.py +0 -0
  297. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_thumbnails.py +0 -0
  298. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_validation.py +0 -0
  299. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/support.py +0 -0
  300. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_app.py +0 -0
  301. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_assignment_writer.py +0 -0
  302. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_backfill_embeddings.py +0 -0
  303. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprint_attributes.py +0 -0
  304. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprint_compliance.py +0 -0
  305. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprints.py +0 -0
  306. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_pushed_at_repair.py +0 -0
  307. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_sync_queue.py +0 -0
  308. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_sync_service.py +0 -0
  309. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_component_facts.py +0 -0
  310. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_status_repair.py +0 -0
  311. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sweeper.py +0 -0
  312. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sync_queue.py +0 -0
  313. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sync_service.py +0 -0
  314. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_drift.py +0 -0
  315. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_entrypoint.py +0 -0
  316. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_graph_sql.py +0 -0
  317. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_identity_attribution.py +0 -0
  318. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_init.py +0 -0
  319. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_installer.py +0 -0
  320. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_lifecycle_dispatch.py +0 -0
  321. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_lifespans.py +0 -0
  322. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_log.py +0 -0
  323. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_operations.py +0 -0
  324. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_state.py +0 -0
  325. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_worker.py +0 -0
  326. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_models.py +0 -0
  327. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_openapi.py +0 -0
  328. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_orphan_releases.py +0 -0
  329. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugin_schemas.py +0 -0
  330. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins.py +0 -0
  331. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins_assignments.py +0 -0
  332. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins_credentials.py +0 -0
  333. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pr_sync_queue.py +0 -0
  334. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pr_sync_service.py +0 -0
  335. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_projects_drift_ranges.py +0 -0
  336. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pull_requests_pending_deploy.py +0 -0
  337. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_release_promote_service.py +0 -0
  338. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_sbom.py +0 -0
  339. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_scoring_queue.py +0 -0
  340. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_scoring_triggers.py +0 -0
  341. {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_settings.py +0 -0
@@ -20,3 +20,11 @@ site/
20
20
  # moon
21
21
  .moon/cache
22
22
  .moon/docker
23
+
24
+ # design-sync
25
+ .design-sync/sb-reference/
26
+ .design-sync/learnings/
27
+ .design-sync/.cache/
28
+ .design-sync/node_modules
29
+ .ds-sync/
30
+ ds-bundle/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imbi-api
3
- Version: 2.35.0
3
+ Version: 2.36.0
4
4
  Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
5
5
  Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
6
6
  License-Expression: BSD-3-Clause
@@ -19,7 +19,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
19
19
  Requires-Dist: fastapi
20
20
  Requires-Dist: filetype
21
21
  Requires-Dist: httpx<1,>=0.28.1
22
- Requires-Dist: imbi-common[databases,llm,sentry,server]==2.35.0
22
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.36.0
23
23
  Requires-Dist: jinja2
24
24
  Requires-Dist: jsonpatch>=1.33
25
25
  Requires-Dist: mcp>=1.26.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.35.0"
3
+ version = "2.36.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.35.0",
31
+ "imbi-common[databases,llm,sentry,server]==2.36.0",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -140,11 +140,16 @@ class EmailClient:
140
140
  await asyncio.sleep(delay)
141
141
  delay *= self._settings.retry_backoff_factor
142
142
 
143
- # All retries exhausted
143
+ # All retries exhausted. The audit keeps the error type and SMTP
144
+ # code only: exception text such as SMTPRecipientsRefused carries
145
+ # the recipient address, and the audit must not store it.
144
146
  error_msg = (
145
147
  f'SMTP error after {self._settings.max_retries + 1} attempts: '
146
- f'{last_error}'
148
+ f'{type(last_error).__name__}'
147
149
  )
150
+ smtp_code = getattr(last_error, 'smtp_code', None)
151
+ if smtp_code is not None:
152
+ error_msg += f' ({smtp_code})'
148
153
  return self._create_audit(message, 'failed', error_msg)
149
154
 
150
155
  async def _send_smtp(self, message: models.EmailMessage) -> None:
@@ -89,6 +89,24 @@ class DashboardMetrics(pydantic.BaseModel):
89
89
  releases_by_environment: list[EnvironmentCount]
90
90
 
91
91
 
92
+ class IggyTopic(iggy.TopicStatus):
93
+ """One Iggy topic and the sink group that consumes it."""
94
+
95
+ # @TODO: Fill `stored_offset` and `lag` when Iggy can read a group
96
+ # offset over HTTP (apache/iggy#4279) or the Python SDK exposes the
97
+ # offset read (apache/iggy#3997). `lag` is `current_offset` minus
98
+ # `stored_offset`. Until then they are always `None`.
99
+ stored_offset: int | None = None
100
+ lag: int | None = None
101
+
102
+
103
+ class IggyTopics(pydantic.BaseModel):
104
+ """Per-topic Iggy state for the admin dashboard."""
105
+
106
+ checked_at: datetime.datetime
107
+ topics: list[IggyTopic]
108
+
109
+
92
110
  def _ms(start: float) -> float:
93
111
  """Milliseconds elapsed since *start* (``time.perf_counter()``)."""
94
112
  return round((time.perf_counter() - start) * 1000, 1)
@@ -306,6 +324,36 @@ async def get_dashboard_status(
306
324
  )
307
325
 
308
326
 
327
+ # Reads one topic and one group per topic, all in flight together, so
328
+ # it gets more time than a single health check.
329
+ _IGGY_TOPICS_TIMEOUT = 5.0
330
+
331
+
332
+ @dashboard_router.get('/iggy', response_model=IggyTopics)
333
+ async def get_dashboard_iggy(
334
+ auth: typing.Annotated[
335
+ permissions.AuthContext,
336
+ fastapi.Depends(
337
+ permissions.require_permission('admin:dashboard:read')
338
+ ),
339
+ ],
340
+ ) -> IggyTopics:
341
+ """Return each Iggy topic with its sink group's members."""
342
+ try:
343
+ topics = await asyncio.wait_for(
344
+ iggy.topic_status(), _IGGY_TOPICS_TIMEOUT
345
+ )
346
+ except Exception as err:
347
+ LOGGER.warning('Iggy topic status failed: %s', err)
348
+ raise fastapi.HTTPException(
349
+ status_code=503, detail='Iggy topic status is unavailable'
350
+ ) from err
351
+ return IggyTopics(
352
+ checked_at=datetime.datetime.now(datetime.UTC),
353
+ topics=[IggyTopic(**topic.model_dump()) for topic in topics],
354
+ )
355
+
356
+
309
357
  _METRICS_WINDOW_DAYS = 7
310
358
 
311
359
 
@@ -830,12 +830,12 @@ async def execute_opslog_backfill(
830
830
  )
831
831
 
832
832
  if pending:
833
- rows: list[dict[str, typing.Any]] = []
833
+ entries: list[dict[str, typing.Any]] = []
834
834
  for entry in pending:
835
835
  dumped = entry.model_dump(by_alias=True, mode='python')
836
836
  dumped['is_deleted'] = 1 if entry.is_deleted else 0
837
- rows.append(dumped)
838
- await iggy.publish_rows('operations_log', 'maintenance', rows)
837
+ entries.append(dumped)
838
+ await iggy.publish_rows('operations_log', 'maintenance', entries)
839
839
  if repairs:
840
840
  # A separate publish: the repaired rows come off ``SELECT *``.
841
841
  LOGGER.debug(
@@ -0,0 +1,101 @@
1
+ """Email audit rows, written through the sink path to a live ClickHouse.
2
+
3
+ ``email_audit`` was in ``imbi.common.iggy.TOPICS`` with no table behind it,
4
+ so every audit was published and then could never be inserted. Before the
5
+ Iggy move the direct insert failed the same way and the error was logged
6
+ and swallowed. Every other email test mocks ClickHouse, which is how that
7
+ went unnoticed. These write a real audit the way the sink does and read it
8
+ back.
9
+ """
10
+
11
+ import datetime
12
+ import hashlib
13
+ import unittest
14
+ import uuid
15
+
16
+ from apps.api.tests import support
17
+ from imbi.api import email
18
+ from imbi.api.email import models
19
+ from imbi.common import clickhouse
20
+ from imbi.common.clickhouse import client
21
+
22
+
23
+ class EmailAuditClickhouseTestCase(unittest.IsolatedAsyncioTestCase):
24
+ async def asyncSetUp(self) -> None:
25
+ # ``Clickhouse`` is a process-wide singleton bound to the loop that
26
+ # opened it, and each test method gets its own loop, so take a
27
+ # private instance and hand the previous one back afterwards. See
28
+ # test_document_reads_sql for the failure this avoids.
29
+ self._previous = client.Clickhouse._instance
30
+ client.Clickhouse._instance = None
31
+ self.addAsyncCleanup(self._restore_singleton)
32
+ await clickhouse.initialize()
33
+ await clickhouse.setup_schema()
34
+
35
+ async def _restore_singleton(self) -> None:
36
+ await clickhouse.aclose()
37
+ client.Clickhouse._instance = self._previous
38
+
39
+ async def _read_back(self, entity_id: str) -> dict[str, object]:
40
+ rows = await clickhouse.query(
41
+ 'SELECT * FROM email_audit'
42
+ ' WHERE related_entity_id = {entity_id:String}',
43
+ {'entity_id': entity_id},
44
+ )
45
+ self.assertEqual(1, len(rows))
46
+ return rows[0]
47
+
48
+ async def test_a_sent_email_is_audited_without_its_address(self) -> None:
49
+ entity_id = str(uuid.uuid4())
50
+ audit = models.EmailAudit(
51
+ to_email='Someone@Example.com',
52
+ template_name='password_reset',
53
+ subject='Reset your password',
54
+ status='sent',
55
+ sent_at=datetime.datetime.now(datetime.UTC),
56
+ user_id='user-1',
57
+ related_entity_type='password_reset_token',
58
+ related_entity_id=entity_id,
59
+ )
60
+ with support.sink_to_clickhouse():
61
+ await email._save_audit(audit)
62
+
63
+ row = await self._read_back(entity_id)
64
+ self.assertEqual('password_reset', row['template_name'])
65
+ self.assertEqual('sent', row['status'])
66
+ self.assertEqual(
67
+ hashlib.sha256(b'someone@example.com').hexdigest(),
68
+ row['to_email_hash'],
69
+ )
70
+ # `EmailStr` lowercases the domain but keeps the local part's case.
71
+ self.assertEqual('example.com', row['to_email_domain'])
72
+ self.assertNotIn('to_email', row)
73
+ for address in (
74
+ 'Someone@Example.com',
75
+ 'Someone@example.com',
76
+ 'someone@example.com',
77
+ ):
78
+ self.assertNotIn(address, row.values())
79
+
80
+ async def test_unset_optional_fields_land_as_empty_strings(self) -> None:
81
+ # `EmailAudit` sends null for its optional fields and the columns are
82
+ # not Nullable, so this pins the server's null-as-default behaviour
83
+ # the table depends on.
84
+ entity_id = str(uuid.uuid4())
85
+ audit = models.EmailAudit(
86
+ to_email='someone@example.com',
87
+ template_name='password_reset',
88
+ subject='Reset your password',
89
+ status='failed',
90
+ error_message=None,
91
+ sent_at=datetime.datetime.now(datetime.UTC),
92
+ related_entity_id=entity_id,
93
+ )
94
+ with support.sink_to_clickhouse():
95
+ await email._save_audit(audit)
96
+
97
+ row = await self._read_back(entity_id)
98
+ self.assertEqual('failed', row['status'])
99
+ self.assertEqual('', row['error_message'])
100
+ self.assertEqual('', row['user_id'])
101
+ self.assertEqual('', row['related_entity_type'])
@@ -336,9 +336,39 @@ class EmailClientTestCase(unittest.IsolatedAsyncioTestCase):
336
336
  self.assertEqual(audit.status, 'failed')
337
337
  assert audit.error_message is not None
338
338
  self.assertIn('after 3 attempts', audit.error_message)
339
- self.assertIn('Persistent error', audit.error_message)
339
+ self.assertIn('SMTPException', audit.error_message)
340
+ self.assertNotIn('Persistent error', audit.error_message)
340
341
  self.assertEqual(self.mock_smtp.send_message.call_count, 3)
341
342
 
343
+ async def test_failed_audit_omits_refused_recipient(self) -> None:
344
+ """Test the failure message does not carry the recipient address."""
345
+ self.mock_settings.max_retries = 0
346
+
347
+ email_client = client.EmailClient()
348
+ await email_client.initialize()
349
+
350
+ message = models.EmailMessage(
351
+ to_email='user@example.com',
352
+ subject='Test',
353
+ html_body='<p>Test</p>',
354
+ text_body='Test',
355
+ template_name='test',
356
+ context={},
357
+ )
358
+
359
+ self.mock_smtp.send_message.side_effect = (
360
+ smtplib.SMTPRecipientsRefused(
361
+ {'user@example.com': (550, b'User unknown')}
362
+ )
363
+ )
364
+
365
+ audit = await email_client.send_email(message)
366
+
367
+ self.assertEqual(audit.status, 'failed')
368
+ assert audit.error_message is not None
369
+ self.assertIn('SMTPRecipientsRefused', audit.error_message)
370
+ self.assertNotIn('user@example.com', audit.error_message)
371
+
342
372
  async def test_retry_exponential_backoff(self) -> None:
343
373
  """Test exponential backoff delays."""
344
374
  self.mock_settings.max_retries = 3
@@ -10,7 +10,7 @@ from apps.api.tests import support
10
10
  from imbi.api import models, settings
11
11
  from imbi.api.auth import password, permissions
12
12
  from imbi.api.endpoints import dashboard
13
- from imbi.common import graph
13
+ from imbi.common import graph, iggy
14
14
 
15
15
 
16
16
  def _ok_status_handler(request: httpx.Request) -> httpx.Response:
@@ -239,6 +239,75 @@ class DashboardStatusEndpointTestCase(support.SharedAppTestCase):
239
239
  self.assertEqual(403, response.status_code)
240
240
 
241
241
 
242
+ class DashboardIggyEndpointTestCase(support.SharedAppTestCase):
243
+ """Test cases for GET /admin/dashboard/iggy."""
244
+
245
+ def setUp(self) -> None:
246
+ self.admin_context = permissions.AuthContext(
247
+ user=models.User(
248
+ email='admin@example.com',
249
+ display_name='Admin User',
250
+ is_active=True,
251
+ is_admin=True,
252
+ password_hash=password.hash_password('testpassword123'),
253
+ created_at=datetime.datetime.now(datetime.UTC),
254
+ ),
255
+ session_id='test-session',
256
+ auth_method='jwt',
257
+ permissions=set(),
258
+ )
259
+
260
+ async def mock_get_current_user():
261
+ return self.admin_context
262
+
263
+ self.test_app.dependency_overrides[permissions.get_current_user] = (
264
+ mock_get_current_user
265
+ )
266
+ self.client = testclient.TestClient(self.test_app)
267
+ self.topic_status = self.enterContext(
268
+ mock.patch.object(
269
+ dashboard.iggy,
270
+ 'topic_status',
271
+ new=mock.AsyncMock(
272
+ return_value=[
273
+ iggy.TopicStatus(
274
+ stream='operations_log',
275
+ topic='deployments',
276
+ messages=223,
277
+ current_offset=222,
278
+ size_bytes=4096,
279
+ consumer_group='operations_log',
280
+ members=2,
281
+ members_owning=1,
282
+ )
283
+ ]
284
+ ),
285
+ )
286
+ )
287
+
288
+ def test_returns_each_topic(self) -> None:
289
+ response = self.client.get('/admin/dashboard/iggy')
290
+ self.assertEqual(200, response.status_code)
291
+ [topic] = response.json()['topics']
292
+ self.assertEqual('operations_log', topic['stream'])
293
+ self.assertEqual(222, topic['current_offset'])
294
+ self.assertEqual(2, topic['members'])
295
+ self.assertEqual(1, topic['members_owning'])
296
+ # Iggy cannot return a group offset over HTTP yet.
297
+ self.assertIsNone(topic['stored_offset'])
298
+ self.assertIsNone(topic['lag'])
299
+
300
+ def test_iggy_failure_is_503(self) -> None:
301
+ self.topic_status.side_effect = RuntimeError('disconnected')
302
+ response = self.client.get('/admin/dashboard/iggy')
303
+ self.assertEqual(503, response.status_code)
304
+
305
+ def test_requires_permission(self) -> None:
306
+ self.admin_context.user.is_admin = False
307
+ response = self.client.get('/admin/dashboard/iggy')
308
+ self.assertEqual(403, response.status_code)
309
+
310
+
242
311
  class DashboardMetricsEndpointTestCase(support.SharedAppTestCase):
243
312
  """Test cases for GET /admin/dashboard/metrics."""
244
313
 
@@ -317,8 +317,10 @@ class WriteRoutesTestCase(_EndpointTestCase):
317
317
  class OpenAPITestCase(_EndpointTestCase):
318
318
  def test_routes_are_not_published(self) -> None:
319
319
  paths = self.client.get('/openapi.json').json()['paths']
320
+ # Only the connector configuration is private; other Iggy
321
+ # routes (e.g. /admin/dashboard/iggy) are public UI endpoints.
320
322
  self.assertFalse(
321
- [path for path in paths if 'iggy' in path],
323
+ [path for path in paths if '/iggy/connectors' in path],
322
324
  'the connector configuration must stay out of the schema',
323
325
  )
324
326
 
@@ -767,11 +767,14 @@ class ProjectDeploymentsTestCase(support.SharedAppTestCase):
767
767
  pattern=r'^\d{4}([.-])\d{1,2}\1\d{1,2}(?:[.-]\w+)?$',
768
768
  )
769
769
  ]
770
+ # Same-day rebuild: the calver suggestion depends on today's date,
771
+ # so derive the tags from it rather than hardcoding one.
772
+ stamp = datetime.datetime.now(datetime.UTC).date().strftime('%Y.%m.%d')
770
773
  self.mock_anthropic.complete_json = mock.AsyncMock(
771
774
  return_value=CompletionResult(
772
775
  data=DraftReleaseNotes(
773
776
  bump='patch',
774
- version='2026.09.17-1',
777
+ version=f'{stamp}-1',
775
778
  reasoning='same-day rebuild',
776
779
  notes_markdown='## Fixed',
777
780
  ),
@@ -783,17 +786,17 @@ class ProjectDeploymentsTestCase(support.SharedAppTestCase):
783
786
  '/organizations/myorg/projects/proj1/deployments/'
784
787
  'draft-release-notes',
785
788
  json={
786
- 'base_sha': '2026.09.17-0',
789
+ 'base_sha': f'{stamp}-0',
787
790
  'head_sha': 'bbb',
788
- 'last_tag': '2026.09.17-0',
791
+ 'last_tag': f'{stamp}-0',
789
792
  },
790
793
  )
791
794
  self.assertEqual(response.status_code, 200)
792
- self.assertEqual(response.json()['version'], '2026.09.17-1')
795
+ self.assertEqual(response.json()['version'], f'{stamp}-1')
793
796
  call = self.mock_anthropic.complete_json.call_args
794
797
  prompt = call.args[0] if call.args else call.kwargs.get('prompt', '')
795
798
  self.assertIn('Allowed tag formats: Calendar versioning', prompt)
796
- self.assertIn('Suggested next version: 2026.09.17-1', prompt)
799
+ self.assertIn(f'Suggested next version: {stamp}-1', prompt)
797
800
 
798
801
  def test_draft_release_notes_rebumps_to_the_policy(self) -> None:
799
802
  """A semver answer under a calver-only policy becomes a date."""
File without changes
File without changes
File without changes