imbi-api 2.1.0__tar.gz → 2.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imbi_api-2.1.0 → imbi_api-2.5.2}/PKG-INFO +3 -2
- imbi_api-2.5.2/docs/restore-backup.md +117 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/mkdocs.yml +1 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/pyproject.toml +41 -36
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/app.py +2 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/permissions.py +89 -10
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/seed.py +10 -0
- imbi_api-2.5.2/src/imbi_api/backfill_embeddings.py +78 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/models.py +17 -2
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/scoring.py +29 -4
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/__init__.py +1 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/admin_plugins.py +9 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/environments.py +8 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/operations_log.py +83 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/organizations.py +14 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_deployments.py +788 -223
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_plugins.py +4 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_type_plugins.py +4 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/projects.py +325 -20
- imbi_api-2.5.2/src/imbi_api/endpoints/pull_requests.py +236 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/releases.py +333 -119
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/scoring_policies.py +138 -92
- imbi_api-2.5.2/src/imbi_api/endpoints/search.py +171 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/service_plugins.py +2 -2
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/third_party_services.py +177 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/user_activity.py +8 -4
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/repository.py +31 -4
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/lifespans.py +15 -9
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/assignments.py +27 -3
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/resolution.py +49 -0
- imbi_api-2.5.2/src/imbi_api/prompts/release_notes_system.md +73 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/scoring/__init__.py +1 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/scoring/queue.py +98 -6
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_environments.py +51 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_operations_log.py +43 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_deployments.py +496 -58
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_projects.py +88 -0
- imbi_api-2.5.2/tests/endpoints/test_projects_helpers.py +305 -0
- imbi_api-2.5.2/tests/endpoints/test_pull_requests.py +181 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_releases.py +360 -59
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_scoring_policies.py +6 -8
- imbi_api-2.5.2/tests/endpoints/test_search.py +410 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_third_party_services.py +141 -0
- imbi_api-2.5.2/tests/storage/__init__.py +0 -0
- imbi_api-2.5.2/tests/test_backfill_embeddings.py +81 -0
- imbi_api-2.5.2/tests/test_graph_sql.py +46 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_scoring_queue.py +134 -1
- {imbi_api-2.1.0 → imbi_api-2.5.2}/uv.lock +80 -54
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/deploy.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/docs.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.github/workflows/testing.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.gitignore +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.pre-commit-config.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/.python-version +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/CLAUDE.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/LICENSE +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/README.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/coderabbit.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/compose.yaml +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0001-record-architecture-decisions.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0002-authentication-and-authorization-architecture.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0003-email-sending-architecture.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0004-phase-5-authentication-enhancements.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0005-file-upload-storage-architecture.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0006-project-identity-and-multi-type.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0007-relationship-blueprints.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0008-plugin-system-architecture.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0009-database-driven-oauth-providers.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0010-identity-plugin-architecture.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0011-graph-based-project-scoring.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0012-plugin-manifest-service-template.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0013-deployment-plugin-type.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr/0014-generic-plugin-entity-abstraction.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/adr.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/configuration.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/docs/index.md +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/justfile +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/mypy.ini +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/local_auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/login_providers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/membership.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/oauth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/password.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/sessions.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/auth/tokens.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/domain/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/client.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/dependencies.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/base.html +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/base.txt +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/password_reset.html +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/password_reset.txt +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/welcome.html +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates/welcome.txt +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/email/templates.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/_helpers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/admin.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/api_keys.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/auth_providers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/blueprints.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/client_credentials.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/document_templates.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/documents.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/events.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/identity_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/link_definitions.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/local_auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/mfa.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/plugin_edges.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/plugin_entities.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_configuration.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_logs.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/project_types.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/roles.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/sa_api_keys.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/scoring.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/service_accounts.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/status.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/tags.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/teams.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/uploads.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/users.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/endpoints/webhooks.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/entrypoint.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/graph_sql.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/endpoints.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/errors.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/flows.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/host_integration.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/resolution.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/state.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/identity/sweeper.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/llm/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/llm/dependencies.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/middleware/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/middleware/rate_limit.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/openapi.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/patch.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/credentials.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/installer.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/lifecycle.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/lifecycle_dispatch.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/reload.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/plugins/schemas.py +0 -0
- {imbi_api-2.1.0/tests → imbi_api-2.5.2/src/imbi_api/prompts}/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/py.typed +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/relationships.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/settings.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/client.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/dependencies.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/thumbnails.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/src/imbi_api/storage/validation.py +0 -0
- {imbi_api-2.1.0/tests/endpoints → imbi_api-2.5.2/tests}/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_api_key_auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_authentication.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_authorization.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_encryption.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_login_providers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_membership.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_oauth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_permissions.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_seed.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/auth/test_sessions.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_client.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_init.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/clickhouse/test_privacy.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_client.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_init.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_integration.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/email/test_templates.py +0 -0
- {imbi_api-2.1.0/tests/storage → imbi_api-2.5.2/tests/endpoints}/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_admin.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_admin_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_api_keys.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_auth_providers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_blueprints.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_client_credentials.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_document_templates.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_documents.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_events.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_init.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_link_definitions.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_local_auth.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_mfa.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_organizations.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_configuration.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_logs.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_type_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_project_types.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_roles.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_sa_api_keys.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_scoring.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_service_accounts.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_service_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_status.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_tags.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_teams.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_uploads.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_user_activity.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_users.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/endpoints/test_webhooks.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/__init__.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_endpoints.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_errors.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_flows.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_host_integration.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_repository.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_resolution.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_state.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/identity/test_sweeper.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/middleware/test_rate_limit.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_client.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_init.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_thumbnails.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/storage/test_validation.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_app.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_blueprints.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_entrypoint.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_init.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_lifecycle_dispatch.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_lifespans.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_models.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_openapi.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_patch.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_plugins.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_scoring_triggers.py +0 -0
- {imbi_api-2.1.0 → imbi_api-2.5.2}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-api
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: Imbi is a DevOps Service Management Platform designed to provide an efficient way to manage a large environment that contains many services and applications.
|
|
5
5
|
Author-email: Alex Campbell <alexc@aweber.com>, Dave Shawley <daves@aweber.com>, "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -19,7 +19,7 @@ Requires-Dist: authlib>=1.6.11
|
|
|
19
19
|
Requires-Dist: fastapi
|
|
20
20
|
Requires-Dist: filetype
|
|
21
21
|
Requires-Dist: httpx<1,>=0.28.1
|
|
22
|
-
Requires-Dist: imbi-common[databases,llm,server]>=
|
|
22
|
+
Requires-Dist: imbi-common[databases,llm,sentry,server]>=2.5.5
|
|
23
23
|
Requires-Dist: jinja2
|
|
24
24
|
Requires-Dist: jsonpatch>=1.33
|
|
25
25
|
Requires-Dist: nanoid>=2.0.0
|
|
@@ -29,6 +29,7 @@ Requires-Dist: pydantic[email]
|
|
|
29
29
|
Requires-Dist: pyotp>=2.9.0
|
|
30
30
|
Requires-Dist: python-multipart>=0.0.26
|
|
31
31
|
Requires-Dist: qrcode>=8.0
|
|
32
|
+
Requires-Dist: sentry-sdk>=2.48.0
|
|
32
33
|
Requires-Dist: slowapi==0.1.9
|
|
33
34
|
Requires-Dist: typer
|
|
34
35
|
Requires-Dist: uvicorn
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# How to Restore an Imbi Database Backup
|
|
2
|
+
|
|
3
|
+
Restore a `pg_dump` backup of the Imbi database, including the Apache AGE graph.
|
|
4
|
+
|
|
5
|
+
**Prerequisites**: AGE extension installed on the target PostgreSQL instance, target DB created and empty.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Prepare the Target Database
|
|
10
|
+
|
|
11
|
+
AGE must be installed before restore. Connect to the target DB and run:
|
|
12
|
+
|
|
13
|
+
```sql
|
|
14
|
+
CREATE EXTENSION age;
|
|
15
|
+
LOAD 'age';
|
|
16
|
+
SET search_path = ag_catalog, "$user", public;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2. Restore the Dump
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pg_restore -d imbi -Fc --no-owner --role=imbi imbi.dump
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or for plain SQL:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
psql -d imbi -f imbi.sql
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 3. Verify Graph Catalog Integrity
|
|
32
|
+
|
|
33
|
+
After restore, confirm all graph labels are registered:
|
|
34
|
+
|
|
35
|
+
```sql
|
|
36
|
+
-- Get the graph OID
|
|
37
|
+
SELECT graphid FROM ag_catalog.ag_graph WHERE name = 'imbi';
|
|
38
|
+
|
|
39
|
+
-- List registered labels (note the graphid from above)
|
|
40
|
+
SELECT name, kind FROM ag_catalog.ag_label WHERE graph = <graphid> ORDER BY name;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 4. Find Unregistered Labels
|
|
44
|
+
|
|
45
|
+
Physical label tables may exist without `ag_catalog.ag_label` entries. This causes
|
|
46
|
+
`DuplicateTable` errors when the application first accesses those labels.
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
SELECT tablename
|
|
50
|
+
FROM pg_tables
|
|
51
|
+
WHERE schemaname = 'imbi'
|
|
52
|
+
AND tablename NOT IN (
|
|
53
|
+
SELECT name FROM ag_catalog.ag_label WHERE graph = <graphid>
|
|
54
|
+
)
|
|
55
|
+
ORDER BY tablename;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If this query returns rows, proceed to step 5. If empty, the restore is complete.
|
|
59
|
+
|
|
60
|
+
## 5. Register Missing Labels
|
|
61
|
+
|
|
62
|
+
For each missing label, determine its kind: vertex (`v`) or edge (`e`). In Imbi,
|
|
63
|
+
edge labels are conventionally `ALL_CAPS` and vertex labels are `PascalCase`.
|
|
64
|
+
|
|
65
|
+
Run this once, substituting the correct list and your `graphid`:
|
|
66
|
+
|
|
67
|
+
```sql
|
|
68
|
+
INSERT INTO ag_catalog.ag_label (name, graph, id, kind, relation, seq_name)
|
|
69
|
+
SELECT
|
|
70
|
+
name,
|
|
71
|
+
<graphid>,
|
|
72
|
+
(SELECT max(id) FROM ag_catalog.ag_label WHERE graph = <graphid>)
|
|
73
|
+
+ row_number() OVER (ORDER BY name),
|
|
74
|
+
CASE WHEN name ~ '^[A-Z][a-z]' THEN 'v' ELSE 'e' END,
|
|
75
|
+
'imbi."' || name || '"',
|
|
76
|
+
name || '_id_seq'
|
|
77
|
+
FROM unnest(ARRAY[
|
|
78
|
+
-- paste the tablename list from step 4 here
|
|
79
|
+
]) AS name;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 6. Verify
|
|
83
|
+
|
|
84
|
+
```sql
|
|
85
|
+
-- All physical tables should now have catalog entries
|
|
86
|
+
SELECT tablename
|
|
87
|
+
FROM pg_tables
|
|
88
|
+
WHERE schemaname = 'imbi'
|
|
89
|
+
AND tablename NOT IN (
|
|
90
|
+
SELECT name FROM ag_catalog.ag_label WHERE graph = <graphid>
|
|
91
|
+
);
|
|
92
|
+
-- Expected: 0 rows
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Restart the application and confirm no `DuplicateTable` errors occur.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
**`DuplicateTable: relation "X" already exists`**
|
|
102
|
+
The label table exists but is not in `ag_catalog.ag_label`. Run steps 4 and 5.
|
|
103
|
+
|
|
104
|
+
**`ag_catalog` functions not found**
|
|
105
|
+
The `age` extension is not loaded for the session. Run:
|
|
106
|
+
|
|
107
|
+
```sql
|
|
108
|
+
LOAD 'age';
|
|
109
|
+
SET search_path = ag_catalog, "$user", public;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Wrong `kind` assigned to a label**
|
|
113
|
+
Update the catalog entry directly:
|
|
114
|
+
|
|
115
|
+
```sql
|
|
116
|
+
UPDATE ag_catalog.ag_label SET kind = 'e' WHERE name = 'LABEL_NAME' AND graph = <graphid>;
|
|
117
|
+
```
|
|
@@ -63,6 +63,7 @@ markdown_extensions:
|
|
|
63
63
|
nav:
|
|
64
64
|
- Home: index.md
|
|
65
65
|
- Configuration: configuration.md
|
|
66
|
+
- Restore Backup: restore-backup.md
|
|
66
67
|
- Architecture Decision Records:
|
|
67
68
|
- Overview: adr.md
|
|
68
69
|
- 0001 Record Architecture Decisions: adr/0001-record-architecture-decisions.md
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["build", "hatchling", "wheel"]
|
|
3
|
-
build-backend = "hatchling.build"
|
|
4
|
-
|
|
5
1
|
[project]
|
|
6
2
|
name = "imbi-api"
|
|
7
|
-
version = "2.
|
|
3
|
+
version = "2.5.2"
|
|
8
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."
|
|
9
5
|
readme = "README.md"
|
|
10
6
|
requires-python = ">=3.14"
|
|
@@ -31,7 +27,7 @@ dependencies = [
|
|
|
31
27
|
"fastapi",
|
|
32
28
|
"filetype",
|
|
33
29
|
"httpx>=0.28.1,<1",
|
|
34
|
-
"imbi-common[databases,llm,server]>=
|
|
30
|
+
"imbi-common[databases,llm,sentry,server]>=2.5.5",
|
|
35
31
|
"jinja2",
|
|
36
32
|
"jsonpatch>=1.33",
|
|
37
33
|
"nanoid>=2.0.0",
|
|
@@ -40,6 +36,7 @@ dependencies = [
|
|
|
40
36
|
"pyotp>=2.9.0",
|
|
41
37
|
"python-multipart>=0.0.26",
|
|
42
38
|
"qrcode>=8.0",
|
|
39
|
+
"sentry-sdk>=2.48.0",
|
|
43
40
|
"slowapi==0.1.9", # Pin: alpha quality, API may change
|
|
44
41
|
"typer",
|
|
45
42
|
"uvicorn",
|
|
@@ -47,6 +44,9 @@ dependencies = [
|
|
|
47
44
|
]
|
|
48
45
|
license-file = "LICENSE"
|
|
49
46
|
|
|
47
|
+
[project.scripts]
|
|
48
|
+
imbi-api = "imbi_api.entrypoint:main"
|
|
49
|
+
|
|
50
50
|
[project.optional-dependencies]
|
|
51
51
|
otel = [
|
|
52
52
|
"opentelemetry-distro",
|
|
@@ -57,8 +57,36 @@ otel = [
|
|
|
57
57
|
]
|
|
58
58
|
sentry = ["sentry-sdk"]
|
|
59
59
|
|
|
60
|
-
[
|
|
61
|
-
|
|
60
|
+
[dependency-groups]
|
|
61
|
+
dev = [
|
|
62
|
+
"basedpyright",
|
|
63
|
+
"build",
|
|
64
|
+
"coverage[toml]",
|
|
65
|
+
"httpx",
|
|
66
|
+
"mypy",
|
|
67
|
+
"pre-commit",
|
|
68
|
+
"pytest",
|
|
69
|
+
"pytest-cov",
|
|
70
|
+
"ruff",
|
|
71
|
+
"watchfiles"
|
|
72
|
+
]
|
|
73
|
+
docs = [
|
|
74
|
+
"griffe-pydantic",
|
|
75
|
+
"mkdocs>=1.5,<2",
|
|
76
|
+
"mkdocs-material>9.5,<10",
|
|
77
|
+
"mkdocstrings-python-xref>=1.6,<2",
|
|
78
|
+
"mkdocstrings[python]"
|
|
79
|
+
]
|
|
80
|
+
plugins = [
|
|
81
|
+
"imbi-plugin-aws>=1.0.0",
|
|
82
|
+
"imbi-plugin-github>=1.0.0",
|
|
83
|
+
"imbi-plugin-logzio>=1.0.0",
|
|
84
|
+
"imbi-plugin-oidc>=1.0.0",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[build-system]
|
|
88
|
+
requires = ["build", "hatchling", "wheel"]
|
|
89
|
+
build-backend = "hatchling.build"
|
|
62
90
|
|
|
63
91
|
[tool.coverage.html]
|
|
64
92
|
directory = "build/coverage"
|
|
@@ -126,6 +154,7 @@ reportAttributeAccessIssue = "none"
|
|
|
126
154
|
reportCallIssue = "none"
|
|
127
155
|
reportMissingParameterType = "none"
|
|
128
156
|
reportMissingTypeArgument = "none"
|
|
157
|
+
reportMissingTypeStubs = "none"
|
|
129
158
|
reportOptionalMemberAccess = "none"
|
|
130
159
|
reportOptionalSubscript = "none"
|
|
131
160
|
reportPrivateUsage = "none"
|
|
@@ -138,12 +167,12 @@ reportUnusedFunction = "none"
|
|
|
138
167
|
extraPaths = ["src", "."]
|
|
139
168
|
|
|
140
169
|
[tool.pytest.ini_options]
|
|
141
|
-
testpaths = ["tests"]
|
|
142
170
|
addopts = [
|
|
143
171
|
"--cov=src/imbi_api",
|
|
144
172
|
"--cov-report=xml:build/coverage.xml",
|
|
145
173
|
"--cov-report=term",
|
|
146
174
|
]
|
|
175
|
+
testpaths = ["tests"]
|
|
147
176
|
|
|
148
177
|
[tool.ruff]
|
|
149
178
|
line-length = 79
|
|
@@ -191,33 +220,9 @@ max-complexity = 15
|
|
|
191
220
|
[tool.ruff.lint.per-file-ignores]
|
|
192
221
|
"tests/**/*.py" = ["S"]
|
|
193
222
|
|
|
194
|
-
[dependency-groups]
|
|
195
|
-
dev = [
|
|
196
|
-
"basedpyright",
|
|
197
|
-
"build",
|
|
198
|
-
"coverage[toml]",
|
|
199
|
-
"httpx",
|
|
200
|
-
"mypy",
|
|
201
|
-
"pre-commit",
|
|
202
|
-
"pytest",
|
|
203
|
-
"pytest-cov",
|
|
204
|
-
"ruff",
|
|
205
|
-
"watchfiles"
|
|
206
|
-
]
|
|
207
|
-
docs = [
|
|
208
|
-
"griffe-pydantic",
|
|
209
|
-
"mkdocs>=1.5,<2",
|
|
210
|
-
"mkdocs-material>9.5,<10",
|
|
211
|
-
"mkdocstrings-python-xref>=1.6,<2",
|
|
212
|
-
"mkdocstrings[python]"
|
|
213
|
-
]
|
|
214
|
-
plugins = [
|
|
215
|
-
"imbi-plugin-aws>=1.0.0",
|
|
216
|
-
"imbi-plugin-github>=1.0.0",
|
|
217
|
-
"imbi-plugin-logzio>=1.0.0",
|
|
218
|
-
"imbi-plugin-oidc>=1.0.0",
|
|
219
|
-
]
|
|
220
|
-
|
|
221
223
|
[[tool.uv.index]]
|
|
222
224
|
url = "https://pypi.org/simple"
|
|
223
225
|
default = true
|
|
226
|
+
|
|
227
|
+
[tool.uv.sources]
|
|
228
|
+
imbi-plugin-github = { path = "../imbi-plugin-github", editable = true }
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import fastapi
|
|
4
4
|
from fastapi import responses
|
|
5
5
|
from fastapi.middleware import cors
|
|
6
|
-
from imbi_common import access_log, graph, lifespan, valkey
|
|
6
|
+
from imbi_common import access_log, graph, lifespan, sentry, valkey
|
|
7
7
|
from imbi_common.plugins.errors import PluginCredentialsMissing
|
|
8
8
|
from uvicorn.middleware import proxy_headers
|
|
9
9
|
|
|
@@ -17,6 +17,7 @@ def create_app() -> fastapi.FastAPI:
|
|
|
17
17
|
app = fastapi.FastAPI(
|
|
18
18
|
title='Imbi',
|
|
19
19
|
lifespan=lifespan.Lifespan(
|
|
20
|
+
sentry.sentry_lifespan,
|
|
20
21
|
lifespans.clickhouse_hook,
|
|
21
22
|
graph.graph_lifespan,
|
|
22
23
|
lifespans.email_hook,
|
|
@@ -21,6 +21,13 @@ LOGGER = logging.getLogger(__name__)
|
|
|
21
21
|
oauth2_scheme = security.HTTPBearer(auto_error=False)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class IdentityInfo(pydantic.BaseModel):
|
|
25
|
+
"""A single third-party identity connection for an authenticated user."""
|
|
26
|
+
|
|
27
|
+
plugin_slug: str
|
|
28
|
+
subject: str
|
|
29
|
+
|
|
30
|
+
|
|
24
31
|
class AuthContext(pydantic.BaseModel):
|
|
25
32
|
"""Authentication context for the current request."""
|
|
26
33
|
|
|
@@ -29,6 +36,14 @@ class AuthContext(pydantic.BaseModel):
|
|
|
29
36
|
session_id: str | None = None
|
|
30
37
|
auth_method: typing.Literal['jwt', 'api_key', 'client_credentials']
|
|
31
38
|
permissions: set[str] = pydantic.Field(default_factory=set)
|
|
39
|
+
identities: list[IdentityInfo] = pydantic.Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
def identity_for(self, plugin_slug: str) -> str | None:
|
|
42
|
+
"""Return the subject for the given plugin_slug, or None."""
|
|
43
|
+
for i in self.identities:
|
|
44
|
+
if i.plugin_slug == plugin_slug:
|
|
45
|
+
return i.subject
|
|
46
|
+
return None
|
|
32
47
|
|
|
33
48
|
@property
|
|
34
49
|
def principal_name(self) -> str:
|
|
@@ -134,6 +149,47 @@ async def load_principal_permissions(
|
|
|
134
149
|
return set()
|
|
135
150
|
|
|
136
151
|
|
|
152
|
+
async def _load_user_identities(
|
|
153
|
+
db: graph.Graph,
|
|
154
|
+
user_id: str,
|
|
155
|
+
) -> list[IdentityInfo]:
|
|
156
|
+
"""Return active third-party identity connections for the user.
|
|
157
|
+
|
|
158
|
+
Identity enrichment is auxiliary — authentication has already
|
|
159
|
+
succeeded by the time this runs. A failure here (DB hiccup,
|
|
160
|
+
AGType parse error) must not turn a valid token into a 5xx, so
|
|
161
|
+
all exceptions are caught, logged, and treated as "no identities".
|
|
162
|
+
"""
|
|
163
|
+
query: typing.LiteralString = """
|
|
164
|
+
MATCH (u:User {{id: {user_id}}})-[:HAS_IDENTITY]->(c:IdentityConnection)
|
|
165
|
+
WHERE c.status = 'active'
|
|
166
|
+
OPTIONAL MATCH (c)-[:USES_PLUGIN]->(p:Plugin)
|
|
167
|
+
RETURN p.plugin_slug AS plugin_slug, c.subject AS subject
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
records = await db.execute(
|
|
171
|
+
query,
|
|
172
|
+
{'user_id': user_id},
|
|
173
|
+
['plugin_slug', 'subject'],
|
|
174
|
+
)
|
|
175
|
+
result: list[IdentityInfo] = []
|
|
176
|
+
for row in records:
|
|
177
|
+
plugin_slug = graph.parse_agtype(row['plugin_slug'])
|
|
178
|
+
subject = graph.parse_agtype(row['subject'])
|
|
179
|
+
if plugin_slug and subject:
|
|
180
|
+
result.append(
|
|
181
|
+
IdentityInfo(plugin_slug=plugin_slug, subject=subject)
|
|
182
|
+
)
|
|
183
|
+
return result
|
|
184
|
+
except Exception: # noqa: BLE001
|
|
185
|
+
LOGGER.warning(
|
|
186
|
+
'Failed to load identity connections for user_id=%s',
|
|
187
|
+
user_id,
|
|
188
|
+
exc_info=True,
|
|
189
|
+
)
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
|
|
137
193
|
async def authenticate_jwt(
|
|
138
194
|
db: graph.Graph,
|
|
139
195
|
token: str,
|
|
@@ -237,15 +293,44 @@ async def authenticate_jwt(
|
|
|
237
293
|
|
|
238
294
|
# Load permissions
|
|
239
295
|
perms = await load_principal_permissions(db, 'User', 'email', subject)
|
|
296
|
+
identities = await _load_user_identities(db, user.id)
|
|
240
297
|
|
|
241
298
|
return AuthContext(
|
|
242
299
|
user=user,
|
|
243
300
|
session_id=jti,
|
|
244
301
|
auth_method='jwt',
|
|
245
302
|
permissions=perms,
|
|
303
|
+
identities=identities,
|
|
246
304
|
)
|
|
247
305
|
|
|
248
306
|
|
|
307
|
+
async def _stamp_api_key_last_used(db: graph.Graph, key_id: str) -> None:
|
|
308
|
+
"""Set last_used on the APIKey node identified by key_id.
|
|
309
|
+
|
|
310
|
+
AGE's cypher() wrapper requires a RETURN clause to properly
|
|
311
|
+
finalize a write operation — omitting it causes an internal
|
|
312
|
+
"Entity failed to be updated" error. Failures are logged and
|
|
313
|
+
swallowed: the stamp is best-effort and must not fail authentication.
|
|
314
|
+
"""
|
|
315
|
+
now = datetime.datetime.now(datetime.UTC).isoformat()
|
|
316
|
+
query: typing.LiteralString = (
|
|
317
|
+
'MATCH (k:APIKey {{key_id: {key_id}}})'
|
|
318
|
+
' SET k.last_used = {now} RETURN k'
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
await db.execute(
|
|
322
|
+
query,
|
|
323
|
+
{'key_id': key_id, 'now': now},
|
|
324
|
+
columns=['k'],
|
|
325
|
+
)
|
|
326
|
+
except Exception: # noqa: BLE001
|
|
327
|
+
LOGGER.warning(
|
|
328
|
+
'Failed to stamp last_used for API key %s',
|
|
329
|
+
key_id,
|
|
330
|
+
exc_info=True,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
249
334
|
async def authenticate_api_key(
|
|
250
335
|
db: graph.Graph,
|
|
251
336
|
key: str,
|
|
@@ -349,11 +434,7 @@ async def authenticate_api_key(
|
|
|
349
434
|
filtered = all_perms.intersection(set(scopes)) if scopes else all_perms
|
|
350
435
|
|
|
351
436
|
# Update last_used only after all validation passes
|
|
352
|
-
|
|
353
|
-
update_query = (
|
|
354
|
-
'MATCH (k:APIKey {{key_id: {key_id}}}) SET k.last_used = {now}'
|
|
355
|
-
)
|
|
356
|
-
await db.execute(update_query, {'key_id': key_id, 'now': now})
|
|
437
|
+
await _stamp_api_key_last_used(db, key_id)
|
|
357
438
|
|
|
358
439
|
return AuthContext(
|
|
359
440
|
service_account=sa,
|
|
@@ -372,19 +453,17 @@ async def authenticate_api_key(
|
|
|
372
453
|
db, 'User', 'email', user.email
|
|
373
454
|
)
|
|
374
455
|
filtered = all_perms.intersection(set(scopes)) if scopes else all_perms
|
|
456
|
+
identities = await _load_user_identities(db, user.id)
|
|
375
457
|
|
|
376
458
|
# Update last_used only after all validation passes
|
|
377
|
-
|
|
378
|
-
update_query = (
|
|
379
|
-
'MATCH (k:APIKey {{key_id: {key_id}}}) SET k.last_used = {now}'
|
|
380
|
-
)
|
|
381
|
-
await db.execute(update_query, {'key_id': key_id, 'now': now})
|
|
459
|
+
await _stamp_api_key_last_used(db, key_id)
|
|
382
460
|
|
|
383
461
|
return AuthContext(
|
|
384
462
|
user=user,
|
|
385
463
|
session_id=key_id,
|
|
386
464
|
auth_method='api_key',
|
|
387
465
|
permissions=filtered,
|
|
466
|
+
identities=identities,
|
|
388
467
|
)
|
|
389
468
|
|
|
390
469
|
|
|
@@ -365,6 +365,13 @@ STANDARD_PERMISSIONS: list[tuple[str, str, str, str]] = [
|
|
|
365
365
|
'deployment:write',
|
|
366
366
|
'Trigger deployments and promotions via plugins',
|
|
367
367
|
),
|
|
368
|
+
# Vector search
|
|
369
|
+
(
|
|
370
|
+
'search:read',
|
|
371
|
+
'search',
|
|
372
|
+
'read',
|
|
373
|
+
'Search nodes by semantic similarity',
|
|
374
|
+
),
|
|
368
375
|
# Identity plugin connections
|
|
369
376
|
(
|
|
370
377
|
'me:identities:manage',
|
|
@@ -436,6 +443,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
|
|
|
436
443
|
'document:delete',
|
|
437
444
|
'document_template:read',
|
|
438
445
|
'me:identities:manage',
|
|
446
|
+
'search:read',
|
|
439
447
|
],
|
|
440
448
|
False,
|
|
441
449
|
),
|
|
@@ -456,6 +464,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
|
|
|
456
464
|
'project:read',
|
|
457
465
|
'project_type:read',
|
|
458
466
|
'role:read',
|
|
467
|
+
'search:read',
|
|
459
468
|
'tag:read',
|
|
460
469
|
'team:read',
|
|
461
470
|
'third_party_service:read',
|
|
@@ -478,6 +487,7 @@ DEFAULT_ROLES: list[tuple[str, str, str, int, list[str], bool]] = [
|
|
|
478
487
|
'project:read',
|
|
479
488
|
'project_type:read',
|
|
480
489
|
'organization:read',
|
|
490
|
+
'search:read',
|
|
481
491
|
'team:read',
|
|
482
492
|
'third_party_service:read',
|
|
483
493
|
'user:read',
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""One-shot backfill: generate embeddings for all existing nodes.
|
|
2
|
+
|
|
3
|
+
Run with:
|
|
4
|
+
uv run python -m imbi_api.backfill_embeddings
|
|
5
|
+
|
|
6
|
+
Iterates every embeddable node type and calls _auto_embed for each
|
|
7
|
+
instance that doesn't already have embeddings (or re-embeds if the
|
|
8
|
+
stored vectors are stale).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from imbi_common import graph, models
|
|
15
|
+
from imbi_common.graph.client import (
|
|
16
|
+
_embeddable_fields, # type: ignore[attr-defined]
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.INFO,
|
|
21
|
+
format='%(asctime)s %(levelname)-8s %(name)s: %(message)s',
|
|
22
|
+
)
|
|
23
|
+
LOGGER = logging.getLogger('backfill_embeddings')
|
|
24
|
+
|
|
25
|
+
# Node subclasses — _auto_embed is called normally.
|
|
26
|
+
_NODE_TYPES: list[type[models.Node]] = [
|
|
27
|
+
models.Organization,
|
|
28
|
+
models.Blueprint,
|
|
29
|
+
models.Team,
|
|
30
|
+
models.Environment,
|
|
31
|
+
models.ProjectType,
|
|
32
|
+
models.ThirdPartyService,
|
|
33
|
+
models.Tag,
|
|
34
|
+
models.LinkDefinition,
|
|
35
|
+
models.DocumentTemplate,
|
|
36
|
+
models.Project,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# GraphModel types that have Embeddable fields but are not Node subclasses.
|
|
40
|
+
# match() still works via model_construct fallback; _auto_embed accepts any
|
|
41
|
+
# GraphModel even though its type annotation says Node.
|
|
42
|
+
_GRAPH_MODEL_TYPES: list[type[models.GraphModel]] = [
|
|
43
|
+
models.Document,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _embed_type(
|
|
48
|
+
db: graph.Graph,
|
|
49
|
+
node_type: type[models.GraphModel],
|
|
50
|
+
) -> int:
|
|
51
|
+
nodes = await db.match(node_type) # type: ignore[arg-type]
|
|
52
|
+
count = 0
|
|
53
|
+
for node in nodes:
|
|
54
|
+
if not _embeddable_fields(node): # type: ignore[arg-type]
|
|
55
|
+
continue
|
|
56
|
+
await db._auto_embed(node) # type: ignore[arg-type]
|
|
57
|
+
count += 1
|
|
58
|
+
return count
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def run() -> None:
|
|
62
|
+
db = graph.Graph()
|
|
63
|
+
await db.open()
|
|
64
|
+
try:
|
|
65
|
+
total = 0
|
|
66
|
+
for node_type in [*_NODE_TYPES, *_GRAPH_MODEL_TYPES]:
|
|
67
|
+
label = node_type.__name__
|
|
68
|
+
LOGGER.info('Embedding %s nodes...', label)
|
|
69
|
+
n = await _embed_type(db, node_type)
|
|
70
|
+
LOGGER.info(' %s: %d nodes embedded', label, n)
|
|
71
|
+
total += n
|
|
72
|
+
LOGGER.info('Done. Total nodes embedded: %d', total)
|
|
73
|
+
finally:
|
|
74
|
+
await db.close()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == '__main__':
|
|
78
|
+
asyncio.run(run())
|
|
@@ -858,10 +858,17 @@ class PluginAssignmentCreate(pydantic.BaseModel):
|
|
|
858
858
|
"""Request model for assigning a plugin to a project-type or project."""
|
|
859
859
|
|
|
860
860
|
plugin_id: str
|
|
861
|
-
tab: typing.Literal['configuration', 'logs', 'deployment']
|
|
861
|
+
tab: typing.Literal['configuration', 'logs', 'deployment', 'lifecycle']
|
|
862
862
|
default: bool = False
|
|
863
863
|
options: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
|
864
864
|
identity_plugin_id: str | None = None
|
|
865
|
+
# Per-env workflow-input overrides keyed by env slug. Merged into
|
|
866
|
+
# the deploy / promote dispatch payload at trigger time. Sibling
|
|
867
|
+
# to ``options`` (which is plugin-level) because the axis is the
|
|
868
|
+
# *environment* being deployed into, not the plugin's own config.
|
|
869
|
+
env_payloads: dict[str, dict[str, typing.Any]] = pydantic.Field(
|
|
870
|
+
default_factory=dict,
|
|
871
|
+
)
|
|
865
872
|
|
|
866
873
|
|
|
867
874
|
class PluginAssignmentResponse(pydantic.BaseModel):
|
|
@@ -870,17 +877,25 @@ class PluginAssignmentResponse(pydantic.BaseModel):
|
|
|
870
877
|
plugin_id: str
|
|
871
878
|
plugin_slug: str
|
|
872
879
|
label: str
|
|
873
|
-
tab: typing.Literal['configuration', 'logs', 'deployment']
|
|
880
|
+
tab: typing.Literal['configuration', 'logs', 'deployment', 'lifecycle']
|
|
874
881
|
default: bool
|
|
875
882
|
options: dict[str, typing.Any] = {}
|
|
876
883
|
source: typing.Literal['project', 'project_type', 'merged'] = (
|
|
877
884
|
'project_type'
|
|
878
885
|
)
|
|
879
886
|
identity_plugin_id: str | None = None
|
|
887
|
+
env_payloads: dict[str, dict[str, typing.Any]] = pydantic.Field(
|
|
888
|
+
default_factory=dict,
|
|
889
|
+
)
|
|
880
890
|
# Pulled from the plugin manifest so the UI can hide affordances
|
|
881
891
|
# (e.g. the histogram strip on the Logs tab) for plugins whose
|
|
882
892
|
# underlying API does not support them.
|
|
883
893
|
supports_histogram: bool = False
|
|
894
|
+
# Set on deployment plugins that implement
|
|
895
|
+
# ``list_recent_deployments`` -- gates the "Resync deployments"
|
|
896
|
+
# card on the project settings page (and the admin TPS page) so
|
|
897
|
+
# the UI doesn't surface a button that the API will 400 on.
|
|
898
|
+
supports_deployment_sync: bool = False
|
|
884
899
|
|
|
885
900
|
|
|
886
901
|
# -- Configuration / Logs response models ---------------------------------
|