imbi-api 2.9.0__tar.gz → 2.9.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 (295) hide show
  1. {imbi_api-2.9.0 → imbi_api-2.9.2}/.pre-commit-config.yaml +2 -0
  2. {imbi_api-2.9.0 → imbi_api-2.9.2}/PKG-INFO +3 -3
  3. {imbi_api-2.9.0 → imbi_api-2.9.2}/pyproject.toml +3 -3
  4. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/app.py +1 -0
  5. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/permissions.py +9 -2
  6. imbi_api-2.9.2/src/imbi_api/commit_sync/__init__.py +28 -0
  7. imbi_api-2.9.2/src/imbi_api/commit_sync/queue.py +269 -0
  8. imbi_api-2.9.2/src/imbi_api/commit_sync/service.py +311 -0
  9. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/models.py +4 -3
  10. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/events.py +7 -1
  11. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/organizations.py +5 -0
  12. imbi_api-2.9.2/src/imbi_api/endpoints/project_commit_sync.py +83 -0
  13. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_deployments.py +1 -1
  14. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_plugins.py +3 -3
  15. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_type_plugins.py +3 -3
  16. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/service_plugins.py +26 -17
  17. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/third_party_services.py +3 -3
  18. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/lifespans.py +34 -0
  19. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/assignment_writer.py +3 -3
  20. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/assignments.py +19 -11
  21. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/lifecycle_dispatch.py +1 -1
  22. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/resolution.py +22 -18
  23. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_permissions.py +52 -0
  24. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_events.py +12 -0
  25. imbi_api-2.9.2/tests/endpoints/test_project_commit_sync.py +131 -0
  26. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_plugins.py +8 -8
  27. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_type_plugins.py +7 -7
  28. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_plugins.py +25 -7
  29. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_assignment_writer.py +3 -3
  30. imbi_api-2.9.2/tests/test_commit_sync_queue.py +122 -0
  31. imbi_api-2.9.2/tests/test_commit_sync_service.py +168 -0
  32. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_plugins.py +13 -13
  33. {imbi_api-2.9.0 → imbi_api-2.9.2}/uv.lock +9 -10
  34. {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/deploy.yaml +0 -0
  35. {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/docs.yaml +0 -0
  36. {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/testing.yaml +0 -0
  37. {imbi_api-2.9.0 → imbi_api-2.9.2}/.gitignore +0 -0
  38. {imbi_api-2.9.0 → imbi_api-2.9.2}/.python-version +0 -0
  39. {imbi_api-2.9.0 → imbi_api-2.9.2}/CLAUDE.md +0 -0
  40. {imbi_api-2.9.0 → imbi_api-2.9.2}/CODE_REVIEW_PUNCHLIST.md +0 -0
  41. {imbi_api-2.9.0 → imbi_api-2.9.2}/LICENSE +0 -0
  42. {imbi_api-2.9.0 → imbi_api-2.9.2}/README.md +0 -0
  43. {imbi_api-2.9.0 → imbi_api-2.9.2}/coderabbit.yaml +0 -0
  44. {imbi_api-2.9.0 → imbi_api-2.9.2}/compose.yaml +0 -0
  45. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
  46. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
  47. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0003-email-sending-architecture.md +0 -0
  48. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
  49. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
  50. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
  51. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0007-relationship-blueprints.md +0 -0
  52. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
  53. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
  54. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
  55. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
  56. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
  57. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
  58. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
  59. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
  60. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr.md +0 -0
  61. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/configuration.md +0 -0
  62. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/index.md +0 -0
  63. {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/restore-backup.md +0 -0
  64. {imbi_api-2.9.0 → imbi_api-2.9.2}/justfile +0 -0
  65. {imbi_api-2.9.0 → imbi_api-2.9.2}/mkdocs.yml +0 -0
  66. {imbi_api-2.9.0 → imbi_api-2.9.2}/mypy.ini +0 -0
  67. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/__init__.py +0 -0
  68. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/__init__.py +0 -0
  69. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/authorization_codes.py +0 -0
  70. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/local_auth.py +0 -0
  71. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/login_providers.py +0 -0
  72. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/membership.py +0 -0
  73. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/models.py +0 -0
  74. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/oauth.py +0 -0
  75. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/oauth_clients.py +0 -0
  76. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/password.py +0 -0
  77. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/seed.py +0 -0
  78. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/sessions.py +0 -0
  79. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/tokens.py +0 -0
  80. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/totp.py +0 -0
  81. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/backfill_embeddings.py +0 -0
  82. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/blueprint_attributes.py +0 -0
  83. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/__init__.py +0 -0
  84. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/scoring.py +0 -0
  85. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/__init__.py +0 -0
  86. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/client.py +0 -0
  87. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/dependencies.py +0 -0
  88. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/models.py +0 -0
  89. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/base.html +0 -0
  90. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/base.txt +0 -0
  91. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/password_reset.html +0 -0
  92. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
  93. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/welcome.html +0 -0
  94. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/welcome.txt +0 -0
  95. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates.py +0 -0
  96. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/__init__.py +0 -0
  97. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_credentials.py +0 -0
  98. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_helpers.py +0 -0
  99. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_json_fields.py +0 -0
  100. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_pagination.py +0 -0
  101. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_request_urls.py +0 -0
  102. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/admin.py +0 -0
  103. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/admin_plugins.py +0 -0
  104. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/api_keys.py +0 -0
  105. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/auth.py +0 -0
  106. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/auth_providers.py +0 -0
  107. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/blueprints.py +0 -0
  108. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
  109. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/comments.py +0 -0
  110. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/dashboard.py +0 -0
  111. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/document_templates.py +0 -0
  112. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/documents.py +0 -0
  113. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/environments.py +0 -0
  114. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/graph_query.py +0 -0
  115. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/identity_plugins.py +0 -0
  116. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
  117. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/local_auth.py +0 -0
  118. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/mcp_servers.py +0 -0
  119. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/mfa.py +0 -0
  120. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
  121. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/operations_log.py +0 -0
  122. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
  123. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
  124. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugins.py +0 -0
  125. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_analysis.py +0 -0
  126. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_configuration.py +0 -0
  127. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_logs.py +0 -0
  128. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_types.py +0 -0
  129. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/projects.py +0 -0
  130. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/pull_requests.py +0 -0
  131. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/releases.py +0 -0
  132. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/roles.py +0 -0
  133. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
  134. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/scoring.py +0 -0
  135. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/scoring_policies.py +0 -0
  136. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/search.py +0 -0
  137. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
  138. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/status.py +0 -0
  139. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/tags.py +0 -0
  140. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/teams.py +0 -0
  141. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/uploads.py +0 -0
  142. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/user_activity.py +0 -0
  143. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/users.py +0 -0
  144. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/webhooks.py +0 -0
  145. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/entrypoint.py +0 -0
  146. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/graph_sql.py +0 -0
  147. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/__init__.py +0 -0
  148. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/endpoints.py +0 -0
  149. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/errors.py +0 -0
  150. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/flows.py +0 -0
  151. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/host_integration.py +0 -0
  152. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/models.py +0 -0
  153. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/repository.py +0 -0
  154. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/resolution.py +0 -0
  155. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/state.py +0 -0
  156. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/sweeper.py +0 -0
  157. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/llm/__init__.py +0 -0
  158. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/llm/dependencies.py +0 -0
  159. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/mcp_test.py +0 -0
  160. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/middleware/__init__.py +0 -0
  161. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/middleware/rate_limit.py +0 -0
  162. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/models.py +0 -0
  163. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/openapi.py +0 -0
  164. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/patch.py +0 -0
  165. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/__init__.py +0 -0
  166. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/credentials.py +0 -0
  167. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/installer.py +0 -0
  168. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/lifecycle.py +0 -0
  169. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/reload.py +0 -0
  170. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/schemas.py +0 -0
  171. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/prompts/__init__.py +0 -0
  172. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/prompts/release_notes_system.md +0 -0
  173. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/py.typed +0 -0
  174. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/relationships.py +0 -0
  175. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/sbom.py +0 -0
  176. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/scoring/__init__.py +0 -0
  177. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/scoring/queue.py +0 -0
  178. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/settings.py +0 -0
  179. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/__init__.py +0 -0
  180. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/client.py +0 -0
  181. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/dependencies.py +0 -0
  182. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/thumbnails.py +0 -0
  183. {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/validation.py +0 -0
  184. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/__init__.py +0 -0
  185. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/__init__.py +0 -0
  186. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_api_key_auth.py +0 -0
  187. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_authentication.py +0 -0
  188. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_authorization.py +0 -0
  189. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_encryption.py +0 -0
  190. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_login_providers.py +0 -0
  191. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_membership.py +0 -0
  192. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_oauth.py +0 -0
  193. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_oauth_server.py +0 -0
  194. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_seed.py +0 -0
  195. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_sessions.py +0 -0
  196. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_totp.py +0 -0
  197. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/__init__.py +0 -0
  198. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_client.py +0 -0
  199. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_init.py +0 -0
  200. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_privacy.py +0 -0
  201. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/__init__.py +0 -0
  202. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_client.py +0 -0
  203. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_init.py +0 -0
  204. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_integration.py +0 -0
  205. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_models.py +0 -0
  206. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_templates.py +0 -0
  207. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/__init__.py +0 -0
  208. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_admin.py +0 -0
  209. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_admin_plugins.py +0 -0
  210. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_api_keys.py +0 -0
  211. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_auth.py +0 -0
  212. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_auth_providers.py +0 -0
  213. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_blueprints.py +0 -0
  214. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_client_credentials.py +0 -0
  215. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_comments.py +0 -0
  216. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_credentials.py +0 -0
  217. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_dashboard.py +0 -0
  218. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_document_templates.py +0 -0
  219. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_documents.py +0 -0
  220. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_environments.py +0 -0
  221. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_graph_query.py +0 -0
  222. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_init.py +0 -0
  223. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_json_fields.py +0 -0
  224. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_link_definitions.py +0 -0
  225. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_local_auth.py +0 -0
  226. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_mcp_servers.py +0 -0
  227. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_mfa.py +0 -0
  228. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_operations_log.py +0 -0
  229. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_organizations.py +0 -0
  230. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_pagination.py +0 -0
  231. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_plugin_label_validation.py +0 -0
  232. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_plugins.py +0 -0
  233. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_analysis.py +0 -0
  234. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_configuration.py +0 -0
  235. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_deployments.py +0 -0
  236. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_logs.py +0 -0
  237. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_response_surfacing.py +0 -0
  238. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_types.py +0 -0
  239. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_projects.py +0 -0
  240. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_projects_helpers.py +0 -0
  241. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_pull_requests.py +0 -0
  242. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_releases.py +0 -0
  243. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_request_urls.py +0 -0
  244. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_roles.py +0 -0
  245. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_sa_api_keys.py +0 -0
  246. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_scoring.py +0 -0
  247. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_scoring_policies.py +0 -0
  248. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_search.py +0 -0
  249. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_accounts.py +0 -0
  250. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_writeback.py +0 -0
  251. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_status.py +0 -0
  252. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_tags.py +0 -0
  253. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_teams.py +0 -0
  254. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_third_party_services.py +0 -0
  255. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_uploads.py +0 -0
  256. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_user_activity.py +0 -0
  257. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_users.py +0 -0
  258. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_webhooks.py +0 -0
  259. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/npm-realistic.json +0 -0
  260. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  261. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/tiny.json +0 -0
  262. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/__init__.py +0 -0
  263. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_endpoints.py +0 -0
  264. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_errors.py +0 -0
  265. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_flows.py +0 -0
  266. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_host_integration.py +0 -0
  267. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_repository.py +0 -0
  268. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_resolution.py +0 -0
  269. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_state.py +0 -0
  270. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_sweeper.py +0 -0
  271. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/middleware/test_rate_limit.py +0 -0
  272. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/__init__.py +0 -0
  273. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_client.py +0 -0
  274. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_init.py +0 -0
  275. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_thumbnails.py +0 -0
  276. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_validation.py +0 -0
  277. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/support.py +0 -0
  278. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_app.py +0 -0
  279. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_backfill_embeddings.py +0 -0
  280. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_blueprint_attributes.py +0 -0
  281. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_blueprints.py +0 -0
  282. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_entrypoint.py +0 -0
  283. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_graph_sql.py +0 -0
  284. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_init.py +0 -0
  285. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_installer.py +0 -0
  286. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_lifecycle_dispatch.py +0 -0
  287. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_lifespans.py +0 -0
  288. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_models.py +0 -0
  289. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_openapi.py +0 -0
  290. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_patch.py +0 -0
  291. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_plugin_schemas.py +0 -0
  292. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_sbom.py +0 -0
  293. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_scoring_queue.py +0 -0
  294. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_scoring_triggers.py +0 -0
  295. {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_settings.py +0 -0
@@ -21,6 +21,8 @@ repos:
21
21
  rev: v0.7.25
22
22
  hooks:
23
23
  - id: tombi-format
24
+ # uv owns uv.lock formatting; avoid tombi reflow churn
25
+ exclude: ^uv\.lock$
24
26
 
25
27
  - repo: local
26
28
  hooks:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-api
3
- Version: 2.9.0
3
+ Version: 2.9.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
@@ -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.0
23
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.9.2
24
24
  Requires-Dist: jinja2
25
25
  Requires-Dist: jsonpatch>=1.33
26
26
  Requires-Dist: mcp>=1.26.0
@@ -31,7 +31,7 @@ Requires-Dist: pydantic[email]
31
31
  Requires-Dist: pyotp>=2.9.0
32
32
  Requires-Dist: python-multipart>=0.0.26
33
33
  Requires-Dist: qrcode>=8.0
34
- Requires-Dist: sentry-sdk>=2.48.0
34
+ Requires-Dist: sentry-sdk<3,>=2.48.0
35
35
  Requires-Dist: slowapi==0.1.9
36
36
  Requires-Dist: typer
37
37
  Requires-Dist: uvicorn
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-api"
3
- version = "2.9.0"
3
+ version = "2.9.2"
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.0",
31
+ "imbi-common[databases,llm,sentry,server]==2.9.2",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -38,7 +38,7 @@ dependencies = [
38
38
  "pyotp>=2.9.0",
39
39
  "python-multipart>=0.0.26",
40
40
  "qrcode>=8.0",
41
- "sentry-sdk>=2.48.0",
41
+ "sentry-sdk>=2.48.0,<3", # 3.0 line abandoned upstream; stay on 2.x
42
42
  "slowapi==0.1.9", # Pin: alpha quality, API may change
43
43
  "typer",
44
44
  "uvicorn",
@@ -25,6 +25,7 @@ def create_app() -> fastapi.FastAPI:
25
25
  lifespans.anthropic_hook,
26
26
  valkey.valkey_lifespan,
27
27
  lifespans.score_worker_hook,
28
+ lifespans.commit_sync_worker_hook,
28
29
  lifespans.identity_refresh_hook,
29
30
  ),
30
31
  version=version,
@@ -13,7 +13,7 @@ import fastapi
13
13
  import jwt
14
14
  import pydantic
15
15
  from fastapi import security
16
- from imbi_common import graph
16
+ from imbi_common import access_log, graph
17
17
  from imbi_common.auth import core
18
18
 
19
19
  from imbi_api import models, settings
@@ -658,7 +658,14 @@ async def _authenticate_token(
658
658
  treats the token as a JWT.
659
659
  """
660
660
  if token.startswith('ik_'):
661
- return await authenticate_api_key(db, token, auth_settings)
661
+ ctx = await authenticate_api_key(db, token, auth_settings)
662
+ # Cache the key's owner so imbi-common's access log renders the
663
+ # person (email / service-account slug) instead of the opaque
664
+ # ``ik_<id>`` it parses from the Authorization header.
665
+ access_log.remember_api_key_principal(
666
+ ctx.session_id or '', ctx.principal_name
667
+ )
668
+ return ctx
662
669
  return await authenticate_jwt(db, token, auth_settings)
663
670
 
664
671
 
@@ -0,0 +1,28 @@
1
+ """On-demand commit/tag history sync for imbi-api.
2
+
3
+ A project-scoped "Sync Commits & Tags" action (Project Doctor) enqueues a
4
+ Valkey-stream job; a background worker resolves the project's
5
+ ``github-commit-sync`` webhook plugin, runs a full default-branch +
6
+ all-tags backfill via the plugin's service credential, and records the
7
+ last-sync status on the ``Project`` node for the UI to poll.
8
+ """
9
+
10
+ from imbi_api.commit_sync.queue import (
11
+ consume_commit_sync,
12
+ enqueue_commit_sync,
13
+ )
14
+ from imbi_api.commit_sync.service import (
15
+ CommitSyncStatus,
16
+ CommitSyncUnavailable,
17
+ read_status,
18
+ run_sync,
19
+ )
20
+
21
+ __all__ = [
22
+ 'CommitSyncStatus',
23
+ 'CommitSyncUnavailable',
24
+ 'consume_commit_sync',
25
+ 'enqueue_commit_sync',
26
+ 'read_status',
27
+ 'run_sync',
28
+ ]
@@ -0,0 +1,269 @@
1
+ """Commit/tag-sync queue (Valkey Streams).
2
+
3
+ Mirrors :mod:`imbi_api.scoring.queue`: a single consumer group drains an
4
+ ``imbi:commit-sync`` stream, with a per-project debounce, stale-entry
5
+ reclaim, and a dead-letter queue after repeated failures. Each job runs
6
+ a full commit/tag backfill via :func:`commit_sync.service.run_sync` and
7
+ records the outcome on the ``Project`` node.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import logging
14
+ import os
15
+ import socket
16
+ import typing
17
+ from collections import abc
18
+
19
+ from imbi_common import graph
20
+ from valkey import asyncio as valkey
21
+
22
+ from imbi_api.commit_sync.service import (
23
+ CommitSyncUnavailable,
24
+ run_sync,
25
+ set_status,
26
+ )
27
+
28
+ STREAM = 'imbi:commit-sync'
29
+ GROUP = 'commit-sync-workers'
30
+ CONSUMER_PREFIX = 'worker'
31
+ DLQ = 'imbi:commit-sync:dlq'
32
+ DEBOUNCE_PREFIX = 'imbi:commit-sync:debounce'
33
+ DEBOUNCE_SECONDS = 10
34
+ MAX_DELIVERIES = 3
35
+ CLAIM_IDLE_MS = 120_000
36
+
37
+ LOGGER = logging.getLogger(__name__)
38
+
39
+
40
+ async def enqueue_commit_sync(
41
+ client: valkey.Valkey | None,
42
+ org_slug: str,
43
+ project_id: str,
44
+ requested_by: str | None = None,
45
+ ) -> bool:
46
+ """Debounce-then-XADD a commit-sync job. Returns True if enqueued.
47
+
48
+ Tolerates *client* being ``None`` (returns False) so the endpoint can
49
+ surface "queueing unavailable" rather than 500 when Valkey is down.
50
+ """
51
+ if client is None:
52
+ return False
53
+ key = f'{DEBOUNCE_PREFIX}:{project_id}'
54
+ try:
55
+ acquired = await client.set(key, b'1', ex=DEBOUNCE_SECONDS, nx=True)
56
+ if not acquired:
57
+ return False
58
+ await client.xadd(
59
+ STREAM,
60
+ {
61
+ 'org_slug': org_slug,
62
+ 'project_id': project_id,
63
+ 'requested_by': requested_by or 'system',
64
+ },
65
+ )
66
+ except Exception:
67
+ # Release the debounce key so a transient Valkey write failure
68
+ # doesn't block the user's immediate retry for DEBOUNCE_SECONDS.
69
+ try:
70
+ await client.delete(key)
71
+ except Exception:
72
+ LOGGER.exception('failed to clear debounce key for %s', project_id)
73
+ LOGGER.exception('enqueue_commit_sync failed for %s', project_id)
74
+ return False
75
+ return True
76
+
77
+
78
+ async def ensure_group(client: valkey.Valkey) -> None:
79
+ try:
80
+ await client.xgroup_create(STREAM, GROUP, id='$', mkstream=True)
81
+ except Exception as err:
82
+ if 'BUSYGROUP' not in str(err):
83
+ LOGGER.exception('xgroup_create failed')
84
+ raise
85
+
86
+
87
+ async def _process_message(db: graph.Graph, fields: dict[str, str]) -> None:
88
+ project_id = fields.get('project_id')
89
+ org_slug = fields.get('org_slug') or ''
90
+ requested_by = fields.get('requested_by') or 'system'
91
+ if not project_id:
92
+ return
93
+ await set_status(
94
+ db, project_id, status='running', requested_by=requested_by
95
+ )
96
+ try:
97
+ commits, tags = await run_sync(db, org_slug, project_id)
98
+ except CommitSyncUnavailable as exc:
99
+ # Misconfiguration, not transient: record and don't retry.
100
+ LOGGER.warning('commit-sync unavailable for %s: %s', project_id, exc)
101
+ await set_status(
102
+ db,
103
+ project_id,
104
+ status='failed',
105
+ requested_by=requested_by,
106
+ error=str(exc),
107
+ )
108
+ return
109
+ except Exception:
110
+ # Persist a generic, user-safe message; the polling endpoint
111
+ # exposes this status, so keep raw exception detail in logs only.
112
+ LOGGER.exception('commit-sync run failed for %s', project_id)
113
+ await set_status(
114
+ db,
115
+ project_id,
116
+ status='failed',
117
+ requested_by=requested_by,
118
+ error='Commit sync failed. See server logs for details.',
119
+ )
120
+ raise
121
+ LOGGER.info(
122
+ 'commit-sync for %s recorded %d commits, %d tags',
123
+ project_id,
124
+ commits,
125
+ tags,
126
+ )
127
+ await set_status(
128
+ db,
129
+ project_id,
130
+ status='success',
131
+ requested_by=requested_by,
132
+ commits=commits,
133
+ tags=tags,
134
+ )
135
+
136
+
137
+ def _decode_fields(
138
+ raw: abc.Mapping[bytes | str, bytes | str],
139
+ ) -> dict[str, str]:
140
+ return {
141
+ (k.decode() if isinstance(k, bytes) else k): (
142
+ v.decode() if isinstance(v, bytes) else v
143
+ )
144
+ for k, v in raw.items()
145
+ }
146
+
147
+
148
+ async def _claim_stale(
149
+ client: valkey.Valkey,
150
+ consumer: str,
151
+ ) -> list[tuple[bytes, abc.Mapping[bytes | str, bytes | str]]]:
152
+ try:
153
+ result = await client.xautoclaim(
154
+ STREAM,
155
+ GROUP,
156
+ consumer,
157
+ min_idle_time=CLAIM_IDLE_MS,
158
+ start_id='0-0',
159
+ count=16,
160
+ )
161
+ except Exception as err: # noqa: BLE001
162
+ LOGGER.debug('xautoclaim failed: %s', err)
163
+ return []
164
+ if isinstance(result, (list, tuple)) and len(result) >= 2: # type: ignore[arg-type]
165
+ msgs: object = result[1] # type: ignore[index]
166
+ if isinstance(msgs, list):
167
+ return msgs # type: ignore[return-value]
168
+ return []
169
+
170
+
171
+ async def _maybe_dead_letter(
172
+ client: valkey.Valkey,
173
+ msg_id: bytes,
174
+ fields: dict[str, str],
175
+ ) -> bool:
176
+ try:
177
+ info = await client.xpending_range(
178
+ STREAM, GROUP, min=msg_id, max=msg_id, count=1
179
+ )
180
+ except Exception: # noqa: BLE001
181
+ return False
182
+ if not info:
183
+ return False
184
+ entry: object = info[0] # type: ignore[index]
185
+ delivered: int | None = None
186
+ if isinstance(entry, dict):
187
+ raw_delivered = entry.get('times_delivered') # type: ignore[union-attr]
188
+ if raw_delivered is not None:
189
+ delivered = int(raw_delivered) # type: ignore[arg-type]
190
+ elif isinstance(entry, (list, tuple)) and len(entry) >= 4: # type: ignore[arg-type]
191
+ raw_delivered = entry[3] # type: ignore[index]
192
+ if raw_delivered is not None:
193
+ delivered = int(raw_delivered) # type: ignore[arg-type]
194
+ if delivered is not None and delivered >= MAX_DELIVERIES:
195
+ await client.xadd(DLQ, fields)
196
+ await client.xack(STREAM, GROUP, msg_id)
197
+ LOGGER.warning(
198
+ 'dead-lettered commit-sync msg %s after %s deliveries',
199
+ msg_id,
200
+ delivered,
201
+ )
202
+ return True
203
+ return False
204
+
205
+
206
+ async def _handle_entries(
207
+ client: valkey.Valkey,
208
+ entries: list[tuple[bytes, abc.Mapping[bytes | str, bytes | str]]],
209
+ db: graph.Graph,
210
+ check_dlq: bool = False,
211
+ ) -> None:
212
+ for msg_id, raw_fields in entries:
213
+ fields = _decode_fields(raw_fields)
214
+ if check_dlq and await _maybe_dead_letter(client, msg_id, fields):
215
+ continue
216
+ try:
217
+ await _process_message(db, fields)
218
+ except Exception:
219
+ LOGGER.exception('commit-sync failed for %s', fields)
220
+ continue
221
+ await client.xack(STREAM, GROUP, msg_id)
222
+
223
+
224
+ async def consume_commit_sync(
225
+ client: valkey.Valkey,
226
+ db: graph.Graph,
227
+ consumer: str | None = None,
228
+ stop: asyncio.Event | None = None,
229
+ ) -> None:
230
+ """Run the commit-sync consumer loop until *stop* is set."""
231
+ # Derive a per-process consumer name so concurrent workers don't share
232
+ # a Pending Entries List and stale-claim each other's in-flight jobs.
233
+ consumer = (
234
+ consumer or f'{CONSUMER_PREFIX}-{socket.gethostname()}-{os.getpid()}'
235
+ )
236
+ await ensure_group(client)
237
+ LOGGER.info('Commit-sync consumer loop running (consumer=%s)', consumer)
238
+ while stop is None or not stop.is_set():
239
+ stale = await _claim_stale(client, consumer)
240
+ if stale:
241
+ try:
242
+ await _handle_entries(client, stale, db, check_dlq=True)
243
+ except Exception:
244
+ LOGGER.exception('commit-sync stale-entry handling failed')
245
+ await asyncio.sleep(1)
246
+ continue
247
+ try:
248
+ response = await client.xreadgroup(
249
+ GROUP,
250
+ consumer,
251
+ {STREAM: '>'},
252
+ count=16,
253
+ block=2000,
254
+ )
255
+ except Exception:
256
+ LOGGER.exception('xreadgroup failed')
257
+ await asyncio.sleep(1)
258
+ continue
259
+ if not response:
260
+ continue
261
+ for _stream, entries in typing.cast(
262
+ 'list[tuple[object, list[typing.Any]]]', response
263
+ ):
264
+ try:
265
+ await _handle_entries(client, entries, db)
266
+ except Exception:
267
+ LOGGER.exception('commit-sync entry handling failed')
268
+ await asyncio.sleep(1)
269
+ break