imbi-api 2.26.0__tar.gz → 2.26.1__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 (328) hide show
  1. {imbi_api-2.26.0 → imbi_api-2.26.1}/PKG-INFO +2 -2
  2. {imbi_api-2.26.0 → imbi_api-2.26.1}/pyproject.toml +2 -2
  3. imbi_api-2.26.1/src/imbi/api/component_facts.py +348 -0
  4. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/components.py +162 -226
  5. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_deployments.py +1 -1
  6. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/projects.py +3 -1
  7. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/releases.py +1 -1
  8. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/search.py +14 -10
  9. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/sbom.py +46 -65
  10. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_components.py +380 -105
  11. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_releases.py +89 -33
  12. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_search.py +30 -6
  13. imbi_api-2.26.1/tests/test_component_facts.py +161 -0
  14. {imbi_api-2.26.0 → imbi_api-2.26.1}/.gitignore +0 -0
  15. {imbi_api-2.26.0 → imbi_api-2.26.1}/README.md +0 -0
  16. {imbi_api-2.26.0 → imbi_api-2.26.1}/moon.yml +0 -0
  17. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/README.md +0 -0
  18. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/__init__.py +0 -0
  19. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/app.py +0 -0
  20. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/__init__.py +0 -0
  21. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/authorization_codes.py +0 -0
  22. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/internal_services.py +0 -0
  23. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/local_auth.py +0 -0
  24. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/login_providers.py +0 -0
  25. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/membership.py +0 -0
  26. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/models.py +0 -0
  27. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/oauth.py +0 -0
  28. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/oauth_clients.py +0 -0
  29. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/password.py +0 -0
  30. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/permissions.py +0 -0
  31. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/principals.py +0 -0
  32. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/seed.py +0 -0
  33. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/sessions.py +0 -0
  34. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/tokens.py +0 -0
  35. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/auth/totp.py +0 -0
  36. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/backfill_embeddings.py +0 -0
  37. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/blueprint_attributes.py +0 -0
  38. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/blueprint_compliance.py +0 -0
  39. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/commit_sync/__init__.py +0 -0
  40. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/commit_sync/queue.py +0 -0
  41. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/commit_sync/service.py +0 -0
  42. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/deployment_migration.py +0 -0
  43. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/deployment_sweeper.py +0 -0
  44. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/deployment_sync/__init__.py +0 -0
  45. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/deployment_sync/queue.py +0 -0
  46. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/deployment_sync/service.py +0 -0
  47. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/documents/__init__.py +0 -0
  48. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/documents/read_sweeper.py +0 -0
  49. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/domain/__init__.py +0 -0
  50. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/domain/models.py +0 -0
  51. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/domain/scoring.py +0 -0
  52. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/drift.py +0 -0
  53. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/__init__.py +0 -0
  54. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/client.py +0 -0
  55. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/dependencies.py +0 -0
  56. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/models.py +0 -0
  57. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/base.html +0 -0
  58. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/base.txt +0 -0
  59. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/password_reset.html +0 -0
  60. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/password_reset.txt +0 -0
  61. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/welcome.html +0 -0
  62. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates/welcome.txt +0 -0
  63. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/email/templates.py +0 -0
  64. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/__init__.py +0 -0
  65. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_credentials.py +0 -0
  66. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_document_events.py +0 -0
  67. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_document_history.py +0 -0
  68. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_document_reads.py +0 -0
  69. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_document_scope.py +0 -0
  70. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_helpers.py +0 -0
  71. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_json_fields.py +0 -0
  72. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_pagination.py +0 -0
  73. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_request_urls.py +0 -0
  74. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/_search_index.py +0 -0
  75. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/admin.py +0 -0
  76. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/admin_plugins.py +0 -0
  77. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/api_keys.py +0 -0
  78. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/auth.py +0 -0
  79. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/auth_providers.py +0 -0
  80. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/blueprints.py +0 -0
  81. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/client_credentials.py +0 -0
  82. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/comments.py +0 -0
  83. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/dashboard.py +0 -0
  84. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/document_analytics.py +0 -0
  85. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/document_likes.py +0 -0
  86. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/document_presence.py +0 -0
  87. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/document_templates.py +0 -0
  88. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/document_versions.py +0 -0
  89. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/documents.py +0 -0
  90. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/environments.py +0 -0
  91. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/events.py +0 -0
  92. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/graph_query.py +0 -0
  93. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/integrations.py +0 -0
  94. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/link_definitions.py +0 -0
  95. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/local_auth.py +0 -0
  96. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/maintenance.py +0 -0
  97. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/mcp_servers.py +0 -0
  98. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/mfa.py +0 -0
  99. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
  100. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/operations_log.py +0 -0
  101. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/organizations.py +0 -0
  102. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/plugin_edges.py +0 -0
  103. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/plugin_entities.py +0 -0
  104. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/plugins.py +0 -0
  105. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_analysis.py +0 -0
  106. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
  107. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_configuration.py +0 -0
  108. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_incidents.py +0 -0
  109. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_integrations.py +0 -0
  110. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
  111. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_logs.py +0 -0
  112. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_plugins.py +0 -0
  113. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
  114. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/project_types.py +0 -0
  115. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/pull_requests.py +0 -0
  116. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/roles.py +0 -0
  117. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
  118. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/scoring.py +0 -0
  119. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/scoring_policies.py +0 -0
  120. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/service_accounts.py +0 -0
  121. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/status.py +0 -0
  122. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/tags.py +0 -0
  123. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/teams.py +0 -0
  124. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/uploads.py +0 -0
  125. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/user_activity.py +0 -0
  126. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/users.py +0 -0
  127. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/endpoints/webhooks.py +0 -0
  128. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/entrypoint.py +0 -0
  129. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/graph_sql.py +0 -0
  130. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/__init__.py +0 -0
  131. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/attribution.py +0 -0
  132. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/endpoints.py +0 -0
  133. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/errors.py +0 -0
  134. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/flows.py +0 -0
  135. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/host_integration.py +0 -0
  136. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/models.py +0 -0
  137. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/repository.py +0 -0
  138. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/resolution.py +0 -0
  139. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/state.py +0 -0
  140. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/identity/sweeper.py +0 -0
  141. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/lifespans.py +0 -0
  142. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/llm/__init__.py +0 -0
  143. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/llm/dependencies.py +0 -0
  144. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/__init__.py +0 -0
  145. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/log.py +0 -0
  146. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/operations.py +0 -0
  147. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/registry.py +0 -0
  148. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/state.py +0 -0
  149. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/maintenance/worker.py +0 -0
  150. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/mcp_test.py +0 -0
  151. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/middleware/__init__.py +0 -0
  152. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/middleware/rate_limit.py +0 -0
  153. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/models.py +0 -0
  154. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/openapi.py +0 -0
  155. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/__init__.py +0 -0
  156. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/assignment_writer.py +0 -0
  157. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/assignments.py +0 -0
  158. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/credentials.py +0 -0
  159. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/installer.py +0 -0
  160. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/lifecycle.py +0 -0
  161. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
  162. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/reload.py +0 -0
  163. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/resolution.py +0 -0
  164. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/plugins/schemas.py +0 -0
  165. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/pr_sync/__init__.py +0 -0
  166. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/pr_sync/queue.py +0 -0
  167. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/pr_sync/service.py +0 -0
  168. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/prompts/__init__.py +0 -0
  169. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/prompts/release_notes_system.md +0 -0
  170. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/py.typed +0 -0
  171. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/relationships.py +0 -0
  172. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/release_promote/__init__.py +0 -0
  173. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/release_promote/queue.py +0 -0
  174. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/release_promote/service.py +0 -0
  175. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/sbom_backfill.py +0 -0
  176. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/scoring/__init__.py +0 -0
  177. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/scoring/queue.py +0 -0
  178. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/settings.py +0 -0
  179. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/storage/__init__.py +0 -0
  180. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/storage/client.py +0 -0
  181. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/storage/dependencies.py +0 -0
  182. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/storage/thumbnails.py +0 -0
  183. {imbi_api-2.26.0 → imbi_api-2.26.1}/src/imbi/api/storage/validation.py +0 -0
  184. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/__init__.py +0 -0
  185. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/__init__.py +0 -0
  186. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_authentication.py +0 -0
  187. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_authorization.py +0 -0
  188. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_encryption.py +0 -0
  189. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_internal_services.py +0 -0
  190. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_login_providers.py +0 -0
  191. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_membership.py +0 -0
  192. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_oauth.py +0 -0
  193. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_oauth_server.py +0 -0
  194. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_permissions.py +0 -0
  195. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_seed.py +0 -0
  196. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_sessions.py +0 -0
  197. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/auth/test_totp.py +0 -0
  198. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/clickhouse/__init__.py +0 -0
  199. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/clickhouse/test_client.py +0 -0
  200. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/clickhouse/test_init.py +0 -0
  201. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/clickhouse/test_privacy.py +0 -0
  202. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/__init__.py +0 -0
  203. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/test_client.py +0 -0
  204. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/test_init.py +0 -0
  205. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/test_integration.py +0 -0
  206. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/test_models.py +0 -0
  207. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/email/test_templates.py +0 -0
  208. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/__init__.py +0 -0
  209. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_admin.py +0 -0
  210. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_admin_plugins.py +0 -0
  211. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_api_keys.py +0 -0
  212. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_auth.py +0 -0
  213. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_auth_providers.py +0 -0
  214. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_blueprints.py +0 -0
  215. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_client_credentials.py +0 -0
  216. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_comments.py +0 -0
  217. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_credentials.py +0 -0
  218. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_dashboard.py +0 -0
  219. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_analytics.py +0 -0
  220. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_likes.py +0 -0
  221. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_presence.py +0 -0
  222. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_reads.py +0 -0
  223. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_reads_sql.py +0 -0
  224. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_templates.py +0 -0
  225. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_document_versions.py +0 -0
  226. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_documents.py +0 -0
  227. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_environments.py +0 -0
  228. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_events.py +0 -0
  229. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_graph_query.py +0 -0
  230. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_init.py +0 -0
  231. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_integrations.py +0 -0
  232. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_json_fields.py +0 -0
  233. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_link_definitions.py +0 -0
  234. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_local_auth.py +0 -0
  235. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_maintenance.py +0 -0
  236. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_mcp_servers.py +0 -0
  237. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_mfa.py +0 -0
  238. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_operations_log.py +0 -0
  239. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_organizations.py +0 -0
  240. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_pagination.py +0 -0
  241. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_plugin_label_validation.py +0 -0
  242. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_plugins.py +0 -0
  243. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_analysis.py +0 -0
  244. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_commit_sync.py +0 -0
  245. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_configuration.py +0 -0
  246. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_deployments.py +0 -0
  247. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_incidents.py +0 -0
  248. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_integrations.py +0 -0
  249. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_lifecycle.py +0 -0
  250. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_logs.py +0 -0
  251. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_plugins.py +0 -0
  252. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_response_surfacing.py +0 -0
  253. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_project_types.py +0 -0
  254. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_projects.py +0 -0
  255. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_projects_helpers.py +0 -0
  256. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_pull_requests.py +0 -0
  257. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_request_urls.py +0 -0
  258. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_roles.py +0 -0
  259. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_sa_api_keys.py +0 -0
  260. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_scoring.py +0 -0
  261. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_scoring_policies.py +0 -0
  262. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_search_index.py +0 -0
  263. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_service_accounts.py +0 -0
  264. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_service_writeback.py +0 -0
  265. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_status.py +0 -0
  266. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_tags.py +0 -0
  267. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_teams.py +0 -0
  268. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_uploads.py +0 -0
  269. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_user_activity.py +0 -0
  270. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_users.py +0 -0
  271. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/endpoints/test_webhooks.py +0 -0
  272. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/fixtures/sbom/npm-realistic.json +0 -0
  273. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/fixtures/sbom/pypi-realistic.json +0 -0
  274. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/fixtures/sbom/tiny.json +0 -0
  275. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/__init__.py +0 -0
  276. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_endpoints.py +0 -0
  277. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_errors.py +0 -0
  278. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_flows.py +0 -0
  279. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_host_integration.py +0 -0
  280. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_repository.py +0 -0
  281. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_resolution.py +0 -0
  282. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_state.py +0 -0
  283. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/identity/test_sweeper.py +0 -0
  284. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/middleware/test_rate_limit.py +0 -0
  285. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/storage/__init__.py +0 -0
  286. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/storage/test_client.py +0 -0
  287. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/storage/test_init.py +0 -0
  288. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/storage/test_thumbnails.py +0 -0
  289. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/storage/test_validation.py +0 -0
  290. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/support.py +0 -0
  291. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_app.py +0 -0
  292. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_assignment_writer.py +0 -0
  293. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_backfill_embeddings.py +0 -0
  294. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_blueprint_attributes.py +0 -0
  295. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_blueprint_compliance.py +0 -0
  296. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_blueprints.py +0 -0
  297. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_commit_sync_queue.py +0 -0
  298. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_commit_sync_service.py +0 -0
  299. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_deployment_migration.py +0 -0
  300. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_deployment_sweeper.py +0 -0
  301. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_deployment_sync_queue.py +0 -0
  302. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_deployment_sync_service.py +0 -0
  303. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_drift.py +0 -0
  304. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_entrypoint.py +0 -0
  305. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_graph_sql.py +0 -0
  306. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_identity_attribution.py +0 -0
  307. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_init.py +0 -0
  308. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_installer.py +0 -0
  309. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_lifecycle_dispatch.py +0 -0
  310. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_lifespans.py +0 -0
  311. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_maintenance_log.py +0 -0
  312. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_maintenance_operations.py +0 -0
  313. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_maintenance_state.py +0 -0
  314. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_maintenance_worker.py +0 -0
  315. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_models.py +0 -0
  316. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_openapi.py +0 -0
  317. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_plugin_schemas.py +0 -0
  318. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_plugins.py +0 -0
  319. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_plugins_assignments.py +0 -0
  320. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_plugins_credentials.py +0 -0
  321. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_pr_sync_queue.py +0 -0
  322. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_pr_sync_service.py +0 -0
  323. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_release_promote_service.py +0 -0
  324. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_sbom.py +0 -0
  325. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_sbom_backfill.py +0 -0
  326. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_scoring_queue.py +0 -0
  327. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_scoring_triggers.py +0 -0
  328. {imbi_api-2.26.0 → imbi_api-2.26.1}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: imbi-api
