imbi-api 2.32.2__tar.gz → 2.33.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.32.2 → imbi_api-2.33.0}/PKG-INFO +2 -2
- {imbi_api-2.32.2 → imbi_api-2.33.0}/pyproject.toml +2 -2
- imbi_api-2.33.0/src/imbi/api/commit_pushed_at_repair.py +249 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/dashboard.py +26 -2
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_deployments.py +98 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/projects.py +4 -2
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/pull_requests.py +41 -18
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/releases.py +198 -44
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/operations.py +79 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/registry.py +35 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_dashboard.py +19 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_deployments.py +140 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_projects_helpers.py +2 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_pull_requests.py +93 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_releases.py +235 -8
- imbi_api-2.33.0/tests/test_commit_pushed_at_repair.py +292 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_operations.py +97 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/.gitignore +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/README.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/moon.yml +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/README.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/app.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/authorization_codes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/autonomous.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/internal_services.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/login_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/membership.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/oauth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/oauth_clients.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/password.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/permissions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/principals.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/seed.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/sessions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/tokens.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/auth/totp.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/backfill_embeddings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/blueprint_attributes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/blueprint_compliance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/commit_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/component_facts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_status_repair.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/deployment_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/documents/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/documents/read_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/domain/scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/drift.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/base.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/base.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/password_reset.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/welcome.html +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates/welcome.txt +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/email/templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_history.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_reads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_document_scope.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_helpers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_pagination.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/_search_index.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/admin.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/ai_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/ai_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/comments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/components.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_analytics.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_likes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_presence.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/document_versions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/documents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/environments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/graph_query.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/iggy_connectors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/maintenance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/mfa.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/operations_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/organizations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_analysis.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_logs.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_pr_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/project_types.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/roles.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/search.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/status.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/tags.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/teams.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/uploads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/user_activity.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/users.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/endpoints/webhooks.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/entrypoint.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/graph_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/attribution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/endpoints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/errors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/flows.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/host_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/repository.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/identity/sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/lifespans.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/llm/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/llm/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/maintenance/worker.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/mcp_test.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/middleware/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/middleware/rate_limit.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/openapi.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/orphan_releases.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/assignments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/installer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/reload.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/plugins/schemas.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/pr_sync/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/prompts/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/py.typed +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/relationships.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/release_promote/service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/sbom.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/scoring/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/scoring/queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/settings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/dependencies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/thumbnails.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/src/imbi/api/storage/validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_internal_services.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/email/test_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_ai_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_ai_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_components.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_analytics.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_likes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_presence.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_reads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_reads_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_document_versions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_iggy_connectors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_maintenance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_deployments_autonomous.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_integrations.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_search_index.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/__init__.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_client.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/support.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_app.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprint_compliance.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_blueprints.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_component_facts.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_status_repair.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sweeper.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_deployment_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_drift.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_init.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_installer.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_lifespans.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_log.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_state.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_maintenance_worker.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_models.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_openapi.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_orphan_releases.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins_assignments.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_plugins_credentials.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_pr_sync_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_pr_sync_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_projects_drift_ranges.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_release_promote_service.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_sbom.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.0}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.32.2 → imbi_api-2.33.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.33.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.33.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.33.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.33.0",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Put back the push time commit re-syncs overwrote.
|
|
2
|
+
|
|
3
|
+
Every commit-sync path used to stamp ``commits.pushed_at`` with the
|
|
4
|
+
clock at ingest, and the table is a ``ReplacingMergeTree`` versioned on
|
|
5
|
+
``recorded_at`` -- so any re-sync of a commit already stored replaced
|
|
6
|
+
its row with a fresh ``pushed_at``. Two producers did that at scale:
|
|
7
|
+
|
|
8
|
+
* the full backfill (``sync_all_history``, the ``commit-sync``
|
|
9
|
+
maintenance operation), which stamps every commit a project has with
|
|
10
|
+
the single instant the run started. Three runs in August 2026 left
|
|
11
|
+
roughly 19,000 commits across 600 projects sharing a handful of
|
|
12
|
+
timestamps, their push order collapsed onto author order;
|
|
13
|
+
* the ``workflow_run`` webhook rule, which re-syncs a run's head commit
|
|
14
|
+
when the run completes. A release workflow runs against the commit
|
|
15
|
+
a release was *cut from*, so that commit was re-stamped minutes after
|
|
16
|
+
the release commit itself and sorted above it (#308).
|
|
17
|
+
|
|
18
|
+
``pushed_at`` orders the recent-commits feed, picks the commit-sync
|
|
19
|
+
gap-healing base, and picks the release-drift head, so the wrong values
|
|
20
|
+
mis-slice release ranges. The sync no longer re-stamps a stored commit,
|
|
21
|
+
but nothing that reads GitHub can put the old values back: the commits
|
|
22
|
+
API carries author and committer dates, never a push time.
|
|
23
|
+
|
|
24
|
+
The ``events`` table can. It holds every webhook delivery, and a
|
|
25
|
+
``push`` delivery names the commits it carried (``payload.commits[]``,
|
|
26
|
+
plus ``payload.after`` for the head) and the branch it was for
|
|
27
|
+
(``payload.ref``). Its ``recorded_at`` is within about a second of the
|
|
28
|
+
``pushed_at`` the sync originally wrote from it. So for each stored
|
|
29
|
+
commit, the earliest push delivery to the commit's own branch that
|
|
30
|
+
lists it is what its ``pushed_at`` was before anything overwrote it.
|
|
31
|
+
|
|
32
|
+
Deliberately conservative:
|
|
33
|
+
|
|
34
|
+
* only pushes to the branch the commit is stored under count. A pull
|
|
35
|
+
request's commits are also pushed to their feature branch, earlier,
|
|
36
|
+
and the sync's branch-gated rule never recorded those -- matching
|
|
37
|
+
them would order the commits by when their branch was pushed rather
|
|
38
|
+
than by when they reached the default branch, which is a different
|
|
39
|
+
wrong order.
|
|
40
|
+
* a row is rewritten only when its ``pushed_at`` is *later* than the
|
|
41
|
+
delivery by more than :data:`TOLERANCE`. Within tolerance it is the
|
|
42
|
+
original stamp; earlier means an even earlier sync recorded it and
|
|
43
|
+
that value is closer to the truth.
|
|
44
|
+
* a commit with no push delivery on record is left alone. Deliveries
|
|
45
|
+
only go back to June 2026, GitHub lists at most twenty commits per
|
|
46
|
+
push payload, and a compare-healed gap has no delivery of its own.
|
|
47
|
+
There is nothing to restore, and inventing a value would be worse
|
|
48
|
+
than the collapsed one.
|
|
49
|
+
|
|
50
|
+
The rewrite is a re-insert of the full row with the corrected
|
|
51
|
+
``pushed_at`` and a fresh ``recorded_at`` -- the same read-modify-insert
|
|
52
|
+
``opslog-backfill`` uses -- so ``FINAL`` readers see it at once and the
|
|
53
|
+
background merge collapses the superseded version. It composes with
|
|
54
|
+
the sync's carry-forward, which reads the *earliest* ``pushed_at``
|
|
55
|
+
across row versions: once the repaired row is on disk, every later
|
|
56
|
+
re-sync of that commit inherits the restored value.
|
|
57
|
+
|
|
58
|
+
There is no lock between this and a webhook re-sync, and the two are not
|
|
59
|
+
serialized: a re-sync that read the stored value *before* the repaired
|
|
60
|
+
row landed and inserted *after* it writes the stale value back with a
|
|
61
|
+
newer ``recorded_at``, and that version wins. The window is the few
|
|
62
|
+
milliseconds between the sync's lookup and its insert, it has to
|
|
63
|
+
coincide with a delivery for that exact commit, and the outcome is one
|
|
64
|
+
row back where it started rather than anything new being wrong. The
|
|
65
|
+
repair is idempotent, so the remedy is to run it again; the
|
|
66
|
+
``commit-pushed-at-check`` dry run after a repair is what shows whether
|
|
67
|
+
anything did slip through. A shared lock would have to span the API
|
|
68
|
+
worker and the gateway's webhook handler for a race this narrow and
|
|
69
|
+
this cheaply undone, which is not worth the coupling.
|
|
70
|
+
|
|
71
|
+
Runs as the ``commit-pushed-at-repair`` maintenance operation, with
|
|
72
|
+
``commit-pushed-at-check`` as its dry run.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
from __future__ import annotations
|
|
76
|
+
|
|
77
|
+
import datetime
|
|
78
|
+
import logging
|
|
79
|
+
import typing
|
|
80
|
+
|
|
81
|
+
import pydantic
|
|
82
|
+
|
|
83
|
+
from imbi.common import clickhouse, models
|
|
84
|
+
|
|
85
|
+
LOGGER = logging.getLogger(__name__)
|
|
86
|
+
|
|
87
|
+
#: How far a stored ``pushed_at`` may trail its push delivery before it
|
|
88
|
+
#: counts as re-stamped. The webhook sync stamps ``now()`` while
|
|
89
|
+
#: handling the delivery, so the original value lands within a second
|
|
90
|
+
#: or two of ``events.recorded_at``; the smallest re-stamp -- a CI run
|
|
91
|
+
#: completing -- is minutes later.
|
|
92
|
+
TOLERANCE = datetime.timedelta(seconds=60)
|
|
93
|
+
|
|
94
|
+
_BRANCH_REF_PREFIX = 'refs/heads/'
|
|
95
|
+
|
|
96
|
+
#: Every column ``CommitRecord`` carries except ``recorded_at``, which
|
|
97
|
+
#: the re-insert regenerates. Listed rather than ``SELECT *`` so a
|
|
98
|
+
#: column added to the table later is a loud failure here, not a row
|
|
99
|
+
#: silently rewritten with a default.
|
|
100
|
+
_COMMIT_COLUMNS: typing.Final = (
|
|
101
|
+
'project_id',
|
|
102
|
+
'sha',
|
|
103
|
+
'short_sha',
|
|
104
|
+
'ref',
|
|
105
|
+
'message',
|
|
106
|
+
'author_name',
|
|
107
|
+
'author_email',
|
|
108
|
+
'author_login',
|
|
109
|
+
'author_user',
|
|
110
|
+
'committer_name',
|
|
111
|
+
'ci_status',
|
|
112
|
+
'authored_at',
|
|
113
|
+
'committed_at',
|
|
114
|
+
'url',
|
|
115
|
+
'pushed_at',
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# ``payload`` is a JSON column; ``payload.commits[]`` is its array
|
|
119
|
+
# subcolumn and ``c.id`` the per-element path. ``metadata.event_type``
|
|
120
|
+
# is the gateway's per-source label ('push'), distinct from the
|
|
121
|
+
# top-level ``type`` category ('webhook').
|
|
122
|
+
_DELIVERIES_SQL: typing.Final = (
|
|
123
|
+
'SELECT recorded_at, '
|
|
124
|
+
'toString(payload.ref) AS ref, '
|
|
125
|
+
'toString(payload.after) AS after, '
|
|
126
|
+
'arrayMap(c -> toString(c.id), payload.commits[]) AS commit_ids '
|
|
127
|
+
'FROM events '
|
|
128
|
+
'WHERE project_id = {project_id:String} '
|
|
129
|
+
"AND type = 'webhook' "
|
|
130
|
+
"AND toString(metadata.event_type) = 'push' "
|
|
131
|
+
"AND toString(payload.ref) LIKE 'refs/heads/%'"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Column list is a module constant; values are bound parameters.
|
|
135
|
+
_COMMITS_SQL: typing.Final = (
|
|
136
|
+
'SELECT ' + ', '.join(_COMMIT_COLUMNS) + ' FROM commits FINAL ' # noqa: S608
|
|
137
|
+
'WHERE project_id = {project_id:String} '
|
|
138
|
+
'AND sha IN {shas:Array(String)}'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RepairSummary(typing.NamedTuple):
|
|
143
|
+
"""What one project's repair did (or, dry run, would do)."""
|
|
144
|
+
|
|
145
|
+
#: Push deliveries to a branch found for the project.
|
|
146
|
+
deliveries: int = 0
|
|
147
|
+
#: Stored commits a delivery to their own branch accounts for.
|
|
148
|
+
examined: int = 0
|
|
149
|
+
#: Rows rewritten -- or, on a dry run, rows that would be.
|
|
150
|
+
repaired: int = 0
|
|
151
|
+
#: Largest correction applied, in seconds. Tells an operator
|
|
152
|
+
#: whether a project's damage was the backfill (days) or a
|
|
153
|
+
#: ``workflow_run`` re-stamp (minutes).
|
|
154
|
+
largest_shift_seconds: int = 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def _push_times(
|
|
158
|
+
project_id: str,
|
|
159
|
+
) -> tuple[int, dict[tuple[str, str], datetime.datetime]]:
|
|
160
|
+
"""Push deliveries read, and ``(branch, sha) -> earliest delivery``.
|
|
161
|
+
|
|
162
|
+
A commit pushed to several branches (a feature branch, then the
|
|
163
|
+
default branch on merge) gets one entry per branch, so the caller
|
|
164
|
+
can pick the delivery for the branch the commit is stored under.
|
|
165
|
+
"""
|
|
166
|
+
rows = await clickhouse.query(_DELIVERIES_SQL, {'project_id': project_id})
|
|
167
|
+
earliest: dict[tuple[str, str], datetime.datetime] = {}
|
|
168
|
+
for row in rows:
|
|
169
|
+
recorded_at = clickhouse.as_utc_or_none(row.get('recorded_at'))
|
|
170
|
+
ref = str(row.get('ref') or '')
|
|
171
|
+
if recorded_at is None or not ref.startswith(_BRANCH_REF_PREFIX):
|
|
172
|
+
continue
|
|
173
|
+
branch = ref[len(_BRANCH_REF_PREFIX) :]
|
|
174
|
+
listed = typing.cast('list[object]', row.get('commit_ids') or [])
|
|
175
|
+
shas = {str(sha) for sha in listed if sha}
|
|
176
|
+
# ``after`` is the head the delivery attests to even when the
|
|
177
|
+
# payload's commit list is capped (GitHub sends at most twenty)
|
|
178
|
+
# or empty (a branch created from an existing commit).
|
|
179
|
+
after = str(row.get('after') or '')
|
|
180
|
+
if after and after != '0' * 40:
|
|
181
|
+
shas.add(after)
|
|
182
|
+
for sha in shas:
|
|
183
|
+
key = (branch, sha.lower())
|
|
184
|
+
if key not in earliest or recorded_at < earliest[key]:
|
|
185
|
+
earliest[key] = recorded_at
|
|
186
|
+
return len(rows), earliest
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _repaired_record(
|
|
190
|
+
row: dict[str, typing.Any], pushed_at: datetime.datetime
|
|
191
|
+
) -> models.CommitRecord:
|
|
192
|
+
"""The row as a ``CommitRecord`` carrying the restored push time.
|
|
193
|
+
|
|
194
|
+
``recorded_at`` is left to the model's default so the re-insert
|
|
195
|
+
ranks newest under ``ReplacingMergeTree(recorded_at)``.
|
|
196
|
+
"""
|
|
197
|
+
fields = {name: row.get(name) for name in _COMMIT_COLUMNS}
|
|
198
|
+
fields['authored_at'] = clickhouse.as_utc_or_none(row.get('authored_at'))
|
|
199
|
+
fields['committed_at'] = clickhouse.as_utc_or_none(row.get('committed_at'))
|
|
200
|
+
fields['pushed_at'] = pushed_at
|
|
201
|
+
return models.CommitRecord.model_validate(fields)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def repair_project(
|
|
205
|
+
project_id: str, *, dry_run: bool = False
|
|
206
|
+
) -> RepairSummary:
|
|
207
|
+
"""Restore one project's re-stamped commit push times.
|
|
208
|
+
|
|
209
|
+
Idempotent: a repaired row sits within :data:`TOLERANCE` of its
|
|
210
|
+
delivery, so a second run finds nothing to rewrite.
|
|
211
|
+
"""
|
|
212
|
+
deliveries, push_times = await _push_times(project_id)
|
|
213
|
+
if not push_times:
|
|
214
|
+
return RepairSummary(deliveries=deliveries)
|
|
215
|
+
shas = sorted({sha for _, sha in push_times})
|
|
216
|
+
rows = await clickhouse.query(
|
|
217
|
+
_COMMITS_SQL, {'project_id': project_id, 'shas': shas}
|
|
218
|
+
)
|
|
219
|
+
examined = 0
|
|
220
|
+
largest = datetime.timedelta(0)
|
|
221
|
+
repaired: list[pydantic.BaseModel] = []
|
|
222
|
+
for row in rows:
|
|
223
|
+
sha = str(row.get('sha') or '').lower()
|
|
224
|
+
branch = str(row.get('ref') or '')
|
|
225
|
+
delivered = push_times.get((branch, sha))
|
|
226
|
+
if delivered is None:
|
|
227
|
+
continue # only pushed to some other branch; see module doc
|
|
228
|
+
examined += 1
|
|
229
|
+
stored = clickhouse.as_utc_or_none(row.get('pushed_at'))
|
|
230
|
+
if stored is None:
|
|
231
|
+
continue
|
|
232
|
+
shift = stored - delivered
|
|
233
|
+
if shift <= TOLERANCE:
|
|
234
|
+
continue
|
|
235
|
+
largest = max(largest, shift)
|
|
236
|
+
repaired.append(_repaired_record(row, delivered))
|
|
237
|
+
if repaired and not dry_run:
|
|
238
|
+
await clickhouse.insert('commits', repaired)
|
|
239
|
+
LOGGER.info(
|
|
240
|
+
'restored pushed_at on %d commit(s) for project %s',
|
|
241
|
+
len(repaired),
|
|
242
|
+
project_id,
|
|
243
|
+
)
|
|
244
|
+
return RepairSummary(
|
|
245
|
+
deliveries=deliveries,
|
|
246
|
+
examined=examined,
|
|
247
|
+
repaired=len(repaired),
|
|
248
|
+
largest_shift_seconds=int(largest.total_seconds()),
|
|
249
|
+
)
|
|
@@ -289,8 +289,24 @@ def _build_series(
|
|
|
289
289
|
return MetricSeries(total=sum(daily), daily=daily)
|
|
290
290
|
|
|
291
291
|
|
|
292
|
+
async def _active_project_ids(db: graph.Graph) -> list[str]:
|
|
293
|
+
"""IDs of every project that is not archived."""
|
|
294
|
+
query: typing.LiteralString = """
|
|
295
|
+
MATCH (p:Project)
|
|
296
|
+
WHERE coalesce(p.archived, false) = false
|
|
297
|
+
RETURN p.id AS id
|
|
298
|
+
"""
|
|
299
|
+
records = await db.execute(query, {}, ['id'])
|
|
300
|
+
return [
|
|
301
|
+
graph.parse_agtype(r['id'])
|
|
302
|
+
for r in records
|
|
303
|
+
if graph.parse_agtype(r['id'])
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
292
307
|
@dashboard_router.get('/metrics', response_model=DashboardMetrics)
|
|
293
308
|
async def get_dashboard_metrics(
|
|
309
|
+
db: graph.Pool,
|
|
294
310
|
auth: typing.Annotated[
|
|
295
311
|
permissions.AuthContext,
|
|
296
312
|
fastapi.Depends(
|
|
@@ -298,7 +314,10 @@ async def get_dashboard_metrics(
|
|
|
298
314
|
),
|
|
299
315
|
],
|
|
300
316
|
) -> DashboardMetrics:
|
|
301
|
-
"""Return 7-day activity metrics with per-day counts for the tiles.
|
|
317
|
+
"""Return 7-day activity metrics with per-day counts for the tiles.
|
|
318
|
+
|
|
319
|
+
The pull request series skips drafts and archived projects.
|
|
320
|
+
"""
|
|
302
321
|
today = datetime.datetime.now(datetime.UTC).date()
|
|
303
322
|
start_date = today - datetime.timedelta(days=_METRICS_WINDOW_DAYS - 1)
|
|
304
323
|
since = datetime.datetime.combine(
|
|
@@ -308,7 +327,10 @@ async def get_dashboard_metrics(
|
|
|
308
327
|
(start_date + datetime.timedelta(days=offset)).isoformat()
|
|
309
328
|
for offset in range(_METRICS_WINDOW_DAYS)
|
|
310
329
|
]
|
|
311
|
-
params: dict[str, typing.Any] = {
|
|
330
|
+
params: dict[str, typing.Any] = {
|
|
331
|
+
'since': since,
|
|
332
|
+
'project_ids': await _active_project_ids(db),
|
|
333
|
+
}
|
|
312
334
|
# One scan of the (FINAL, merge-on-read) deploy rows yields both the
|
|
313
335
|
# per-day series and the per-environment rollup.
|
|
314
336
|
deploys, events, ops, prs = await asyncio.gather(
|
|
@@ -339,6 +361,8 @@ async def get_dashboard_metrics(
|
|
|
339
361
|
'SELECT toDate(created_at) AS day, count() AS c'
|
|
340
362
|
' FROM pull_requests FINAL'
|
|
341
363
|
' WHERE created_at >= {since:DateTime64(3)}'
|
|
364
|
+
' AND NOT draft'
|
|
365
|
+
' AND project_id IN {project_ids:Array(String)}'
|
|
342
366
|
' GROUP BY day',
|
|
343
367
|
params,
|
|
344
368
|
),
|
|
@@ -1545,6 +1545,104 @@ async def fetch_release_notes_for_tag(
|
|
|
1545
1545
|
)
|
|
1546
1546
|
|
|
1547
1547
|
|
|
1548
|
+
async def resolve_committish_for_tag(
|
|
1549
|
+
db: graph.Graph,
|
|
1550
|
+
*,
|
|
1551
|
+
org_slug: str,
|
|
1552
|
+
project_id: str,
|
|
1553
|
+
tag: str,
|
|
1554
|
+
auth: permissions.AuthContext,
|
|
1555
|
+
) -> str | None:
|
|
1556
|
+
"""Resolve ``tag`` to the short committish it points at.
|
|
1557
|
+
|
|
1558
|
+
The release-create path uses this when a caller supplies a tag but no
|
|
1559
|
+
commit -- a source host's ``release`` webhook names the tag and the
|
|
1560
|
+
branch it was cut from, never the SHA. The lookup goes through the
|
|
1561
|
+
deployment capability rather than the ClickHouse ``tags`` table
|
|
1562
|
+
because the two race: the ``release`` delivery lands before commit
|
|
1563
|
+
sync records the tag (32s apart on the delivery that motivated this),
|
|
1564
|
+
so the table is reliably empty at exactly the moment this is called.
|
|
1565
|
+
The remote, by contrast, has the tag by definition -- it is what it
|
|
1566
|
+
just announced.
|
|
1567
|
+
|
|
1568
|
+
``None`` means the answer is *no commit*, and the caller may reject
|
|
1569
|
+
the release on it: the remote positively reported the ref does not
|
|
1570
|
+
exist (HTTP 404/422), no deployment capability is bound to the
|
|
1571
|
+
project at all, or the plugin does not implement
|
|
1572
|
+
``resolve_committish``. Every other failure raises instead -- the
|
|
1573
|
+
same distinction ``resolve_remote_tags`` draws between ``'absent'``
|
|
1574
|
+
and ``'error'``, and for the same reason: a caller that refuses a
|
|
1575
|
+
release on ``None`` must never read unreachability as absence. A
|
|
1576
|
+
timed-out, rate-limited, unauthorized or uncredentialed lookup
|
|
1577
|
+
would otherwise turn a "retry shortly" into a terminal "your tag is
|
|
1578
|
+
invalid". Both the capability resolution and the call itself can
|
|
1579
|
+
fail either way, so both are sorted, not just the call.
|
|
1580
|
+
"""
|
|
1581
|
+
try:
|
|
1582
|
+
resolved, ctx, credentials = await _resolve_and_context(
|
|
1583
|
+
db, org_slug, project_id, auth, best_effort_identity=True
|
|
1584
|
+
)
|
|
1585
|
+
except fastapi.HTTPException as exc:
|
|
1586
|
+
# Only 404 -- "no integration provides the deployment
|
|
1587
|
+
# capability" -- means this project has no way to name a
|
|
1588
|
+
# commit. The other answers here are failures to *ask*: a
|
|
1589
|
+
# missing service credential is a deliberate, retryable 503
|
|
1590
|
+
# (see ``_resolve_and_context``), a 403 is an authorization
|
|
1591
|
+
# problem, and a 400 means the capability is bound more than
|
|
1592
|
+
# once and needs ``?source=``. Reporting any of those as an
|
|
1593
|
+
# absent tag is the same mistake as swallowing the timeout.
|
|
1594
|
+
if exc.status_code == 404:
|
|
1595
|
+
return None
|
|
1596
|
+
raise
|
|
1597
|
+
handler = _handler(resolved)
|
|
1598
|
+
try:
|
|
1599
|
+
commit = await call_with_timeout(
|
|
1600
|
+
handler.resolve_committish(
|
|
1601
|
+
ctx, _resolve_credentials(ctx, credentials), tag
|
|
1602
|
+
)
|
|
1603
|
+
)
|
|
1604
|
+
except NotImplementedError:
|
|
1605
|
+
return None
|
|
1606
|
+
except plugin_errors.PluginRateLimited:
|
|
1607
|
+
raise
|
|
1608
|
+
except fastapi.HTTPException:
|
|
1609
|
+
# ``call_with_timeout`` raises 503 + ``Retry-After`` on timeout.
|
|
1610
|
+
raise
|
|
1611
|
+
except httpx.HTTPStatusError as exc:
|
|
1612
|
+
# 404 is the remote saying "no such ref"; GitHub answers 422 for
|
|
1613
|
+
# a ref it cannot even parse. Everything else (403, 5xx) is the
|
|
1614
|
+
# remote failing to answer, not answering "no".
|
|
1615
|
+
if exc.response.status_code in (404, 422):
|
|
1616
|
+
return None
|
|
1617
|
+
LOGGER.warning(
|
|
1618
|
+
'committish lookup for project=%s tag=%s failed with %s',
|
|
1619
|
+
project_id,
|
|
1620
|
+
tag,
|
|
1621
|
+
exc.response.status_code,
|
|
1622
|
+
)
|
|
1623
|
+
raise fastapi.HTTPException(
|
|
1624
|
+
status_code=503,
|
|
1625
|
+
detail=f'Could not reach the source host to resolve {tag!r}',
|
|
1626
|
+
headers={'Retry-After': '5'},
|
|
1627
|
+
) from exc
|
|
1628
|
+
except httpx.HTTPError as exc:
|
|
1629
|
+
# Transport-level: connect, read, protocol. Transient, so it
|
|
1630
|
+
# must not read as "the tag does not exist" either.
|
|
1631
|
+
LOGGER.warning(
|
|
1632
|
+
'committish lookup for project=%s tag=%s failed: %r',
|
|
1633
|
+
project_id,
|
|
1634
|
+
tag,
|
|
1635
|
+
exc,
|
|
1636
|
+
)
|
|
1637
|
+
raise fastapi.HTTPException(
|
|
1638
|
+
status_code=503,
|
|
1639
|
+
detail=f'Could not reach the source host to resolve {tag!r}',
|
|
1640
|
+
headers={'Retry-After': '5'},
|
|
1641
|
+
) from exc
|
|
1642
|
+
await persist_link_writeback(db, ctx)
|
|
1643
|
+
return versioning.short_committish(commit.sha)
|
|
1644
|
+
|
|
1645
|
+
|
|
1548
1646
|
async def backfill_release_notes(
|
|
1549
1647
|
db: graph.Graph,
|
|
1550
1648
|
*,
|
|
@@ -553,8 +553,8 @@ async def _fetch_pr_counts(
|
|
|
553
553
|
) -> dict[str, tuple[int, int, int, int]]:
|
|
554
554
|
"""Return {project_id: (open, closed, viewer_open, viewer_closed)}.
|
|
555
555
|
|
|
556
|
-
Errors are swallowed — PR counts are
|
|
557
|
-
fail the project list endpoint.
|
|
556
|
+
Draft PRs are not counted. Errors are swallowed — PR counts are
|
|
557
|
+
best-effort and should not fail the project list endpoint.
|
|
558
558
|
"""
|
|
559
559
|
if not project_ids:
|
|
560
560
|
return {}
|
|
@@ -570,6 +570,7 @@ async def _fetch_pr_counts(
|
|
|
570
570
|
' AS viewer_closed_count'
|
|
571
571
|
' FROM pull_requests FINAL'
|
|
572
572
|
' WHERE project_id IN {project_ids:Array(String)}'
|
|
573
|
+
' AND NOT draft'
|
|
573
574
|
' GROUP BY project_id'
|
|
574
575
|
)
|
|
575
576
|
params['viewer'] = viewer
|
|
@@ -582,6 +583,7 @@ async def _fetch_pr_counts(
|
|
|
582
583
|
' 0 AS viewer_closed_count'
|
|
583
584
|
' FROM pull_requests FINAL'
|
|
584
585
|
' WHERE project_id IN {project_ids:Array(String)}'
|
|
586
|
+
' AND NOT draft'
|
|
585
587
|
' GROUP BY project_id'
|
|
586
588
|
)
|
|
587
589
|
try:
|
|
@@ -111,13 +111,27 @@ def _row_to_response(row: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
|
111
111
|
async def _fetch_org_project_ids(
|
|
112
112
|
db: graph.Pool,
|
|
113
113
|
org_slug: str,
|
|
114
|
+
*,
|
|
115
|
+
include_archived: bool = False,
|
|
114
116
|
) -> list[str]:
|
|
115
|
-
"""Return
|
|
116
|
-
|
|
117
|
+
"""Return the project IDs belonging to the org.
|
|
118
|
+
|
|
119
|
+
Archived projects are excluded unless ``include_archived`` is set;
|
|
120
|
+
org-wide PR views should not count PRs nobody will act on.
|
|
121
|
+
"""
|
|
122
|
+
archived_filter: typing.LiteralString = (
|
|
123
|
+
'' if include_archived else 'WHERE coalesce(p.archived, false) = false'
|
|
124
|
+
)
|
|
125
|
+
query: typing.LiteralString = (
|
|
126
|
+
"""
|
|
117
127
|
MATCH (p:Project)-[:OWNED_BY]->(:Team)
|
|
118
128
|
-[:BELONGS_TO]->(o:Organization {{slug: {org_slug}}})
|
|
129
|
+
"""
|
|
130
|
+
+ archived_filter
|
|
131
|
+
+ """
|
|
119
132
|
RETURN p.id AS id
|
|
120
133
|
"""
|
|
134
|
+
)
|
|
121
135
|
records = await db.execute(query, {'org_slug': org_slug}, ['id'])
|
|
122
136
|
return [
|
|
123
137
|
graph.parse_agtype(r['id'])
|
|
@@ -133,8 +147,12 @@ async def _list_prs(
|
|
|
133
147
|
author: str | None = None,
|
|
134
148
|
limit: int = DEFAULT_LIMIT,
|
|
135
149
|
offset: int = 0,
|
|
150
|
+
include_drafts: bool = False,
|
|
136
151
|
) -> PullRequestListResponse:
|
|
137
|
-
"""Run the ClickHouse query and return a paginated response.
|
|
152
|
+
"""Run the ClickHouse query and return a paginated response.
|
|
153
|
+
|
|
154
|
+
Draft PRs are excluded unless ``include_drafts`` is set.
|
|
155
|
+
"""
|
|
138
156
|
if limit < 1 or limit > MAX_LIMIT:
|
|
139
157
|
raise fastapi.HTTPException(
|
|
140
158
|
status_code=400,
|
|
@@ -159,6 +177,9 @@ async def _list_prs(
|
|
|
159
177
|
clauses.append('author = {author:String}')
|
|
160
178
|
params['author'] = author
|
|
161
179
|
|
|
180
|
+
if not include_drafts:
|
|
181
|
+
clauses.append('NOT draft')
|
|
182
|
+
|
|
162
183
|
where = ' AND '.join(clauses)
|
|
163
184
|
count_sql = (
|
|
164
185
|
'SELECT count() AS total,' # noqa: S608
|
|
@@ -212,9 +233,13 @@ async def list_project_pull_requests(
|
|
|
212
233
|
|
|
213
234
|
Optional ``state`` filter accepts ``open`` or ``closed``.
|
|
214
235
|
Optional ``author`` filter accepts a GitHub login.
|
|
215
|
-
Results are ordered newest first.
|
|
236
|
+
Results are ordered newest first. Drafts are included and archived
|
|
237
|
+
projects are allowed: the project page filters drafts itself and
|
|
238
|
+
stays readable after the project is archived.
|
|
216
239
|
"""
|
|
217
|
-
org_project_ids = await _fetch_org_project_ids(
|
|
240
|
+
org_project_ids = await _fetch_org_project_ids(
|
|
241
|
+
db, org_slug, include_archived=True
|
|
242
|
+
)
|
|
218
243
|
if project_id not in org_project_ids:
|
|
219
244
|
raise fastapi.HTTPException(
|
|
220
245
|
status_code=404,
|
|
@@ -229,6 +254,7 @@ async def list_project_pull_requests(
|
|
|
229
254
|
author=author,
|
|
230
255
|
limit=limit,
|
|
231
256
|
offset=offset,
|
|
257
|
+
include_drafts=True,
|
|
232
258
|
)
|
|
233
259
|
|
|
234
260
|
|
|
@@ -363,16 +389,13 @@ async def _fetch_pr_activity(
|
|
|
363
389
|
) -> list[dict[str, typing.Any]]:
|
|
364
390
|
"""Aggregate created/open/closed/merged counts per author.
|
|
365
391
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
without merging. Both anchor on ``created_at`` because the table
|
|
374
|
-
carries no close timestamp -- only ``merged_at`` -- so a PR closed
|
|
375
|
-
in-window but opened earlier cannot be attributed to the window.
|
|
392
|
+
Every column covers the PRs created on/after ``since``: ``open``,
|
|
393
|
+
``closed`` (closed without merging) and ``merged`` are the current
|
|
394
|
+
state of that same set, so ``created == open + closed + merged`` on
|
|
395
|
+
every row. A PR opened before ``since`` is not counted anywhere,
|
|
396
|
+
even if it merged in-window; anchoring ``merged`` on ``merged_at``
|
|
397
|
+
instead made the columns disagree with ``created``. Draft PRs are
|
|
398
|
+
excluded from every count.
|
|
376
399
|
"""
|
|
377
400
|
if not project_ids:
|
|
378
401
|
return []
|
|
@@ -386,12 +409,12 @@ async def _fetch_pr_activity(
|
|
|
386
409
|
' AS open_count,'
|
|
387
410
|
" countIf(state != 'open' AND NOT merged"
|
|
388
411
|
' AND created_at >= {since:DateTime64(3)}) AS closed_count,'
|
|
389
|
-
' countIf(merged AND
|
|
412
|
+
' countIf(merged AND created_at >= {since:DateTime64(3)})'
|
|
390
413
|
' AS merged_count'
|
|
391
414
|
' FROM pull_requests FINAL'
|
|
392
415
|
' WHERE project_id IN {project_ids:Array(String)}'
|
|
393
|
-
' AND
|
|
394
|
-
'
|
|
416
|
+
' AND NOT draft'
|
|
417
|
+
' AND created_at >= {since:DateTime64(3)}'
|
|
395
418
|
' GROUP BY author'
|
|
396
419
|
)
|
|
397
420
|
return await clickhouse.query(
|