imbi-api 2.11.2__tar.gz → 2.11.4__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.11.2 → imbi_api-2.11.4}/PKG-INFO +2 -2
- {imbi_api-2.11.2 → imbi_api-2.11.4}/pyproject.toml +2 -2
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/app.py +1 -0
- imbi_api-2.11.4/src/imbi_api/blueprint_compliance.py +393 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/organizations.py +5 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_analysis.py +69 -10
- imbi_api-2.11.4/src/imbi_api/endpoints/project_pr_sync.py +85 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/lifespans.py +34 -0
- imbi_api-2.11.4/src/imbi_api/pr_sync/__init__.py +28 -0
- imbi_api-2.11.4/src/imbi_api/pr_sync/queue.py +308 -0
- imbi_api-2.11.4/src/imbi_api/pr_sync/service.py +321 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_events.py +2 -2
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_analysis.py +53 -0
- imbi_api-2.11.4/tests/test_pr_sync_queue.py +204 -0
- imbi_api-2.11.4/tests/test_pr_sync_service.py +192 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/uv.lock +70 -63
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.gitignore +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.pre-commit-config.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/.python-version +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/CLAUDE.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/CODE_REVIEW_PUNCHLIST.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/LICENSE +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/README.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/coderabbit.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/compose.yaml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/adr.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/configuration.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/index.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/docs/restore-backup.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/justfile +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/mkdocs.yml +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/mypy.ini +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/authorization_codes.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/login_providers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/oauth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/oauth_clients.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/permissions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/seed.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/auth/totp.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/backfill_embeddings.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/blueprint_attributes.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/commit_sync/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/commit_sync/queue.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/commit_sync/service.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/domain/models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/domain/scoring.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/_credentials.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/_helpers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/_pagination.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/blueprints.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/comments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/dashboard.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/environments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/events.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/graph_query.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/identity_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/operations_log.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_commit_sync.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_deployments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_incidents.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_lifecycle.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_logs.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_type_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/projects.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/releases.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/search.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/service_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/third_party_services.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/user_activity.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/users.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/endpoints/webhooks.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/entrypoint.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/attribution.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/endpoints.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/errors.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/flows.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/host_integration.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/repository.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/resolution.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/state.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/identity/sweeper.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/mcp_test.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/assignment_writer.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/assignments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/credentials.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/lifecycle.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/resolution.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/prompts/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/sbom.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/scoring/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/scoring/queue.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/test_client.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/test_init.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/test_integration.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/test_models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/email/test_templates.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_commit_sync.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_deployments.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_incidents.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_lifecycle.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_type_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_service_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_third_party_services.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_state.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/storage/__init__.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/storage/test_client.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/storage/test_init.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/support.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_app.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_assignment_writer.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_blueprints.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_commit_sync_queue.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_commit_sync_service.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_identity_attribution.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_init.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_installer.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_lifespans.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_models.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_openapi.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_patch.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_plugins.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_sbom.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.11.2 → imbi_api-2.11.4}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.11.
|
|
3
|
+
Version: 2.11.4
|
|
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.11.
|
|
23
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]==2.11.4
|
|
24
24
|
Requires-Dist: jinja2
|
|
25
25
|
Requires-Dist: jsonpatch>=1.33
|
|
26
26
|
Requires-Dist: mcp>=1.26.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-api"
|
|
3
|
-
version = "2.11.
|
|
3
|
+
version = "2.11.4"
|
|
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.11.
|
|
31
|
+
"imbi-common[databases,llm,sentry,server]==2.11.4",
|
|
32
32
|
"jinja2",
|
|
33
33
|
"jsonpatch>=1.33",
|
|
34
34
|
"mcp>=1.26.0",
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Blueprint property compliance check for Project Doctor.
|
|
2
|
+
|
|
3
|
+
Compares a project's current AGE node properties against every
|
|
4
|
+
applicable Project blueprint's JSON Schema and emits an
|
|
5
|
+
:class:`~imbi_common.plugins.base.AnalysisResultItem` for each
|
|
6
|
+
non-conformant property.
|
|
7
|
+
|
|
8
|
+
This is a *built-in* check — no external plugin or credentials are
|
|
9
|
+
required; it queries the graph directly. The caller is responsible
|
|
10
|
+
for wrapping the returned items into :class:`AnalysisResult` objects
|
|
11
|
+
(adding ``plugin_slug`` and ``plugin_id``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
import typing
|
|
19
|
+
|
|
20
|
+
from imbi_common import graph, models
|
|
21
|
+
from imbi_common.plugins.base import AnalysisResultItem
|
|
22
|
+
|
|
23
|
+
from imbi_api.blueprint_attributes import project_blueprints
|
|
24
|
+
|
|
25
|
+
LOGGER = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
BLUEPRINT_PLUGIN_SLUG = 'blueprint-compliance'
|
|
28
|
+
BLUEPRINT_PLUGIN_ID = 'built-in'
|
|
29
|
+
|
|
30
|
+
_SENTINEL = object() # distinguishes "not present" from explicit None
|
|
31
|
+
|
|
32
|
+
# Blueprint property names must be safe to embed in Cypher SET clauses.
|
|
33
|
+
# Only allow names that look like identifiers.
|
|
34
|
+
_SAFE_PROP_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
|
|
35
|
+
|
|
36
|
+
_FETCH_PROPS_QUERY = """
|
|
37
|
+
MATCH (p:Project {{id: {project_id}}})
|
|
38
|
+
RETURN p{{.*}} AS props
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _fetch_project_props(
|
|
43
|
+
db: graph.Graph, project_id: str
|
|
44
|
+
) -> dict[str, typing.Any]:
|
|
45
|
+
rows = await db.execute(
|
|
46
|
+
_FETCH_PROPS_QUERY, {'project_id': project_id}, ['props']
|
|
47
|
+
)
|
|
48
|
+
if not rows:
|
|
49
|
+
return {}
|
|
50
|
+
raw = graph.parse_agtype(rows[0]['props'])
|
|
51
|
+
if not isinstance(raw, dict):
|
|
52
|
+
return {}
|
|
53
|
+
return typing.cast('dict[str, typing.Any]', raw)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_missing(value: typing.Any) -> bool:
|
|
57
|
+
return value is _SENTINEL or value is None or value == ''
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _check_property(
|
|
61
|
+
section_slug: str,
|
|
62
|
+
prop_name: str,
|
|
63
|
+
prop_schema: models.Schema,
|
|
64
|
+
required: bool,
|
|
65
|
+
current_value: typing.Any,
|
|
66
|
+
) -> AnalysisResultItem | None:
|
|
67
|
+
"""Return a finding for a non-conformant property, or ``None``."""
|
|
68
|
+
display = getattr(prop_schema, 'title', None) or prop_name
|
|
69
|
+
missing = _is_missing(current_value)
|
|
70
|
+
|
|
71
|
+
if required and missing:
|
|
72
|
+
return AnalysisResultItem(
|
|
73
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:{section_slug}:{prop_name}:missing',
|
|
74
|
+
title=f'Required property not set: {display}',
|
|
75
|
+
description=(
|
|
76
|
+
f'`{prop_name}` is required by the **{section_slug}** '
|
|
77
|
+
f'blueprint but has no value. Edit the project to set it.'
|
|
78
|
+
),
|
|
79
|
+
status='fail',
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not missing:
|
|
83
|
+
enum = getattr(prop_schema, 'enum', None)
|
|
84
|
+
if enum is not None and current_value not in enum:
|
|
85
|
+
choices = ', '.join(f'`{v}`' for v in enum)
|
|
86
|
+
return AnalysisResultItem(
|
|
87
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:{section_slug}:{prop_name}:invalid-enum',
|
|
88
|
+
title=f'Property value not in allowed set: {display}',
|
|
89
|
+
description=(
|
|
90
|
+
f'`{prop_name}` is `{current_value!r}` but the '
|
|
91
|
+
f'allowed values are: {choices}. '
|
|
92
|
+
f'Edit the project to correct it.'
|
|
93
|
+
),
|
|
94
|
+
status='fail',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
minimum = getattr(prop_schema, 'minimum', None)
|
|
98
|
+
maximum = getattr(prop_schema, 'maximum', None)
|
|
99
|
+
if isinstance(current_value, (int, float)):
|
|
100
|
+
if minimum is not None and current_value < minimum:
|
|
101
|
+
return AnalysisResultItem(
|
|
102
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:{section_slug}:{prop_name}:below-minimum',
|
|
103
|
+
title=f'Property below minimum: {display}',
|
|
104
|
+
description=(
|
|
105
|
+
f'`{prop_name}` is `{current_value}` but '
|
|
106
|
+
f'the minimum is `{minimum}`.'
|
|
107
|
+
),
|
|
108
|
+
status='warn',
|
|
109
|
+
)
|
|
110
|
+
if maximum is not None and current_value > maximum:
|
|
111
|
+
return AnalysisResultItem(
|
|
112
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:{section_slug}:{prop_name}:above-maximum',
|
|
113
|
+
title=f'Property above maximum: {display}',
|
|
114
|
+
description=(
|
|
115
|
+
f'`{prop_name}` is `{current_value}` but '
|
|
116
|
+
f'the maximum is `{maximum}`.'
|
|
117
|
+
),
|
|
118
|
+
status='warn',
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Missing but not required — warn when a default is available
|
|
122
|
+
if missing:
|
|
123
|
+
default = getattr(prop_schema, 'default', None)
|
|
124
|
+
if default is not None:
|
|
125
|
+
return AnalysisResultItem(
|
|
126
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:{section_slug}:{prop_name}:use-default',
|
|
127
|
+
title=f'Property not set — default available: {display}',
|
|
128
|
+
description=(
|
|
129
|
+
f'`{prop_name}` has no value. '
|
|
130
|
+
f'The blueprint default is `{default!r}`. '
|
|
131
|
+
f'Use **Apply Blueprint Fixes** to set it.'
|
|
132
|
+
),
|
|
133
|
+
status='warn',
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _stale_blueprint_properties(
|
|
140
|
+
all_blueprints: list[models.Blueprint],
|
|
141
|
+
applicable: list[models.Blueprint],
|
|
142
|
+
current_props: dict[str, typing.Any],
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
"""Return property names set on the project with no applicable blueprint.
|
|
145
|
+
|
|
146
|
+
A property is "stale" when it was set by a blueprint that no longer
|
|
147
|
+
applies to this project's type(s). Only considers properties defined in
|
|
148
|
+
*some* blueprint (so core model fields like ``id`` or ``name`` are never
|
|
149
|
+
flagged).
|
|
150
|
+
"""
|
|
151
|
+
all_bp_props: set[str] = set()
|
|
152
|
+
for bp in all_blueprints:
|
|
153
|
+
if bp.kind != 'node':
|
|
154
|
+
continue
|
|
155
|
+
for prop_name in bp.json_schema.properties or {}:
|
|
156
|
+
all_bp_props.add(prop_name)
|
|
157
|
+
|
|
158
|
+
applicable_props: set[str] = set()
|
|
159
|
+
for bp in applicable:
|
|
160
|
+
for prop_name in bp.json_schema.properties or {}:
|
|
161
|
+
applicable_props.add(prop_name)
|
|
162
|
+
|
|
163
|
+
stale = all_bp_props - applicable_props
|
|
164
|
+
return [
|
|
165
|
+
p for p in stale if not _is_missing(current_props.get(p, _SENTINEL))
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _applicable_blueprints(
|
|
170
|
+
blueprints: list[models.Blueprint],
|
|
171
|
+
type_slug_set: set[str],
|
|
172
|
+
) -> list[models.Blueprint]:
|
|
173
|
+
out: list[models.Blueprint] = []
|
|
174
|
+
for bp in blueprints:
|
|
175
|
+
if bp.kind != 'node':
|
|
176
|
+
continue
|
|
177
|
+
f = bp.filter
|
|
178
|
+
if (
|
|
179
|
+
f is not None
|
|
180
|
+
and f.project_type
|
|
181
|
+
and not type_slug_set.intersection(f.project_type)
|
|
182
|
+
):
|
|
183
|
+
continue
|
|
184
|
+
if bp.json_schema.properties:
|
|
185
|
+
out.append(bp)
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def check_blueprint_compliance(
|
|
190
|
+
db: graph.Graph,
|
|
191
|
+
project_id: str,
|
|
192
|
+
type_slugs: list[str],
|
|
193
|
+
) -> list[AnalysisResultItem]:
|
|
194
|
+
"""Return blueprint compliance findings for a project.
|
|
195
|
+
|
|
196
|
+
Loads every enabled Project blueprint, filters to those that apply
|
|
197
|
+
to the project's types, and checks each property against the
|
|
198
|
+
project's current AGE node properties. Returns a single ``pass``
|
|
199
|
+
item when everything is compliant so the Doctor card always shows
|
|
200
|
+
something for this check.
|
|
201
|
+
"""
|
|
202
|
+
all_blueprints = await project_blueprints(db)
|
|
203
|
+
if not all_blueprints:
|
|
204
|
+
return [
|
|
205
|
+
AnalysisResultItem(
|
|
206
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:no-blueprints',
|
|
207
|
+
title='No Project blueprints configured',
|
|
208
|
+
description=(
|
|
209
|
+
'No enabled Project blueprints are defined. '
|
|
210
|
+
'Add blueprints to track property compliance.'
|
|
211
|
+
),
|
|
212
|
+
status='pass',
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
type_slug_set = set(type_slugs)
|
|
217
|
+
applicable = _applicable_blueprints(all_blueprints, type_slug_set)
|
|
218
|
+
|
|
219
|
+
props = await _fetch_project_props(db, project_id)
|
|
220
|
+
findings: list[AnalysisResultItem] = []
|
|
221
|
+
|
|
222
|
+
for bp in applicable:
|
|
223
|
+
schema = bp.json_schema
|
|
224
|
+
required_names: set[str] = set(schema.required or [])
|
|
225
|
+
section_slug = bp.slug or ''
|
|
226
|
+
for prop_name, prop_schema in (schema.properties or {}).items():
|
|
227
|
+
extra = prop_schema.model_extra or {}
|
|
228
|
+
x_ui = dict(extra.get('x-ui') or {})
|
|
229
|
+
required = (
|
|
230
|
+
prop_name in required_names or x_ui.get('required') is True
|
|
231
|
+
)
|
|
232
|
+
current_value = props.get(prop_name, _SENTINEL)
|
|
233
|
+
finding = _check_property(
|
|
234
|
+
section_slug, prop_name, prop_schema, required, current_value
|
|
235
|
+
)
|
|
236
|
+
if finding is not None:
|
|
237
|
+
findings.append(finding)
|
|
238
|
+
|
|
239
|
+
# Detect blueprint-managed properties no longer in any applicable
|
|
240
|
+
# blueprint.
|
|
241
|
+
stale = _stale_blueprint_properties(all_blueprints, applicable, props)
|
|
242
|
+
for prop_name in sorted(stale):
|
|
243
|
+
findings.append(
|
|
244
|
+
AnalysisResultItem(
|
|
245
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:stale:{prop_name}',
|
|
246
|
+
title=f'Property not in any applicable blueprint: {prop_name}',
|
|
247
|
+
description=(
|
|
248
|
+
f'`{prop_name}` is set on this project but is not defined '
|
|
249
|
+
f'in any currently applicable blueprint. '
|
|
250
|
+
f'Use **Apply Blueprint Fixes** to remove it.'
|
|
251
|
+
),
|
|
252
|
+
status='warn',
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if not findings:
|
|
257
|
+
if not applicable:
|
|
258
|
+
return [
|
|
259
|
+
AnalysisResultItem(
|
|
260
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:no-applicable',
|
|
261
|
+
title='No blueprints apply to this project type',
|
|
262
|
+
description=(
|
|
263
|
+
"No enabled blueprints match this project's type(s)."
|
|
264
|
+
),
|
|
265
|
+
status='pass',
|
|
266
|
+
)
|
|
267
|
+
]
|
|
268
|
+
return [
|
|
269
|
+
AnalysisResultItem(
|
|
270
|
+
slug=f'{BLUEPRINT_PLUGIN_SLUG}:all-pass',
|
|
271
|
+
title='All blueprint properties are correctly set',
|
|
272
|
+
description=(
|
|
273
|
+
'Every required and recommended blueprint property '
|
|
274
|
+
'has a valid value.'
|
|
275
|
+
),
|
|
276
|
+
status='pass',
|
|
277
|
+
)
|
|
278
|
+
]
|
|
279
|
+
return findings
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
async def apply_blueprint_defaults(
|
|
283
|
+
db: graph.Graph,
|
|
284
|
+
project_id: str,
|
|
285
|
+
type_slugs: list[str],
|
|
286
|
+
) -> int:
|
|
287
|
+
"""Set blueprint default values on project properties that are unset.
|
|
288
|
+
|
|
289
|
+
Only touches properties that are currently ``None`` / absent AND
|
|
290
|
+
have a ``default`` in the blueprint schema. Does not overwrite
|
|
291
|
+
existing values.
|
|
292
|
+
|
|
293
|
+
Returns the number of properties updated.
|
|
294
|
+
"""
|
|
295
|
+
all_blueprints = await project_blueprints(db)
|
|
296
|
+
if not all_blueprints:
|
|
297
|
+
return 0
|
|
298
|
+
|
|
299
|
+
type_slug_set = set(type_slugs)
|
|
300
|
+
applicable = _applicable_blueprints(all_blueprints, type_slug_set)
|
|
301
|
+
if not applicable:
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
props = await _fetch_project_props(db, project_id)
|
|
305
|
+
|
|
306
|
+
# Collect prop_name → default for every unset property with a default.
|
|
307
|
+
# Use an ordered dict so later (higher-priority) blueprints win.
|
|
308
|
+
defaults: dict[str, typing.Any] = {}
|
|
309
|
+
for bp in applicable:
|
|
310
|
+
for prop_name, prop_schema in (
|
|
311
|
+
bp.json_schema.properties or {}
|
|
312
|
+
).items():
|
|
313
|
+
if not _SAFE_PROP_RE.match(prop_name):
|
|
314
|
+
LOGGER.warning(
|
|
315
|
+
'Skipping unsafe blueprint property name %r', prop_name
|
|
316
|
+
)
|
|
317
|
+
continue
|
|
318
|
+
if not _is_missing(props.get(prop_name)):
|
|
319
|
+
continue
|
|
320
|
+
default = getattr(prop_schema, 'default', None)
|
|
321
|
+
if default is not None:
|
|
322
|
+
defaults[prop_name] = default
|
|
323
|
+
|
|
324
|
+
if not defaults:
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
# Build a single SET clause; parameter names are indexed to avoid
|
|
328
|
+
# collisions with the property names themselves.
|
|
329
|
+
set_parts = [f'p.{k} = {{v{i}}}' for i, k in enumerate(defaults)]
|
|
330
|
+
cypher = (
|
|
331
|
+
'MATCH (p:Project {{id: {project_id}}}) SET '
|
|
332
|
+
+ ', '.join(set_parts)
|
|
333
|
+
+ ' RETURN p.id AS id'
|
|
334
|
+
)
|
|
335
|
+
params: dict[str, typing.Any] = {'project_id': project_id}
|
|
336
|
+
for i, (_, v) in enumerate(defaults.items()):
|
|
337
|
+
params[f'v{i}'] = v
|
|
338
|
+
|
|
339
|
+
await db.execute(cypher, params, ['id'])
|
|
340
|
+
LOGGER.info(
|
|
341
|
+
'Applied %d blueprint default(s) to project %s: %s',
|
|
342
|
+
len(defaults),
|
|
343
|
+
project_id,
|
|
344
|
+
list(defaults.keys()),
|
|
345
|
+
)
|
|
346
|
+
return len(defaults)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
async def remove_stale_blueprint_properties(
|
|
350
|
+
db: graph.Graph,
|
|
351
|
+
project_id: str,
|
|
352
|
+
type_slugs: list[str],
|
|
353
|
+
) -> int:
|
|
354
|
+
"""Remove blueprint-managed properties no longer in any applicable
|
|
355
|
+
blueprint.
|
|
356
|
+
|
|
357
|
+
Only removes properties defined in *some* blueprint, so core model
|
|
358
|
+
fields (``id``, ``name``, etc.) are never touched. Stale properties are
|
|
359
|
+
removed by setting them to ``null`` in a single Cypher ``SET`` clause.
|
|
360
|
+
|
|
361
|
+
Returns the number of properties removed.
|
|
362
|
+
"""
|
|
363
|
+
all_blueprints = await project_blueprints(db)
|
|
364
|
+
if not all_blueprints:
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
type_slug_set = set(type_slugs)
|
|
368
|
+
applicable = _applicable_blueprints(all_blueprints, type_slug_set)
|
|
369
|
+
props = await _fetch_project_props(db, project_id)
|
|
370
|
+
|
|
371
|
+
stale = [
|
|
372
|
+
p
|
|
373
|
+
for p in _stale_blueprint_properties(all_blueprints, applicable, props)
|
|
374
|
+
if _SAFE_PROP_RE.match(p)
|
|
375
|
+
]
|
|
376
|
+
if not stale:
|
|
377
|
+
return 0
|
|
378
|
+
|
|
379
|
+
# Setting a property to null removes it from the AGE node.
|
|
380
|
+
set_parts = [f'p.{k} = null' for k in stale]
|
|
381
|
+
cypher = (
|
|
382
|
+
'MATCH (p:Project {{id: {project_id}}}) SET '
|
|
383
|
+
+ ', '.join(set_parts)
|
|
384
|
+
+ ' RETURN p.id AS id'
|
|
385
|
+
)
|
|
386
|
+
await db.execute(cypher, {'project_id': project_id}, ['id'])
|
|
387
|
+
LOGGER.info(
|
|
388
|
+
'Removed %d stale blueprint property/ies from project %s: %s',
|
|
389
|
+
len(stale),
|
|
390
|
+
project_id,
|
|
391
|
+
stale,
|
|
392
|
+
)
|
|
393
|
+
return len(stale)
|
|
@@ -34,6 +34,7 @@ from .project_incidents import project_incidents_router
|
|
|
34
34
|
from .project_lifecycle import project_lifecycle_router
|
|
35
35
|
from .project_logs import project_logs_router
|
|
36
36
|
from .project_plugins import project_plugins_router
|
|
37
|
+
from .project_pr_sync import project_pr_sync_router
|
|
37
38
|
from .project_type_plugins import project_type_plugins_router
|
|
38
39
|
from .project_types import project_types_router
|
|
39
40
|
from .projects import projects_router
|
|
@@ -176,6 +177,10 @@ organizations_router.include_router(
|
|
|
176
177
|
project_commit_sync_router,
|
|
177
178
|
prefix='/{org_slug}/projects/{project_id}/commits',
|
|
178
179
|
)
|
|
180
|
+
organizations_router.include_router(
|
|
181
|
+
project_pr_sync_router,
|
|
182
|
+
prefix='/{org_slug}/projects/{project_id}/pull-requests',
|
|
183
|
+
)
|
|
179
184
|
organizations_router.include_router(
|
|
180
185
|
pull_requests_project_router,
|
|
181
186
|
prefix='/{org_slug}/projects/{project_id}/pull-requests',
|
|
@@ -29,6 +29,13 @@ from imbi_common.plugins.base import (
|
|
|
29
29
|
from imbi_common.plugins.errors import PluginCredentialsMissing
|
|
30
30
|
|
|
31
31
|
from imbi_api.auth import permissions
|
|
32
|
+
from imbi_api.blueprint_compliance import (
|
|
33
|
+
BLUEPRINT_PLUGIN_ID,
|
|
34
|
+
BLUEPRINT_PLUGIN_SLUG,
|
|
35
|
+
apply_blueprint_defaults,
|
|
36
|
+
check_blueprint_compliance,
|
|
37
|
+
remove_stale_blueprint_properties,
|
|
38
|
+
)
|
|
32
39
|
from imbi_api.endpoints._helpers import (
|
|
33
40
|
lookup_project_exists_in,
|
|
34
41
|
lookup_project_links,
|
|
@@ -381,6 +388,13 @@ async def get_project_analysis(
|
|
|
381
388
|
return report
|
|
382
389
|
|
|
383
390
|
|
|
391
|
+
class ApplyDefaultsResponse(pydantic.BaseModel):
|
|
392
|
+
"""Result of syncing blueprint properties on a project."""
|
|
393
|
+
|
|
394
|
+
properties_updated: int
|
|
395
|
+
properties_removed: int = 0
|
|
396
|
+
|
|
397
|
+
|
|
384
398
|
@project_analysis_router.post('/run', response_model=AnalysisReport)
|
|
385
399
|
async def run_project_analysis(
|
|
386
400
|
org_slug: str,
|
|
@@ -391,20 +405,27 @@ async def run_project_analysis(
|
|
|
391
405
|
fastapi.Depends(permissions.require_permission('project:write')),
|
|
392
406
|
],
|
|
393
407
|
) -> AnalysisReport:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
408
|
+
type_slugs = await lookup_project_type_slugs(db, project_id)
|
|
409
|
+
compliance_items = await check_blueprint_compliance(
|
|
410
|
+
db, project_id, type_slugs
|
|
411
|
+
)
|
|
412
|
+
compliance_results = [
|
|
413
|
+
AnalysisResult(
|
|
414
|
+
slug=item.slug,
|
|
415
|
+
title=item.title,
|
|
416
|
+
description=item.description,
|
|
417
|
+
status=item.status,
|
|
418
|
+
plugin_slug=BLUEPRINT_PLUGIN_SLUG,
|
|
419
|
+
plugin_id=BLUEPRINT_PLUGIN_ID,
|
|
402
420
|
)
|
|
403
|
-
|
|
421
|
+
for item in compliance_items
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
plugins = await resolve_analysis_plugins(db, project_id)
|
|
404
425
|
per_plugin = await asyncio.gather(
|
|
405
426
|
*(_run_one(db, org_slug, project_id, rp) for rp in plugins)
|
|
406
427
|
)
|
|
407
|
-
results: list[AnalysisResult] =
|
|
428
|
+
results: list[AnalysisResult] = list(compliance_results)
|
|
408
429
|
for batch in per_plugin:
|
|
409
430
|
results.extend(batch)
|
|
410
431
|
results.sort(key=lambda r: (-_STATUS_RANK.get(r.status, 0), r.title))
|
|
@@ -417,3 +438,41 @@ async def run_project_analysis(
|
|
|
417
438
|
triggered_by_user_id=triggered,
|
|
418
439
|
results=results,
|
|
419
440
|
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@project_analysis_router.post(
|
|
444
|
+
'/apply-blueprint-defaults',
|
|
445
|
+
response_model=ApplyDefaultsResponse,
|
|
446
|
+
)
|
|
447
|
+
async def apply_project_blueprint_defaults(
|
|
448
|
+
org_slug: str,
|
|
449
|
+
project_id: str,
|
|
450
|
+
db: graph.Pool,
|
|
451
|
+
auth: typing.Annotated[
|
|
452
|
+
permissions.AuthContext,
|
|
453
|
+
fastapi.Depends(permissions.require_permission('project:write')),
|
|
454
|
+
],
|
|
455
|
+
) -> ApplyDefaultsResponse:
|
|
456
|
+
"""Apply blueprint default values to any unset project properties.
|
|
457
|
+
|
|
458
|
+
Only sets properties that are currently absent or ``null`` and that
|
|
459
|
+
have a ``default`` defined in the applicable blueprint schema.
|
|
460
|
+
Existing values are never overwritten.
|
|
461
|
+
"""
|
|
462
|
+
del org_slug
|
|
463
|
+
type_slugs = await lookup_project_type_slugs(db, project_id)
|
|
464
|
+
count = await apply_blueprint_defaults(db, project_id, type_slugs)
|
|
465
|
+
removed = await remove_stale_blueprint_properties(
|
|
466
|
+
db, project_id, type_slugs
|
|
467
|
+
)
|
|
468
|
+
if auth.user:
|
|
469
|
+
LOGGER.info(
|
|
470
|
+
'User %s applied %d default(s), removed %d stale on project %s',
|
|
471
|
+
auth.user.id,
|
|
472
|
+
count,
|
|
473
|
+
removed,
|
|
474
|
+
project_id,
|
|
475
|
+
)
|
|
476
|
+
return ApplyDefaultsResponse(
|
|
477
|
+
properties_updated=count, properties_removed=removed
|
|
478
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""On-demand PR history sync endpoints (Project Doctor).
|
|
2
|
+
|
|
3
|
+
``POST /pull-requests/sync`` enqueues a background backfill of the
|
|
4
|
+
project's full pull-request history; ``GET /pull-requests/sync-status``
|
|
5
|
+
returns the last-run state for the UI to poll. The work runs as a
|
|
6
|
+
Valkey-stream job using the ``github-pr-sync`` plugin's service
|
|
7
|
+
credential, so the endpoint only validates eligibility and enqueues.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import typing
|
|
11
|
+
|
|
12
|
+
import fastapi
|
|
13
|
+
import pydantic
|
|
14
|
+
from imbi_common import graph
|
|
15
|
+
|
|
16
|
+
from imbi_api.auth import permissions
|
|
17
|
+
from imbi_api.plugins.resolution import resolve_plugin
|
|
18
|
+
from imbi_api.pr_sync import service
|
|
19
|
+
from imbi_api.pr_sync.queue import enqueue_pr_sync
|
|
20
|
+
from imbi_api.scoring import OptionalValkeyClient
|
|
21
|
+
|
|
22
|
+
project_pr_sync_router = fastapi.APIRouter(tags=['Project: Pull Requests'])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PRSyncEnqueueResponse(pydantic.BaseModel):
|
|
26
|
+
"""Result of enqueueing a PR history sync."""
|
|
27
|
+
|
|
28
|
+
enqueued: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@project_pr_sync_router.post('/sync', status_code=202)
|
|
32
|
+
async def sync_pull_requests(
|
|
33
|
+
org_slug: str,
|
|
34
|
+
project_id: str,
|
|
35
|
+
db: graph.Pool,
|
|
36
|
+
valkey_client: OptionalValkeyClient,
|
|
37
|
+
auth: typing.Annotated[
|
|
38
|
+
permissions.AuthContext,
|
|
39
|
+
fastapi.Depends(
|
|
40
|
+
permissions.require_permission('project:deployment:write'),
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
) -> PRSyncEnqueueResponse:
|
|
44
|
+
"""Enqueue a full pull-request history backfill for the project.
|
|
45
|
+
|
|
46
|
+
Requires the project to have a GitHub deployment plugin (the same
|
|
47
|
+
gate as deployment resync) and a connected ``github-pr-sync`` plugin
|
|
48
|
+
to provide the service credential. Returns ``enqueued=False`` when
|
|
49
|
+
the job was debounced or Valkey is unavailable.
|
|
50
|
+
"""
|
|
51
|
+
await resolve_plugin(db, project_id, 'deployment', None)
|
|
52
|
+
try:
|
|
53
|
+
await service.check_available(db, project_id)
|
|
54
|
+
except service.PRSyncUnavailable as exc:
|
|
55
|
+
raise fastapi.HTTPException(status_code=400, detail=str(exc)) from exc
|
|
56
|
+
requested_by = auth.principal_name
|
|
57
|
+
enqueued = await enqueue_pr_sync(
|
|
58
|
+
valkey_client, org_slug, project_id, requested_by
|
|
59
|
+
)
|
|
60
|
+
if enqueued:
|
|
61
|
+
await service.set_status(
|
|
62
|
+
db,
|
|
63
|
+
project_id,
|
|
64
|
+
status='queued',
|
|
65
|
+
requested_by=requested_by,
|
|
66
|
+
retry=False,
|
|
67
|
+
)
|
|
68
|
+
return PRSyncEnqueueResponse(enqueued=enqueued)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@project_pr_sync_router.get('/sync-status')
|
|
72
|
+
async def get_pr_sync_status(
|
|
73
|
+
org_slug: str,
|
|
74
|
+
project_id: str,
|
|
75
|
+
db: graph.Pool,
|
|
76
|
+
_auth: typing.Annotated[
|
|
77
|
+
permissions.AuthContext,
|
|
78
|
+
fastapi.Depends(
|
|
79
|
+
permissions.require_permission('project:deployment:read'),
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
) -> service.PRSyncStatus:
|
|
83
|
+
"""Return the project's last PR sync state."""
|
|
84
|
+
del org_slug
|
|
85
|
+
return await service.read_status(db, project_id)
|
|
@@ -19,6 +19,7 @@ from imbi_api.email.client import EmailClient
|
|
|
19
19
|
from imbi_api.email.templates import TemplateManager
|
|
20
20
|
from imbi_api.identity import sweeper as identity_sweeper
|
|
21
21
|
from imbi_api.plugins import lifecycle as plugin_lifecycle
|
|
22
|
+
from imbi_api.pr_sync import queue as pr_sync_queue
|
|
22
23
|
from imbi_api.scoring import queue as score_queue
|
|
23
24
|
from imbi_api.storage.client import StorageClient
|
|
24
25
|
|
|
@@ -165,6 +166,39 @@ async def commit_sync_worker_hook() -> abc.AsyncGenerator[None]:
|
|
|
165
166
|
)
|
|
166
167
|
|
|
167
168
|
|
|
169
|
+
@contextlib.asynccontextmanager
|
|
170
|
+
async def pr_sync_worker_hook() -> abc.AsyncGenerator[None]:
|
|
171
|
+
"""Run the on-demand PR-sync consumer loop."""
|
|
172
|
+
try:
|
|
173
|
+
client = valkey.get_client()
|
|
174
|
+
except RuntimeError:
|
|
175
|
+
LOGGER.warning('Valkey unavailable; pr-sync worker not started')
|
|
176
|
+
yield None
|
|
177
|
+
return
|
|
178
|
+
if _graph is None:
|
|
179
|
+
LOGGER.warning('Graph not ready; pr-sync worker not started')
|
|
180
|
+
yield None
|
|
181
|
+
return
|
|
182
|
+
stop = asyncio.Event()
|
|
183
|
+
LOGGER.info('PR-sync worker starting')
|
|
184
|
+
consumer_task = asyncio.create_task(
|
|
185
|
+
pr_sync_queue.consume_pr_sync(client, _graph, stop=stop)
|
|
186
|
+
)
|
|
187
|
+
try:
|
|
188
|
+
yield None
|
|
189
|
+
finally:
|
|
190
|
+
stop.set()
|
|
191
|
+
consumer_task.cancel()
|
|
192
|
+
try:
|
|
193
|
+
await consumer_task
|
|
194
|
+
except asyncio.CancelledError:
|
|
195
|
+
pass
|
|
196
|
+
except Exception: # noqa: BLE001
|
|
197
|
+
LOGGER.warning(
|
|
198
|
+
'PR-sync worker task exited with error', exc_info=True
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
168
202
|
@contextlib.asynccontextmanager
|
|
169
203
|
async def score_worker_hook() -> abc.AsyncGenerator[None]:
|
|
170
204
|
"""Run the score-recompute consumer and the daily tick."""
|