3
- Version: 2.26.0
3
+ Version: 2.26.1
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.26.0
22
+ Requires-Dist: imbi-common[databases,llm,sentry,server]==2.26.1
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.26.0"
3
+ version = "2.26.1"
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.26.0",
31
+ "imbi-common[databases,llm,sentry,server]==2.26.1",
32
32
  "jinja2",
33
33
  "jsonpatch>=1.33",
34
34
  "mcp>=1.26.0",
@@ -0,0 +1,348 @@
1
+ """Read SBoM component usage facts from ClickHouse.
2
+
3
+ Phase three of the SBoM component migration. Every read that used to
4
+ answer "which components does this release use" by traversing
5
+ ``USES_COMPONENT_RELEASE`` asks this module instead. The edge's fan-in
6
+ is what made those reads unusable -- counting the label outright fails
7
+ with ``could not resize shared memory segment`` -- and no amount of
8
+ Cypher rewriting fixes an unbounded incoming traversal.
9
+
10
+ What stays in the graph is identity and governance: the ``Component``
11
+ and ``ComponentRelease`` nodes, their statuses, advisories, notes, and
12
+ the project/team/environment topology. What moves here is the usage
13
+ fact, which is a per-release immutable snapshot and was never a good
14
+ fit for a graph edge.
15
+
16
+ Two graph primitives feed these reads and they are not interchangeable:
17
+
18
+ - The **org project set** -- non-archived project ids of an
19
+ organization. Hundreds of ids, no fan-in. It scopes authorization,
20
+ search, and the catalog totals, and it deliberately spans every
21
+ release a project has ever had.
22
+ - The **deployment pointer set** -- the releases that
23
+ ``(:Project)-[d:DEPLOYED_IN]->(:Environment)`` currently points at
24
+ via ``d.current_release``. Around a thousand ids. It scopes the
25
+ reports, which describe what is running now.
26
+
27
+ Substituting one for the other silently changes what a screen means, so
28
+ every function here takes the ids it needs rather than deriving them.
29
+
30
+ Every read resolves each release to exactly one batch before reading
31
+ its rows, using ``argMax(batch_id, (source = 'ingest', recorded_at))``.
32
+ ``source`` leads the tuple deliberately: an ingest batch outranks a
33
+ backfill one whatever order the two land in, so a backfill running
34
+ beside live traffic can never displace a real SBoM. Reading
35
+ ``release_components`` without that join reads every snapshot a release
36
+ has ever had, superseded ones included.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import typing
42
+
43
+ from imbi.common import clickhouse, graph
44
+
45
+ #: Non-archived project ids of an organization -- the org project set.
46
+ #: Followed outward from the organization, so it is bounded by the
47
+ #: number of projects rather than by the component catalog.
48
+ ORG_PROJECT_IDS: typing.LiteralString = """
49
+ MATCH (:Organization {{slug: {org_slug}}})<-[:BELONGS_TO]-(:Team)
50
+ <-[:OWNED_BY]-(p:Project)
51
+ WHERE coalesce(p.archived, false) = false
52
+ RETURN p.id AS project_id
53
+ """
54
+
55
+ #: The deployment pointer set, with the topology each report row needs.
56
+ #: ``d.current_release`` is a property lookup, not an event replay, so
57
+ #: this stays cheap however long a project's deployment history is.
58
+ ORG_DEPLOYED_RELEASES: typing.LiteralString = """
59
+ MATCH (p:Project)-[d:DEPLOYED_IN]->(e:Environment)
60
+ MATCH (p)-[:OWNED_BY]->(t:Team)-[:BELONGS_TO]->
61
+ (:Organization {{slug: {org_slug}}})
62
+ WHERE coalesce(p.archived, false) = false
63
+ OPTIONAL MATCH (p)-[:TYPE]->(pt:ProjectType)
64
+ RETURN d.current_release AS release_id,
65
+ p.id AS project_id,
66
+ p.name AS project_name,
67
+ p.slug AS project_slug,
68
+ t.name AS team_name,
69
+ t.slug AS team_slug,
70
+ e.name AS environment_name,
71
+ e.slug AS environment_slug,
72
+ e.label_color AS environment_color,
73
+ collect(DISTINCT pt.name) AS project_types
74
+ """
75
+
76
+ #: Resolve each release of a set of projects to its winning batch.
77
+ _BATCHES_BY_PROJECT = (
78
+ 'SELECT release_id,'
79
+ " argMax(batch_id, (source = 'ingest', recorded_at)) AS batch_id"
80
+ ' FROM imbi.release_component_batches'
81
+ ' WHERE project_id IN {project_ids:Array(String)}'
82
+ ' GROUP BY release_id'
83
+ )
84
+
85
+ #: The same resolution anchored on releases. Used by the reports, whose
86
+ #: scope is the deployment pointer set rather than whole projects.
87
+ _BATCHES_BY_RELEASE = (
88
+ 'SELECT release_id,'
89
+ " argMax(batch_id, (source = 'ingest', recorded_at)) AS batch_id"
90
+ ' FROM imbi.release_component_batches'
91
+ ' WHERE release_id IN {release_ids:Array(String)}'
92
+ ' GROUP BY release_id'
93
+ )
94
+
95
+ _JOIN = (
96
+ ' INNER JOIN ({batches}) AS b'
97
+ ' ON c.release_id = b.release_id AND c.batch_id = b.batch_id'
98
+ )
99
+
100
+
101
+ def _by_project(select: str, where: str = '') -> str:
102
+ """Compose a project-scoped read against the winning batches."""
103
+ return (
104
+ f'{select} FROM imbi.release_components AS c'
105
+ + _JOIN.format(batches=_BATCHES_BY_PROJECT)
106
+ + ' WHERE c.project_id IN {project_ids:Array(String)}'
107
+ + where
108
+ )
109
+
110
+
111
+ def _by_release(select: str, where: str = '') -> str:
112
+ """Compose a release-scoped read against the winning batches."""
113
+ return (
114
+ f'{select} FROM imbi.release_components AS c'
115
+ + _JOIN.format(batches=_BATCHES_BY_RELEASE)
116
+ + ' WHERE c.release_id IN {release_ids:Array(String)}'
117
+ + where
118
+ )
119
+
120
+
121
+ async def org_project_ids(db: graph.Graph, org_slug: str) -> list[str]:
122
+ """Return the org project set, or an empty list for an unknown org."""
123
+ rows = await db.execute(
124
+ ORG_PROJECT_IDS, {'org_slug': org_slug}, ['project_id']
125
+ )
126
+ # AGE hands back JSON-encoded scalars; ``str()`` on one of those
127
+ # yields a quoted id that matches nothing in ClickHouse.
128
+ decoded = (graph.parse_agtype(row.get('project_id')) for row in rows)
129
+ return [str(value) for value in decoded if value]
130
+
131
+
132
+ async def component_ids_in_org(
133
+ project_ids: list[str],
134
+ component_ids: list[str] | None = None,
135
+ ) -> set[str]:
136
+ """Which component ids the org's projects currently depend on.
137
+
138
+ Feeds the global search org scope, which needs the whole set, and
139
+ package search, which needs only the subset its name match already
140
+ produced. Passing *component_ids* narrows the aggregate to those,
141
+ so a keystroke intersects against its own candidates rather than
142
+ materialising the org's entire catalog to discard most of it.
143
+
144
+ Unlike the traversal this replaces, the cost either way is one
145
+ aggregate over the org's own rows rather than a fan-in from every
146
+ ``ComponentRelease`` in the graph.
147
+ """
148
+ if not project_ids or component_ids == []:
149
+ return set()
150
+ where = ''
151
+ params: dict[str, typing.Any] = {'project_ids': project_ids}
152
+ if component_ids is not None:
153
+ where = ' AND c.component_id IN {component_ids:Array(String)}'
154
+ params['component_ids'] = component_ids
155
+ rows = await clickhouse.query(
156
+ _by_project('SELECT DISTINCT c.component_id AS component_id', where),
157
+ params,
158
+ )
159
+ return {str(row['component_id']) for row in rows}
160
+
161
+
162
+ async def component_in_org(project_ids: list[str], component_id: str) -> bool:
163
+ """Whether any of ``project_ids`` depends on ``component_id``.
164
+
165
+ ``component_id`` leads ``release_components``' sort key, so this is
166
+ a prefix seek rather than a scan however large the table gets.
167
+ """
168
+ if not project_ids:
169
+ return False
170
+ rows = await clickhouse.query(
171
+ _by_project(
172
+ 'SELECT 1 AS hit',
173
+ ' AND c.component_id = {component_id:String} LIMIT 1',
174
+ ),
175
+ {'project_ids': project_ids, 'component_id': component_id},
176
+ )
177
+ return bool(rows)
178
+
179
+
180
+ async def component_release_in_org(
181
+ project_ids: list[str],
182
+ component_id: str,
183
+ component_release_id: str,
184
+ ) -> bool:
185
+ """Whether any of ``project_ids`` depends on that specific version.
186
+
187
+ Takes the owning ``component_id`` as well as the version's own id
188
+ purely to keep the sort-key prefix seek: ``component_release_id``
189
+ is the fourth key column, so filtering on it alone scans. The
190
+ caller already has the parent from the graph, where one
191
+ ``HAS_RELEASE`` hop resolves it.
192
+ """
193
+ if not project_ids:
194
+ return False
195
+ rows = await clickhouse.query(
196
+ _by_project(
197
+ 'SELECT 1 AS hit',
198
+ ' AND c.component_id = {component_id:String}'
199
+ ' AND c.component_release_id = {component_release_id:String}'
200
+ ' LIMIT 1',
201
+ ),
202
+ {
203
+ 'project_ids': project_ids,
204
+ 'component_id': component_id,
205
+ 'component_release_id': component_release_id,
206
+ },
207
+ )
208
+ return bool(rows)
209
+
210
+
211
+ async def search_counts(
212
+ project_ids: list[str], component_ids: list[str]
213
+ ) -> dict[str, tuple[int, int]]:
214
+ """Return ``(version_count, project_count)`` per component id.
215
+
216
+ ``uniqExact`` rather than ``uniq``: these are small cardinalities
217
+ shown as literal numbers beside a package name, so an approximate
218
+ count would be visibly wrong for no useful saving.
219
+ """
220
+ if not project_ids or not component_ids:
221
+ return {}
222
+ rows = await clickhouse.query(
223
+ _by_project(
224
+ 'SELECT c.component_id AS component_id,'
225
+ ' uniqExact(c.component_release_id) AS version_count,'
226
+ ' uniqExact(c.project_id) AS project_count',
227
+ ' AND c.component_id IN {component_ids:Array(String)}'
228
+ ' GROUP BY c.component_id',
229
+ ),
230
+ {'project_ids': project_ids, 'component_ids': component_ids},
231
+ )
232
+ return {
233
+ str(row['component_id']): (
234
+ int(row['version_count']),
235
+ int(row['project_count']),
236
+ )
237
+ for row in rows
238
+ }
239
+
240
+
241
+ async def ecosystem_totals(project_ids: list[str]) -> dict[str, int]:
242
+ """Component counts per ecosystem, scoped to the organization.
243
+
244
+ Restores the scoping traded away for speed when this was a graph
245
+ traversal: it counted every component in the catalog because
246
+ traversing to the org cost 36.5s against 1.07s. The aggregate here
247
+ is org-scoped and still cheap, so the chip labels describe what the
248
+ reader's organization actually depends on again.
249
+ """
250
+ if not project_ids:
251
+ return {}
252
+ rows = await clickhouse.query(
253
+ _by_project(
254
+ 'SELECT c.ecosystem AS ecosystem,'
255
+ ' uniqExact(c.component_id) AS total',
256
+ " AND c.ecosystem != '' GROUP BY c.ecosystem",
257
+ ),
258
+ {'project_ids': project_ids},
259
+ )
260
+ return {str(row['ecosystem']): int(row['total']) for row in rows}
261
+
262
+
263
+ async def component_usage(
264
+ release_ids: list[str], component_id: str
265
+ ) -> list[tuple[str, str, str]]:
266
+ """``(release_id, component_release_id, version)`` for one package.
267
+
268
+ Scoped to the deployment pointer set, so the result describes what
269
+ is running now rather than every release that ever named the
270
+ package. The caller joins each ``release_id`` back to the project,
271
+ team, and environment it came from -- that topology stayed in the
272
+ graph, and it is what produced the ids passed in here.
273
+ """
274
+ if not release_ids:
275
+ return []
276
+ rows = await clickhouse.query(
277
+ _by_release(
278
+ 'SELECT DISTINCT c.release_id AS release_id,'
279
+ ' c.component_release_id AS component_release_id,'
280
+ ' c.version AS version',
281
+ ' AND c.component_id = {component_id:String}',
282
+ ),
283
+ {'release_ids': release_ids, 'component_id': component_id},
284
+ )
285
+ return [
286
+ (
287
+ str(row['release_id']),
288
+ str(row['component_release_id']),
289
+ str(row['version']),
290
+ )
291
+ for row in rows
292
+ ]
293
+
294
+
295
+ async def governed_usage(
296
+ release_ids: list[str], component_release_ids: list[str]
297
+ ) -> list[dict[str, str]]:
298
+ """Deployed usages of a governed set of component versions.
299
+
300
+ Both bounds come from the graph and both are small: the deployment
301
+ pointer set on one side, and on the other the versions carrying a
302
+ status or an advisory -- the anchor that kept Problem Packages
303
+ starting from the governed set rather than from the catalog.
304
+ """
305
+ if not release_ids or not component_release_ids:
306
+ return []
307
+ rows = await clickhouse.query(
308
+ _by_release(
309
+ 'SELECT DISTINCT c.release_id AS release_id,'
310
+ ' c.component_id AS component_id,'
311
+ ' c.component_release_id AS component_release_id,'
312
+ ' c.version AS version',
313
+ ' AND c.component_release_id IN'
314
+ ' {component_release_ids:Array(String)}',
315
+ ),
316
+ {
317
+ 'release_ids': release_ids,
318
+ 'component_release_ids': component_release_ids,
319
+ },
320
+ )
321
+ return [{key: str(value) for key, value in row.items()} for row in rows]
322
+
323
+
324
+ async def release_components(
325
+ release_id: str,
326
+ ) -> list[dict[str, typing.Any]]:
327
+ """One release's currently-published component set.
328
+
329
+ Returns the denormalized columns plus the ids the caller hydrates
330
+ the rest from. ``license``, ``supplier``, ``hashes``, and the
331
+ component identifiers are governance attributes of the
332
+ ``ComponentRelease`` node and were never copied here, so the
333
+ listing endpoint fetches them from the graph by id -- a bounded
334
+ ``IN`` over one release's versions, not a traversal.
335
+ """
336
+ rows = await clickhouse.query(
337
+ _by_release(
338
+ 'SELECT c.component_id AS component_id,'
339
+ ' c.component_release_id AS component_release_id,'
340
+ ' c.purl_name AS purl_name,'
341
+ ' c.ecosystem AS ecosystem,'
342
+ ' c.version AS version,'
343
+ ' c.scope AS scope,'
344
+ ' c.groups AS groups',
345
+ ),
346
+ {'release_ids': [release_id]},
347
+ )
348
+ return list(rows)