imbi-api 2.32.2__tar.gz → 2.32.3__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.32.2 → imbi_api-2.32.3}/PKG-INFO +2 -2
- {imbi_api-2.32.2 → imbi_api-2.32.3}/pyproject.toml +2 -2
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_deployments.py +98 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/releases.py +198 -44
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_deployments.py +140 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_releases.py +235 -8
- {imbi_api-2.32.2 → imbi_api-2.32.3}/.gitignore +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/README.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/moon.yml +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/README.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/app.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/authorization_codes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/autonomous.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/internal_services.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/login_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/membership.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/oauth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/oauth_clients.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/password.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/permissions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/principals.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/seed.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/sessions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/tokens.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/auth/totp.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/backfill_embeddings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/blueprint_attributes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/blueprint_compliance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/commit_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/commit_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/commit_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/component_facts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/deployment_status_repair.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/deployment_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/deployment_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/deployment_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/deployment_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/documents/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/documents/read_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/domain/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/domain/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/domain/scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/drift.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/base.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/base.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/password_reset.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/welcome.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates/welcome.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/email/templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_document_events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_document_history.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_document_reads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_document_scope.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_helpers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_pagination.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/_search_index.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/admin.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/ai_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/ai_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/comments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/components.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/dashboard.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/document_analytics.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/document_likes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/document_presence.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/document_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/document_versions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/documents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/environments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/graph_query.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/maintenance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/mfa.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/operations_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/organizations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_analysis.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_logs.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/project_types.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/projects.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/roles.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/search.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/status.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/tags.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/teams.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/uploads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/user_activity.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/users.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/endpoints/webhooks.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/entrypoint.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/graph_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/attribution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/endpoints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/errors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/flows.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/host_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/repository.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/identity/sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/lifespans.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/llm/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/llm/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/operations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/registry.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/maintenance/worker.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/mcp_test.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/middleware/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/middleware/rate_limit.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/openapi.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/orphan_releases.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/assignments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/installer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/reload.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/plugins/schemas.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/pr_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/pr_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/pr_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/prompts/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/py.typed +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/relationships.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/release_promote/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/release_promote/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/release_promote/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/sbom.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/scoring/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/scoring/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/settings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/storage/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/storage/client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/storage/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/storage/thumbnails.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/src/imbi/api/storage/validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_internal_services.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/test_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/test_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/email/test_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_ai_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_ai_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_components.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_analytics.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_likes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_presence.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_reads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_reads_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_document_versions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_iggy_connectors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_maintenance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_search_index.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/storage/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/storage/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/storage/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/support.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_app.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_component_facts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_deployment_status_repair.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_deployment_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_deployment_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_deployment_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_drift.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_installer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_lifespans.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_maintenance_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_maintenance_operations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_maintenance_state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_maintenance_worker.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_openapi.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_orphan_releases.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_plugins_assignments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_plugins_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_pr_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_projects_drift_ranges.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_release_promote_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_sbom.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.32.3}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.32.
|
|
3
|
+
Version: 2.32.3
|
|
4
4
|
Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
|
|
5
5
|
Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -19,7 +19,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
|
|
|
19
19
|
Requires-Dist: fastapi
|
|
20
20
|
Requires-Dist: filetype
|
|
21
21
|
Requires-Dist: httpx<1,>=0.28.1
|
|
22
|
-
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.32.
|
|
22
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.32.3
|
|
23
23
|
Requires-Dist: jinja2
|
|
24
24
|
Requires-Dist: jsonpatch>=1.33
|
|
25
25
|
Requires-Dist: mcp>=1.26.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-api"
|
|
3
|
-
version = "2.32.
|
|
3
|
+
version = "2.32.3"
|
|
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.32.
|
|
31
|
+
"imbi-common[databases,llm,sentry,server]==2.32.3",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -1545,6 +1545,104 @@ async def fetch_release_notes_for_tag(
|
|
|
1545
1545
|
)
|
|
1546
1546
|
|
|
1547
1547
|
|
|
1548
|
+
async def resolve_committish_for_tag(
|
|
1549
|
+
db: graph.Graph,
|
|
1550
|
+
*,
|
|
1551
|
+
org_slug: str,
|
|
1552
|
+
project_id: str,
|
|
1553
|
+
tag: str,
|
|
1554
|
+
auth: permissions.AuthContext,
|
|
1555
|
+
) -> str | None:
|
|
1556
|
+
"""Resolve ``tag`` to the short committish it points at.
|
|
1557
|
+
|
|
1558
|
+
The release-create path uses this when a caller supplies a tag but no
|
|
1559
|
+
commit -- a source host's ``release`` webhook names the tag and the
|
|
1560
|
+
branch it was cut from, never the SHA. The lookup goes through the
|
|
1561
|
+
deployment capability rather than the ClickHouse ``tags`` table
|
|
1562
|
+
because the two race: the ``release`` delivery lands before commit
|
|
1563
|
+
sync records the tag (32s apart on the delivery that motivated this),
|
|
1564
|
+
so the table is reliably empty at exactly the moment this is called.
|
|
1565
|
+
The remote, by contrast, has the tag by definition -- it is what it
|
|
1566
|
+
just announced.
|
|
1567
|
+
|
|
1568
|
+
``None`` means the answer is *no commit*, and the caller may reject
|
|
1569
|
+
the release on it: the remote positively reported the ref does not
|
|
1570
|
+
exist (HTTP 404/422), no deployment capability is bound to the
|
|
1571
|
+
project at all, or the plugin does not implement
|
|
1572
|
+
``resolve_committish``. Every other failure raises instead -- the
|
|
1573
|
+
same distinction ``resolve_remote_tags`` draws between ``'absent'``
|
|
1574
|
+
and ``'error'``, and for the same reason: a caller that refuses a
|
|
1575
|
+
release on ``None`` must never read unreachability as absence. A
|
|
1576
|
+
timed-out, rate-limited, unauthorized or uncredentialed lookup
|
|
1577
|
+
would otherwise turn a "retry shortly" into a terminal "your tag is
|
|
1578
|
+
invalid". Both the capability resolution and the call itself can
|
|
1579
|
+
fail either way, so both are sorted, not just the call.
|
|
1580
|
+
"""
|
|
1581
|
+
try:
|
|
1582
|
+
resolved, ctx, credentials = await _resolve_and_context(
|
|
1583
|
+
db, org_slug, project_id, auth, best_effort_identity=True
|
|
1584
|
+
)
|
|
1585
|
+
except fastapi.HTTPException as exc:
|
|
1586
|
+
# Only 404 -- "no integration provides the deployment
|
|
1587
|
+
# capability" -- means this project has no way to name a
|
|
1588
|
+
# commit. The other answers here are failures to *ask*: a
|
|
1589
|
+
# missing service credential is a deliberate, retryable 503
|
|
1590
|
+
# (see ``_resolve_and_context``), a 403 is an authorization
|
|
1591
|
+
# problem, and a 400 means the capability is bound more than
|
|
1592
|
+
# once and needs ``?source=``. Reporting any of those as an
|
|
1593
|
+
# absent tag is the same mistake as swallowing the timeout.
|
|
1594
|
+
if exc.status_code == 404:
|
|
1595
|
+
return None
|
|
1596
|
+
raise
|
|
1597
|
+
handler = _handler(resolved)
|
|
1598
|
+
try:
|
|
1599
|
+
commit = await call_with_timeout(
|
|
1600
|
+
handler.resolve_committish(
|
|
1601
|
+
ctx, _resolve_credentials(ctx, credentials), tag
|
|
1602
|
+
)
|
|
1603
|
+
)
|
|
1604
|
+
except NotImplementedError:
|
|
1605
|
+
return None
|
|
1606
|
+
except plugin_errors.PluginRateLimited:
|
|
1607
|
+
raise
|
|
1608
|
+
except fastapi.HTTPException:
|
|
1609
|
+
# ``call_with_timeout`` raises 503 + ``Retry-After`` on timeout.
|
|
1610
|
+
raise
|
|
1611
|
+
except httpx.HTTPStatusError as exc:
|
|
1612
|
+
# 404 is the remote saying "no such ref"; GitHub answers 422 for
|
|
1613
|
+
# a ref it cannot even parse. Everything else (403, 5xx) is the
|
|
1614
|
+
# remote failing to answer, not answering "no".
|
|
1615
|
+
if exc.response.status_code in (404, 422):
|
|
1616
|
+
return None
|
|
1617
|
+
LOGGER.warning(
|
|
1618
|
+
'committish lookup for project=%s tag=%s failed with %s',
|
|
1619
|
+
project_id,
|
|
1620
|
+
tag,
|
|
1621
|
+
exc.response.status_code,
|
|
1622
|
+
)
|
|
1623
|
+
raise fastapi.HTTPException(
|
|
1624
|
+
status_code=503,
|
|
1625
|
+
detail=f'Could not reach the source host to resolve {tag!r}',
|
|
1626
|
+
headers={'Retry-After': '5'},
|
|
1627
|
+
) from exc
|
|
1628
|
+
except httpx.HTTPError as exc:
|
|
1629
|
+
# Transport-level: connect, read, protocol. Transient, so it
|
|
1630
|
+
# must not read as "the tag does not exist" either.
|
|
1631
|
+
LOGGER.warning(
|
|
1632
|
+
'committish lookup for project=%s tag=%s failed: %r',
|
|
1633
|
+
project_id,
|
|
1634
|
+
tag,
|
|
1635
|
+
exc,
|
|
1636
|
+
)
|
|
1637
|
+
raise fastapi.HTTPException(
|
|
1638
|
+
status_code=503,
|
|
1639
|
+
detail=f'Could not reach the source host to resolve {tag!r}',
|
|
1640
|
+
headers={'Retry-After': '5'},
|
|
1641
|
+
) from exc
|
|
1642
|
+
await persist_link_writeback(db, ctx)
|
|
1643
|
+
return versioning.short_committish(commit.sha)
|
|
1644
|
+
|
|
1645
|
+
|
|
1548
1646
|
async def backfill_release_notes(
|
|
1549
1647
|
db: graph.Graph,
|
|
1550
1648
|
*,
|
|
@@ -74,26 +74,53 @@ _RELEASE_HYDRATION_SEMAPHORE = asyncio.Semaphore(
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
class ReleaseCreate(pydantic.BaseModel):
|
|
77
|
-
"""Request body for creating a release.
|
|
77
|
+
"""Request body for creating a release.
|
|
78
|
+
|
|
79
|
+
At least one of ``committish`` / ``tag`` is required. A source
|
|
80
|
+
host's *release* notification carries no commit -- GitHub's
|
|
81
|
+
``release`` payload offers only ``target_commitish``, which is the
|
|
82
|
+
branch the release was cut from (``main`` on 99.7% of deliveries)
|
|
83
|
+
rather than a SHA -- so a caller that has only the tag omits the
|
|
84
|
+
committish and the endpoint resolves it through the project's
|
|
85
|
+
deployment capability.
|
|
86
|
+
"""
|
|
78
87
|
|
|
79
88
|
model_config = pydantic.ConfigDict(extra='forbid')
|
|
80
89
|
|
|
81
90
|
tag: str | None = None
|
|
82
91
|
committish: typing.Annotated[
|
|
83
|
-
str,
|
|
92
|
+
str | None,
|
|
84
93
|
pydantic.Field(
|
|
85
94
|
pattern=r'^[0-9a-f]{7}$',
|
|
86
95
|
description=(
|
|
87
96
|
'Short commit SHA (7 lowercase hexadecimal chars) '
|
|
88
|
-
'identifying the source revision for this release.'
|
|
97
|
+
'identifying the source revision for this release. '
|
|
98
|
+
'Optional when a tag is supplied: the tag is then '
|
|
99
|
+
'resolved to its commit through the deployment '
|
|
100
|
+
'capability.'
|
|
89
101
|
),
|
|
90
102
|
),
|
|
91
|
-
]
|
|
103
|
+
] = None
|
|
92
104
|
title: str
|
|
93
105
|
description: str | None = None
|
|
94
106
|
links: list[models.ReleaseLink] = []
|
|
95
107
|
created_by: str | None = None
|
|
96
108
|
|
|
109
|
+
@pydantic.model_validator(mode='after')
|
|
110
|
+
def validate_identity(self) -> typing.Self:
|
|
111
|
+
"""Reject a body that identifies no revision at all.
|
|
112
|
+
|
|
113
|
+
An empty ``tag`` is normalized to ``None`` first, so it cannot
|
|
114
|
+
reach the graph: the untagged-release lookup treats ``''`` and
|
|
115
|
+
NULL alike, and a node stored with ``''`` would be matched as
|
|
116
|
+
untagged but could not then be named.
|
|
117
|
+
"""
|
|
118
|
+
if self.tag == '':
|
|
119
|
+
self.tag = None
|
|
120
|
+
if not self.committish and not self.tag:
|
|
121
|
+
raise ValueError('one of committish or tag is required')
|
|
122
|
+
return self
|
|
123
|
+
|
|
97
124
|
|
|
98
125
|
class ReleaseUpdate(pydantic.BaseModel):
|
|
99
126
|
"""JSON Patch-compatible release shape.
|
|
@@ -308,6 +335,107 @@ async def _fetch_release(
|
|
|
308
335
|
)
|
|
309
336
|
|
|
310
337
|
|
|
338
|
+
# Per-project release identity, in priority order.
|
|
339
|
+
#
|
|
340
|
+
# A tag is the identity whenever one exists: it names one shippable
|
|
341
|
+
# artifact, while the commit it points at moves (the release workflow
|
|
342
|
+
# bumps the version, then tags the bump commit). Keying on the
|
|
343
|
+
# committish is what let the post-bump SHA miss the node Imbi had
|
|
344
|
+
# already created.
|
|
345
|
+
#
|
|
346
|
+
# The second clause is the cross-webhook case. A ``deployment_status``
|
|
347
|
+
# delivery creates the release from a commit alone, then the ``release``
|
|
348
|
+
# delivery names the same artifact by tag; matching on the tag alone
|
|
349
|
+
# misses that node and creates a sibling for one release. A tagged
|
|
350
|
+
# create therefore also claims an *untagged* node carrying its commit.
|
|
351
|
+
# The reverse is deliberately not true -- an untagged create never
|
|
352
|
+
# claims a tagged node, because a tag is a narrower identity than the
|
|
353
|
+
# commit under it and two tags can share one commit.
|
|
354
|
+
#
|
|
355
|
+
# AGE has no NULL equality, so nullable comparisons go through COALESCE
|
|
356
|
+
# to a sentinel.
|
|
357
|
+
_RELEASE_BY_TAG: typing.Final[typing.LiteralString] = """
|
|
358
|
+
MATCH (p:Project {{id: {project_id}}})-[:HAS_RELEASE]->(r:Release)
|
|
359
|
+
WHERE r.tag = {tag}
|
|
360
|
+
RETURN r.id AS id
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
_RELEASE_BY_COMMITTISH: typing.Final[typing.LiteralString] = """
|
|
364
|
+
MATCH (p:Project {{id: {project_id}}})-[:HAS_RELEASE]->(r:Release)
|
|
365
|
+
WHERE r.committish = {committish} AND COALESCE(r.tag, '') = ''
|
|
366
|
+
RETURN r.id AS id
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
# The predicate has to be the same one ``_RELEASE_BY_COMMITTISH``
|
|
370
|
+
# matched on. ``COALESCE(r.tag, {tag})`` replaces only NULL, so a node
|
|
371
|
+
# stored with an empty-string tag would be matched as untagged, adopted,
|
|
372
|
+
# and left untagged -- dropping the incoming tag and answering 200 with
|
|
373
|
+
# a release that still has none. ``ReleaseCreate`` now normalizes ``''``
|
|
374
|
+
# to NULL so this endpoint cannot create such a node, but rows predating
|
|
375
|
+
# that (or written by another client) still have to be nameable.
|
|
376
|
+
_RELEASE_ADOPT_TAG: typing.Final[typing.LiteralString] = """
|
|
377
|
+
MATCH (r:Release {{id: {release_id}}})
|
|
378
|
+
SET r.tag = CASE WHEN COALESCE(r.tag, '') = '' THEN {tag} ELSE r.tag END,
|
|
379
|
+
r.updated_at = {now}
|
|
380
|
+
RETURN r.id AS id
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class _ExistingRelease(typing.NamedTuple):
|
|
385
|
+
"""A release a create would duplicate, and how it was matched."""
|
|
386
|
+
|
|
387
|
+
id: str
|
|
388
|
+
#: False when the match came from the committish, meaning the node is
|
|
389
|
+
#: untagged and a tagged create should adopt it.
|
|
390
|
+
by_tag: bool
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
async def _find_existing_release(
|
|
394
|
+
db: graph.Graph,
|
|
395
|
+
*,
|
|
396
|
+
project_id: str,
|
|
397
|
+
tag: str | None,
|
|
398
|
+
committish: str,
|
|
399
|
+
) -> _ExistingRelease | None:
|
|
400
|
+
"""Return the release this create would duplicate, if any."""
|
|
401
|
+
if tag:
|
|
402
|
+
rows = await db.execute(
|
|
403
|
+
_RELEASE_BY_TAG,
|
|
404
|
+
{'project_id': project_id, 'tag': tag},
|
|
405
|
+
['id'],
|
|
406
|
+
)
|
|
407
|
+
if rows:
|
|
408
|
+
return _ExistingRelease(
|
|
409
|
+
str(graph.parse_agtype(rows[0]['id'])), True
|
|
410
|
+
)
|
|
411
|
+
rows = await db.execute(
|
|
412
|
+
_RELEASE_BY_COMMITTISH,
|
|
413
|
+
{'project_id': project_id, 'committish': committish},
|
|
414
|
+
['id'],
|
|
415
|
+
)
|
|
416
|
+
if rows:
|
|
417
|
+
return _ExistingRelease(str(graph.parse_agtype(rows[0]['id'])), False)
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
async def _adopt_release_tag(
|
|
422
|
+
db: graph.Graph,
|
|
423
|
+
*,
|
|
424
|
+
release_id: str,
|
|
425
|
+
tag: str,
|
|
426
|
+
) -> None:
|
|
427
|
+
"""Name an untagged release, leaving an already-tagged one alone."""
|
|
428
|
+
await db.execute(
|
|
429
|
+
_RELEASE_ADOPT_TAG,
|
|
430
|
+
{
|
|
431
|
+
'release_id': release_id,
|
|
432
|
+
'tag': tag,
|
|
433
|
+
'now': datetime.datetime.now(datetime.UTC).isoformat(),
|
|
434
|
+
},
|
|
435
|
+
['id'],
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
311
439
|
#: Alias kept for readers of this module; the map is shared with the
|
|
312
440
|
#: sweeper, which folds the same plugin answers into the same statuses.
|
|
313
441
|
_RUN_STATUS_TO_EVENT_STATUS = deployment_nodes.RUN_STATUS_TO_STATUS
|
|
@@ -588,12 +716,26 @@ async def _hydrate_release_train(
|
|
|
588
716
|
# -- Endpoints ----------------------------------------------------------
|
|
589
717
|
|
|
590
718
|
|
|
591
|
-
@releases_router.post(
|
|
719
|
+
@releases_router.post(
|
|
720
|
+
'/',
|
|
721
|
+
status_code=201,
|
|
722
|
+
response_model=ReleaseResponse,
|
|
723
|
+
responses={
|
|
724
|
+
200: {
|
|
725
|
+
'model': ReleaseResponse,
|
|
726
|
+
'description': (
|
|
727
|
+
'The release already existed; it is returned unchanged '
|
|
728
|
+
'rather than conflicting.'
|
|
729
|
+
),
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
)
|
|
592
733
|
async def create_release(
|
|
593
734
|
org_slug: str,
|
|
594
735
|
project_id: str,
|
|
595
736
|
data: ReleaseCreate,
|
|
596
737
|
db: graph.Pool,
|
|
738
|
+
response: fastapi.Response,
|
|
597
739
|
background_tasks: fastapi.BackgroundTasks,
|
|
598
740
|
auth: typing.Annotated[
|
|
599
741
|
permissions.AuthContext,
|
|
@@ -602,46 +744,64 @@ async def create_release(
|
|
|
602
744
|
),
|
|
603
745
|
],
|
|
604
746
|
) -> ReleaseResponse:
|
|
605
|
-
"""Create a
|
|
747
|
+
"""Create a release for a project, or return the one that exists.
|
|
748
|
+
|
|
749
|
+
The call is idempotent: a release matching the body is returned with
|
|
750
|
+
``200`` rather than conflicting, because more than one webhook can
|
|
751
|
+
announce the same release. A ``deployment_status`` delivery and a
|
|
752
|
+
``release`` delivery describe one artifact from two angles, and the
|
|
753
|
+
first to arrive must not make the second an error -- nor a second
|
|
754
|
+
node. Only a first create answers ``201``.
|
|
755
|
+
"""
|
|
606
756
|
if not await _project_exists(db, org_slug, project_id):
|
|
607
757
|
raise fastapi.HTTPException(
|
|
608
758
|
status_code=404,
|
|
609
759
|
detail=f'Project {project_id!r} not found',
|
|
610
760
|
)
|
|
611
761
|
|
|
612
|
-
#
|
|
613
|
-
#
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
# duplicate. Only an untagged release falls back to the committish.
|
|
618
|
-
# AGE has no NULL equality, so nullable comparisons go through
|
|
619
|
-
# COALESCE to a sentinel.
|
|
620
|
-
existing_query: typing.LiteralString = """
|
|
621
|
-
MATCH (p:Project {{id: {project_id}}})-[:HAS_RELEASE]->(r:Release)
|
|
622
|
-
WHERE (COALESCE({tag}, '') <> '' AND r.tag = {tag})
|
|
623
|
-
OR (COALESCE({tag}, '') = ''
|
|
624
|
-
AND r.committish = {committish}
|
|
625
|
-
AND COALESCE(r.tag, '') = '')
|
|
626
|
-
RETURN r.id AS id
|
|
627
|
-
"""
|
|
628
|
-
existing = await db.execute(
|
|
629
|
-
existing_query,
|
|
630
|
-
{
|
|
631
|
-
'project_id': project_id,
|
|
632
|
-
'committish': data.committish,
|
|
633
|
-
'tag': data.tag,
|
|
634
|
-
},
|
|
635
|
-
['id'],
|
|
762
|
+
# Local import avoids a circular import: project_deployments imports
|
|
763
|
+
# append_deployment_event from this module.
|
|
764
|
+
from imbi.api.endpoints.project_deployments import (
|
|
765
|
+
fetch_release_notes_for_tag,
|
|
766
|
+
resolve_committish_for_tag,
|
|
636
767
|
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
768
|
+
|
|
769
|
+
committish = data.committish
|
|
770
|
+
if committish is None:
|
|
771
|
+
# Guaranteed by ReleaseCreate.validate_identity.
|
|
772
|
+
assert data.tag is not None # noqa: S101
|
|
773
|
+
committish = await resolve_committish_for_tag(
|
|
774
|
+
db,
|
|
775
|
+
org_slug=org_slug,
|
|
776
|
+
project_id=project_id,
|
|
777
|
+
tag=data.tag,
|
|
778
|
+
auth=auth,
|
|
644
779
|
)
|
|
780
|
+
if committish is None:
|
|
781
|
+
raise fastapi.HTTPException(
|
|
782
|
+
status_code=422,
|
|
783
|
+
detail=(
|
|
784
|
+
f'Could not resolve tag {data.tag!r} to a commit for'
|
|
785
|
+
f' project {project_id!r}; supply committish explicitly'
|
|
786
|
+
),
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
match = await _find_existing_release(
|
|
790
|
+
db, project_id=project_id, tag=data.tag, committish=committish
|
|
791
|
+
)
|
|
792
|
+
if match is not None:
|
|
793
|
+
# A tagged create that matched an untagged node adopts it: the
|
|
794
|
+
# deployment webhook records the commit before the release
|
|
795
|
+
# webhook names it, and leaving them apart is what produces the
|
|
796
|
+
# duplicate this endpoint exists to avoid.
|
|
797
|
+
if data.tag and not match.by_tag:
|
|
798
|
+
await _adopt_release_tag(db, release_id=match.id, tag=data.tag)
|
|
799
|
+
existing = await _fetch_release(db, org_slug, project_id, match.id)
|
|
800
|
+
if existing is not None:
|
|
801
|
+
response.status_code = 200
|
|
802
|
+
return _release_to_response(existing, project_id)
|
|
803
|
+
# Raced with a delete between the two reads; fall through and
|
|
804
|
+
# create the node rather than 404 a create.
|
|
645
805
|
|
|
646
806
|
# Spec calls for ``auth.user.username``; the User model uses
|
|
647
807
|
# ``email`` as the principal identity (no ``username`` field).
|
|
@@ -656,12 +816,6 @@ async def create_release(
|
|
|
656
816
|
# and the release is still created (identified by committish + tag).
|
|
657
817
|
description = data.description
|
|
658
818
|
if data.tag and not description:
|
|
659
|
-
# Local import avoids a circular import: project_deployments
|
|
660
|
-
# imports append_deployment_event from this module.
|
|
661
|
-
from imbi.api.endpoints.project_deployments import (
|
|
662
|
-
fetch_release_notes_for_tag,
|
|
663
|
-
)
|
|
664
|
-
|
|
665
819
|
description = await fetch_release_notes_for_tag(
|
|
666
820
|
db,
|
|
667
821
|
org_slug=org_slug,
|
|
@@ -673,7 +827,7 @@ async def create_release(
|
|
|
673
827
|
props: dict[str, typing.Any] = {
|
|
674
828
|
'id': nanoid.generate(),
|
|
675
829
|
'tag': data.tag,
|
|
676
|
-
'committish':
|
|
830
|
+
'committish': committish,
|
|
677
831
|
'title': data.title,
|
|
678
832
|
'description': description,
|
|
679
833
|
'links': _serialize_links(data.links),
|
|
@@ -6399,6 +6399,146 @@ class ResolveRemoteTagsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
6399
6399
|
await self._resolve(['1.0.0'])
|
|
6400
6400
|
|
|
6401
6401
|
|
|
6402
|
+
class ResolveCommittishForTagTestCase(unittest.IsolatedAsyncioTestCase):
|
|
6403
|
+
"""The tag lookup behind a tag-only release create.
|
|
6404
|
+
|
|
6405
|
+
``create_release`` rejects the release with a 422 when this answers
|
|
6406
|
+
``None``, so the same absence-vs-failure distinction
|
|
6407
|
+
``resolve_remote_tags`` draws has to hold here: only a positive "no
|
|
6408
|
+
such ref" may answer ``None``. A failure to reach the remote must
|
|
6409
|
+
raise, or a timeout would read as an invalid tag.
|
|
6410
|
+
"""
|
|
6411
|
+
|
|
6412
|
+
def setUp(self) -> None:
|
|
6413
|
+
self.resolve_committish = mock.AsyncMock(
|
|
6414
|
+
return_value=mock.Mock(sha='9b2356a5da56631e3faf75bc5710bad1')
|
|
6415
|
+
)
|
|
6416
|
+
for target, replacement in (
|
|
6417
|
+
('_resolve_and_context', mock.AsyncMock()),
|
|
6418
|
+
(
|
|
6419
|
+
'_handler',
|
|
6420
|
+
mock.Mock(
|
|
6421
|
+
return_value=mock.Mock(
|
|
6422
|
+
resolve_committish=self.resolve_committish
|
|
6423
|
+
)
|
|
6424
|
+
),
|
|
6425
|
+
),
|
|
6426
|
+
('_resolve_credentials', mock.Mock(return_value={})),
|
|
6427
|
+
('persist_link_writeback', mock.AsyncMock()),
|
|
6428
|
+
):
|
|
6429
|
+
patcher = mock.patch(f'{_MODULE}.{target}', replacement)
|
|
6430
|
+
self.addCleanup(patcher.stop)
|
|
6431
|
+
patcher.start()
|
|
6432
|
+
project_deployments._resolve_and_context.return_value = (
|
|
6433
|
+
mock.Mock(plugin_slug='github'),
|
|
6434
|
+
mock.Mock(environment_config=None),
|
|
6435
|
+
{},
|
|
6436
|
+
)
|
|
6437
|
+
|
|
6438
|
+
async def _resolve(self, tag: str = '5.0.2') -> str | None:
|
|
6439
|
+
return await project_deployments.resolve_committish_for_tag(
|
|
6440
|
+
mock.AsyncMock(),
|
|
6441
|
+
org_slug='octo',
|
|
6442
|
+
project_id='p1',
|
|
6443
|
+
tag=tag,
|
|
6444
|
+
auth=mock.Mock(),
|
|
6445
|
+
)
|
|
6446
|
+
|
|
6447
|
+
async def test_resolves_the_tag_to_a_short_committish(self) -> None:
|
|
6448
|
+
self.assertEqual('9b2356a', await self._resolve())
|
|
6449
|
+
|
|
6450
|
+
async def test_a_remote_404_answers_none(self) -> None:
|
|
6451
|
+
self.resolve_committish.side_effect = _http_status_error(404)
|
|
6452
|
+
self.assertIsNone(await self._resolve())
|
|
6453
|
+
|
|
6454
|
+
async def test_a_remote_422_answers_none(self) -> None:
|
|
6455
|
+
self.resolve_committish.side_effect = _http_status_error(422)
|
|
6456
|
+
self.assertIsNone(await self._resolve())
|
|
6457
|
+
|
|
6458
|
+
async def test_no_capability_answers_none(self) -> None:
|
|
6459
|
+
project_deployments._resolve_and_context.side_effect = (
|
|
6460
|
+
fastapi.HTTPException(status_code=404, detail='none')
|
|
6461
|
+
)
|
|
6462
|
+
self.assertIsNone(await self._resolve())
|
|
6463
|
+
|
|
6464
|
+
async def test_a_missing_credential_propagates_its_503(self) -> None:
|
|
6465
|
+
# ``_resolve_and_context`` raises this deliberately, and its own
|
|
6466
|
+
# comment calls a retry reasonable. Reading it as an absent tag
|
|
6467
|
+
# is the same mistake as swallowing the timeout, one call up.
|
|
6468
|
+
project_deployments._resolve_and_context.side_effect = (
|
|
6469
|
+
fastapi.HTTPException(
|
|
6470
|
+
status_code=503,
|
|
6471
|
+
detail={'error': 'no_service_credential'},
|
|
6472
|
+
)
|
|
6473
|
+
)
|
|
6474
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6475
|
+
await self._resolve()
|
|
6476
|
+
self.assertEqual(503, ctx.exception.status_code)
|
|
6477
|
+
|
|
6478
|
+
async def test_an_authorization_failure_propagates_its_403(self) -> None:
|
|
6479
|
+
# The gateway posts as a service principal, so act-as-service
|
|
6480
|
+
# checks are reachable here. Not an answer about the tag.
|
|
6481
|
+
project_deployments._resolve_and_context.side_effect = (
|
|
6482
|
+
fastapi.HTTPException(status_code=403, detail='nope')
|
|
6483
|
+
)
|
|
6484
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6485
|
+
await self._resolve()
|
|
6486
|
+
self.assertEqual(403, ctx.exception.status_code)
|
|
6487
|
+
|
|
6488
|
+
async def test_an_ambiguous_capability_propagates_its_400(self) -> None:
|
|
6489
|
+
# "Multiple integrations provide this capability" is a config
|
|
6490
|
+
# error to surface, not a tag that does not exist.
|
|
6491
|
+
project_deployments._resolve_and_context.side_effect = (
|
|
6492
|
+
fastapi.HTTPException(status_code=400, detail='ambiguous')
|
|
6493
|
+
)
|
|
6494
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6495
|
+
await self._resolve()
|
|
6496
|
+
self.assertEqual(400, ctx.exception.status_code)
|
|
6497
|
+
|
|
6498
|
+
async def test_an_unimplemented_plugin_answers_none(self) -> None:
|
|
6499
|
+
self.resolve_committish.side_effect = NotImplementedError
|
|
6500
|
+
self.assertIsNone(await self._resolve())
|
|
6501
|
+
|
|
6502
|
+
async def test_a_remote_500_raises_503_rather_than_absence(self) -> None:
|
|
6503
|
+
self.resolve_committish.side_effect = _http_status_error(500)
|
|
6504
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6505
|
+
await self._resolve()
|
|
6506
|
+
self.assertEqual(503, ctx.exception.status_code)
|
|
6507
|
+
|
|
6508
|
+
async def test_a_remote_403_raises_503_rather_than_absence(self) -> None:
|
|
6509
|
+
# A revoked credential is not the tag failing to exist.
|
|
6510
|
+
self.resolve_committish.side_effect = _http_status_error(403)
|
|
6511
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6512
|
+
await self._resolve()
|
|
6513
|
+
self.assertEqual(503, ctx.exception.status_code)
|
|
6514
|
+
|
|
6515
|
+
async def test_a_transport_failure_raises_503(self) -> None:
|
|
6516
|
+
self.resolve_committish.side_effect = httpx.ConnectError('down')
|
|
6517
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6518
|
+
await self._resolve()
|
|
6519
|
+
self.assertEqual(503, ctx.exception.status_code)
|
|
6520
|
+
|
|
6521
|
+
async def test_a_timeout_keeps_its_503_and_retry_after(self) -> None:
|
|
6522
|
+
# ``call_with_timeout`` raises this; swallowing it turned a
|
|
6523
|
+
# "retry in 5s" into a terminal "your tag is invalid".
|
|
6524
|
+
self.resolve_committish.side_effect = fastapi.HTTPException(
|
|
6525
|
+
status_code=503,
|
|
6526
|
+
detail='Plugin timed out',
|
|
6527
|
+
headers={'Retry-After': '5'},
|
|
6528
|
+
)
|
|
6529
|
+
with self.assertRaises(fastapi.HTTPException) as ctx:
|
|
6530
|
+
await self._resolve()
|
|
6531
|
+
self.assertEqual(503, ctx.exception.status_code)
|
|
6532
|
+
self.assertEqual({'Retry-After': '5'}, ctx.exception.headers)
|
|
6533
|
+
|
|
6534
|
+
async def test_rate_limiting_propagates(self) -> None:
|
|
6535
|
+
self.resolve_committish.side_effect = plugin_errors.PluginRateLimited(
|
|
6536
|
+
retry_at=1.0
|
|
6537
|
+
)
|
|
6538
|
+
with self.assertRaises(plugin_errors.PluginRateLimited):
|
|
6539
|
+
await self._resolve()
|
|
6540
|
+
|
|
6541
|
+
|
|
6402
6542
|
class IngestDriftNotesTests(ProjectDeploymentsTestCase):
|
|
6403
6543
|
URL = '/organizations/myorg/projects/proj1/deployments/drift-notes'
|
|
6404
6544
|
BODY: typing.ClassVar[dict[str, str]] = {
|