imbi-api 2.12.2__tar.gz → 2.13.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.12.2 → imbi_api-2.13.0}/PKG-INFO +2 -2
- {imbi_api-2.12.2 → imbi_api-2.13.0}/pyproject.toml +2 -2
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/app.py +1 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/seed.py +13 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/__init__.py +2 -0
- imbi_api-2.13.0/src/imbi_api/endpoints/maintenance.py +218 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_analysis.py +4 -4
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/lifespans.py +34 -0
- imbi_api-2.13.0/src/imbi_api/maintenance/__init__.py +9 -0
- imbi_api-2.13.0/src/imbi_api/maintenance/operations.py +241 -0
- imbi_api-2.13.0/src/imbi_api/maintenance/registry.py +110 -0
- imbi_api-2.13.0/src/imbi_api/maintenance/state.py +346 -0
- imbi_api-2.13.0/src/imbi_api/maintenance/worker.py +147 -0
- imbi_api-2.13.0/tests/endpoints/test_maintenance.py +198 -0
- imbi_api-2.13.0/tests/test_maintenance_operations.py +237 -0
- imbi_api-2.13.0/tests/test_maintenance_state.py +239 -0
- imbi_api-2.13.0/tests/test_maintenance_worker.py +191 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/uv.lock +5 -5
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.gitignore +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.pre-commit-config.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/.python-version +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/CLAUDE.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/LICENSE +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/README.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/coderabbit.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/compose.yaml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr/0016-delegated-execution-for-scheduled-jobs.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/adr.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/configuration.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/index.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/docs/restore-backup.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/justfile +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/mkdocs.yml +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/mypy.ini +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/authorization_codes.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/login_providers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/oauth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/oauth_clients.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/permissions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/auth/totp.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/backfill_embeddings.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/blueprint_attributes.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/blueprint_compliance.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/commit_sync/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/commit_sync/queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/commit_sync/service.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/domain/models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/domain/scoring.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/_credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/_helpers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/_pagination.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/blueprints.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/comments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/dashboard.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/environments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/events.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/graph_query.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/integrations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/operations_log.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/organizations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_deployments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_integrations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_logs.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_pr_sync.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/projects.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/releases.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/search.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/user_activity.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/users.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/endpoints/webhooks.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/entrypoint.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/attribution.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/endpoints.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/errors.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/flows.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/host_integration.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/repository.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/resolution.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/state.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/identity/sweeper.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/mcp_test.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/assignments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/lifecycle.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/resolution.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/pr_sync/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/pr_sync/queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/pr_sync/service.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/prompts/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/sbom.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/scoring/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/scoring/queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/test_client.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/test_init.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/test_integration.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/test_models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/email/test_templates.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_integrations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_deployments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_integrations.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_state.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/storage/__init__.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/storage/test_client.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/storage/test_init.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/support.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_app.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_blueprints.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_init.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_installer.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_lifespans.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_models.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_openapi.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_patch.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_plugins.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_plugins_assignments.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_plugins_credentials.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_pr_sync_service.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_sbom.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.0}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.12.2 → imbi_api-2.13.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.13.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.13.0
|
|
24
24
|
Requires-Dist: jinja2
|
|
25
25
|
Requires-Dist: jsonpatch>=1.33
|
|
26
26
|
Requires-Dist: mcp>=1.26.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-api"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.13.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.13.0",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -378,6 +378,19 @@ STANDARD_PERMISSIONS: list[tuple[str, str, str, str]] = [
|
|
|
378
378
|
'plugins:manage',
|
|
379
379
|
'Install and uninstall plugins',
|
|
380
380
|
),
|
|
381
|
+
# Global maintenance operations
|
|
382
|
+
(
|
|
383
|
+
'admin:maintenance:read',
|
|
384
|
+
'admin',
|
|
385
|
+
'maintenance:read',
|
|
386
|
+
'View global maintenance operations',
|
|
387
|
+
),
|
|
388
|
+
(
|
|
389
|
+
'admin:maintenance:manage',
|
|
390
|
+
'admin',
|
|
391
|
+
'maintenance:manage',
|
|
392
|
+
'Run and cancel global maintenance operations',
|
|
393
|
+
),
|
|
381
394
|
# Project plugin access
|
|
382
395
|
(
|
|
383
396
|
'project:configuration:read',
|
|
@@ -16,6 +16,7 @@ from .dashboard import dashboard_router
|
|
|
16
16
|
from .events import events_router
|
|
17
17
|
from .graph_query import graph_query_router
|
|
18
18
|
from .local_auth import local_auth_router
|
|
19
|
+
from .maintenance import maintenance_router
|
|
19
20
|
from .mcp_servers import mcp_servers_router
|
|
20
21
|
from .mfa import mfa_router
|
|
21
22
|
from .oauth_metadata import oauth_metadata_router
|
|
@@ -46,6 +47,7 @@ prefixed_routers: list[fastapi.APIRouter] = [
|
|
|
46
47
|
events_router,
|
|
47
48
|
graph_query_router,
|
|
48
49
|
local_auth_router,
|
|
50
|
+
maintenance_router,
|
|
49
51
|
mcp_servers_router,
|
|
50
52
|
me_identities_router,
|
|
51
53
|
mfa_router,
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Global maintenance operation endpoints (admin Maintenance page).
|
|
2
|
+
|
|
3
|
+
The operation list is registry-driven (:mod:`imbi_api.maintenance`):
|
|
4
|
+
the UI renders whatever this returns, so adding an operation to the
|
|
5
|
+
registry is all that is required for a new button to appear.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import logging
|
|
10
|
+
import typing
|
|
11
|
+
|
|
12
|
+
import fastapi
|
|
13
|
+
import pydantic
|
|
14
|
+
from imbi_common import graph
|
|
15
|
+
|
|
16
|
+
from imbi_api.auth import permissions
|
|
17
|
+
from imbi_api.maintenance import (
|
|
18
|
+
OPERATIONS,
|
|
19
|
+
MaintenanceSlug,
|
|
20
|
+
OperationDefinition,
|
|
21
|
+
state,
|
|
22
|
+
)
|
|
23
|
+
from imbi_api.scoring import OptionalValkeyClient
|
|
24
|
+
|
|
25
|
+
LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
maintenance_router = fastapi.APIRouter(
|
|
28
|
+
prefix='/maintenance', tags=['Admin: Maintenance']
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
RequireRead = typing.Annotated[
|
|
32
|
+
permissions.AuthContext,
|
|
33
|
+
fastapi.Depends(
|
|
34
|
+
permissions.require_permission('admin:maintenance:read'),
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
RequireManage = typing.Annotated[
|
|
38
|
+
permissions.AuthContext,
|
|
39
|
+
fastapi.Depends(
|
|
40
|
+
permissions.require_permission('admin:maintenance:manage'),
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MaintenanceProgress(pydantic.BaseModel):
|
|
46
|
+
"""Counters for an operation's current or last run."""
|
|
47
|
+
|
|
48
|
+
total: int = 0
|
|
49
|
+
remaining: int = 0
|
|
50
|
+
in_flight: int = 0
|
|
51
|
+
succeeded: int = 0
|
|
52
|
+
failed: int = 0
|
|
53
|
+
skipped: int = 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MaintenanceOperation(pydantic.BaseModel):
|
|
57
|
+
"""One registry operation merged with its run state."""
|
|
58
|
+
|
|
59
|
+
slug: str
|
|
60
|
+
label: str
|
|
61
|
+
description: str
|
|
62
|
+
running: bool
|
|
63
|
+
state: state.RunState
|
|
64
|
+
progress: MaintenanceProgress | None = None
|
|
65
|
+
started_at: datetime.datetime | None = None
|
|
66
|
+
started_by: str | None = None
|
|
67
|
+
completed_at: datetime.datetime | None = None
|
|
68
|
+
#: Per-project failure detail; only populated on the per-operation
|
|
69
|
+
#: GET to keep the list response small.
|
|
70
|
+
failures: dict[str, str] | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MaintenanceRunResponse(pydantic.BaseModel):
|
|
74
|
+
"""Acknowledgement that a run was started."""
|
|
75
|
+
|
|
76
|
+
run_id: str
|
|
77
|
+
total: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _to_operation(
|
|
81
|
+
definition: OperationDefinition,
|
|
82
|
+
status: state.RunStatus,
|
|
83
|
+
failures: dict[str, str] | None = None,
|
|
84
|
+
) -> MaintenanceOperation:
|
|
85
|
+
progress: MaintenanceProgress | None = None
|
|
86
|
+
if status.state != 'idle':
|
|
87
|
+
progress = MaintenanceProgress(
|
|
88
|
+
total=status.total,
|
|
89
|
+
remaining=status.remaining,
|
|
90
|
+
in_flight=status.in_flight,
|
|
91
|
+
succeeded=status.succeeded,
|
|
92
|
+
failed=status.failed,
|
|
93
|
+
skipped=status.skipped,
|
|
94
|
+
)
|
|
95
|
+
return MaintenanceOperation(
|
|
96
|
+
slug=definition.slug,
|
|
97
|
+
label=definition.label,
|
|
98
|
+
description=definition.description,
|
|
99
|
+
running=status.state == 'running',
|
|
100
|
+
state=status.state,
|
|
101
|
+
progress=progress,
|
|
102
|
+
started_at=status.started_at,
|
|
103
|
+
started_by=status.started_by,
|
|
104
|
+
completed_at=status.completed_at,
|
|
105
|
+
failures=failures,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _definition_or_404(slug: str) -> OperationDefinition:
|
|
110
|
+
definition = OPERATIONS.get(typing.cast('MaintenanceSlug', slug))
|
|
111
|
+
if definition is None:
|
|
112
|
+
raise fastapi.HTTPException(
|
|
113
|
+
status_code=404, detail=f'Unknown maintenance operation {slug!r}'
|
|
114
|
+
)
|
|
115
|
+
return definition
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@maintenance_router.get('/operations')
|
|
119
|
+
async def list_maintenance_operations(
|
|
120
|
+
auth: RequireRead,
|
|
121
|
+
client: OptionalValkeyClient,
|
|
122
|
+
) -> list[MaintenanceOperation]:
|
|
123
|
+
"""The operation registry merged with each operation's run state."""
|
|
124
|
+
_ = auth
|
|
125
|
+
if client is None:
|
|
126
|
+
raise fastapi.HTTPException(
|
|
127
|
+
status_code=503,
|
|
128
|
+
detail='Maintenance state is unavailable '
|
|
129
|
+
'(Valkey is not connected).',
|
|
130
|
+
)
|
|
131
|
+
results: list[MaintenanceOperation] = []
|
|
132
|
+
for definition in OPERATIONS.values():
|
|
133
|
+
status = await state.read_status(client, definition.slug)
|
|
134
|
+
results.append(_to_operation(definition, status))
|
|
135
|
+
return results
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@maintenance_router.get('/operations/{slug}')
|
|
139
|
+
async def get_maintenance_operation(
|
|
140
|
+
slug: str,
|
|
141
|
+
auth: RequireRead,
|
|
142
|
+
client: OptionalValkeyClient,
|
|
143
|
+
) -> MaintenanceOperation:
|
|
144
|
+
"""One operation's run state, including per-project failures."""
|
|
145
|
+
_ = auth
|
|
146
|
+
definition = _definition_or_404(slug)
|
|
147
|
+
if client is None:
|
|
148
|
+
raise fastapi.HTTPException(
|
|
149
|
+
status_code=503,
|
|
150
|
+
detail='Maintenance state is unavailable '
|
|
151
|
+
'(Valkey is not connected).',
|
|
152
|
+
)
|
|
153
|
+
status = await state.read_status(client, definition.slug)
|
|
154
|
+
failures = await state.read_failures(client, definition.slug)
|
|
155
|
+
return _to_operation(definition, status, failures or None)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@maintenance_router.post('/operations/{slug}/run', status_code=202)
|
|
159
|
+
async def run_maintenance_operation(
|
|
160
|
+
slug: str,
|
|
161
|
+
auth: RequireManage,
|
|
162
|
+
db: graph.Pool,
|
|
163
|
+
client: OptionalValkeyClient,
|
|
164
|
+
) -> MaintenanceRunResponse:
|
|
165
|
+
"""Start a global run of the operation across all projects."""
|
|
166
|
+
definition = _definition_or_404(slug)
|
|
167
|
+
if client is None:
|
|
168
|
+
raise fastapi.HTTPException(
|
|
169
|
+
status_code=503,
|
|
170
|
+
detail='Queueing is unavailable (Valkey is not connected).',
|
|
171
|
+
)
|
|
172
|
+
project_ids = await definition.enumerate(db)
|
|
173
|
+
status = await state.start_run(
|
|
174
|
+
client, definition.slug, project_ids, auth.principal_name
|
|
175
|
+
)
|
|
176
|
+
if status is None:
|
|
177
|
+
raise fastapi.HTTPException(
|
|
178
|
+
status_code=409,
|
|
179
|
+
detail=f'{definition.label} is already running.',
|
|
180
|
+
)
|
|
181
|
+
LOGGER.info(
|
|
182
|
+
'maintenance %s run %s started by %s (%d projects)',
|
|
183
|
+
definition.slug,
|
|
184
|
+
status.run_id,
|
|
185
|
+
auth.principal_name,
|
|
186
|
+
status.total,
|
|
187
|
+
)
|
|
188
|
+
return MaintenanceRunResponse(
|
|
189
|
+
run_id=status.run_id or '', total=status.total
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@maintenance_router.post('/operations/{slug}/cancel')
|
|
194
|
+
async def cancel_maintenance_operation(
|
|
195
|
+
slug: str,
|
|
196
|
+
auth: RequireManage,
|
|
197
|
+
client: OptionalValkeyClient,
|
|
198
|
+
) -> MaintenanceOperation:
|
|
199
|
+
"""Cancel the operation's in-progress run."""
|
|
200
|
+
definition = _definition_or_404(slug)
|
|
201
|
+
if client is None:
|
|
202
|
+
raise fastapi.HTTPException(
|
|
203
|
+
status_code=503,
|
|
204
|
+
detail='Queueing is unavailable (Valkey is not connected).',
|
|
205
|
+
)
|
|
206
|
+
cancelled = await state.cancel_run(client, definition.slug)
|
|
207
|
+
if not cancelled:
|
|
208
|
+
raise fastapi.HTTPException(
|
|
209
|
+
status_code=409,
|
|
210
|
+
detail=f'{definition.label} is not running.',
|
|
211
|
+
)
|
|
212
|
+
LOGGER.info(
|
|
213
|
+
'maintenance %s run cancelled by %s',
|
|
214
|
+
definition.slug,
|
|
215
|
+
auth.principal_name,
|
|
216
|
+
)
|
|
217
|
+
status = await state.read_status(client, definition.slug)
|
|
218
|
+
return _to_operation(definition, status)
|
|
@@ -499,7 +499,7 @@ async def _collect_results(
|
|
|
499
499
|
return results
|
|
500
500
|
|
|
501
501
|
|
|
502
|
-
async def
|
|
502
|
+
async def run_and_persist(
|
|
503
503
|
db: graph.Graph,
|
|
504
504
|
org_slug: str,
|
|
505
505
|
project_id: str,
|
|
@@ -526,7 +526,7 @@ async def run_project_analysis(
|
|
|
526
526
|
fastapi.Depends(permissions.require_permission('project:write')),
|
|
527
527
|
],
|
|
528
528
|
) -> AnalysisReport:
|
|
529
|
-
return await
|
|
529
|
+
return await run_and_persist(db, org_slug, project_id, auth)
|
|
530
530
|
|
|
531
531
|
|
|
532
532
|
class RemediateRequest(pydantic.BaseModel):
|
|
@@ -659,7 +659,7 @@ async def remediate_project_finding(
|
|
|
659
659
|
result = await _remediate_one(
|
|
660
660
|
db, org_slug, project_id, body.plugin_id, body.remediation_id, auth
|
|
661
661
|
)
|
|
662
|
-
report = await
|
|
662
|
+
report = await run_and_persist(db, org_slug, project_id, auth)
|
|
663
663
|
return RemediateResponse(result=result, report=report)
|
|
664
664
|
|
|
665
665
|
|
|
@@ -728,5 +728,5 @@ async def remediate_all_project_findings(
|
|
|
728
728
|
result=result,
|
|
729
729
|
)
|
|
730
730
|
)
|
|
731
|
-
fresh = await
|
|
731
|
+
fresh = await run_and_persist(db, org_slug, project_id, auth)
|
|
732
732
|
return RemediateAllResponse(outcomes=outcomes, report=fresh)
|
|
@@ -18,6 +18,7 @@ from imbi_api.commit_sync import queue as commit_sync_queue
|
|
|
18
18
|
from imbi_api.email.client import EmailClient
|
|
19
19
|
from imbi_api.email.templates import TemplateManager
|
|
20
20
|
from imbi_api.identity import sweeper as identity_sweeper
|
|
21
|
+
from imbi_api.maintenance import worker as maintenance_worker
|
|
21
22
|
from imbi_api.plugins import lifecycle as plugin_lifecycle
|
|
22
23
|
from imbi_api.pr_sync import queue as pr_sync_queue
|
|
23
24
|
from imbi_api.scoring import queue as score_queue
|
|
@@ -199,6 +200,39 @@ async def pr_sync_worker_hook() -> abc.AsyncGenerator[None]:
|
|
|
199
200
|
)
|
|
200
201
|
|
|
201
202
|
|
|
203
|
+
@contextlib.asynccontextmanager
|
|
204
|
+
async def maintenance_worker_hook() -> abc.AsyncGenerator[None]:
|
|
205
|
+
"""Run the global maintenance operation consumer loop."""
|
|
206
|
+
try:
|
|
207
|
+
client = valkey.get_client()
|
|
208
|
+
except RuntimeError:
|
|
209
|
+
LOGGER.warning('Valkey unavailable; maintenance worker not started')
|
|
210
|
+
yield None
|
|
211
|
+
return
|
|
212
|
+
if _graph is None:
|
|
213
|
+
LOGGER.warning('Graph not ready; maintenance worker not started')
|
|
214
|
+
yield None
|
|
215
|
+
return
|
|
216
|
+
stop = asyncio.Event()
|
|
217
|
+
LOGGER.info('Maintenance worker starting')
|
|
218
|
+
consumer_task = asyncio.create_task(
|
|
219
|
+
maintenance_worker.run_worker(client, _graph, stop=stop)
|
|
220
|
+
)
|
|
221
|
+
try:
|
|
222
|
+
yield None
|
|
223
|
+
finally:
|
|
224
|
+
stop.set()
|
|
225
|
+
consumer_task.cancel()
|
|
226
|
+
try:
|
|
227
|
+
await consumer_task
|
|
228
|
+
except asyncio.CancelledError:
|
|
229
|
+
pass
|
|
230
|
+
except Exception: # noqa: BLE001
|
|
231
|
+
LOGGER.warning(
|
|
232
|
+
'Maintenance worker task exited with error', exc_info=True
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
202
236
|
@contextlib.asynccontextmanager
|
|
203
237
|
async def score_worker_hook() -> abc.AsyncGenerator[None]:
|
|
204
238
|
"""Run the score-recompute consumer and the daily tick."""
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Per-project execute functions for global maintenance operations.
|
|
2
|
+
|
|
3
|
+
Each ``execute_*`` runs one project's slice of a global run by reusing
|
|
4
|
+
the same service code the per-project Doctor endpoints call. Outcomes:
|
|
5
|
+
|
|
6
|
+
- return ``'succeeded'`` / ``'skipped'`` (skipped means the operation
|
|
7
|
+
does not apply -- e.g. no integration provides the capability);
|
|
8
|
+
- raise :class:`MaintenanceItemFailed` with a user-safe message for
|
|
9
|
+
recordable failures (raw detail belongs in logs only);
|
|
10
|
+
- let :class:`~imbi_common.plugins.errors.PluginRateLimited` propagate
|
|
11
|
+
so the worker can requeue the project and pause the operation.
|
|
12
|
+
|
|
13
|
+
Endpoint modules are imported inside function bodies (the
|
|
14
|
+
``commit_sync.service`` pattern) so this module never pulls the
|
|
15
|
+
endpoints package at import time.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import logging
|
|
22
|
+
import typing
|
|
23
|
+
|
|
24
|
+
import fastapi
|
|
25
|
+
from imbi_common import graph
|
|
26
|
+
from imbi_common.plugins.errors import PluginRateLimited
|
|
27
|
+
from valkey import asyncio as valkey
|
|
28
|
+
|
|
29
|
+
from imbi_api import models
|
|
30
|
+
from imbi_api.auth import permissions
|
|
31
|
+
from imbi_api.scoring import queue as score_queue
|
|
32
|
+
|
|
33
|
+
LOGGER = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
#: ``requested_by`` / ``principal_name`` recorded on work this runs.
|
|
36
|
+
REQUESTED_BY = 'maintenance'
|
|
37
|
+
|
|
38
|
+
ExecuteOutcome = typing.Literal['succeeded', 'skipped']
|
|
39
|
+
|
|
40
|
+
_ORG_SLUG_QUERY: typing.LiteralString = (
|
|
41
|
+
'MATCH (p:Project {{id: {project_id}}})-[:OWNED_BY]->(:Team)'
|
|
42
|
+
'-[:BELONGS_TO]->(o:Organization) RETURN o.slug AS slug'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MaintenanceItemFailed(Exception):
|
|
47
|
+
"""One project's operation failed; the message is user-safe."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@functools.cache
|
|
51
|
+
def _system_auth() -> permissions.AuthContext:
|
|
52
|
+
"""Synthetic principal for background maintenance work.
|
|
53
|
+
|
|
54
|
+
Never persisted; exists so service functions that record
|
|
55
|
+
``principal_name`` attribute the work to ``'maintenance'``.
|
|
56
|
+
"""
|
|
57
|
+
return permissions.AuthContext(
|
|
58
|
+
auth_method='client_credentials',
|
|
59
|
+
service_account=models.ServiceAccount(
|
|
60
|
+
slug=REQUESTED_BY, display_name='Imbi Maintenance'
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def enumerate_all_projects(db: graph.Graph) -> list[str]:
|
|
66
|
+
"""Every project id -- maintenance operations self-classify
|
|
67
|
+
inapplicable projects as skipped rather than pre-filtering (which
|
|
68
|
+
would cost a capability resolution per project up front)."""
|
|
69
|
+
return await score_queue.all_project_ids(db)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _org_slug_for(db: graph.Graph, project_id: str) -> str | None:
|
|
73
|
+
rows = await db.execute(
|
|
74
|
+
_ORG_SLUG_QUERY, {'project_id': project_id}, ['slug']
|
|
75
|
+
)
|
|
76
|
+
if not rows:
|
|
77
|
+
return None
|
|
78
|
+
value = graph.parse_agtype(rows[0].get('slug'))
|
|
79
|
+
return str(value) if value else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def execute_analysis(
|
|
83
|
+
db: graph.Graph, client: valkey.Valkey, project_id: str
|
|
84
|
+
) -> ExecuteOutcome:
|
|
85
|
+
"""Run the Doctor analysis and persist the report.
|
|
86
|
+
|
|
87
|
+
Per-plugin errors already surface as synthetic ``fail`` findings
|
|
88
|
+
inside the report, so an exception here is infrastructural.
|
|
89
|
+
"""
|
|
90
|
+
from imbi_api.endpoints import project_analysis
|
|
91
|
+
|
|
92
|
+
org_slug = await _org_slug_for(db, project_id)
|
|
93
|
+
if org_slug is None:
|
|
94
|
+
return 'skipped'
|
|
95
|
+
await project_analysis.run_and_persist(
|
|
96
|
+
db, org_slug, project_id, _system_auth()
|
|
97
|
+
)
|
|
98
|
+
return 'succeeded'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def execute_commit_sync(
|
|
102
|
+
db: graph.Graph, client: valkey.Valkey, project_id: str
|
|
103
|
+
) -> ExecuteOutcome:
|
|
104
|
+
"""Full commit/tag backfill, mirroring the queue consumer's status
|
|
105
|
+
transitions so the per-project Doctor status stays truthful."""
|
|
106
|
+
from imbi_api.commit_sync import service
|
|
107
|
+
|
|
108
|
+
org_slug = await _org_slug_for(db, project_id)
|
|
109
|
+
if org_slug is None:
|
|
110
|
+
return 'skipped'
|
|
111
|
+
await service.set_status(
|
|
112
|
+
db, project_id, status='running', requested_by=REQUESTED_BY
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
commits, tags = await service.run_sync(db, org_slug, project_id)
|
|
116
|
+
except service.CommitSyncUnavailable as exc:
|
|
117
|
+
await service.set_status(
|
|
118
|
+
db,
|
|
119
|
+
project_id,
|
|
120
|
+
status='failed',
|
|
121
|
+
requested_by=REQUESTED_BY,
|
|
122
|
+
error=str(exc),
|
|
123
|
+
)
|
|
124
|
+
return 'skipped'
|
|
125
|
+
except PluginRateLimited:
|
|
126
|
+
# Leave the project requeue-able; the worker pauses the op.
|
|
127
|
+
await service.set_status(
|
|
128
|
+
db, project_id, status='queued', requested_by=REQUESTED_BY
|
|
129
|
+
)
|
|
130
|
+
raise
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
LOGGER.exception('maintenance commit-sync failed for %s', project_id)
|
|
133
|
+
message = 'Commit sync failed. See server logs for details.'
|
|
134
|
+
await service.set_status(
|
|
135
|
+
db,
|
|
136
|
+
project_id,
|
|
137
|
+
status='failed',
|
|
138
|
+
requested_by=REQUESTED_BY,
|
|
139
|
+
error=message,
|
|
140
|
+
)
|
|
141
|
+
raise MaintenanceItemFailed(message) from exc
|
|
142
|
+
await service.set_status(
|
|
143
|
+
db,
|
|
144
|
+
project_id,
|
|
145
|
+
status='success',
|
|
146
|
+
requested_by=REQUESTED_BY,
|
|
147
|
+
commits=commits,
|
|
148
|
+
tags=tags,
|
|
149
|
+
)
|
|
150
|
+
return 'succeeded'
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def execute_pr_sync(
|
|
154
|
+
db: graph.Graph, client: valkey.Valkey, project_id: str
|
|
155
|
+
) -> ExecuteOutcome:
|
|
156
|
+
"""Full PR-history backfill; same shape as commit sync."""
|
|
157
|
+
from imbi_api.pr_sync import service
|
|
158
|
+
|
|
159
|
+
org_slug = await _org_slug_for(db, project_id)
|
|
160
|
+
if org_slug is None:
|
|
161
|
+
return 'skipped'
|
|
162
|
+
await service.set_status(
|
|
163
|
+
db, project_id, status='running', requested_by=REQUESTED_BY
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
prs = await service.run_sync(db, org_slug, project_id)
|
|
167
|
+
except service.PRSyncUnavailable as exc:
|
|
168
|
+
await service.set_status(
|
|
169
|
+
db,
|
|
170
|
+
project_id,
|
|
171
|
+
status='failed',
|
|
172
|
+
requested_by=REQUESTED_BY,
|
|
173
|
+
error=str(exc),
|
|
174
|
+
)
|
|
175
|
+
return 'skipped'
|
|
176
|
+
except PluginRateLimited:
|
|
177
|
+
# Leave the project requeue-able; the worker pauses the op.
|
|
178
|
+
await service.set_status(
|
|
179
|
+
db, project_id, status='queued', requested_by=REQUESTED_BY
|
|
180
|
+
)
|
|
181
|
+
raise
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
LOGGER.exception('maintenance pr-sync failed for %s', project_id)
|
|
184
|
+
message = 'PR sync failed. See server logs for details.'
|
|
185
|
+
await service.set_status(
|
|
186
|
+
db,
|
|
187
|
+
project_id,
|
|
188
|
+
status='failed',
|
|
189
|
+
requested_by=REQUESTED_BY,
|
|
190
|
+
error=message,
|
|
191
|
+
)
|
|
192
|
+
raise MaintenanceItemFailed(message) from exc
|
|
193
|
+
await service.set_status(
|
|
194
|
+
db,
|
|
195
|
+
project_id,
|
|
196
|
+
status='success',
|
|
197
|
+
requested_by=REQUESTED_BY,
|
|
198
|
+
prs=prs,
|
|
199
|
+
)
|
|
200
|
+
return 'succeeded'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def execute_deployment_resync(
|
|
204
|
+
db: graph.Graph, client: valkey.Valkey, project_id: str
|
|
205
|
+
) -> ExecuteOutcome:
|
|
206
|
+
"""Backfill recent remote deployments via the deployment plugin."""
|
|
207
|
+
from imbi_api.endpoints import project_deployments
|
|
208
|
+
|
|
209
|
+
org_slug = await _org_slug_for(db, project_id)
|
|
210
|
+
if org_slug is None:
|
|
211
|
+
return 'skipped'
|
|
212
|
+
try:
|
|
213
|
+
await project_deployments.resync_for_project(
|
|
214
|
+
db,
|
|
215
|
+
org_slug=org_slug,
|
|
216
|
+
project_id=project_id,
|
|
217
|
+
auth=_system_auth(),
|
|
218
|
+
limit=1,
|
|
219
|
+
)
|
|
220
|
+
except fastapi.HTTPException as exc:
|
|
221
|
+
# 404: no deployment capability bound; 400: the plugin doesn't
|
|
222
|
+
# support deployment sync. Neither is a failure of this run.
|
|
223
|
+
if exc.status_code in (400, 404):
|
|
224
|
+
return 'skipped'
|
|
225
|
+
raise MaintenanceItemFailed(str(exc.detail)) from exc
|
|
226
|
+
return 'succeeded'
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def execute_rescore(
|
|
230
|
+
db: graph.Graph, client: valkey.Valkey, project_id: str
|
|
231
|
+
) -> ExecuteOutcome:
|
|
232
|
+
"""Enqueue a score recompute onto the existing scoring stream.
|
|
233
|
+
|
|
234
|
+
Succeeded means enqueued -- the scoring workers do the computation
|
|
235
|
+
with their own debounce/DLQ/history handling. Skipped means the
|
|
236
|
+
project was debounced (a recompute is already queued).
|
|
237
|
+
"""
|
|
238
|
+
enqueued = await score_queue.enqueue_recompute(
|
|
239
|
+
client, project_id, 'bulk_rescore', REQUESTED_BY
|
|
240
|
+
)
|
|
241
|
+
return 'succeeded' if enqueued else 'skipped'
|