imbi-api 2.36.0__tar.gz → 2.37.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.36.0 → imbi_api-2.37.0}/PKG-INFO +2 -2
- {imbi_api-2.36.0 → imbi_api-2.37.0}/pyproject.toml +2 -2
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_helpers.py +150 -1
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_analysis.py +22 -1
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_deployments.py +33 -17
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/projects.py +25 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/operations.py +62 -12
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/registry.py +16 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/lifecycle_dispatch.py +24 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_analysis.py +85 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_deployments.py +41 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_projects.py +40 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_service_writeback.py +107 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_lifecycle_dispatch.py +69 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_operations.py +60 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/.gitignore +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/README.md +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/moon.yml +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/README.md +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/app.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/authorization_codes.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/autonomous.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/internal_services.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/local_auth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/login_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/membership.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/oauth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/oauth_clients.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/password.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/permissions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/principals.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/seed.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/sessions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/tokens.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/auth/totp.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/backfill_embeddings.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/blueprint_attributes.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/blueprint_compliance.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_pushed_at_repair.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/commit_sync/service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/component_facts.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_status_repair.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sweeper.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/deployment_sync/service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/documents/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/documents/read_sweeper.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/domain/scoring.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/drift.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/client.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/dependencies.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/base.html +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/base.txt +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/password_reset.html +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/welcome.html +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates/welcome.txt +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/email/templates.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_events.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_history.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_pagination.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/_search_index.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/admin.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/ai_models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/api_keys.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/auth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/blueprints.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/comments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/components.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/dashboard.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_likes.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_presence.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_templates.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/document_versions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/documents.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/environments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/events.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/graph_query.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/integrations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/local_auth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/maintenance.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/mfa.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/operations_log.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/organizations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_logs.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/project_types.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/releases.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/roles.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/scoring.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/search.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/status.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/tags.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/teams.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/uploads.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/user_activity.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/users.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/endpoints/webhooks.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/entrypoint.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/graph_sql.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/attribution.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/endpoints.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/errors.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/flows.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/host_integration.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/repository.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/resolution.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/state.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/identity/sweeper.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/lifespans.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/llm/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/llm/dependencies.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/log.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/state.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/maintenance/worker.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/mcp_test.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/middleware/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/middleware/rate_limit.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/openapi.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/orphan_releases.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/assignments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/installer.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/lifecycle.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/reload.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/resolution.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/plugins/schemas.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/pr_sync/service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/prompts/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/py.typed +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/relationships.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/release_promote/service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/sbom.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/scoring/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/scoring/queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/settings.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/client.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/dependencies.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/thumbnails.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/src/imbi/api/storage/validation.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_internal_services.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_audit_clickhouse.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_client.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_init.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_integration.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/email/test_templates.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_ai_models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_ai_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_components.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_analytics.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_likes.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_presence.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_reads.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_reads_sql.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_document_versions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_iggy_connectors.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_integrations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_maintenance.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_integrations.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_search_index.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_state.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/__init__.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_client.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_init.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/support.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_app.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_blueprints.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_pushed_at_repair.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_component_facts.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_status_repair.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sweeper.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sync_queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_deployment_sync_service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_drift.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_init.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_installer.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_lifespans.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_log.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_state.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_maintenance_worker.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_models.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_openapi.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_orphan_releases.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins_assignments.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_plugins_credentials.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pr_sync_service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_projects_drift_ranges.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_pull_requests_pending_deploy.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_release_promote_service.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_sbom.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.36.0 → imbi_api-2.37.0}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.37.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-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.
|
|
22
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.37.0
|
|
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.
|
|
3
|
+
version = "2.37.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.37.0",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -14,7 +14,11 @@ import psycopg
|
|
|
14
14
|
from imbi.common import graph
|
|
15
15
|
from imbi.common import models as common_models
|
|
16
16
|
from imbi.common import patch as json_patch
|
|
17
|
-
from imbi.common.plugins.base import
|
|
17
|
+
from imbi.common.plugins.base import (
|
|
18
|
+
PluginContext,
|
|
19
|
+
ProjectDependency,
|
|
20
|
+
ServiceConnection,
|
|
21
|
+
)
|
|
18
22
|
|
|
19
23
|
LOGGER = logging.getLogger(__name__)
|
|
20
24
|
|
|
@@ -523,6 +527,151 @@ async def lookup_project_exists_in(
|
|
|
523
527
|
return connections
|
|
524
528
|
|
|
525
529
|
|
|
530
|
+
_DEPENDENCY_QUERIES: dict[str, typing.LiteralString] = {
|
|
531
|
+
'outbound': (
|
|
532
|
+
'MATCH (p:Project {{id: {project_id}}})-[:DEPENDS_ON]->(n:Project) '
|
|
533
|
+
'OPTIONAL MATCH (n)-[ei:EXISTS_IN]->(i:Integration) '
|
|
534
|
+
'RETURN n.id AS project_id, n.slug AS project_slug, '
|
|
535
|
+
'i.slug AS integration_slug, ei.identifier AS identifier, '
|
|
536
|
+
'ei.canonical_url AS canonical_url'
|
|
537
|
+
),
|
|
538
|
+
'inbound': (
|
|
539
|
+
'MATCH (p:Project {{id: {project_id}}})<-[:DEPENDS_ON]-(n:Project) '
|
|
540
|
+
'OPTIONAL MATCH (n)-[ei:EXISTS_IN]->(i:Integration) '
|
|
541
|
+
'RETURN n.id AS project_id, n.slug AS project_slug, '
|
|
542
|
+
'i.slug AS integration_slug, ei.identifier AS identifier, '
|
|
543
|
+
'ei.canonical_url AS canonical_url'
|
|
544
|
+
),
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
async def lookup_project_dependencies(
|
|
549
|
+
db: graph.Graph,
|
|
550
|
+
project_id: str,
|
|
551
|
+
) -> list[ProjectDependency] | None:
|
|
552
|
+
"""Return the project's ``DEPENDS_ON`` neighbours in both directions.
|
|
553
|
+
|
|
554
|
+
Each :class:`ProjectDependency` carries the neighbour's own
|
|
555
|
+
``EXISTS_IN`` connections. Populated onto
|
|
556
|
+
:attr:`PluginContext.dependencies`. Returns ``None`` on lookup
|
|
557
|
+
failure, so callers can tell "no dependencies" from "unknown".
|
|
558
|
+
When the result is ``None``, the host must not wire
|
|
559
|
+
:attr:`PluginContext.resolve_linked_identifiers`: without a
|
|
560
|
+
resolver, capabilities do not remove remote state.
|
|
561
|
+
"""
|
|
562
|
+
columns = [
|
|
563
|
+
'project_id',
|
|
564
|
+
'project_slug',
|
|
565
|
+
'integration_slug',
|
|
566
|
+
'identifier',
|
|
567
|
+
'canonical_url',
|
|
568
|
+
]
|
|
569
|
+
dependencies: list[ProjectDependency] = []
|
|
570
|
+
for direction, query in _DEPENDENCY_QUERIES.items():
|
|
571
|
+
try:
|
|
572
|
+
records = await db.execute(
|
|
573
|
+
query, {'project_id': project_id}, columns
|
|
574
|
+
)
|
|
575
|
+
except Exception: # noqa: BLE001
|
|
576
|
+
LOGGER.warning('Project dependency lookup failed', exc_info=True)
|
|
577
|
+
return None
|
|
578
|
+
by_id: dict[str, ProjectDependency] = {}
|
|
579
|
+
for r in records:
|
|
580
|
+
neighbour_id = graph.parse_agtype(r.get('project_id'))
|
|
581
|
+
if not neighbour_id:
|
|
582
|
+
continue
|
|
583
|
+
dep = by_id.get(str(neighbour_id))
|
|
584
|
+
if dep is None:
|
|
585
|
+
dep = ProjectDependency(
|
|
586
|
+
direction=typing.cast(
|
|
587
|
+
typing.Literal['inbound', 'outbound'], direction
|
|
588
|
+
),
|
|
589
|
+
project_id=str(neighbour_id),
|
|
590
|
+
project_slug=str(
|
|
591
|
+
graph.parse_agtype(r.get('project_slug')) or ''
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
by_id[dep.project_id] = dep
|
|
595
|
+
slug = graph.parse_agtype(r.get('integration_slug'))
|
|
596
|
+
identifier = graph.parse_agtype(r.get('identifier'))
|
|
597
|
+
if not slug or not identifier:
|
|
598
|
+
continue
|
|
599
|
+
canonical_url = graph.parse_agtype(r.get('canonical_url'))
|
|
600
|
+
dep.service_connections.append(
|
|
601
|
+
ServiceConnection(
|
|
602
|
+
integration_slug=str(slug),
|
|
603
|
+
identifier=str(identifier),
|
|
604
|
+
canonical_url=(
|
|
605
|
+
None if canonical_url is None else str(canonical_url)
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
dependencies.extend(by_id.values())
|
|
610
|
+
return dependencies
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
async def lookup_linked_identifiers(
|
|
614
|
+
db: graph.Graph,
|
|
615
|
+
org_slug: str,
|
|
616
|
+
integration_slug: str,
|
|
617
|
+
identifiers: list[str],
|
|
618
|
+
) -> set[str]:
|
|
619
|
+
"""Return the ``identifiers`` bound to a project in ``org_slug``.
|
|
620
|
+
|
|
621
|
+
Matches ``EXISTS_IN`` edges to the ``integration_slug`` Integration
|
|
622
|
+
from any project owned by a team in the organization. Returns an
|
|
623
|
+
empty set on lookup failure, so a caller that removes remote state
|
|
624
|
+
only for linked identifiers fails safe.
|
|
625
|
+
"""
|
|
626
|
+
if not identifiers:
|
|
627
|
+
return set()
|
|
628
|
+
query: typing.LiteralString = (
|
|
629
|
+
'MATCH (:Organization {{slug: {org_slug}}})<-[:BELONGS_TO]-(:Team)'
|
|
630
|
+
'<-[:OWNED_BY]-(:Project)-[ei:EXISTS_IN]->'
|
|
631
|
+
'(:Integration {{slug: {integration_slug}}}) '
|
|
632
|
+
'WHERE ei.identifier IN {identifiers} '
|
|
633
|
+
'RETURN DISTINCT ei.identifier AS identifier'
|
|
634
|
+
)
|
|
635
|
+
try:
|
|
636
|
+
records = await db.execute(
|
|
637
|
+
query,
|
|
638
|
+
{
|
|
639
|
+
'org_slug': org_slug,
|
|
640
|
+
'integration_slug': integration_slug,
|
|
641
|
+
'identifiers': identifiers,
|
|
642
|
+
},
|
|
643
|
+
['identifier'],
|
|
644
|
+
)
|
|
645
|
+
except Exception: # noqa: BLE001
|
|
646
|
+
LOGGER.debug('Linked identifier lookup failed', exc_info=True)
|
|
647
|
+
return set()
|
|
648
|
+
return {
|
|
649
|
+
str(value)
|
|
650
|
+
for r in records
|
|
651
|
+
if (value := graph.parse_agtype(r.get('identifier')))
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def linked_identifier_resolver(
|
|
656
|
+
db: graph.Graph, org_slug: str, integration_slug: str | None
|
|
657
|
+
) -> (
|
|
658
|
+
collections.abc.Callable[[list[str]], collections.abc.Awaitable[set[str]]]
|
|
659
|
+
| None
|
|
660
|
+
):
|
|
661
|
+
"""Build the :attr:`PluginContext.resolve_linked_identifiers` hook.
|
|
662
|
+
|
|
663
|
+
Returns ``None`` for a capability not bound to an Integration.
|
|
664
|
+
"""
|
|
665
|
+
if not integration_slug:
|
|
666
|
+
return None
|
|
667
|
+
slug = integration_slug
|
|
668
|
+
|
|
669
|
+
async def _resolve(identifiers: list[str]) -> set[str]:
|
|
670
|
+
return await lookup_linked_identifiers(db, org_slug, slug, identifiers)
|
|
671
|
+
|
|
672
|
+
return _resolve
|
|
673
|
+
|
|
674
|
+
|
|
526
675
|
async def persist_service_writeback(
|
|
527
676
|
db: graph.Graph, ctx: PluginContext
|
|
528
677
|
) -> None:
|
|
@@ -29,6 +29,8 @@ from imbi.api.blueprint_compliance import (
|
|
|
29
29
|
remediate_blueprint,
|
|
30
30
|
)
|
|
31
31
|
from imbi.api.endpoints._helpers import (
|
|
32
|
+
linked_identifier_resolver,
|
|
33
|
+
lookup_project_dependencies,
|
|
32
34
|
lookup_project_exists_in,
|
|
33
35
|
lookup_project_links,
|
|
34
36
|
lookup_project_slugs,
|
|
@@ -110,6 +112,7 @@ async def _build_context(
|
|
|
110
112
|
project_links = await lookup_project_links(db, project_id)
|
|
111
113
|
project_type_slugs = await lookup_project_type_slugs(db, project_id)
|
|
112
114
|
service_connections = await lookup_project_exists_in(db, project_id)
|
|
115
|
+
dependencies = await lookup_project_dependencies(db, project_id)
|
|
113
116
|
return PluginContext(
|
|
114
117
|
project_id=project_id,
|
|
115
118
|
project_slug=project_slug,
|
|
@@ -122,6 +125,16 @@ async def _build_context(
|
|
|
122
125
|
project_type_slugs=project_type_slugs,
|
|
123
126
|
integration_slug=resolved.integration_slug,
|
|
124
127
|
service_connections=service_connections,
|
|
128
|
+
dependencies=dependencies or [],
|
|
129
|
+
# No resolver when the dependency lookup failed, so a fix does
|
|
130
|
+
# not remove remote state on partial data.
|
|
131
|
+
resolve_linked_identifiers=(
|
|
132
|
+
None
|
|
133
|
+
if dependencies is None
|
|
134
|
+
else linked_identifier_resolver(
|
|
135
|
+
db, org_slug, resolved.integration_slug
|
|
136
|
+
)
|
|
137
|
+
),
|
|
125
138
|
)
|
|
126
139
|
|
|
127
140
|
|
|
@@ -669,6 +682,7 @@ async def remediate_all_for_project(
|
|
|
669
682
|
org_slug: str,
|
|
670
683
|
project_id: str,
|
|
671
684
|
auth: permissions.AuthContext,
|
|
685
|
+
sweepable_only: bool = False,
|
|
672
686
|
) -> RemediateAllResponse | None:
|
|
673
687
|
"""Apply every fixable finding in a project's report (best-effort).
|
|
674
688
|
|
|
@@ -678,7 +692,10 @@ async def remediate_all_for_project(
|
|
|
678
692
|
re-run once at the end so the returned report reflects every fix.
|
|
679
693
|
|
|
680
694
|
Shared by the per-project endpoint and the ``remediate`` maintenance
|
|
681
|
-
sweep, so both apply findings identically.
|
|
695
|
+
sweep, so both apply findings identically. ``sweepable_only``
|
|
696
|
+
restricts the pass to offers marked
|
|
697
|
+
:attr:`RemediationOffer.sweepable` (the ``sync-fixes`` sweep) and
|
|
698
|
+
skips the closing re-analysis when nothing was applied.
|
|
682
699
|
"""
|
|
683
700
|
report = await _fetch_report(db, project_id)
|
|
684
701
|
if report is None:
|
|
@@ -692,6 +709,8 @@ async def remediate_all_for_project(
|
|
|
692
709
|
for finding in report.results:
|
|
693
710
|
if finding.remediation is None:
|
|
694
711
|
continue
|
|
712
|
+
if sweepable_only and not finding.remediation.sweepable:
|
|
713
|
+
continue
|
|
695
714
|
try:
|
|
696
715
|
result = await _remediate_one(
|
|
697
716
|
db,
|
|
@@ -724,6 +743,8 @@ async def remediate_all_for_project(
|
|
|
724
743
|
result=result,
|
|
725
744
|
)
|
|
726
745
|
)
|
|
746
|
+
if sweepable_only and not outcomes:
|
|
747
|
+
return RemediateAllResponse(outcomes=outcomes, report=report)
|
|
727
748
|
fresh = await run_and_persist(db, org_slug, project_id, auth)
|
|
728
749
|
return RemediateAllResponse(outcomes=outcomes, report=fresh)
|
|
729
750
|
|
|
@@ -5263,23 +5263,39 @@ async def complete_promote_build(
|
|
|
5263
5263
|
resolved.plugin_slug,
|
|
5264
5264
|
run.run_id,
|
|
5265
5265
|
)
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5266
|
+
# The deployment is dispatched and will run whatever happens next, so
|
|
5267
|
+
# recording it is best-effort from here on. Raising would abandon the
|
|
5268
|
+
# promote before the audit row below is written and before the watcher
|
|
5269
|
+
# closes the deployment -- the two writes that attribute it to
|
|
5270
|
+
# ``requested_by`` -- leaving the webhook's anonymous event as the only
|
|
5271
|
+
# record and "Deployed by" blank. The watcher's close re-creates the
|
|
5272
|
+
# node on the same ``external_run_id`` if this write is lost.
|
|
5273
|
+
try:
|
|
5274
|
+
appended = await append_deployment_event(
|
|
5275
|
+
db,
|
|
5276
|
+
org_slug=org_slug,
|
|
5277
|
+
project_id=project_id,
|
|
5278
|
+
release_id=release_id,
|
|
5279
|
+
env_slug=to_environment,
|
|
5280
|
+
status='in_progress',
|
|
5281
|
+
note=f'via {resolved.plugin_slug}',
|
|
5282
|
+
external_run_id=str(run.run_id) if run.run_id else None,
|
|
5283
|
+
external_run_url=run.run_url,
|
|
5284
|
+
# The person who pressed promote, not the worker that got here.
|
|
5285
|
+
performed_by=requested_by or None,
|
|
5286
|
+
credential=ctx.credential_note,
|
|
5287
|
+
# Stamps ``origin`` on the node, which is what lets a reclaimed
|
|
5288
|
+
# job recognize its own in-flight deployment.
|
|
5289
|
+
source='promote',
|
|
5290
|
+
)
|
|
5291
|
+
except Exception:
|
|
5292
|
+
LOGGER.exception(
|
|
5293
|
+
'release-promote could not record the dispatched deployment '
|
|
5294
|
+
'for project %s release %s',
|
|
5295
|
+
project_id,
|
|
5296
|
+
release_id,
|
|
5297
|
+
)
|
|
5298
|
+
appended = None
|
|
5283
5299
|
if isinstance(appended, str):
|
|
5284
5300
|
LOGGER.warning(
|
|
5285
5301
|
'release-promote could not record the deployment event for '
|
|
@@ -2918,6 +2918,29 @@ async def list_project_relationships(
|
|
|
2918
2918
|
)
|
|
2919
2919
|
|
|
2920
2920
|
|
|
2921
|
+
async def _dispatch_dependencies_changed(
|
|
2922
|
+
db: graph.Pool,
|
|
2923
|
+
org_slug: str,
|
|
2924
|
+
project_id: str,
|
|
2925
|
+
auth: permissions.AuthContext,
|
|
2926
|
+
) -> None:
|
|
2927
|
+
"""Tell lifecycle plugins the source project's dependencies changed.
|
|
2928
|
+
|
|
2929
|
+
Only the source is dispatched: its plugin reconciles the dependency
|
|
2930
|
+
in both directions, so the target needs no second call. Failures
|
|
2931
|
+
are logged and never undo the Imbi-side edge change.
|
|
2932
|
+
"""
|
|
2933
|
+
try:
|
|
2934
|
+
await dispatch_lifecycle(
|
|
2935
|
+
db, project_id, org_slug, 'dependencies_changed', auth
|
|
2936
|
+
)
|
|
2937
|
+
except Exception:
|
|
2938
|
+
LOGGER.exception(
|
|
2939
|
+
'Lifecycle dependencies_changed dispatch failed for project %s',
|
|
2940
|
+
project_id,
|
|
2941
|
+
)
|
|
2942
|
+
|
|
2943
|
+
|
|
2921
2944
|
@projects_router.post(
|
|
2922
2945
|
'/{project_id}/relationships/{target_id}',
|
|
2923
2946
|
status_code=204,
|
|
@@ -2975,6 +2998,7 @@ async def create_project_relationship(
|
|
|
2975
2998
|
f'Project {project_id!r} or target {target_id!r} not found'
|
|
2976
2999
|
),
|
|
2977
3000
|
)
|
|
3001
|
+
await _dispatch_dependencies_changed(db, org_slug, project_id, auth)
|
|
2978
3002
|
# The source project gained a dependency, which can change a
|
|
2979
3003
|
# condition-policy score that reads its neighbours. No-op unless a
|
|
2980
3004
|
# condition policy exists.
|
|
@@ -3034,6 +3058,7 @@ async def delete_project_relationship(
|
|
|
3034
3058
|
f'Relationship from {project_id!r} to {target_id!r} not found'
|
|
3035
3059
|
),
|
|
3036
3060
|
)
|
|
3061
|
+
await _dispatch_dependencies_changed(db, org_slug, project_id, auth)
|
|
3037
3062
|
# The source project lost a dependency; re-score it so a condition
|
|
3038
3063
|
# policy reading its neighbours reflects the change. No-op unless a
|
|
3039
3064
|
# condition policy exists.
|
|
@@ -31,6 +31,9 @@ from imbi.common import clickhouse, graph, iggy
|
|
|
31
31
|
from imbi.common import models as common_models
|
|
32
32
|
from imbi.common.plugins.errors import PluginRateLimited
|
|
33
33
|
|
|
34
|
+
if typing.TYPE_CHECKING:
|
|
35
|
+
from imbi.api.endpoints.project_analysis import RemediateOutcome
|
|
36
|
+
|
|
34
37
|
LOGGER = logging.getLogger(__name__)
|
|
35
38
|
|
|
36
39
|
#: ``requested_by`` / ``principal_name`` recorded on work this runs.
|
|
@@ -144,32 +147,79 @@ async def execute_remediate(
|
|
|
144
147
|
return _skip(
|
|
145
148
|
ctx, 'remediate', 'No persisted report, or no fixable findings.'
|
|
146
149
|
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
return _record_remediations(ctx, 'remediate', response.outcomes)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def execute_sync_fixes(
|
|
154
|
+
db: graph.Graph,
|
|
155
|
+
client: valkey.Valkey,
|
|
156
|
+
project_id: str,
|
|
157
|
+
*,
|
|
158
|
+
ctx: log.MaintenanceContext,
|
|
159
|
+
) -> ExecuteOutcome:
|
|
160
|
+
"""Analyze one project, then apply only its sweepable fixes.
|
|
161
|
+
|
|
162
|
+
Unlike ``remediate`` this does not trust a stored report: it runs
|
|
163
|
+
the analysis first so drift that appeared since the last run is
|
|
164
|
+
found, then applies the findings whose offer is marked
|
|
165
|
+
:attr:`~imbi.common.plugins.base.RemediationOffer.sweepable` --
|
|
166
|
+
idempotent fixes that bring a remote back in line with Imbi, never
|
|
167
|
+
ones that create or delete a remote resource.
|
|
168
|
+
"""
|
|
169
|
+
from imbi.api.endpoints import project_analysis
|
|
170
|
+
|
|
171
|
+
org_slug = await _org_slug_for(db, project_id)
|
|
172
|
+
if org_slug is None:
|
|
173
|
+
return _skip(ctx, 'no-organization', _NO_ORG)
|
|
174
|
+
auth = _system_auth()
|
|
175
|
+
await project_analysis.run_and_persist(db, org_slug, project_id, auth)
|
|
176
|
+
response = await project_analysis.remediate_all_for_project(
|
|
177
|
+
db,
|
|
178
|
+
org_slug=org_slug,
|
|
179
|
+
project_id=project_id,
|
|
180
|
+
auth=auth,
|
|
181
|
+
sweepable_only=True,
|
|
182
|
+
)
|
|
183
|
+
if response is None or not response.outcomes:
|
|
184
|
+
return _skip(ctx, 'sync-fixes', 'No sweepable findings.')
|
|
185
|
+
return _record_remediations(ctx, 'sync-fixes', response.outcomes)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _record_remediations(
|
|
189
|
+
ctx: log.MaintenanceContext,
|
|
190
|
+
action: str,
|
|
191
|
+
outcomes: list[RemediateOutcome],
|
|
192
|
+
) -> ExecuteOutcome:
|
|
193
|
+
"""Log a remediation pass and classify the item.
|
|
194
|
+
|
|
195
|
+
One row per remediation that did not work: which finding, which
|
|
196
|
+
plugin, and what it said. Successful fixes stay as a count -- a
|
|
197
|
+
project with forty findings would otherwise write forty rows to say
|
|
198
|
+
nothing an operator will read. Raises
|
|
199
|
+
:class:`MaintenanceItemFailed` when any remediation failed.
|
|
200
|
+
"""
|
|
201
|
+
for outcome in outcomes:
|
|
152
202
|
if outcome.result.status == 'failed':
|
|
153
203
|
ctx.log.record(
|
|
154
204
|
'failed',
|
|
155
|
-
|
|
205
|
+
action,
|
|
156
206
|
outcome.result.message,
|
|
157
207
|
finding=outcome.slug,
|
|
158
208
|
plugin=outcome.plugin_id,
|
|
159
209
|
)
|
|
160
|
-
failed = sum(1 for o in
|
|
161
|
-
fixed = sum(1 for o in
|
|
210
|
+
failed = sum(1 for o in outcomes if o.result.status == 'failed')
|
|
211
|
+
fixed = sum(1 for o in outcomes if o.result.status == 'fixed')
|
|
162
212
|
ctx.log.record(
|
|
163
213
|
'failed' if failed else 'succeeded',
|
|
164
|
-
|
|
165
|
-
f'{fixed} of {len(
|
|
214
|
+
action,
|
|
215
|
+
f'{fixed} of {len(outcomes)} findings fixed.',
|
|
166
216
|
fixed=fixed,
|
|
167
217
|
failed=failed,
|
|
168
|
-
total=len(
|
|
218
|
+
total=len(outcomes),
|
|
169
219
|
)
|
|
170
220
|
if failed:
|
|
171
221
|
raise MaintenanceItemFailed(
|
|
172
|
-
f'{failed} of {len(
|
|
222
|
+
f'{failed} of {len(outcomes)} remediations failed; '
|
|
173
223
|
'see server logs for details.'
|
|
174
224
|
)
|
|
175
225
|
return 'succeeded'
|
|
@@ -22,6 +22,7 @@ from imbi.common import graph
|
|
|
22
22
|
MaintenanceSlug = typing.Literal[
|
|
23
23
|
'run-analysis',
|
|
24
24
|
'remediate',
|
|
25
|
+
'sync-fixes',
|
|
25
26
|
'rescore',
|
|
26
27
|
'deployment-resync',
|
|
27
28
|
'deployment-sweep',
|
|
@@ -104,6 +105,21 @@ OPERATIONS: dict[MaintenanceSlug, OperationDefinition] = {
|
|
|
104
105
|
enumerate=operations.enumerate_all_projects,
|
|
105
106
|
execute=operations.execute_remediate,
|
|
106
107
|
),
|
|
108
|
+
OperationDefinition(
|
|
109
|
+
slug='sync-fixes',
|
|
110
|
+
label='Apply Sync Fixes',
|
|
111
|
+
description=(
|
|
112
|
+
'Run the Project Doctor analysis for every project, then '
|
|
113
|
+
'apply only the fixes a plugin marks safe to sweep -- '
|
|
114
|
+
'e.g. repointing a PagerDuty escalation policy or syncing '
|
|
115
|
+
'PagerDuty service dependencies. Never creates or deletes '
|
|
116
|
+
'a remote resource. Projects with no such findings are '
|
|
117
|
+
'skipped.'
|
|
118
|
+
),
|
|
119
|
+
pause_key=None,
|
|
120
|
+
enumerate=operations.enumerate_all_projects,
|
|
121
|
+
execute=operations.execute_sync_fixes,
|
|
122
|
+
),
|
|
107
123
|
OperationDefinition(
|
|
108
124
|
slug='rescore',
|
|
109
125
|
label='Recompute Scores',
|
|
@@ -39,6 +39,7 @@ from imbi.common.plugins.base import (
|
|
|
39
39
|
LifecycleResult,
|
|
40
40
|
LinkWriteback,
|
|
41
41
|
PluginContext,
|
|
42
|
+
ProjectDependency,
|
|
42
43
|
ServiceConnection,
|
|
43
44
|
ServiceWriteback,
|
|
44
45
|
)
|
|
@@ -60,6 +61,7 @@ LifecycleEvent = typing.Literal[
|
|
|
60
61
|
'unarchived',
|
|
61
62
|
'deleted',
|
|
62
63
|
'relocated',
|
|
64
|
+
'dependencies_changed',
|
|
63
65
|
]
|
|
64
66
|
|
|
65
67
|
#: Map of lifecycle events to the matching ``LifecycleCapability`` hook
|
|
@@ -72,6 +74,7 @@ _EVENT_METHOD: dict[LifecycleEvent, str] = {
|
|
|
72
74
|
'unarchived': 'on_project_unarchived',
|
|
73
75
|
'deleted': 'on_project_deleted',
|
|
74
76
|
'relocated': 'on_project_relocated',
|
|
77
|
+
'dependencies_changed': 'on_project_dependencies_changed',
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
|
|
@@ -137,6 +140,10 @@ class LifecycleContextBundle:
|
|
|
137
140
|
)
|
|
138
141
|
project_name: str | None = None
|
|
139
142
|
project_description: str | None = None
|
|
143
|
+
# ``None`` when the dependency lookup failed.
|
|
144
|
+
dependencies: list[ProjectDependency] | None = dataclasses.field(
|
|
145
|
+
default_factory=list
|
|
146
|
+
)
|
|
140
147
|
|
|
141
148
|
|
|
142
149
|
async def build_lifecycle_context_bundle(
|
|
@@ -148,6 +155,7 @@ async def build_lifecycle_context_bundle(
|
|
|
148
155
|
write, and pass it as ``bundle=`` to :func:`dispatch_lifecycle`.
|
|
149
156
|
"""
|
|
150
157
|
from imbi.api.endpoints._helpers import (
|
|
158
|
+
lookup_project_dependencies,
|
|
151
159
|
lookup_project_exists_in,
|
|
152
160
|
lookup_project_links,
|
|
153
161
|
lookup_project_name_description,
|
|
@@ -163,12 +171,14 @@ async def build_lifecycle_context_bundle(
|
|
|
163
171
|
project_type_slugs,
|
|
164
172
|
service_connections,
|
|
165
173
|
(project_name, project_description),
|
|
174
|
+
dependencies,
|
|
166
175
|
) = await asyncio.gather(
|
|
167
176
|
lookup_project_slugs(db, project_id),
|
|
168
177
|
lookup_project_links(db, project_id),
|
|
169
178
|
lookup_project_type_slugs(db, project_id),
|
|
170
179
|
lookup_project_exists_in(db, project_id),
|
|
171
180
|
lookup_project_name_description(db, project_id),
|
|
181
|
+
lookup_project_dependencies(db, project_id),
|
|
172
182
|
)
|
|
173
183
|
return LifecycleContextBundle(
|
|
174
184
|
project_slug=project_slug,
|
|
@@ -178,6 +188,7 @@ async def build_lifecycle_context_bundle(
|
|
|
178
188
|
service_connections=service_connections,
|
|
179
189
|
project_name=project_name,
|
|
180
190
|
project_description=project_description,
|
|
191
|
+
dependencies=dependencies,
|
|
181
192
|
)
|
|
182
193
|
|
|
183
194
|
|
|
@@ -225,6 +236,9 @@ async def dispatch_lifecycle(
|
|
|
225
236
|
:class:`PluginContext` for plugins that need a before/after view
|
|
226
237
|
(``'updated'`` / ``'relocated'``).
|
|
227
238
|
"""
|
|
239
|
+
# Lazy import to avoid the endpoints/_helpers <-> this-module cycle.
|
|
240
|
+
from imbi.api.endpoints._helpers import linked_identifier_resolver
|
|
241
|
+
|
|
228
242
|
if resolved_list is None:
|
|
229
243
|
resolved_list = await resolve_all_capabilities(
|
|
230
244
|
db, project_id, 'lifecycle'
|
|
@@ -277,6 +291,16 @@ async def dispatch_lifecycle(
|
|
|
277
291
|
integration_options=resolved.integration_options,
|
|
278
292
|
capability_options=resolved.capability_options,
|
|
279
293
|
service_connections=bundle.service_connections,
|
|
294
|
+
dependencies=bundle.dependencies or [],
|
|
295
|
+
# No resolver when the dependency lookup failed, so a
|
|
296
|
+
# capability does not remove remote state on partial data.
|
|
297
|
+
resolve_linked_identifiers=(
|
|
298
|
+
None
|
|
299
|
+
if bundle.dependencies is None
|
|
300
|
+
else linked_identifier_resolver(
|
|
301
|
+
db, org_slug, resolved.integration_slug
|
|
302
|
+
)
|
|
303
|
+
),
|
|
280
304
|
)
|
|
281
305
|
invocation = await _invoke_one(db, ctx, resolved, event, auth)
|
|
282
306
|
results.append(invocation)
|