imbi-api 2.9.0__tar.gz → 2.9.2__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.9.0 → imbi_api-2.9.2}/.pre-commit-config.yaml +2 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/PKG-INFO +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/pyproject.toml +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/app.py +1 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/permissions.py +9 -2
- imbi_api-2.9.2/src/imbi_api/commit_sync/__init__.py +28 -0
- imbi_api-2.9.2/src/imbi_api/commit_sync/queue.py +269 -0
- imbi_api-2.9.2/src/imbi_api/commit_sync/service.py +311 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/models.py +4 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/events.py +7 -1
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/organizations.py +5 -0
- imbi_api-2.9.2/src/imbi_api/endpoints/project_commit_sync.py +83 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_deployments.py +1 -1
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_plugins.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_type_plugins.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/service_plugins.py +26 -17
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/third_party_services.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/lifespans.py +34 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/assignment_writer.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/assignments.py +19 -11
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/lifecycle_dispatch.py +1 -1
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/resolution.py +22 -18
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_permissions.py +52 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_events.py +12 -0
- imbi_api-2.9.2/tests/endpoints/test_project_commit_sync.py +131 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_plugins.py +8 -8
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_type_plugins.py +7 -7
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_plugins.py +25 -7
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_assignment_writer.py +3 -3
- imbi_api-2.9.2/tests/test_commit_sync_queue.py +122 -0
- imbi_api-2.9.2/tests/test_commit_sync_service.py +168 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_plugins.py +13 -13
- {imbi_api-2.9.0 → imbi_api-2.9.2}/uv.lock +9 -10
- {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/.gitignore +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/.python-version +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/CLAUDE.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/CODE_REVIEW_PUNCHLIST.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/LICENSE +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/README.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/coderabbit.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/compose.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/adr.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/configuration.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/index.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/docs/restore-backup.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/justfile +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/mkdocs.yml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/mypy.ini +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/authorization_codes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/login_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/oauth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/oauth_clients.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/seed.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/auth/totp.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/backfill_embeddings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/blueprint_attributes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/domain/scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_helpers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_pagination.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/comments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/dashboard.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/environments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/graph_query.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/identity_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/operations_log.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_analysis.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_logs.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/projects.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/releases.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/search.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/user_activity.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/users.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/endpoints/webhooks.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/entrypoint.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/endpoints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/errors.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/flows.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/host_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/repository.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/resolution.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/state.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/identity/sweeper.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/mcp_test.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/lifecycle.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/prompts/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/sbom.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/scoring/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/scoring/queue.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/email/test_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_deployments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_third_party_services.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_state.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/support.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_app.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_installer.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_lifespans.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_openapi.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_patch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_sbom.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.2}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.2
|
|
4
4
|
Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
|
|
5
5
|
Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -20,7 +20,7 @@ Requires-Dist: cyclonedx-python-lib<12,>=11.7
|
|
|
20
20
|
Requires-Dist: fastapi
|
|
21
21
|
Requires-Dist: filetype
|
|
22
22
|
Requires-Dist: httpx<1,>=0.28.1
|
|
23
|
-
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.9.
|
|
23
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.9.2
|
|
24
24
|
Requires-Dist: jinja2
|
|
25
25
|
Requires-Dist: jsonpatch>=1.33
|
|
26
26
|
Requires-Dist: mcp>=1.26.0
|
|
@@ -31,7 +31,7 @@ Requires-Dist: pydantic[email]
|
|
|
31
31
|
Requires-Dist: pyotp>=2.9.0
|
|
32
32
|
Requires-Dist: python-multipart>=0.0.26
|
|
33
33
|
Requires-Dist: qrcode>=8.0
|
|
34
|
-
Requires-Dist: sentry-sdk
|
|
34
|
+
Requires-Dist: sentry-sdk<3,>=2.48.0
|
|
35
35
|
Requires-Dist: slowapi==0.1.9
|
|
36
36
|
Requires-Dist: typer
|
|
37
37
|
Requires-Dist: uvicorn
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-api"
|
|
3
|
-
version = "2.9.
|
|
3
|
+
version = "2.9.2"
|
|
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.9.
|
|
31
|
+
"imbi-common[databases,llm,sentry,server]==2.9.2",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -38,7 +38,7 @@ dependencies = [
|
|
|
38
38
|
"pyotp>=2.9.0",
|
|
39
39
|
"python-multipart>=0.0.26",
|
|
40
40
|
"qrcode>=8.0",
|
|
41
|
-
"sentry-sdk>=2.48.0",
|
|
41
|
+
"sentry-sdk>=2.48.0,<3", # 3.0 line abandoned upstream; stay on 2.x
|
|
42
42
|
"slowapi==0.1.9", # Pin: alpha quality, API may change
|
|
43
43
|
"typer",
|
|
44
44
|
"uvicorn",
|
|
@@ -13,7 +13,7 @@ import fastapi
|
|
|
13
13
|
import jwt
|
|
14
14
|
import pydantic
|
|
15
15
|
from fastapi import security
|
|
16
|
-
from imbi_common import graph
|
|
16
|
+
from imbi_common import access_log, graph
|
|
17
17
|
from imbi_common.auth import core
|
|
18
18
|
|
|
19
19
|
from imbi_api import models, settings
|
|
@@ -658,7 +658,14 @@ async def _authenticate_token(
|
|
|
658
658
|
treats the token as a JWT.
|
|
659
659
|
"""
|
|
660
660
|
if token.startswith('ik_'):
|
|
661
|
-
|
|
661
|
+
ctx = await authenticate_api_key(db, token, auth_settings)
|
|
662
|
+
# Cache the key's owner so imbi-common's access log renders the
|
|
663
|
+
# person (email / service-account slug) instead of the opaque
|
|
664
|
+
# ``ik_<id>`` it parses from the Authorization header.
|
|
665
|
+
access_log.remember_api_key_principal(
|
|
666
|
+
ctx.session_id or '', ctx.principal_name
|
|
667
|
+
)
|
|
668
|
+
return ctx
|
|
662
669
|
return await authenticate_jwt(db, token, auth_settings)
|
|
663
670
|
|
|
664
671
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""On-demand commit/tag history sync for imbi-api.
|
|
2
|
+
|
|
3
|
+
A project-scoped "Sync Commits & Tags" action (Project Doctor) enqueues a
|
|
4
|
+
Valkey-stream job; a background worker resolves the project's
|
|
5
|
+
``github-commit-sync`` webhook plugin, runs a full default-branch +
|
|
6
|
+
all-tags backfill via the plugin's service credential, and records the
|
|
7
|
+
last-sync status on the ``Project`` node for the UI to poll.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from imbi_api.commit_sync.queue import (
|
|
11
|
+
consume_commit_sync,
|
|
12
|
+
enqueue_commit_sync,
|
|
13
|
+
)
|
|
14
|
+
from imbi_api.commit_sync.service import (
|
|
15
|
+
CommitSyncStatus,
|
|
16
|
+
CommitSyncUnavailable,
|
|
17
|
+
read_status,
|
|
18
|
+
run_sync,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'CommitSyncStatus',
|
|
23
|
+
'CommitSyncUnavailable',
|
|
24
|
+
'consume_commit_sync',
|
|
25
|
+
'enqueue_commit_sync',
|
|
26
|
+
'read_status',
|
|
27
|
+
'run_sync',
|
|
28
|
+
]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Commit/tag-sync queue (Valkey Streams).
|
|
2
|
+
|
|
3
|
+
Mirrors :mod:`imbi_api.scoring.queue`: a single consumer group drains an
|
|
4
|
+
``imbi:commit-sync`` stream, with a per-project debounce, stale-entry
|
|
5
|
+
reclaim, and a dead-letter queue after repeated failures. Each job runs
|
|
6
|
+
a full commit/tag backfill via :func:`commit_sync.service.run_sync` and
|
|
7
|
+
records the outcome on the ``Project`` node.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
import typing
|
|
17
|
+
from collections import abc
|
|
18
|
+
|
|
19
|
+
from imbi_common import graph
|
|
20
|
+
from valkey import asyncio as valkey
|
|
21
|
+
|
|
22
|
+
from imbi_api.commit_sync.service import (
|
|
23
|
+
CommitSyncUnavailable,
|
|
24
|
+
run_sync,
|
|
25
|
+
set_status,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
STREAM = 'imbi:commit-sync'
|
|
29
|
+
GROUP = 'commit-sync-workers'
|
|
30
|
+
CONSUMER_PREFIX = 'worker'
|
|
31
|
+
DLQ = 'imbi:commit-sync:dlq'
|
|
32
|
+
DEBOUNCE_PREFIX = 'imbi:commit-sync:debounce'
|
|
33
|
+
DEBOUNCE_SECONDS = 10
|
|
34
|
+
MAX_DELIVERIES = 3
|
|
35
|
+
CLAIM_IDLE_MS = 120_000
|
|
36
|
+
|
|
37
|
+
LOGGER = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def enqueue_commit_sync(
|
|
41
|
+
client: valkey.Valkey | None,
|
|
42
|
+
org_slug: str,
|
|
43
|
+
project_id: str,
|
|
44
|
+
requested_by: str | None = None,
|
|
45
|
+
) -> bool:
|
|
46
|
+
"""Debounce-then-XADD a commit-sync job. Returns True if enqueued.
|
|
47
|
+
|
|
48
|
+
Tolerates *client* being ``None`` (returns False) so the endpoint can
|
|
49
|
+
surface "queueing unavailable" rather than 500 when Valkey is down.
|
|
50
|
+
"""
|
|
51
|
+
if client is None:
|
|
52
|
+
return False
|
|
53
|
+
key = f'{DEBOUNCE_PREFIX}:{project_id}'
|
|
54
|
+
try:
|
|
55
|
+
acquired = await client.set(key, b'1', ex=DEBOUNCE_SECONDS, nx=True)
|
|
56
|
+
if not acquired:
|
|
57
|
+
return False
|
|
58
|
+
await client.xadd(
|
|
59
|
+
STREAM,
|
|
60
|
+
{
|
|
61
|
+
'org_slug': org_slug,
|
|
62
|
+
'project_id': project_id,
|
|
63
|
+
'requested_by': requested_by or 'system',
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
# Release the debounce key so a transient Valkey write failure
|
|
68
|
+
# doesn't block the user's immediate retry for DEBOUNCE_SECONDS.
|
|
69
|
+
try:
|
|
70
|
+
await client.delete(key)
|
|
71
|
+
except Exception:
|
|
72
|
+
LOGGER.exception('failed to clear debounce key for %s', project_id)
|
|
73
|
+
LOGGER.exception('enqueue_commit_sync failed for %s', project_id)
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def ensure_group(client: valkey.Valkey) -> None:
|
|
79
|
+
try:
|
|
80
|
+
await client.xgroup_create(STREAM, GROUP, id='$', mkstream=True)
|
|
81
|
+
except Exception as err:
|
|
82
|
+
if 'BUSYGROUP' not in str(err):
|
|
83
|
+
LOGGER.exception('xgroup_create failed')
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _process_message(db: graph.Graph, fields: dict[str, str]) -> None:
|
|
88
|
+
project_id = fields.get('project_id')
|
|
89
|
+
org_slug = fields.get('org_slug') or ''
|
|
90
|
+
requested_by = fields.get('requested_by') or 'system'
|
|
91
|
+
if not project_id:
|
|
92
|
+
return
|
|
93
|
+
await set_status(
|
|
94
|
+
db, project_id, status='running', requested_by=requested_by
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
commits, tags = await run_sync(db, org_slug, project_id)
|
|
98
|
+
except CommitSyncUnavailable as exc:
|
|
99
|
+
# Misconfiguration, not transient: record and don't retry.
|
|
100
|
+
LOGGER.warning('commit-sync unavailable for %s: %s', project_id, exc)
|
|
101
|
+
await set_status(
|
|
102
|
+
db,
|
|
103
|
+
project_id,
|
|
104
|
+
status='failed',
|
|
105
|
+
requested_by=requested_by,
|
|
106
|
+
error=str(exc),
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
except Exception:
|
|
110
|
+
# Persist a generic, user-safe message; the polling endpoint
|
|
111
|
+
# exposes this status, so keep raw exception detail in logs only.
|
|
112
|
+
LOGGER.exception('commit-sync run failed for %s', project_id)
|
|
113
|
+
await set_status(
|
|
114
|
+
db,
|
|
115
|
+
project_id,
|
|
116
|
+
status='failed',
|
|
117
|
+
requested_by=requested_by,
|
|
118
|
+
error='Commit sync failed. See server logs for details.',
|
|
119
|
+
)
|
|
120
|
+
raise
|
|
121
|
+
LOGGER.info(
|
|
122
|
+
'commit-sync for %s recorded %d commits, %d tags',
|
|
123
|
+
project_id,
|
|
124
|
+
commits,
|
|
125
|
+
tags,
|
|
126
|
+
)
|
|
127
|
+
await set_status(
|
|
128
|
+
db,
|
|
129
|
+
project_id,
|
|
130
|
+
status='success',
|
|
131
|
+
requested_by=requested_by,
|
|
132
|
+
commits=commits,
|
|
133
|
+
tags=tags,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _decode_fields(
|
|
138
|
+
raw: abc.Mapping[bytes | str, bytes | str],
|
|
139
|
+
) -> dict[str, str]:
|
|
140
|
+
return {
|
|
141
|
+
(k.decode() if isinstance(k, bytes) else k): (
|
|
142
|
+
v.decode() if isinstance(v, bytes) else v
|
|
143
|
+
)
|
|
144
|
+
for k, v in raw.items()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _claim_stale(
|
|
149
|
+
client: valkey.Valkey,
|
|
150
|
+
consumer: str,
|
|
151
|
+
) -> list[tuple[bytes, abc.Mapping[bytes | str, bytes | str]]]:
|
|
152
|
+
try:
|
|
153
|
+
result = await client.xautoclaim(
|
|
154
|
+
STREAM,
|
|
155
|
+
GROUP,
|
|
156
|
+
consumer,
|
|
157
|
+
min_idle_time=CLAIM_IDLE_MS,
|
|
158
|
+
start_id='0-0',
|
|
159
|
+
count=16,
|
|
160
|
+
)
|
|
161
|
+
except Exception as err: # noqa: BLE001
|
|
162
|
+
LOGGER.debug('xautoclaim failed: %s', err)
|
|
163
|
+
return []
|
|
164
|
+
if isinstance(result, (list, tuple)) and len(result) >= 2: # type: ignore[arg-type]
|
|
165
|
+
msgs: object = result[1] # type: ignore[index]
|
|
166
|
+
if isinstance(msgs, list):
|
|
167
|
+
return msgs # type: ignore[return-value]
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def _maybe_dead_letter(
|
|
172
|
+
client: valkey.Valkey,
|
|
173
|
+
msg_id: bytes,
|
|
174
|
+
fields: dict[str, str],
|
|
175
|
+
) -> bool:
|
|
176
|
+
try:
|
|
177
|
+
info = await client.xpending_range(
|
|
178
|
+
STREAM, GROUP, min=msg_id, max=msg_id, count=1
|
|
179
|
+
)
|
|
180
|
+
except Exception: # noqa: BLE001
|
|
181
|
+
return False
|
|
182
|
+
if not info:
|
|
183
|
+
return False
|
|
184
|
+
entry: object = info[0] # type: ignore[index]
|
|
185
|
+
delivered: int | None = None
|
|
186
|
+
if isinstance(entry, dict):
|
|
187
|
+
raw_delivered = entry.get('times_delivered') # type: ignore[union-attr]
|
|
188
|
+
if raw_delivered is not None:
|
|
189
|
+
delivered = int(raw_delivered) # type: ignore[arg-type]
|
|
190
|
+
elif isinstance(entry, (list, tuple)) and len(entry) >= 4: # type: ignore[arg-type]
|
|
191
|
+
raw_delivered = entry[3] # type: ignore[index]
|
|
192
|
+
if raw_delivered is not None:
|
|
193
|
+
delivered = int(raw_delivered) # type: ignore[arg-type]
|
|
194
|
+
if delivered is not None and delivered >= MAX_DELIVERIES:
|
|
195
|
+
await client.xadd(DLQ, fields)
|
|
196
|
+
await client.xack(STREAM, GROUP, msg_id)
|
|
197
|
+
LOGGER.warning(
|
|
198
|
+
'dead-lettered commit-sync msg %s after %s deliveries',
|
|
199
|
+
msg_id,
|
|
200
|
+
delivered,
|
|
201
|
+
)
|
|
202
|
+
return True
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
async def _handle_entries(
|
|
207
|
+
client: valkey.Valkey,
|
|
208
|
+
entries: list[tuple[bytes, abc.Mapping[bytes | str, bytes | str]]],
|
|
209
|
+
db: graph.Graph,
|
|
210
|
+
check_dlq: bool = False,
|
|
211
|
+
) -> None:
|
|
212
|
+
for msg_id, raw_fields in entries:
|
|
213
|
+
fields = _decode_fields(raw_fields)
|
|
214
|
+
if check_dlq and await _maybe_dead_letter(client, msg_id, fields):
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
await _process_message(db, fields)
|
|
218
|
+
except Exception:
|
|
219
|
+
LOGGER.exception('commit-sync failed for %s', fields)
|
|
220
|
+
continue
|
|
221
|
+
await client.xack(STREAM, GROUP, msg_id)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def consume_commit_sync(
|
|
225
|
+
client: valkey.Valkey,
|
|
226
|
+
db: graph.Graph,
|
|
227
|
+
consumer: str | None = None,
|
|
228
|
+
stop: asyncio.Event | None = None,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Run the commit-sync consumer loop until *stop* is set."""
|
|
231
|
+
# Derive a per-process consumer name so concurrent workers don't share
|
|
232
|
+
# a Pending Entries List and stale-claim each other's in-flight jobs.
|
|
233
|
+
consumer = (
|
|
234
|
+
consumer or f'{CONSUMER_PREFIX}-{socket.gethostname()}-{os.getpid()}'
|
|
235
|
+
)
|
|
236
|
+
await ensure_group(client)
|
|
237
|
+
LOGGER.info('Commit-sync consumer loop running (consumer=%s)', consumer)
|
|
238
|
+
while stop is None or not stop.is_set():
|
|
239
|
+
stale = await _claim_stale(client, consumer)
|
|
240
|
+
if stale:
|
|
241
|
+
try:
|
|
242
|
+
await _handle_entries(client, stale, db, check_dlq=True)
|
|
243
|
+
except Exception:
|
|
244
|
+
LOGGER.exception('commit-sync stale-entry handling failed')
|
|
245
|
+
await asyncio.sleep(1)
|
|
246
|
+
continue
|
|
247
|
+
try:
|
|
248
|
+
response = await client.xreadgroup(
|
|
249
|
+
GROUP,
|
|
250
|
+
consumer,
|
|
251
|
+
{STREAM: '>'},
|
|
252
|
+
count=16,
|
|
253
|
+
block=2000,
|
|
254
|
+
)
|
|
255
|
+
except Exception:
|
|
256
|
+
LOGGER.exception('xreadgroup failed')
|
|
257
|
+
await asyncio.sleep(1)
|
|
258
|
+
continue
|
|
259
|
+
if not response:
|
|
260
|
+
continue
|
|
261
|
+
for _stream, entries in typing.cast(
|
|
262
|
+
'list[tuple[object, list[typing.Any]]]', response
|
|
263
|
+
):
|
|
264
|
+
try:
|
|
265
|
+
await _handle_entries(client, entries, db)
|
|
266
|
+
except Exception:
|
|
267
|
+
LOGGER.exception('commit-sync entry handling failed')
|
|
268
|
+
await asyncio.sleep(1)
|
|
269
|
+
break
|