imbi-api 2.35.0__tar.gz → 2.36.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.35.0 → imbi_api-2.36.0}/.gitignore +8 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/PKG-INFO +2 -2
- {imbi_api-2.35.0 → imbi_api-2.36.0}/pyproject.toml +2 -2
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/client.py +7 -2
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/dashboard.py +48 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/operations.py +3 -3
- imbi_api-2.36.0/tests/email/test_audit_clickhouse.py +101 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_client.py +31 -1
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_dashboard.py +70 -1
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_iggy_connectors.py +3 -1
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_deployments.py +8 -5
- {imbi_api-2.35.0 → imbi_api-2.36.0}/README.md +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/moon.yml +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/README.md +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/app.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/authorization_codes.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/autonomous.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/internal_services.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/local_auth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/login_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/membership.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/oauth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/oauth_clients.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/password.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/permissions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/principals.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/seed.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/sessions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/tokens.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/auth/totp.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/backfill_embeddings.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/blueprint_attributes.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/blueprint_compliance.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_pushed_at_repair.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/commit_sync/service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/component_facts.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_status_repair.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sweeper.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/deployment_sync/service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/documents/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/documents/read_sweeper.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/domain/scoring.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/drift.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/dependencies.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/base.html +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/base.txt +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/password_reset.html +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/welcome.html +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates/welcome.txt +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/email/templates.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_events.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_history.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_helpers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_pagination.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/_search_index.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/admin.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/ai_models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/api_keys.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/auth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/blueprints.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/comments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/components.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_likes.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_presence.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_templates.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/document_versions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/documents.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/environments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/events.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/graph_query.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/integrations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/local_auth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/maintenance.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/mfa.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/operations_log.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/organizations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_analysis.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_deployments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_logs.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/project_types.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/projects.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/releases.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/roles.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/scoring.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/search.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/status.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/tags.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/teams.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/uploads.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/user_activity.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/users.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/endpoints/webhooks.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/entrypoint.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/graph_sql.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/attribution.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/endpoints.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/errors.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/flows.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/host_integration.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/repository.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/resolution.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/state.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/identity/sweeper.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/lifespans.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/llm/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/llm/dependencies.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/log.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/registry.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/state.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/maintenance/worker.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/mcp_test.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/middleware/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/middleware/rate_limit.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/openapi.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/orphan_releases.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/assignments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/installer.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/lifecycle.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/reload.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/resolution.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/plugins/schemas.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/pr_sync/service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/prompts/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/py.typed +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/relationships.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/release_promote/service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/sbom.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/scoring/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/scoring/queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/settings.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/client.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/dependencies.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/thumbnails.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/src/imbi/api/storage/validation.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_internal_services.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_init.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_integration.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/email/test_templates.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_ai_models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_ai_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_components.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_analytics.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_likes.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_presence.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_reads.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_reads_sql.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_document_versions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_integrations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_maintenance.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_integrations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_search_index.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_state.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/__init__.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_client.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_init.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/support.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_app.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_blueprints.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_pushed_at_repair.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_component_facts.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_status_repair.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sweeper.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sync_queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_deployment_sync_service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_drift.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_init.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_installer.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_lifespans.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_log.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_operations.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_state.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_maintenance_worker.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_models.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_openapi.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_orphan_releases.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins_assignments.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_plugins_credentials.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pr_sync_service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_projects_drift_ranges.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_pull_requests_pending_deploy.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_release_promote_service.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_sbom.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.0}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.35.0 → imbi_api-2.36.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.36.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.36.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.36.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.36.0",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -140,11 +140,16 @@ class EmailClient:
|
|
|
140
140
|
await asyncio.sleep(delay)
|
|
141
141
|
delay *= self._settings.retry_backoff_factor
|
|
142
142
|
|
|
143
|
-
# All retries exhausted
|
|
143
|
+
# All retries exhausted. The audit keeps the error type and SMTP
|
|
144
|
+
# code only: exception text such as SMTPRecipientsRefused carries
|
|
145
|
+
# the recipient address, and the audit must not store it.
|
|
144
146
|
error_msg = (
|
|
145
147
|
f'SMTP error after {self._settings.max_retries + 1} attempts: '
|
|
146
|
-
f'{last_error}'
|
|
148
|
+
f'{type(last_error).__name__}'
|
|
147
149
|
)
|
|
150
|
+
smtp_code = getattr(last_error, 'smtp_code', None)
|
|
151
|
+
if smtp_code is not None:
|
|
152
|
+
error_msg += f' ({smtp_code})'
|
|
148
153
|
return self._create_audit(message, 'failed', error_msg)
|
|
149
154
|
|
|
150
155
|
async def _send_smtp(self, message: models.EmailMessage) -> None:
|
|
@@ -89,6 +89,24 @@ class DashboardMetrics(pydantic.BaseModel):
|
|
|
89
89
|
releases_by_environment: list[EnvironmentCount]
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
class IggyTopic(iggy.TopicStatus):
|
|
93
|
+
"""One Iggy topic and the sink group that consumes it."""
|
|
94
|
+
|
|
95
|
+
# @TODO: Fill `stored_offset` and `lag` when Iggy can read a group
|
|
96
|
+
# offset over HTTP (apache/iggy#4279) or the Python SDK exposes the
|
|
97
|
+
# offset read (apache/iggy#3997). `lag` is `current_offset` minus
|
|
98
|
+
# `stored_offset`. Until then they are always `None`.
|
|
99
|
+
stored_offset: int | None = None
|
|
100
|
+
lag: int | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class IggyTopics(pydantic.BaseModel):
|
|
104
|
+
"""Per-topic Iggy state for the admin dashboard."""
|
|
105
|
+
|
|
106
|
+
checked_at: datetime.datetime
|
|
107
|
+
topics: list[IggyTopic]
|
|
108
|
+
|
|
109
|
+
|
|
92
110
|
def _ms(start: float) -> float:
|
|
93
111
|
"""Milliseconds elapsed since *start* (``time.perf_counter()``)."""
|
|
94
112
|
return round((time.perf_counter() - start) * 1000, 1)
|
|
@@ -306,6 +324,36 @@ async def get_dashboard_status(
|
|
|
306
324
|
)
|
|
307
325
|
|
|
308
326
|
|
|
327
|
+
# Reads one topic and one group per topic, all in flight together, so
|
|
328
|
+
# it gets more time than a single health check.
|
|
329
|
+
_IGGY_TOPICS_TIMEOUT = 5.0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dashboard_router.get('/iggy', response_model=IggyTopics)
|
|
333
|
+
async def get_dashboard_iggy(
|
|
334
|
+
auth: typing.Annotated[
|
|
335
|
+
permissions.AuthContext,
|
|
336
|
+
fastapi.Depends(
|
|
337
|
+
permissions.require_permission('admin:dashboard:read')
|
|
338
|
+
),
|
|
339
|
+
],
|
|
340
|
+
) -> IggyTopics:
|
|
341
|
+
"""Return each Iggy topic with its sink group's members."""
|
|
342
|
+
try:
|
|
343
|
+
topics = await asyncio.wait_for(
|
|
344
|
+
iggy.topic_status(), _IGGY_TOPICS_TIMEOUT
|
|
345
|
+
)
|
|
346
|
+
except Exception as err:
|
|
347
|
+
LOGGER.warning('Iggy topic status failed: %s', err)
|
|
348
|
+
raise fastapi.HTTPException(
|
|
349
|
+
status_code=503, detail='Iggy topic status is unavailable'
|
|
350
|
+
) from err
|
|
351
|
+
return IggyTopics(
|
|
352
|
+
checked_at=datetime.datetime.now(datetime.UTC),
|
|
353
|
+
topics=[IggyTopic(**topic.model_dump()) for topic in topics],
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
309
357
|
_METRICS_WINDOW_DAYS = 7
|
|
310
358
|
|
|
311
359
|
|
|
@@ -830,12 +830,12 @@ async def execute_opslog_backfill(
|
|
|
830
830
|
)
|
|
831
831
|
|
|
832
832
|
if pending:
|
|
833
|
-
|
|
833
|
+
entries: list[dict[str, typing.Any]] = []
|
|
834
834
|
for entry in pending:
|
|
835
835
|
dumped = entry.model_dump(by_alias=True, mode='python')
|
|
836
836
|
dumped['is_deleted'] = 1 if entry.is_deleted else 0
|
|
837
|
-
|
|
838
|
-
await iggy.publish_rows('operations_log', 'maintenance',
|
|
837
|
+
entries.append(dumped)
|
|
838
|
+
await iggy.publish_rows('operations_log', 'maintenance', entries)
|
|
839
839
|
if repairs:
|
|
840
840
|
# A separate publish: the repaired rows come off ``SELECT *``.
|
|
841
841
|
LOGGER.debug(
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Email audit rows, written through the sink path to a live ClickHouse.
|
|
2
|
+
|
|
3
|
+
``email_audit`` was in ``imbi.common.iggy.TOPICS`` with no table behind it,
|
|
4
|
+
so every audit was published and then could never be inserted. Before the
|
|
5
|
+
Iggy move the direct insert failed the same way and the error was logged
|
|
6
|
+
and swallowed. Every other email test mocks ClickHouse, which is how that
|
|
7
|
+
went unnoticed. These write a real audit the way the sink does and read it
|
|
8
|
+
back.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import hashlib
|
|
13
|
+
import unittest
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
from apps.api.tests import support
|
|
17
|
+
from imbi.api import email
|
|
18
|
+
from imbi.api.email import models
|
|
19
|
+
from imbi.common import clickhouse
|
|
20
|
+
from imbi.common.clickhouse import client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EmailAuditClickhouseTestCase(unittest.IsolatedAsyncioTestCase):
|
|
24
|
+
async def asyncSetUp(self) -> None:
|
|
25
|
+
# ``Clickhouse`` is a process-wide singleton bound to the loop that
|
|
26
|
+
# opened it, and each test method gets its own loop, so take a
|
|
27
|
+
# private instance and hand the previous one back afterwards. See
|
|
28
|
+
# test_document_reads_sql for the failure this avoids.
|
|
29
|
+
self._previous = client.Clickhouse._instance
|
|
30
|
+
client.Clickhouse._instance = None
|
|
31
|
+
self.addAsyncCleanup(self._restore_singleton)
|
|
32
|
+
await clickhouse.initialize()
|
|
33
|
+
await clickhouse.setup_schema()
|
|
34
|
+
|
|
35
|
+
async def _restore_singleton(self) -> None:
|
|
36
|
+
await clickhouse.aclose()
|
|
37
|
+
client.Clickhouse._instance = self._previous
|
|
38
|
+
|
|
39
|
+
async def _read_back(self, entity_id: str) -> dict[str, object]:
|
|
40
|
+
rows = await clickhouse.query(
|
|
41
|
+
'SELECT * FROM email_audit'
|
|
42
|
+
' WHERE related_entity_id = {entity_id:String}',
|
|
43
|
+
{'entity_id': entity_id},
|
|
44
|
+
)
|
|
45
|
+
self.assertEqual(1, len(rows))
|
|
46
|
+
return rows[0]
|
|
47
|
+
|
|
48
|
+
async def test_a_sent_email_is_audited_without_its_address(self) -> None:
|
|
49
|
+
entity_id = str(uuid.uuid4())
|
|
50
|
+
audit = models.EmailAudit(
|
|
51
|
+
to_email='Someone@Example.com',
|
|
52
|
+
template_name='password_reset',
|
|
53
|
+
subject='Reset your password',
|
|
54
|
+
status='sent',
|
|
55
|
+
sent_at=datetime.datetime.now(datetime.UTC),
|
|
56
|
+
user_id='user-1',
|
|
57
|
+
related_entity_type='password_reset_token',
|
|
58
|
+
related_entity_id=entity_id,
|
|
59
|
+
)
|
|
60
|
+
with support.sink_to_clickhouse():
|
|
61
|
+
await email._save_audit(audit)
|
|
62
|
+
|
|
63
|
+
row = await self._read_back(entity_id)
|
|
64
|
+
self.assertEqual('password_reset', row['template_name'])
|
|
65
|
+
self.assertEqual('sent', row['status'])
|
|
66
|
+
self.assertEqual(
|
|
67
|
+
hashlib.sha256(b'someone@example.com').hexdigest(),
|
|
68
|
+
row['to_email_hash'],
|
|
69
|
+
)
|
|
70
|
+
# `EmailStr` lowercases the domain but keeps the local part's case.
|
|
71
|
+
self.assertEqual('example.com', row['to_email_domain'])
|
|
72
|
+
self.assertNotIn('to_email', row)
|
|
73
|
+
for address in (
|
|
74
|
+
'Someone@Example.com',
|
|
75
|
+
'Someone@example.com',
|
|
76
|
+
'someone@example.com',
|
|
77
|
+
):
|
|
78
|
+
self.assertNotIn(address, row.values())
|
|
79
|
+
|
|
80
|
+
async def test_unset_optional_fields_land_as_empty_strings(self) -> None:
|
|
81
|
+
# `EmailAudit` sends null for its optional fields and the columns are
|
|
82
|
+
# not Nullable, so this pins the server's null-as-default behaviour
|
|
83
|
+
# the table depends on.
|
|
84
|
+
entity_id = str(uuid.uuid4())
|
|
85
|
+
audit = models.EmailAudit(
|
|
86
|
+
to_email='someone@example.com',
|
|
87
|
+
template_name='password_reset',
|
|
88
|
+
subject='Reset your password',
|
|
89
|
+
status='failed',
|
|
90
|
+
error_message=None,
|
|
91
|
+
sent_at=datetime.datetime.now(datetime.UTC),
|
|
92
|
+
related_entity_id=entity_id,
|
|
93
|
+
)
|
|
94
|
+
with support.sink_to_clickhouse():
|
|
95
|
+
await email._save_audit(audit)
|
|
96
|
+
|
|
97
|
+
row = await self._read_back(entity_id)
|
|
98
|
+
self.assertEqual('failed', row['status'])
|
|
99
|
+
self.assertEqual('', row['error_message'])
|
|
100
|
+
self.assertEqual('', row['user_id'])
|
|
101
|
+
self.assertEqual('', row['related_entity_type'])
|
|
@@ -336,9 +336,39 @@ class EmailClientTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
336
336
|
self.assertEqual(audit.status, 'failed')
|
|
337
337
|
assert audit.error_message is not None
|
|
338
338
|
self.assertIn('after 3 attempts', audit.error_message)
|
|
339
|
-
self.assertIn('
|
|
339
|
+
self.assertIn('SMTPException', audit.error_message)
|
|
340
|
+
self.assertNotIn('Persistent error', audit.error_message)
|
|
340
341
|
self.assertEqual(self.mock_smtp.send_message.call_count, 3)
|
|
341
342
|
|
|
343
|
+
async def test_failed_audit_omits_refused_recipient(self) -> None:
|
|
344
|
+
"""Test the failure message does not carry the recipient address."""
|
|
345
|
+
self.mock_settings.max_retries = 0
|
|
346
|
+
|
|
347
|
+
email_client = client.EmailClient()
|
|
348
|
+
await email_client.initialize()
|
|
349
|
+
|
|
350
|
+
message = models.EmailMessage(
|
|
351
|
+
to_email='user@example.com',
|
|
352
|
+
subject='Test',
|
|
353
|
+
html_body='<p>Test</p>',
|
|
354
|
+
text_body='Test',
|
|
355
|
+
template_name='test',
|
|
356
|
+
context={},
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
self.mock_smtp.send_message.side_effect = (
|
|
360
|
+
smtplib.SMTPRecipientsRefused(
|
|
361
|
+
{'user@example.com': (550, b'User unknown')}
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
audit = await email_client.send_email(message)
|
|
366
|
+
|
|
367
|
+
self.assertEqual(audit.status, 'failed')
|
|
368
|
+
assert audit.error_message is not None
|
|
369
|
+
self.assertIn('SMTPRecipientsRefused', audit.error_message)
|
|
370
|
+
self.assertNotIn('user@example.com', audit.error_message)
|
|
371
|
+
|
|
342
372
|
async def test_retry_exponential_backoff(self) -> None:
|
|
343
373
|
"""Test exponential backoff delays."""
|
|
344
374
|
self.mock_settings.max_retries = 3
|
|
@@ -10,7 +10,7 @@ from apps.api.tests import support
|
|
|
10
10
|
from imbi.api import models, settings
|
|
11
11
|
from imbi.api.auth import password, permissions
|
|
12
12
|
from imbi.api.endpoints import dashboard
|
|
13
|
-
from imbi.common import graph
|
|
13
|
+
from imbi.common import graph, iggy
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _ok_status_handler(request: httpx.Request) -> httpx.Response:
|
|
@@ -239,6 +239,75 @@ class DashboardStatusEndpointTestCase(support.SharedAppTestCase):
|
|
|
239
239
|
self.assertEqual(403, response.status_code)
|
|
240
240
|
|
|
241
241
|
|
|
242
|
+
class DashboardIggyEndpointTestCase(support.SharedAppTestCase):
|
|
243
|
+
"""Test cases for GET /admin/dashboard/iggy."""
|
|
244
|
+
|
|
245
|
+
def setUp(self) -> None:
|
|
246
|
+
self.admin_context = permissions.AuthContext(
|
|
247
|
+
user=models.User(
|
|
248
|
+
email='admin@example.com',
|
|
249
|
+
display_name='Admin User',
|
|
250
|
+
is_active=True,
|
|
251
|
+
is_admin=True,
|
|
252
|
+
password_hash=password.hash_password('testpassword123'),
|
|
253
|
+
created_at=datetime.datetime.now(datetime.UTC),
|
|
254
|
+
),
|
|
255
|
+
session_id='test-session',
|
|
256
|
+
auth_method='jwt',
|
|
257
|
+
permissions=set(),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def mock_get_current_user():
|
|
261
|
+
return self.admin_context
|
|
262
|
+
|
|
263
|
+
self.test_app.dependency_overrides[permissions.get_current_user] = (
|
|
264
|
+
mock_get_current_user
|
|
265
|
+
)
|
|
266
|
+
self.client = testclient.TestClient(self.test_app)
|
|
267
|
+
self.topic_status = self.enterContext(
|
|
268
|
+
mock.patch.object(
|
|
269
|
+
dashboard.iggy,
|
|
270
|
+
'topic_status',
|
|
271
|
+
new=mock.AsyncMock(
|
|
272
|
+
return_value=[
|
|
273
|
+
iggy.TopicStatus(
|
|
274
|
+
stream='operations_log',
|
|
275
|
+
topic='deployments',
|
|
276
|
+
messages=223,
|
|
277
|
+
current_offset=222,
|
|
278
|
+
size_bytes=4096,
|
|
279
|
+
consumer_group='operations_log',
|
|
280
|
+
members=2,
|
|
281
|
+
members_owning=1,
|
|
282
|
+
)
|
|
283
|
+
]
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def test_returns_each_topic(self) -> None:
|
|
289
|
+
response = self.client.get('/admin/dashboard/iggy')
|
|
290
|
+
self.assertEqual(200, response.status_code)
|
|
291
|
+
[topic] = response.json()['topics']
|
|
292
|
+
self.assertEqual('operations_log', topic['stream'])
|
|
293
|
+
self.assertEqual(222, topic['current_offset'])
|
|
294
|
+
self.assertEqual(2, topic['members'])
|
|
295
|
+
self.assertEqual(1, topic['members_owning'])
|
|
296
|
+
# Iggy cannot return a group offset over HTTP yet.
|
|
297
|
+
self.assertIsNone(topic['stored_offset'])
|
|
298
|
+
self.assertIsNone(topic['lag'])
|
|
299
|
+
|
|
300
|
+
def test_iggy_failure_is_503(self) -> None:
|
|
301
|
+
self.topic_status.side_effect = RuntimeError('disconnected')
|
|
302
|
+
response = self.client.get('/admin/dashboard/iggy')
|
|
303
|
+
self.assertEqual(503, response.status_code)
|
|
304
|
+
|
|
305
|
+
def test_requires_permission(self) -> None:
|
|
306
|
+
self.admin_context.user.is_admin = False
|
|
307
|
+
response = self.client.get('/admin/dashboard/iggy')
|
|
308
|
+
self.assertEqual(403, response.status_code)
|
|
309
|
+
|
|
310
|
+
|
|
242
311
|
class DashboardMetricsEndpointTestCase(support.SharedAppTestCase):
|
|
243
312
|
"""Test cases for GET /admin/dashboard/metrics."""
|
|
244
313
|
|
|
@@ -317,8 +317,10 @@ class WriteRoutesTestCase(_EndpointTestCase):
|
|
|
317
317
|
class OpenAPITestCase(_EndpointTestCase):
|
|
318
318
|
def test_routes_are_not_published(self) -> None:
|
|
319
319
|
paths = self.client.get('/openapi.json').json()['paths']
|
|
320
|
+
# Only the connector configuration is private; other Iggy
|
|
321
|
+
# routes (e.g. /admin/dashboard/iggy) are public UI endpoints.
|
|
320
322
|
self.assertFalse(
|
|
321
|
-
[path for path in paths if 'iggy' in path],
|
|
323
|
+
[path for path in paths if '/iggy/connectors' in path],
|
|
322
324
|
'the connector configuration must stay out of the schema',
|
|
323
325
|
)
|
|
324
326
|
|
|
@@ -767,11 +767,14 @@ class ProjectDeploymentsTestCase(support.SharedAppTestCase):
|
|
|
767
767
|
pattern=r'^\d{4}([.-])\d{1,2}\1\d{1,2}(?:[.-]\w+)?$',
|
|
768
768
|
)
|
|
769
769
|
]
|
|
770
|
+
# Same-day rebuild: the calver suggestion depends on today's date,
|
|
771
|
+
# so derive the tags from it rather than hardcoding one.
|
|
772
|
+
stamp = datetime.datetime.now(datetime.UTC).date().strftime('%Y.%m.%d')
|
|
770
773
|
self.mock_anthropic.complete_json = mock.AsyncMock(
|
|
771
774
|
return_value=CompletionResult(
|
|
772
775
|
data=DraftReleaseNotes(
|
|
773
776
|
bump='patch',
|
|
774
|
-
version='
|
|
777
|
+
version=f'{stamp}-1',
|
|
775
778
|
reasoning='same-day rebuild',
|
|
776
779
|
notes_markdown='## Fixed',
|
|
777
780
|
),
|
|
@@ -783,17 +786,17 @@ class ProjectDeploymentsTestCase(support.SharedAppTestCase):
|
|
|
783
786
|
'/organizations/myorg/projects/proj1/deployments/'
|
|
784
787
|
'draft-release-notes',
|
|
785
788
|
json={
|
|
786
|
-
'base_sha': '
|
|
789
|
+
'base_sha': f'{stamp}-0',
|
|
787
790
|
'head_sha': 'bbb',
|
|
788
|
-
'last_tag': '
|
|
791
|
+
'last_tag': f'{stamp}-0',
|
|
789
792
|
},
|
|
790
793
|
)
|
|
791
794
|
self.assertEqual(response.status_code, 200)
|
|
792
|
-
self.assertEqual(response.json()['version'], '
|
|
795
|
+
self.assertEqual(response.json()['version'], f'{stamp}-1')
|
|
793
796
|
call = self.mock_anthropic.complete_json.call_args
|
|
794
797
|
prompt = call.args[0] if call.args else call.kwargs.get('prompt', '')
|
|
795
798
|
self.assertIn('Allowed tag formats: Calendar versioning', prompt)
|
|
796
|
-
self.assertIn('Suggested next version:
|
|
799
|
+
self.assertIn(f'Suggested next version: {stamp}-1', prompt)
|
|
797
800
|
|
|
798
801
|
def test_draft_release_notes_rebumps_to_the_policy(self) -> None:
|
|
799
802
|
"""A semver answer under a calver-only policy becomes a date."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|