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