imbi-api 2.9.0__tar.gz → 2.9.1__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.1}/PKG-INFO +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/pyproject.toml +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/domain/models.py +4 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_deployments.py +1 -1
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_plugins.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_type_plugins.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/service_plugins.py +26 -17
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/third_party_services.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/assignment_writer.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/assignments.py +19 -11
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/lifecycle_dispatch.py +1 -1
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/resolution.py +22 -18
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_plugins.py +8 -8
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_type_plugins.py +7 -7
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_service_plugins.py +25 -7
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_assignment_writer.py +3 -3
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_plugins.py +13 -13
- {imbi_api-2.9.0 → imbi_api-2.9.1}/uv.lock +9 -10
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.gitignore +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.pre-commit-config.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/.python-version +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/CLAUDE.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/CODE_REVIEW_PUNCHLIST.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/LICENSE +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/README.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/coderabbit.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/compose.yaml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr/0015-cyclonedx-1.7-sbom-standard.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/adr.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/configuration.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/index.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/docs/restore-backup.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/justfile +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/mkdocs.yml +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/mypy.ini +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/app.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/authorization_codes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/login_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/oauth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/oauth_clients.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/permissions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/seed.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/auth/totp.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/backfill_embeddings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/blueprint_attributes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/domain/scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/_helpers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/_json_fields.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/_pagination.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/_request_urls.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/admin_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/comments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/dashboard.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/environments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/events.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/graph_query.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/identity_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/mcp_servers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/oauth_metadata.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/operations_log.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/organizations.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_analysis.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_logs.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/projects.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/pull_requests.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/releases.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/scoring_policies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/search.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/user_activity.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/users.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/endpoints/webhooks.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/entrypoint.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/endpoints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/errors.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/flows.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/host_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/repository.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/resolution.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/state.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/identity/sweeper.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/lifespans.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/mcp_test.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/lifecycle.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/prompts/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/prompts/release_notes_system.md +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/sbom.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/scoring/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/scoring/queue.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_oauth_server.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/auth/test_totp.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/test_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/test_models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/email/test_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_comments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_credentials.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_dashboard.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_environments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_graph_query.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_json_fields.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_mcp_servers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_operations_log.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_pagination.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_plugin_label_validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_plugins.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_analysis.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_deployments.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_response_surfacing.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_projects.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_projects_helpers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_pull_requests.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_releases.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_request_urls.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_scoring_policies.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_search.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_service_writeback.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_third_party_services.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/fixtures/sbom/npm-realistic.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/fixtures/sbom/pypi-realistic.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/fixtures/sbom/tiny.json +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_state.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/storage/__init__.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/storage/test_client.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/storage/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/support.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_app.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_backfill_embeddings.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_blueprint_attributes.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_blueprints.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_graph_sql.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_init.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_installer.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_lifespans.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_models.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_openapi.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_patch.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_plugin_schemas.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_sbom.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_scoring_queue.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.9.0 → imbi_api-2.9.1}/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.1
|
|
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.1
|
|
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.1"
|
|
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.1",
|
|
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",
|
|
@@ -12,6 +12,7 @@ import typing
|
|
|
12
12
|
|
|
13
13
|
import pydantic
|
|
14
14
|
from imbi_common import graph, models
|
|
15
|
+
from imbi_common.plugins import PluginType
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
'SECRET_FIELDS',
|
|
@@ -880,7 +881,7 @@ class PluginAssignmentCreate(pydantic.BaseModel):
|
|
|
880
881
|
"""Request model for assigning a plugin to a project-type or project."""
|
|
881
882
|
|
|
882
883
|
plugin_id: str
|
|
883
|
-
|
|
884
|
+
plugin_type: PluginType
|
|
884
885
|
default: bool = False
|
|
885
886
|
options: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
|
886
887
|
identity_plugin_id: str | None = None
|
|
@@ -899,7 +900,7 @@ class PluginAssignmentResponse(pydantic.BaseModel):
|
|
|
899
900
|
plugin_id: str
|
|
900
901
|
plugin_slug: str
|
|
901
902
|
label: str
|
|
902
|
-
|
|
903
|
+
plugin_type: PluginType
|
|
903
904
|
default: bool
|
|
904
905
|
options: dict[str, typing.Any] = {}
|
|
905
906
|
source: typing.Literal['project', 'project_type', 'merged'] = (
|
|
@@ -1227,7 +1228,7 @@ class WebhookResponse(pydantic.BaseModel):
|
|
|
1227
1228
|
config: dict[str, typing.Any] | list[typing.Any]
|
|
1228
1229
|
try:
|
|
1229
1230
|
config = json.loads(raw_config) if raw_config else {}
|
|
1230
|
-
except
|
|
1231
|
+
except json.JSONDecodeError, TypeError:
|
|
1231
1232
|
config = {}
|
|
1232
1233
|
rules.append(
|
|
1233
1234
|
WebhookRuleResponse(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Project deployment plugin endpoints.
|
|
2
2
|
|
|
3
|
-
Pass-through endpoints that resolve the project's ``
|
|
3
|
+
Pass-through endpoints that resolve the project's ``plugin_type='deployment'``
|
|
4
4
|
plugin and call its handler methods. Covers ref / commit discovery,
|
|
5
5
|
comparison, ``deploy`` / ``redeploy`` workflow dispatch (Phase 1), and
|
|
6
6
|
the ``promote`` flow with AI-drafted release notes plus tag + Release
|
|
@@ -12,7 +12,7 @@ from imbi_api.plugins.assignments import (
|
|
|
12
12
|
PluginAssignmentRow,
|
|
13
13
|
build_assignment_response,
|
|
14
14
|
validate_identity_plugin_ids,
|
|
15
|
-
|
|
15
|
+
validate_one_default_per_plugin_type,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
project_plugins_router = fastapi.APIRouter(tags=['Project Plugins'])
|
|
@@ -104,7 +104,7 @@ async def replace_project_plugins(
|
|
|
104
104
|
rows: list[PluginAssignmentRow] = [
|
|
105
105
|
PluginAssignmentRow(
|
|
106
106
|
plugin_id=a.plugin_id,
|
|
107
|
-
|
|
107
|
+
plugin_type=a.plugin_type,
|
|
108
108
|
default=a.default,
|
|
109
109
|
options=a.options,
|
|
110
110
|
identity_plugin_id=a.identity_plugin_id,
|
|
@@ -115,7 +115,7 @@ async def replace_project_plugins(
|
|
|
115
115
|
|
|
116
116
|
if rows:
|
|
117
117
|
try:
|
|
118
|
-
|
|
118
|
+
validate_one_default_per_plugin_type(rows)
|
|
119
119
|
except ValueError as exc:
|
|
120
120
|
raise fastapi.HTTPException(
|
|
121
121
|
status_code=400,
|
|
@@ -12,7 +12,7 @@ from imbi_api.plugins.assignments import (
|
|
|
12
12
|
PluginAssignmentRow,
|
|
13
13
|
build_assignment_response,
|
|
14
14
|
validate_identity_plugin_ids,
|
|
15
|
-
|
|
15
|
+
validate_one_default_per_plugin_type,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
project_type_plugins_router = fastapi.APIRouter(
|
|
@@ -74,7 +74,7 @@ async def replace_project_type_plugins(
|
|
|
74
74
|
rows: list[PluginAssignmentRow] = [
|
|
75
75
|
PluginAssignmentRow(
|
|
76
76
|
plugin_id=a.plugin_id,
|
|
77
|
-
|
|
77
|
+
plugin_type=a.plugin_type,
|
|
78
78
|
default=a.default,
|
|
79
79
|
options=a.options,
|
|
80
80
|
identity_plugin_id=a.identity_plugin_id,
|
|
@@ -83,7 +83,7 @@ async def replace_project_type_plugins(
|
|
|
83
83
|
for a in assignments
|
|
84
84
|
]
|
|
85
85
|
try:
|
|
86
|
-
|
|
86
|
+
validate_one_default_per_plugin_type(rows)
|
|
87
87
|
except ValueError as exc:
|
|
88
88
|
raise fastapi.HTTPException(
|
|
89
89
|
status_code=400,
|
|
@@ -7,6 +7,7 @@ import fastapi
|
|
|
7
7
|
import nanoid
|
|
8
8
|
import pydantic
|
|
9
9
|
from imbi_common import graph
|
|
10
|
+
from imbi_common.plugins import PluginType
|
|
10
11
|
from imbi_common.plugins.errors import (
|
|
11
12
|
PluginNotFoundError,
|
|
12
13
|
)
|
|
@@ -654,7 +655,7 @@ async def patch_service_plugin_configuration(
|
|
|
654
655
|
|
|
655
656
|
class _AssignmentInput(pydantic.BaseModel):
|
|
656
657
|
project_type_slug: str
|
|
657
|
-
|
|
658
|
+
plugin_type: PluginType
|
|
658
659
|
default: bool = False
|
|
659
660
|
options: dict[str, typing.Any] = {}
|
|
660
661
|
identity_plugin_id: str | None = None
|
|
@@ -663,7 +664,7 @@ class _AssignmentInput(pydantic.BaseModel):
|
|
|
663
664
|
class _AssignmentRow(pydantic.BaseModel):
|
|
664
665
|
project_type_slug: str
|
|
665
666
|
project_type_name: str
|
|
666
|
-
|
|
667
|
+
plugin_type: PluginType
|
|
667
668
|
default: bool
|
|
668
669
|
options: dict[str, typing.Any]
|
|
669
670
|
identity_plugin_id: str | None = None
|
|
@@ -671,7 +672,7 @@ class _AssignmentRow(pydantic.BaseModel):
|
|
|
671
672
|
|
|
672
673
|
_ASSIGNMENT_ROW_FIELDS: tuple[tuple[str, str], ...] = (
|
|
673
674
|
('project_type_slug', 'pt'),
|
|
674
|
-
('
|
|
675
|
+
('plugin_type', 'ptype'),
|
|
675
676
|
('default', 'default'),
|
|
676
677
|
('options', 'options'),
|
|
677
678
|
('identity_plugin_id', 'idp'),
|
|
@@ -694,7 +695,7 @@ def _assignment_rows_template(
|
|
|
694
695
|
for i, a in enumerate(body):
|
|
695
696
|
values: dict[str, typing.Any] = {
|
|
696
697
|
'pt': a.project_type_slug,
|
|
697
|
-
'
|
|
698
|
+
'ptype': a.plugin_type,
|
|
698
699
|
'default': a.default,
|
|
699
700
|
'options': json.dumps(a.options),
|
|
700
701
|
'idp': a.identity_plugin_id or None,
|
|
@@ -735,7 +736,11 @@ async def _list_assignments(
|
|
|
735
736
|
_AssignmentRow(
|
|
736
737
|
project_type_slug=pt.get('slug', ''),
|
|
737
738
|
project_type_name=pt.get('name', ''),
|
|
738
|
-
|
|
739
|
+
# Transitional: prefer the new ``plugin_type`` edge
|
|
740
|
+
# property, fall back to legacy ``tab`` until migrated.
|
|
741
|
+
plugin_type=edge.get(
|
|
742
|
+
'plugin_type', edge.get('tab', 'configuration')
|
|
743
|
+
),
|
|
739
744
|
default=bool(edge.get('default', False)),
|
|
740
745
|
options=_parse_options(edge.get('options')),
|
|
741
746
|
identity_plugin_id=coerce_identity_plugin_id(
|
|
@@ -795,26 +800,27 @@ async def replace_plugin_assignments(
|
|
|
795
800
|
detail=f'Plugin {plugin_slug!r} is not loaded',
|
|
796
801
|
) from exc
|
|
797
802
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if
|
|
803
|
+
allowed_plugin_type = entry.manifest.plugin_type
|
|
804
|
+
mismatched = [a for a in body if a.plugin_type != allowed_plugin_type]
|
|
805
|
+
if mismatched:
|
|
801
806
|
raise fastapi.HTTPException(
|
|
802
807
|
status_code=400,
|
|
803
808
|
detail=(
|
|
804
|
-
f'
|
|
805
|
-
f'
|
|
809
|
+
f'Assignments for this plugin must use plugin_type'
|
|
810
|
+
f' {allowed_plugin_type!r}'
|
|
806
811
|
),
|
|
807
812
|
)
|
|
808
813
|
|
|
809
814
|
seen: set[tuple[str, str]] = set()
|
|
810
815
|
for a in body:
|
|
811
|
-
key = (a.project_type_slug, a.
|
|
816
|
+
key = (a.project_type_slug, a.plugin_type)
|
|
812
817
|
if key in seen:
|
|
813
818
|
raise fastapi.HTTPException(
|
|
814
819
|
status_code=400,
|
|
815
820
|
detail=(
|
|
816
821
|
f'Duplicate assignment for project type'
|
|
817
|
-
f' {a.project_type_slug!r}
|
|
822
|
+
f' {a.project_type_slug!r} with plugin_type'
|
|
823
|
+
f' {a.plugin_type!r}'
|
|
818
824
|
),
|
|
819
825
|
)
|
|
820
826
|
seen.add(key)
|
|
@@ -860,9 +866,10 @@ async def replace_plugin_assignments(
|
|
|
860
866
|
# plugin; without it the ``OPTIONAL MATCH`` would emit one row per
|
|
861
867
|
# pre-existing edge and the ``UNWIND`` would multiply the CREATEs.
|
|
862
868
|
#
|
|
863
|
-
# The trailing demotion runs after the CREATEs: for every
|
|
864
|
-
# where this plugin is now default, any *other*
|
|
865
|
-
# default
|
|
869
|
+
# The trailing demotion runs after the CREATEs: for every
|
|
870
|
+
# (pt, plugin_type) where this plugin is now default, any *other*
|
|
871
|
+
# plugin that was default for the same plugin_type is demoted. Plain
|
|
872
|
+
# ``MATCH``/``SET`` only --
|
|
866
873
|
# a zero-row match (no default edges, or no competing siblings) is a
|
|
867
874
|
# harmless no-op and the already-applied CREATEs still commit.
|
|
868
875
|
if not body:
|
|
@@ -889,14 +896,16 @@ async def replace_plugin_assignments(
|
|
|
889
896
|
f' UNWIND {rows_tpl} AS row'
|
|
890
897
|
' MATCH (:Organization {{slug: {org_slug}}})<-[:BELONGS_TO]-'
|
|
891
898
|
'(cpt:ProjectType {{slug: row.project_type_slug}})'
|
|
892
|
-
' CREATE (cpt)-[:USES_PLUGIN {{
|
|
899
|
+
' CREATE (cpt)-[:USES_PLUGIN {{plugin_type: row.plugin_type,'
|
|
893
900
|
' default: row.default, options: row.options,'
|
|
894
901
|
' identity_plugin_id: row.identity_plugin_id}}]->(p)'
|
|
895
902
|
' WITH p, count(cpt) AS _new'
|
|
896
903
|
' MATCH (mpt:ProjectType)-[mine:USES_PLUGIN]->(p)'
|
|
897
904
|
' WHERE mine.default = true'
|
|
898
905
|
' MATCH (mpt)-[sibling:USES_PLUGIN]->(other:Plugin)'
|
|
899
|
-
' WHERE other.id <> {plugin_id}
|
|
906
|
+
' WHERE other.id <> {plugin_id}'
|
|
907
|
+
' AND coalesce(sibling.plugin_type, sibling.tab) ='
|
|
908
|
+
' coalesce(mine.plugin_type, mine.tab)'
|
|
900
909
|
' AND sibling.default = true'
|
|
901
910
|
' SET sibling.default = false'
|
|
902
911
|
)
|
|
@@ -469,7 +469,7 @@ async def _projects_using_tps_for_deployment(
|
|
|
469
469
|
"""Return project ids whose deployment plugin is owned by this TPS.
|
|
470
470
|
|
|
471
471
|
Walks both project-level and project-type-level ``USES_PLUGIN``
|
|
472
|
-
edges with ``
|
|
472
|
+
edges with ``plugin_type='deployment'`` whose target ``Plugin`` is owned
|
|
473
473
|
(via ``HAS_PLUGIN``) by the named ThirdPartyService. Returns a
|
|
474
474
|
de-duplicated, deterministic list so the resync fan-out is stable
|
|
475
475
|
across calls.
|
|
@@ -492,11 +492,11 @@ async def _projects_using_tps_for_deployment(
|
|
|
492
492
|
MATCH (tps)-[:HAS_PLUGIN]->(plugin:Plugin)
|
|
493
493
|
OPTIONAL MATCH (p:Project)-[upe:USES_PLUGIN]->(plugin),
|
|
494
494
|
(p)-[:OWNED_BY]->(:Team)-[:BELONGS_TO]->(o)
|
|
495
|
-
WHERE upe.tab = 'deployment'
|
|
495
|
+
WHERE coalesce(upe.plugin_type, upe.tab) = 'deployment'
|
|
496
496
|
OPTIONAL MATCH (p2:Project)-[:TYPE]
|
|
497
497
|
->(:ProjectType)-[upte:USES_PLUGIN]->(plugin),
|
|
498
498
|
(p2)-[:OWNED_BY]->(:Team)-[:BELONGS_TO]->(o)
|
|
499
|
-
WHERE upte.tab = 'deployment'
|
|
499
|
+
WHERE coalesce(upte.plugin_type, upte.tab) = 'deployment'
|
|
500
500
|
WITH collect(DISTINCT p.id) AS direct_ids,
|
|
501
501
|
collect(DISTINCT p2.id) AS typed_ids
|
|
502
502
|
RETURN direct_ids + typed_ids AS project_ids
|
|
@@ -25,7 +25,7 @@ from imbi_api.plugins.assignments import PluginAssignmentRow
|
|
|
25
25
|
|
|
26
26
|
_ROW_KEYS: tuple[str, ...] = (
|
|
27
27
|
'plugin_id',
|
|
28
|
-
'
|
|
28
|
+
'plugin_type',
|
|
29
29
|
'default',
|
|
30
30
|
'options',
|
|
31
31
|
'identity_plugin_id',
|
|
@@ -87,7 +87,7 @@ async def replace_assignments(
|
|
|
87
87
|
edges by guessing the parent key.
|
|
88
88
|
|
|
89
89
|
Callers must validate plugin ids, identity_plugin_ids, and the
|
|
90
|
-
one-default-per-
|
|
90
|
+
one-default-per-plugin-type invariant up front; passing an empty ``rows``
|
|
91
91
|
list clears all assignments.
|
|
92
92
|
"""
|
|
93
93
|
parent_head = (
|
|
@@ -117,7 +117,7 @@ async def replace_assignments(
|
|
|
117
117
|
' WITH parent, count(old) AS _del'
|
|
118
118
|
f' UNWIND {rows_tpl} AS row'
|
|
119
119
|
' MATCH (p:Plugin {{id: row.plugin_id}})'
|
|
120
|
-
' CREATE (parent)-[:USES_PLUGIN {{
|
|
120
|
+
' CREATE (parent)-[:USES_PLUGIN {{plugin_type: row.plugin_type,'
|
|
121
121
|
' default: row.default,'
|
|
122
122
|
' options: row.options,'
|
|
123
123
|
' identity_plugin_id: row.identity_plugin_id,'
|
|
@@ -4,6 +4,7 @@ import typing
|
|
|
4
4
|
|
|
5
5
|
import fastapi
|
|
6
6
|
from imbi_common import graph
|
|
7
|
+
from imbi_common.plugins import PluginType
|
|
7
8
|
from imbi_common.plugins.errors import PluginNotFoundError
|
|
8
9
|
from imbi_common.plugins.registry import get_plugin
|
|
9
10
|
|
|
@@ -13,33 +14,37 @@ from imbi_api.plugins import parse_options
|
|
|
13
14
|
|
|
14
15
|
class PluginAssignmentRow(typing.TypedDict):
|
|
15
16
|
plugin_id: str
|
|
16
|
-
|
|
17
|
+
plugin_type: PluginType
|
|
17
18
|
default: bool
|
|
18
19
|
options: dict[str, typing.Any]
|
|
19
20
|
identity_plugin_id: typing.NotRequired[str | None]
|
|
20
21
|
env_payloads: typing.NotRequired[dict[str, dict[str, typing.Any]]]
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def
|
|
24
|
+
def validate_one_default_per_plugin_type(
|
|
24
25
|
assignments: list[PluginAssignmentRow],
|
|
25
26
|
) -> None:
|
|
26
|
-
"""Ensure exactly one default per
|
|
27
|
+
"""Ensure exactly one default per plugin type in the assignment list.
|
|
27
28
|
|
|
28
29
|
Raises:
|
|
29
|
-
ValueError: If a
|
|
30
|
+
ValueError: If a plugin type has 0 or >1 defaults.
|
|
30
31
|
"""
|
|
31
|
-
|
|
32
|
+
by_plugin_type: dict[str, list[bool]] = {}
|
|
32
33
|
for row in assignments:
|
|
33
|
-
|
|
34
|
+
by_plugin_type.setdefault(row['plugin_type'], []).append(
|
|
35
|
+
row['default']
|
|
36
|
+
)
|
|
34
37
|
|
|
35
|
-
for
|
|
38
|
+
for plugin_type, defaults in by_plugin_type.items():
|
|
36
39
|
count = sum(1 for d in defaults if d)
|
|
37
40
|
if count == 0:
|
|
38
|
-
raise ValueError(
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f'Plugin type {plugin_type!r} has no default plugin assignment'
|
|
43
|
+
)
|
|
39
44
|
if count > 1:
|
|
40
45
|
raise ValueError(
|
|
41
|
-
f'
|
|
42
|
-
f' exactly one is required'
|
|
46
|
+
f'Plugin type {plugin_type!r} has {count} default plugin'
|
|
47
|
+
f' assignments; exactly one is required'
|
|
43
48
|
)
|
|
44
49
|
|
|
45
50
|
|
|
@@ -121,7 +126,10 @@ def build_assignment_response(
|
|
|
121
126
|
plugin_id=plugin['id'],
|
|
122
127
|
plugin_slug=plugin['plugin_slug'],
|
|
123
128
|
label=plugin['label'],
|
|
124
|
-
|
|
129
|
+
# Transitional: read the new ``plugin_type`` edge property,
|
|
130
|
+
# falling back to the legacy ``tab`` name until the rename
|
|
131
|
+
# migration has run. Drop the ``tab`` fallback afterward.
|
|
132
|
+
plugin_type=edge.get('plugin_type', edge.get('tab', 'configuration')),
|
|
125
133
|
default=bool(edge.get('default', False)),
|
|
126
134
|
options=parse_options(edge.get('options')),
|
|
127
135
|
source=source,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
When a project state changes -- create, update, archive, unarchive,
|
|
4
4
|
delete, or relocate -- the API dispatches the event to every plugin
|
|
5
|
-
assigned to ``
|
|
5
|
+
assigned to ``plugin_type='lifecycle'`` (project or project-type level).
|
|
6
6
|
Plugins receive a :class:`PluginContext` with hydrated identity and
|
|
7
7
|
return a :class:`LifecycleResult` describing their per-plugin outcome.
|
|
8
8
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Plugin resolution — find the
|
|
1
|
+
"""Plugin resolution — find the plugin for a project + plugin_type."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import typing
|
|
@@ -71,10 +71,10 @@ def _merge_env_payloads(
|
|
|
71
71
|
async def resolve_plugin(
|
|
72
72
|
db: graph.Graph,
|
|
73
73
|
project_id: str,
|
|
74
|
-
|
|
74
|
+
plugin_type: str,
|
|
75
75
|
source: str | None,
|
|
76
76
|
) -> ResolvedPlugin:
|
|
77
|
-
"""Find the plugin assigned to a project for a given
|
|
77
|
+
"""Find the plugin assigned to a project for a given plugin type.
|
|
78
78
|
|
|
79
79
|
Merges project-type defaults with project-level overrides.
|
|
80
80
|
If multiple plugins are assigned, ``source`` selects which one
|
|
@@ -96,10 +96,10 @@ async def resolve_plugin(
|
|
|
96
96
|
query: typing.LiteralString = """
|
|
97
97
|
MATCH (proj:Project {{id: {project_id}}})
|
|
98
98
|
OPTIONAL MATCH (proj)-[pe:USES_PLUGIN]->(p:Plugin)
|
|
99
|
-
WHERE pe.tab = {
|
|
99
|
+
WHERE coalesce(pe.plugin_type, pe.tab) = {plugin_type}
|
|
100
100
|
OPTIONAL MATCH (proj)-[:TYPE]->(pt:ProjectType)
|
|
101
101
|
-[pte:USES_PLUGIN]->(p2:Plugin)
|
|
102
|
-
WHERE pte.tab = {
|
|
102
|
+
WHERE coalesce(pte.plugin_type, pte.tab) = {plugin_type}
|
|
103
103
|
WITH
|
|
104
104
|
collect(DISTINCT {{id: p.id, slug: p.plugin_slug,
|
|
105
105
|
edge_options: pe.options,
|
|
@@ -123,7 +123,7 @@ async def resolve_plugin(
|
|
|
123
123
|
"""
|
|
124
124
|
records = await db.execute(
|
|
125
125
|
query,
|
|
126
|
-
{'project_id': project_id, '
|
|
126
|
+
{'project_id': project_id, 'plugin_type': plugin_type},
|
|
127
127
|
['proj_plugins', 'pt_plugins'],
|
|
128
128
|
)
|
|
129
129
|
if not records:
|
|
@@ -176,7 +176,10 @@ async def resolve_plugin(
|
|
|
176
176
|
if not candidates:
|
|
177
177
|
raise fastapi.HTTPException(
|
|
178
178
|
status_code=404,
|
|
179
|
-
detail=
|
|
179
|
+
detail=(
|
|
180
|
+
f'No plugin assigned for plugin_type {plugin_type!r}'
|
|
181
|
+
f' for this project'
|
|
182
|
+
),
|
|
180
183
|
)
|
|
181
184
|
|
|
182
185
|
if source:
|
|
@@ -266,9 +269,9 @@ async def resolve_plugin(
|
|
|
266
269
|
async def resolve_all_plugins(
|
|
267
270
|
db: graph.Graph,
|
|
268
271
|
project_id: str,
|
|
269
|
-
|
|
272
|
+
plugin_type: str,
|
|
270
273
|
) -> list[ResolvedPlugin]:
|
|
271
|
-
"""Return every plugin assigned to ``project_id`` for ``
|
|
274
|
+
"""Return every plugin assigned to ``project_id`` for ``plugin_type``.
|
|
272
275
|
|
|
273
276
|
Sibling to :func:`resolve_plugin` for fan-out call sites (e.g.
|
|
274
277
|
lifecycle hooks) that must invoke *every* assigned plugin rather
|
|
@@ -287,11 +290,11 @@ async def resolve_all_plugins(
|
|
|
287
290
|
query: typing.LiteralString = """
|
|
288
291
|
MATCH (proj:Project {{id: {project_id}}})
|
|
289
292
|
OPTIONAL MATCH (proj)-[pe:USES_PLUGIN]->(p:Plugin)
|
|
290
|
-
WHERE pe.tab = {
|
|
293
|
+
WHERE coalesce(pe.plugin_type, pe.tab) = {plugin_type}
|
|
291
294
|
OPTIONAL MATCH (tps1:ThirdPartyService)-[:HAS_PLUGIN]->(p)
|
|
292
295
|
OPTIONAL MATCH (proj)-[:TYPE]->(pt:ProjectType)
|
|
293
296
|
-[pte:USES_PLUGIN]->(p2:Plugin)
|
|
294
|
-
WHERE pte.tab = {
|
|
297
|
+
WHERE coalesce(pte.plugin_type, pte.tab) = {plugin_type}
|
|
295
298
|
OPTIONAL MATCH (tps2:ThirdPartyService)-[:HAS_PLUGIN]->(p2)
|
|
296
299
|
WITH
|
|
297
300
|
collect(DISTINCT {{id: p.id, slug: p.plugin_slug,
|
|
@@ -318,7 +321,7 @@ async def resolve_all_plugins(
|
|
|
318
321
|
"""
|
|
319
322
|
records = await db.execute(
|
|
320
323
|
query,
|
|
321
|
-
{'project_id': project_id, '
|
|
324
|
+
{'project_id': project_id, 'plugin_type': plugin_type},
|
|
322
325
|
['proj_plugins', 'pt_plugins'],
|
|
323
326
|
)
|
|
324
327
|
if not records:
|
|
@@ -454,10 +457,11 @@ async def resolve_analysis_plugins(
|
|
|
454
457
|
|
|
455
458
|
The union covers three discovery paths:
|
|
456
459
|
|
|
457
|
-
1. ``(:Project)-[:USES_PLUGIN {
|
|
458
|
-
override.
|
|
459
|
-
2. ``(:Project)-[:TYPE]->(:ProjectType)
|
|
460
|
-
->(:Plugin)`` — project-type
|
|
460
|
+
1. ``(:Project)-[:USES_PLUGIN {plugin_type:'analysis'}]->(:Plugin)`` —
|
|
461
|
+
project override.
|
|
462
|
+
2. ``(:Project)-[:TYPE]->(:ProjectType)
|
|
463
|
+
-[:USES_PLUGIN {plugin_type:'analysis'}]->(:Plugin)`` — project-type
|
|
464
|
+
default. Project override wins per
|
|
461
465
|
plugin id, mirroring :func:`resolve_all_plugins`.
|
|
462
466
|
3. ``(:Project)-[:EXISTS_IN]->(:ThirdPartyService)-[:HAS_PLUGIN]
|
|
463
467
|
->(:Plugin)`` filtered to ``plugin_type='analysis'``. The
|
|
@@ -476,10 +480,10 @@ async def resolve_analysis_plugins(
|
|
|
476
480
|
query: typing.LiteralString = """
|
|
477
481
|
MATCH (proj:Project {{id: {project_id}}})
|
|
478
482
|
OPTIONAL MATCH (proj)-[pe:USES_PLUGIN]->(p:Plugin)
|
|
479
|
-
WHERE pe.tab = 'analysis'
|
|
483
|
+
WHERE coalesce(pe.plugin_type, pe.tab) = 'analysis'
|
|
480
484
|
OPTIONAL MATCH (proj)-[:TYPE]->(pt:ProjectType)
|
|
481
485
|
-[pte:USES_PLUGIN]->(p2:Plugin)
|
|
482
|
-
WHERE pte.tab = 'analysis'
|
|
486
|
+
WHERE coalesce(pte.plugin_type, pte.tab) = 'analysis'
|
|
483
487
|
OPTIONAL MATCH (proj)-[:EXISTS_IN]->(tps:ThirdPartyService)
|
|
484
488
|
-[:HAS_PLUGIN]->(p3:Plugin)
|
|
485
489
|
WHERE p3.plugin_type = 'analysis'
|
|
@@ -36,8 +36,8 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
36
36
|
mock_get_current_user
|
|
37
37
|
)
|
|
38
38
|
self.mock_db = mock.AsyncMock(spec=graph.Graph)
|
|
39
|
-
self.test_app.dependency_overrides[graph._inject_graph] = (
|
|
40
|
-
|
|
39
|
+
self.test_app.dependency_overrides[graph._inject_graph] = lambda: (
|
|
40
|
+
self.mock_db
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
def test_get_project_plugins_not_found(self) -> None:
|
|
@@ -70,7 +70,7 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
70
70
|
{
|
|
71
71
|
'plugin': plugin_a,
|
|
72
72
|
'edge': {
|
|
73
|
-
'
|
|
73
|
+
'plugin_type': 'configuration',
|
|
74
74
|
'default': True,
|
|
75
75
|
'options': '{}',
|
|
76
76
|
},
|
|
@@ -83,7 +83,7 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
83
83
|
{
|
|
84
84
|
'plugin': plugin_b,
|
|
85
85
|
'edge': {
|
|
86
|
-
'
|
|
86
|
+
'plugin_type': 'logs',
|
|
87
87
|
'default': True,
|
|
88
88
|
'options': '{}',
|
|
89
89
|
},
|
|
@@ -110,13 +110,13 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
110
110
|
json=[
|
|
111
111
|
{
|
|
112
112
|
'plugin_id': 'p1',
|
|
113
|
-
'
|
|
113
|
+
'plugin_type': 'configuration',
|
|
114
114
|
'default': True,
|
|
115
115
|
'options': {},
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
'plugin_id': 'p2',
|
|
119
|
-
'
|
|
119
|
+
'plugin_type': 'configuration',
|
|
120
120
|
'default': True,
|
|
121
121
|
'options': {},
|
|
122
122
|
},
|
|
@@ -164,7 +164,7 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
164
164
|
{
|
|
165
165
|
'plugin': plugin_raw,
|
|
166
166
|
'edge': {
|
|
167
|
-
'
|
|
167
|
+
'plugin_type': 'configuration',
|
|
168
168
|
'default': True,
|
|
169
169
|
'options': '{}',
|
|
170
170
|
},
|
|
@@ -181,7 +181,7 @@ class ProjectPluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
181
181
|
json=[
|
|
182
182
|
{
|
|
183
183
|
'plugin_id': 'p1',
|
|
184
|
-
'
|
|
184
|
+
'plugin_type': 'configuration',
|
|
185
185
|
'default': True,
|
|
186
186
|
'options': {},
|
|
187
187
|
}
|
|
@@ -36,8 +36,8 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
36
36
|
mock_get_current_user
|
|
37
37
|
)
|
|
38
38
|
self.mock_db = mock.AsyncMock(spec=graph.Graph)
|
|
39
|
-
self.test_app.dependency_overrides[graph._inject_graph] = (
|
|
40
|
-
|
|
39
|
+
self.test_app.dependency_overrides[graph._inject_graph] = lambda: (
|
|
40
|
+
self.mock_db
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
def test_list_empty(self) -> None:
|
|
@@ -60,7 +60,7 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
60
60
|
'api_version': 1,
|
|
61
61
|
},
|
|
62
62
|
'edge': {
|
|
63
|
-
'
|
|
63
|
+
'plugin_type': 'configuration',
|
|
64
64
|
'default': True,
|
|
65
65
|
'options': '{}',
|
|
66
66
|
},
|
|
@@ -82,7 +82,7 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
82
82
|
json=[
|
|
83
83
|
{
|
|
84
84
|
'plugin_id': 'p1',
|
|
85
|
-
'
|
|
85
|
+
'plugin_type': 'configuration',
|
|
86
86
|
'default': False,
|
|
87
87
|
'options': {},
|
|
88
88
|
}
|
|
@@ -101,7 +101,7 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
101
101
|
'api_version': 1,
|
|
102
102
|
},
|
|
103
103
|
'edge': {
|
|
104
|
-
'
|
|
104
|
+
'plugin_type': 'configuration',
|
|
105
105
|
'default': True,
|
|
106
106
|
'options': '{}',
|
|
107
107
|
},
|
|
@@ -117,7 +117,7 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
117
117
|
json=[
|
|
118
118
|
{
|
|
119
119
|
'plugin_id': 'p1',
|
|
120
|
-
'
|
|
120
|
+
'plugin_type': 'configuration',
|
|
121
121
|
'default': True,
|
|
122
122
|
'options': {},
|
|
123
123
|
}
|
|
@@ -141,7 +141,7 @@ class ProjectTypePluginsEndpointTestCase(support.SharedAppTestCase):
|
|
|
141
141
|
),
|
|
142
142
|
'edge': json.dumps(
|
|
143
143
|
{
|
|
144
|
-
'
|
|
144
|
+
'plugin_type': 'configuration',
|
|
145
145
|
'default': True,
|
|
146
146
|
'options': '{}',
|
|
147
147
|
}
|