simpleaudit-studio 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/PKG-INFO +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/comparison.py +3 -3
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/urls.py +6 -4
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/architecture.md +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/domain-model.md +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/context_processors.py +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/seo.py +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_connection_delete.py +87 -12
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_connection_sharing.py +6 -6
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_smoke_all_pages.py +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_write_guard.py +14 -14
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/ui.py +44 -6
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/services.py +9 -3
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/pyproject.toml +1 -1
- simpleaudit_studio-0.4.2/templates/models.html → simpleaudit_studio-0.5.0/templates/connections.html +40 -4
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/model_pickers.html +2 -2
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/run_detail.html +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/uv.lock +1 -1
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.dockerignore +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.env.example +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.env.local.example +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/ci.yml +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/docker.yml +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/publish.yml +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.gitignore +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/Dockerfile +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/LICENSE +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/README.md +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/admin_urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/auth_urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/0002_user_preferences.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/models.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/project_urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/serializers.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/services.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/views.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/workos_auth.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/events.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/experiments.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0002_strip_api_keys_from_snapshots.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0003_judges.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0004_judge_model_per_run.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/models.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/monitors.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/serializers.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/services.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/views.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/asgi.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/settings.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/wsgi.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/compose/Dockerfile +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/mock_openai_server.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/postgres-init.sql +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docker-compose.yml +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/deployment.md +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/e2e/test_ui.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/codegen.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/db.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/engine.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/exceptions.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/fixtures/demo_audit_results.json +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/hashing.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/health.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/health_api.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/logging.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/bootstrap_platform.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/dev_server.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/purge_test_data.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/run_monitors.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/run_worker.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/seed_demo_audits.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/seed_platform.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/setup_local.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/middleware.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/minimal_config.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/readiness.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/runs_table.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/seed.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/simpleaudit_package.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/startup_checks.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/templatetags/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/templatetags/json_filters.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/factories.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_admin_api.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_admin_page.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_api_e2e.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_archive.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_clone.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_events.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_poll_results.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_rename.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_results_live.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_run_freeze.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_sse.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_submission.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_timing.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_auth_projects.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_bootstrap.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_codegen.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_comparison.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_db_resilience.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_engine_integration.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_experiments.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_health.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_health_panel.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_judges.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_middleware.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_minimal_config.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_model_registry.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_monitors.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_profile.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_profile_ui.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_purge_test_data.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_runs_table.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_scenario_import_export.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_scenarios.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_seed_platform.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_snapshot_secrets.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspace_archive.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspaces.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspaces_ui.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/utils.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/worker.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0001_judges.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0002_judge_model_per_run.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0003_composable_judges.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/models.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/services.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/views.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/manage.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0002_descriptions.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0003_modelconnection_shared_with_and_more.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/models.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/serializers.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/views.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/admin.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/apps.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/0001_initial.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/0002_add_severity_ceiling_documents_file_uri.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/models.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/serializers.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/services.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/urls.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/views.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/simpleaudit_studio/__init__.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/simpleaudit_studio/cli.py +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon-16x16.svg +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon-32x32.svg +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon.svg +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/js/diff.min.js +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/js/version-diff.js +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/logo.png +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/logo.svg +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/og-image.png +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/404.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/admin.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/login.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/register.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/workos_login.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/workos_verify.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/base.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/compare.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/dashboard.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_detail.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_new.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_review.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiments.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/health.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/judge_detail.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/judges.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/monitor_detail.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/monitors.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/auth_header.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/chip_input.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/compare_run_header.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/footer.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/judge_picker.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/local_time_js.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/multi_filter.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/need_help.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/not_found.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/pinned_version.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/result_rep_panel.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_results.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_settings.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_settings_js.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/severity_badge.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/sidebar.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/text_file_modal.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/tooltips.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/ui_js.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_badge.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_diff_js.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_diff_modal.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/profile.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/public/base_public.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/public/landing.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/run_result.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/scenarios.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/seo/head.html +0 -0
- {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/workspaces.html +0 -0
|
@@ -156,13 +156,13 @@ def compare_runs(project, run_ids: list[int]) -> dict:
|
|
|
156
156
|
text = getter(r)
|
|
157
157
|
url = None
|
|
158
158
|
if label == "Target model" and r.target_model_id:
|
|
159
|
-
url = f"/
|
|
159
|
+
url = f"/connections/#conn-{r.target_model.connection_id}"
|
|
160
160
|
elif label == "Auditor model" and r.auditor_model_id:
|
|
161
|
-
url = f"/
|
|
161
|
+
url = f"/connections/#conn-{r.auditor_model.connection_id}"
|
|
162
162
|
elif label == "Judge" and frozen_judge(r)["judge_id"]:
|
|
163
163
|
url = f"/judges/{frozen_judge(r)['judge_id']}/?v={frozen_judge(r)['version']}"
|
|
164
164
|
elif label == "Judge model" and r.judge_model_id:
|
|
165
|
-
url = f"/
|
|
165
|
+
url = f"/connections/#conn-{r.judge_model.connection_id}"
|
|
166
166
|
elif label == "Scenario set version" and r.scenario_set_version:
|
|
167
167
|
url = f"/scenarios/?set={r.scenario_set_version.scenario_set_id}"
|
|
168
168
|
cells.append(_cell(text, url))
|
|
@@ -16,7 +16,9 @@ from infra.seo import LandingView, llms_txt, robots_txt, sitemap_xml
|
|
|
16
16
|
from infra.ui import (
|
|
17
17
|
AdminView,
|
|
18
18
|
CompareView,
|
|
19
|
+
ConnectionCheckView,
|
|
19
20
|
ConnectionDeleteView,
|
|
21
|
+
ConnectionsView,
|
|
20
22
|
DashboardView,
|
|
21
23
|
DiscoverModelsView,
|
|
22
24
|
ExperimentDetailView,
|
|
@@ -24,7 +26,6 @@ from infra.ui import (
|
|
|
24
26
|
HealthView,
|
|
25
27
|
JudgeScriptView,
|
|
26
28
|
LoginView,
|
|
27
|
-
ModelsView,
|
|
28
29
|
MonitorActionView,
|
|
29
30
|
MonitorDetailView,
|
|
30
31
|
MonitorsView,
|
|
@@ -176,9 +177,10 @@ urlpatterns = [
|
|
|
176
177
|
path("scenarios/diff/<int:set_id>/", ScenarioDiffView.as_view(), name="scenario_diff"),
|
|
177
178
|
path("scenarios/<int:set_id>/export/", ScenarioExportView.as_view(), name="scenario_export"),
|
|
178
179
|
path("scenarios/<int:set_id>/import/", ScenarioImportView.as_view(), name="scenario_import"),
|
|
179
|
-
path("
|
|
180
|
-
path("
|
|
181
|
-
path("
|
|
180
|
+
path("connections/", ConnectionsView.as_view(), name="connections"),
|
|
181
|
+
path("connections/discover/", DiscoverModelsView.as_view(), name="models_discover"),
|
|
182
|
+
path("connections/check/", ConnectionCheckView.as_view(), name="connection_check"),
|
|
183
|
+
path("connections/<int:conn_id>/delete/", ConnectionDeleteView.as_view(), name="connection_delete"),
|
|
182
184
|
path("judges/", JudgesView.as_view(), name="judges"),
|
|
183
185
|
path("judges/new/", JudgeDetailView.as_view(), name="judge_new"),
|
|
184
186
|
path("judges/preview/", JudgePreviewView.as_view(), name="judge_preview"),
|
|
@@ -73,7 +73,7 @@ data directory (`~/.simpleaudit-studio/embedded-pg`, override with
|
|
|
73
73
|
| `/monitors/`, `/monitors/<id>/` | Monitors and drift charts |
|
|
74
74
|
| `/runs/<id>/`, `/runs/<id>/results/<rid>/` | Run detail (live) and per-scenario result |
|
|
75
75
|
| `/compare/?runs=a,b` | Side-by-side comparison of completed runs |
|
|
76
|
-
| `/scenarios/`, `/
|
|
76
|
+
| `/scenarios/`, `/connections/` | Scenario library; connections (servers + API keys) and the models registered on them |
|
|
77
77
|
| `/judges/`, `/judges/<id>/` | Judges: start from a SimpleAudit judge or write own criteria, edit (new version), clone, version history, live prompt preview (`/judges/preview/`) |
|
|
78
78
|
| `/workspaces/`, `/admin-settings/`, `/profile/`, `/health/` | Workspaces, super-admin settings, profile, system health |
|
|
79
79
|
| `/healthz`, `/readyz` | Liveness and readiness probes |
|
|
@@ -216,7 +216,7 @@ Rules:
|
|
|
216
216
|
A connection authenticates with either `api_key_direct` (stored on the
|
|
217
217
|
connection) or `secret_reference`, the name of an environment variable read at
|
|
218
218
|
execution time (e.g. `OPENAI_API_KEY`). The direct key wins when both are set.
|
|
219
|
-
Keys are never rendered into pages: the
|
|
219
|
+
Keys are never rendered into pages: the Connections page and model discovery look the
|
|
220
220
|
connection up server-side (`model_registry.services`).
|
|
221
221
|
|
|
222
222
|
### 4.4 `Judge` and `JudgeVersion` (`judges/`)
|
|
@@ -92,7 +92,7 @@ _NAV = (
|
|
|
92
92
|
("experiments", "Experiments", "⊞", ("/experiments/",), False),
|
|
93
93
|
("monitors", "Monitors", "↻", ("/monitors/",), False),
|
|
94
94
|
("scenarios", "Scenarios", "▤", ("/scenarios/",), False),
|
|
95
|
-
("
|
|
95
|
+
("connections", "Connections", "⬡", ("/connections/",), False),
|
|
96
96
|
("judges", "Judges", "⚖", ("/judges/",), False),
|
|
97
97
|
("compare", "Compare", "⇄", ("/compare/",), False),
|
|
98
98
|
("health", "Health", "⚕", ("/health/",), True),
|
|
@@ -34,10 +34,10 @@ class ConnectionDeleteTest(TestCase):
|
|
|
34
34
|
conn = ModelConnectionFactory(project=self.project, name="unused-conn")
|
|
35
35
|
RegisteredModelFactory(connection=conn, project=self.project)
|
|
36
36
|
|
|
37
|
-
resp = self.client.post(f"/
|
|
37
|
+
resp = self.client.post(f"/connections/{conn.id}/delete/")
|
|
38
38
|
|
|
39
39
|
assert resp.status_code == 302
|
|
40
|
-
assert resp.url == "/
|
|
40
|
+
assert resp.url == "/connections/"
|
|
41
41
|
assert not conn.__class__.objects.filter(pk=conn.id).exists()
|
|
42
42
|
|
|
43
43
|
def test_delete_referenced_connection_is_blocked_with_message(self):
|
|
@@ -45,16 +45,16 @@ class ConnectionDeleteTest(TestCase):
|
|
|
45
45
|
pinned_model = RegisteredModelFactory(connection=conn, project=self.project)
|
|
46
46
|
AuditRunFactory(project=self.project, target_model=pinned_model)
|
|
47
47
|
|
|
48
|
-
resp = self.client.post(f"/
|
|
48
|
+
resp = self.client.post(f"/connections/{conn.id}/delete/")
|
|
49
49
|
|
|
50
50
|
assert resp.status_code == 302
|
|
51
|
-
assert resp.url == "/
|
|
51
|
+
assert resp.url == "/connections/"
|
|
52
52
|
# Connection and its models must still exist.
|
|
53
53
|
assert conn.__class__.objects.filter(pk=conn.id).exists()
|
|
54
54
|
assert pinned_model.__class__.objects.filter(pk=pinned_model.id).exists()
|
|
55
55
|
# The error banner is rendered on the next page load (base.html iterates
|
|
56
56
|
# the messages framework), so follow the redirect and check the body.
|
|
57
|
-
get_resp = self.client.get("/
|
|
57
|
+
get_resp = self.client.get("/connections/")
|
|
58
58
|
body = get_resp.content.decode()
|
|
59
59
|
assert "runs or monitors use it" in body
|
|
60
60
|
|
|
@@ -62,7 +62,7 @@ class ConnectionDeleteTest(TestCase):
|
|
|
62
62
|
other_project = ProjectFactory()
|
|
63
63
|
conn = ModelConnectionFactory(project=other_project, name="foreign-conn")
|
|
64
64
|
|
|
65
|
-
resp = self.client.post(f"/
|
|
65
|
+
resp = self.client.post(f"/connections/{conn.id}/delete/")
|
|
66
66
|
|
|
67
67
|
assert resp.status_code == 302
|
|
68
68
|
assert conn.__class__.objects.filter(pk=conn.id).exists()
|
|
@@ -89,7 +89,7 @@ class RegisteredModelDeleteTest(TestCase):
|
|
|
89
89
|
conn = ModelConnectionFactory(project=self.project, name="conn-a")
|
|
90
90
|
rm = RegisteredModelFactory(connection=conn, project=self.project)
|
|
91
91
|
|
|
92
|
-
resp = self.client.post("/
|
|
92
|
+
resp = self.client.post("/connections/", {"action": "delete_model", "rm_id": rm.id}, follow=True)
|
|
93
93
|
|
|
94
94
|
assert resp.status_code == 200
|
|
95
95
|
assert not RegisteredModel.objects.filter(pk=rm.id).exists()
|
|
@@ -101,7 +101,7 @@ class RegisteredModelDeleteTest(TestCase):
|
|
|
101
101
|
rm = RegisteredModelFactory(connection=conn, project=self.project)
|
|
102
102
|
AuditRunFactory(project=self.project, target_model=rm)
|
|
103
103
|
|
|
104
|
-
resp = self.client.post("/
|
|
104
|
+
resp = self.client.post("/connections/", {"action": "delete_model", "rm_id": rm.id}, follow=True)
|
|
105
105
|
|
|
106
106
|
assert resp.status_code == 200
|
|
107
107
|
# Model must still exist.
|
|
@@ -111,7 +111,7 @@ class RegisteredModelDeleteTest(TestCase):
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
class DiscoverModelsKeyTests(TestCase):
|
|
114
|
-
"""The
|
|
114
|
+
"""The Connections page must never render API keys; discover resolves them server-side."""
|
|
115
115
|
|
|
116
116
|
def setUp(self):
|
|
117
117
|
self.user = UserFactory()
|
|
@@ -127,7 +127,7 @@ class DiscoverModelsKeyTests(TestCase):
|
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
def test_models_page_does_not_contain_the_key(self):
|
|
130
|
-
page = self.client.get("/
|
|
130
|
+
page = self.client.get("/connections/")
|
|
131
131
|
self.assertEqual(page.status_code, 200)
|
|
132
132
|
self.assertNotContains(page, "sk-super-secret-123")
|
|
133
133
|
self.assertContains(page, f'data-discover="{self.conn.id}"')
|
|
@@ -138,11 +138,86 @@ class DiscoverModelsKeyTests(TestCase):
|
|
|
138
138
|
fake = mock.MagicMock()
|
|
139
139
|
fake.json.return_value = {"data": [{"id": "gpt-x"}]}
|
|
140
140
|
with mock.patch("model_registry.services.httpx.get", return_value=fake) as get:
|
|
141
|
-
resp = self.client.post("/
|
|
141
|
+
resp = self.client.post("/connections/discover/", {"connection_id": self.conn.id})
|
|
142
142
|
self.assertEqual(resp.json()["models"], ["gpt-x"])
|
|
143
143
|
url, kwargs = get.call_args.args[0], get.call_args.kwargs
|
|
144
144
|
self.assertEqual(kwargs["headers"]["Authorization"], "Bearer sk-super-secret-123")
|
|
145
145
|
self.assertEqual(url, "https://api.example.invalid/v1/models")
|
|
146
146
|
|
|
147
147
|
other = ModelConnection.objects.create(project=ProjectFactory(), name="Other", base_url="https://x.invalid/v1")
|
|
148
|
-
self.assertEqual(self.client.post("/
|
|
148
|
+
self.assertEqual(self.client.post("/connections/discover/", {"connection_id": other.id}).status_code, 404)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ConnectionCheckTests(TestCase):
|
|
152
|
+
"""The connection dialog's Check tries unsaved settings before saving."""
|
|
153
|
+
|
|
154
|
+
def setUp(self):
|
|
155
|
+
from unittest import mock
|
|
156
|
+
|
|
157
|
+
self.mock = mock
|
|
158
|
+
self.user = UserFactory()
|
|
159
|
+
self.user.set_password("pw")
|
|
160
|
+
self.user.save()
|
|
161
|
+
self.project = ProjectFactory()
|
|
162
|
+
self.membership = MembershipFactory(user=self.user, project=self.project, role="admin")
|
|
163
|
+
self.client = Client()
|
|
164
|
+
self.client.login(username=self.user.username, password="pw")
|
|
165
|
+
|
|
166
|
+
def _server(self, ids=("a", "b")):
|
|
167
|
+
fake = self.mock.MagicMock()
|
|
168
|
+
fake.json.return_value = {"data": [{"id": i} for i in ids]}
|
|
169
|
+
return self.mock.patch("model_registry.services.httpx.get", return_value=fake)
|
|
170
|
+
|
|
171
|
+
def test_typed_settings_are_tried(self):
|
|
172
|
+
with self._server(["m1", "m2"]) as get:
|
|
173
|
+
out = self.client.post("/connections/check/", {
|
|
174
|
+
"conn_provider": "openai", "conn_base_url": "https://new.invalid/v1",
|
|
175
|
+
"key_mode": "stored", "conn_api_key": "sk-typed",
|
|
176
|
+
}).json()
|
|
177
|
+
self.assertEqual(out, {"ok": True, "count": 2, "models": ["m1", "m2"]})
|
|
178
|
+
self.assertEqual(get.call_args.args[0], "https://new.invalid/v1/models")
|
|
179
|
+
self.assertEqual(get.call_args.kwargs["headers"]["Authorization"], "Bearer sk-typed")
|
|
180
|
+
self.assertFalse(ModelConnection.objects.exists()) # nothing saved
|
|
181
|
+
|
|
182
|
+
def test_editing_with_blank_key_uses_the_stored_one(self):
|
|
183
|
+
conn = ModelConnection.objects.create(project=self.project, name="C", base_url="https://c.invalid/v1",
|
|
184
|
+
api_key_direct="sk-stored")
|
|
185
|
+
with self._server() as get:
|
|
186
|
+
self.client.post("/connections/check/", {"conn_id": conn.id, "conn_base_url": conn.base_url,
|
|
187
|
+
"key_mode": "stored", "conn_api_key": ""})
|
|
188
|
+
self.assertEqual(get.call_args.kwargs["headers"]["Authorization"], "Bearer sk-stored")
|
|
189
|
+
|
|
190
|
+
def test_another_workspaces_key_is_never_used(self):
|
|
191
|
+
other = ModelConnection.objects.create(project=ProjectFactory(), name="O", base_url="https://o.invalid/v1",
|
|
192
|
+
api_key_direct="sk-not-yours")
|
|
193
|
+
with self._server() as get:
|
|
194
|
+
self.client.post("/connections/check/", {"conn_id": other.id, "conn_base_url": other.base_url,
|
|
195
|
+
"key_mode": "stored"})
|
|
196
|
+
self.assertNotIn("Authorization", get.call_args.kwargs["headers"])
|
|
197
|
+
|
|
198
|
+
def test_errors_are_reported(self):
|
|
199
|
+
import httpx
|
|
200
|
+
|
|
201
|
+
self.assertIn("base URL", self.client.post("/connections/check/", {"conn_base_url": ""}).json()["error"])
|
|
202
|
+
with self.mock.patch("model_registry.services.httpx.get", side_effect=httpx.ConnectError("refused")):
|
|
203
|
+
out = self.client.post("/connections/check/", {"conn_base_url": "https://down.invalid/v1"}).json()
|
|
204
|
+
self.assertFalse(out["ok"])
|
|
205
|
+
self.assertTrue(out["error"])
|
|
206
|
+
|
|
207
|
+
def test_a_web_page_is_explained(self):
|
|
208
|
+
fake = self.mock.MagicMock()
|
|
209
|
+
fake.json.side_effect = ValueError("Expecting value")
|
|
210
|
+
with self.mock.patch("model_registry.services.httpx.get", return_value=fake):
|
|
211
|
+
out = self.client.post("/connections/check/", {"conn_base_url": "https://site.invalid"}).json()
|
|
212
|
+
self.assertIn("usually ends in /v1", out["error"])
|
|
213
|
+
|
|
214
|
+
def test_viewers_cannot_check(self):
|
|
215
|
+
self.membership.role = "viewer"
|
|
216
|
+
self.membership.save()
|
|
217
|
+
self.assertEqual(self.client.post("/connections/check/", {"conn_base_url": "https://x.invalid/v1"}).status_code, 403)
|
|
218
|
+
|
|
219
|
+
def test_each_provider_listed_once(self):
|
|
220
|
+
ModelConnection.objects.create(project=self.project, name="G", provider="gemini", base_url="https://g.invalid")
|
|
221
|
+
providers = self.client.get("/connections/").context["providers"]
|
|
222
|
+
self.assertEqual(providers.count("openai"), 1)
|
|
223
|
+
self.assertIn("gemini", providers) # a connection's own provider stays pickable
|
{simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_connection_sharing.py
RENAMED
|
@@ -111,8 +111,8 @@ class VisibilityHelperTests(TestCase):
|
|
|
111
111
|
self.assertEqual(names, {"Owner WS", "Other WS"})
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
class
|
|
115
|
-
"""The
|
|
114
|
+
class ConnectionsPageSharingTests(TestCase):
|
|
115
|
+
"""The Connections page shows shared connections read-only to consumers."""
|
|
116
116
|
|
|
117
117
|
def setUp(self):
|
|
118
118
|
self.owner_ws = ProjectFactory(name="Owner WS")
|
|
@@ -133,7 +133,7 @@ class ModelsPageSharingTests(TestCase):
|
|
|
133
133
|
_login(self.client, user)
|
|
134
134
|
self.client.session["active_project_id"] = ws.id
|
|
135
135
|
self.client.session.save()
|
|
136
|
-
return self.client.get("/
|
|
136
|
+
return self.client.get("/connections/")
|
|
137
137
|
|
|
138
138
|
def test_consumer_sees_shared_connection_readonly(self):
|
|
139
139
|
page = self._page_as(self.consumer_user, self.consumer_ws)
|
|
@@ -154,7 +154,7 @@ class ModelsPageSharingTests(TestCase):
|
|
|
154
154
|
self.client.session["active_project_id"] = self.consumer_ws.id
|
|
155
155
|
self.client.session.save()
|
|
156
156
|
before = self.conn.description
|
|
157
|
-
resp = self.client.post("/
|
|
157
|
+
resp = self.client.post("/connections/", {
|
|
158
158
|
"action": "edit_connection", "conn_id": self.conn.id,
|
|
159
159
|
"conn_name": "Hacked", "conn_base_url": "https://evil.example/v1",
|
|
160
160
|
"conn_enabled": "1", "key_mode": "none",
|
|
@@ -168,7 +168,7 @@ class ModelsPageSharingTests(TestCase):
|
|
|
168
168
|
_login(self.client, self.consumer_user)
|
|
169
169
|
self.client.session["active_project_id"] = self.consumer_ws.id
|
|
170
170
|
self.client.session.save()
|
|
171
|
-
resp = self.client.post(f"/
|
|
171
|
+
resp = self.client.post(f"/connections/{self.conn.id}/delete/")
|
|
172
172
|
self.assertEqual(resp.status_code, 302)
|
|
173
173
|
self.assertTrue(ModelConnection.objects.filter(pk=self.conn.id).exists())
|
|
174
174
|
|
|
@@ -176,7 +176,7 @@ class ModelsPageSharingTests(TestCase):
|
|
|
176
176
|
_login(self.client, self.owner_user)
|
|
177
177
|
self.client.session["active_project_id"] = self.owner_ws.id
|
|
178
178
|
self.client.session.save()
|
|
179
|
-
self.client.post("/
|
|
179
|
+
self.client.post("/connections/", {
|
|
180
180
|
"action": "edit_connection", "conn_id": self.conn.id,
|
|
181
181
|
"conn_name": "Public OpenAI", "conn_base_url": "https://api.openai.com/v1",
|
|
182
182
|
"conn_enabled": "1", "key_mode": "none",
|
|
@@ -80,7 +80,7 @@ class AllPagesSmokeTest(TestCase):
|
|
|
80
80
|
|
|
81
81
|
def test_main_pages(self):
|
|
82
82
|
self._ok("/scenarios/", "Scenario Library")
|
|
83
|
-
self._ok("/
|
|
83
|
+
self._ok("/connections/", "Connections")
|
|
84
84
|
self._ok("/workspaces/", "Workspaces")
|
|
85
85
|
self._ok("/profile/", "Profile")
|
|
86
86
|
|
|
@@ -33,7 +33,7 @@ class ViewerWriteTests(_Base):
|
|
|
33
33
|
self.assertFalse(ScenarioSet.objects.filter(name="Sneaky").exists())
|
|
34
34
|
|
|
35
35
|
def test_viewer_cannot_add_connection(self):
|
|
36
|
-
resp = self.client.post("/
|
|
36
|
+
resp = self.client.post("/connections/", {
|
|
37
37
|
"action": "add_connection", "conn_name": "X", "conn_base_url": "https://api.example.com/v1",
|
|
38
38
|
}, follow=True)
|
|
39
39
|
self.assertContains(resp, "Admin or auditor role required")
|
|
@@ -48,10 +48,10 @@ class AuditorWriteTests(_Base):
|
|
|
48
48
|
self.assertTrue(ScenarioSet.objects.filter(project=self.project, name="Allowed").exists())
|
|
49
49
|
|
|
50
50
|
def test_connection_base_url_is_validated(self):
|
|
51
|
-
resp = self.client.post("/
|
|
51
|
+
resp = self.client.post("/connections/", {"action": "add_connection", "conn_name": "Bad", "conn_base_url": "not a url"}, follow=True)
|
|
52
52
|
self.assertContains(resp, "Base URL must be an http(s) URL")
|
|
53
53
|
self.assertFalse(ModelConnection.objects.filter(project=self.project).exists())
|
|
54
|
-
self.client.post("/
|
|
54
|
+
self.client.post("/connections/", {"action": "add_connection", "conn_name": "Ok", "conn_base_url": "http://mock-model:8080/v1"})
|
|
55
55
|
self.assertTrue(ModelConnection.objects.filter(project=self.project, name="Ok").exists())
|
|
56
56
|
|
|
57
57
|
|
|
@@ -80,7 +80,7 @@ class ModelsPageTests(_Base):
|
|
|
80
80
|
|
|
81
81
|
conn = ModelConnectionFactory(project=self.project)
|
|
82
82
|
RegisteredModel.objects.create(connection=conn, project=self.project, model_id="a", display_name="a")
|
|
83
|
-
resp = self.client.post("/
|
|
83
|
+
resp = self.client.post("/connections/", {"action": "add_models", "conn_id": conn.id, "model_id": ["a", "b", "c"]})
|
|
84
84
|
self.assertEqual(resp.status_code, 302) # redirect after save: refresh won't resubmit
|
|
85
85
|
self.assertEqual(sorted(conn.models.values_list("model_id", flat=True)), ["a", "b", "c"])
|
|
86
86
|
page = self.client.get(resp.url)
|
|
@@ -90,17 +90,17 @@ class ModelsPageTests(_Base):
|
|
|
90
90
|
conn = ModelConnectionFactory(project=self.project, api_key_direct="sk-old", secret_reference="")
|
|
91
91
|
data = {"action": "edit_connection", "conn_id": conn.id, "conn_name": conn.name,
|
|
92
92
|
"conn_base_url": "https://api.example.com/v1", "conn_enabled": "1"}
|
|
93
|
-
self.client.post("/
|
|
93
|
+
self.client.post("/connections/", {**data, "key_mode": "stored", "conn_api_key": ""})
|
|
94
94
|
conn.refresh_from_db()
|
|
95
95
|
self.assertEqual(conn.api_key_direct, "sk-old") # blank keeps the stored key
|
|
96
|
-
resp = self.client.post("/
|
|
96
|
+
resp = self.client.post("/connections/", {**data, "key_mode": "env", "conn_secret_ref": ""}, follow=True)
|
|
97
97
|
self.assertContains(resp, "Enter the environment variable")
|
|
98
98
|
conn.refresh_from_db()
|
|
99
99
|
self.assertEqual(conn.api_key_direct, "sk-old") # rejected: nothing changed
|
|
100
|
-
self.client.post("/
|
|
100
|
+
self.client.post("/connections/", {**data, "key_mode": "env", "conn_secret_ref": "MY_KEY"})
|
|
101
101
|
conn.refresh_from_db()
|
|
102
102
|
self.assertEqual((conn.api_key_direct, conn.secret_reference), ("", "MY_KEY"))
|
|
103
|
-
self.client.post("/
|
|
103
|
+
self.client.post("/connections/", {**data, "key_mode": "none"})
|
|
104
104
|
conn.refresh_from_db()
|
|
105
105
|
self.assertEqual((conn.api_key_direct, conn.secret_reference), ("", ""))
|
|
106
106
|
|
|
@@ -108,24 +108,24 @@ class ModelsPageTests(_Base):
|
|
|
108
108
|
from infra.tests.factories import RegisteredModelFactory
|
|
109
109
|
|
|
110
110
|
conn = ModelConnectionFactory(project=self.project)
|
|
111
|
-
self.client.post("/
|
|
111
|
+
self.client.post("/connections/", {"action": "edit_connection", "conn_id": conn.id, "conn_name": conn.name,
|
|
112
112
|
"conn_base_url": "https://api.example.com/v1", "conn_enabled": "1",
|
|
113
113
|
"key_mode": "none", "conn_description": " Team account "})
|
|
114
114
|
rm = RegisteredModelFactory(connection=conn, project=self.project)
|
|
115
|
-
self.client.post("/
|
|
115
|
+
self.client.post("/connections/", {"action": "edit_model", "rm_id": rm.id, "model_id_new": rm.model_id,
|
|
116
116
|
"model_display_name": "Fast", "model_description": "Cheap & quick"})
|
|
117
|
-
self.client.post("/
|
|
117
|
+
self.client.post("/connections/", {"action": "add_models", "conn_id": conn.id, "model_id": ["solo"],
|
|
118
118
|
"model_description": "Single add"})
|
|
119
119
|
conn.refresh_from_db()
|
|
120
120
|
rm.refresh_from_db()
|
|
121
121
|
self.assertEqual(conn.description, "Team account")
|
|
122
122
|
self.assertEqual((rm.display_name, rm.description), ("Fast", "Cheap & quick"))
|
|
123
123
|
self.assertEqual(conn.models.get(model_id="solo").description, "Single add")
|
|
124
|
-
page = self.client.get("/
|
|
124
|
+
page = self.client.get("/connections/")
|
|
125
125
|
self.assertContains(page, "Team account")
|
|
126
126
|
self.assertContains(page, "Cheap & quick")
|
|
127
127
|
# Bulk adds don't copy one description onto every model.
|
|
128
|
-
self.client.post("/
|
|
128
|
+
self.client.post("/connections/", {"action": "add_models", "conn_id": conn.id, "model_id": ["x", "y"],
|
|
129
129
|
"model_description": "ignored"})
|
|
130
130
|
self.assertFalse(conn.models.filter(model_id__in=["x", "y"]).exclude(description="").exists())
|
|
131
131
|
|
|
@@ -135,7 +135,7 @@ class ModelsPageTests(_Base):
|
|
|
135
135
|
conn = ModelConnectionFactory(project=self.project)
|
|
136
136
|
used = RegisteredModelFactory(connection=conn, project=self.project)
|
|
137
137
|
AuditRunFactory(project=self.project, target_model=used)
|
|
138
|
-
page = self.client.get("/
|
|
138
|
+
page = self.client.get("/connections/").content.decode()
|
|
139
139
|
self.assertIn("1 run", page)
|
|
140
140
|
self.assertNotIn(f'value="{used.id}">\n <button title="Remove model"', page)
|
|
141
141
|
self.assertIn("kept for reproducibility", page)
|
|
@@ -14,6 +14,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|
|
14
14
|
from django.db.models import Count, F, Max, ProtectedError, Q, RestrictedError
|
|
15
15
|
from django.http import Http404, HttpResponse, HttpResponseForbidden, JsonResponse
|
|
16
16
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
17
|
+
from django.urls import reverse
|
|
17
18
|
from django.utils import timezone
|
|
18
19
|
from django.views.generic import DetailView, TemplateView, View
|
|
19
20
|
|
|
@@ -1674,10 +1675,10 @@ class ScenarioImportView(ProjectMixin, View):
|
|
|
1674
1675
|
DESCRIPTION_MAX = 1000
|
|
1675
1676
|
|
|
1676
1677
|
|
|
1677
|
-
class
|
|
1678
|
-
"""
|
|
1678
|
+
class ConnectionsView(ProjectMixin, TemplateView):
|
|
1679
|
+
"""Connections (a server and its API key) and the models registered on each."""
|
|
1679
1680
|
|
|
1680
|
-
template_name = "
|
|
1681
|
+
template_name = "connections.html"
|
|
1681
1682
|
|
|
1682
1683
|
def get_context_data(self, **kw):
|
|
1683
1684
|
from model_registry.models import ModelConnection
|
|
@@ -1730,6 +1731,9 @@ class ModelsView(ProjectMixin, TemplateView):
|
|
|
1730
1731
|
conn_data=conn_data,
|
|
1731
1732
|
model_total=sum(len(c.model_list) for c in connections),
|
|
1732
1733
|
provider_presets=PROVIDER_PRESETS,
|
|
1734
|
+
# Each provider once: presets share some (OpenAI and "Custom" are
|
|
1735
|
+
# both openai), and a connection's own provider must stay pickable.
|
|
1736
|
+
providers=list(dict.fromkeys([p[2] for p in PROVIDER_PRESETS] + [c.provider for c in connections if c.provider])),
|
|
1733
1737
|
share_targets=share_targets,
|
|
1734
1738
|
visibility_choices=[(v, label) for v, label in ModelConnection.Visibility.choices],
|
|
1735
1739
|
)
|
|
@@ -1748,7 +1752,7 @@ class ModelsView(ProjectMixin, TemplateView):
|
|
|
1748
1752
|
|
|
1749
1753
|
def fail(msg):
|
|
1750
1754
|
messages.error(request, msg)
|
|
1751
|
-
return redirect(
|
|
1755
|
+
return redirect(reverse("connections") + anchor)
|
|
1752
1756
|
|
|
1753
1757
|
if action in ("add_connection", "edit_connection"):
|
|
1754
1758
|
name = post.get("conn_name", "").strip()
|
|
@@ -1842,7 +1846,7 @@ class ModelsView(ProjectMixin, TemplateView):
|
|
|
1842
1846
|
except (ProtectedError, RestrictedError):
|
|
1843
1847
|
# Runs pin models (RESTRICT FKs); deleting would break their records.
|
|
1844
1848
|
return fail(f"Can't delete “{rm.display_name}”: runs or monitors use it. Kept for reproducibility.")
|
|
1845
|
-
return redirect(
|
|
1849
|
+
return redirect(reverse("connections") + anchor)
|
|
1846
1850
|
|
|
1847
1851
|
|
|
1848
1852
|
class ConnectionDeleteView(ProjectMixin, View):
|
|
@@ -1866,7 +1870,7 @@ class ConnectionDeleteView(ProjectMixin, View):
|
|
|
1866
1870
|
request,
|
|
1867
1871
|
"Cannot delete: this connection's models are referenced by audit runs or monitors.",
|
|
1868
1872
|
)
|
|
1869
|
-
return redirect("
|
|
1873
|
+
return redirect("connections")
|
|
1870
1874
|
|
|
1871
1875
|
|
|
1872
1876
|
def _base_url_error(raw: str) -> str | None:
|
|
@@ -1884,6 +1888,40 @@ def _base_url_error(raw: str) -> str | None:
|
|
|
1884
1888
|
return None
|
|
1885
1889
|
|
|
1886
1890
|
|
|
1891
|
+
class ConnectionCheckView(ProjectMixin, View):
|
|
1892
|
+
"""Try the connection dialog's values before saving: list the server's
|
|
1893
|
+
models with that base URL, provider and key. Editing with the key left
|
|
1894
|
+
blank uses the connection's stored key."""
|
|
1895
|
+
|
|
1896
|
+
def post(self, request):
|
|
1897
|
+
from model_registry.models import ModelConnection
|
|
1898
|
+
from model_registry.services import (
|
|
1899
|
+
can_edit_connection,
|
|
1900
|
+
fetch_remote_model_ids,
|
|
1901
|
+
http_error_detail,
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
blocked = _require_write_access(request)
|
|
1905
|
+
if blocked:
|
|
1906
|
+
return JsonResponse({"ok": False, "error": "You don't have permission to change connections."}, status=403)
|
|
1907
|
+
post = request.POST
|
|
1908
|
+
mode = post.get("key_mode", "stored")
|
|
1909
|
+
conn = ModelConnection(provider=(post.get("conn_provider") or "openai").strip(),
|
|
1910
|
+
base_url=(post.get("conn_base_url") or "").strip())
|
|
1911
|
+
if mode == "stored":
|
|
1912
|
+
conn.api_key_direct = (post.get("conn_api_key") or "").strip()
|
|
1913
|
+
saved = ModelConnection.objects.filter(pk=_int(post.get("conn_id"))).first()
|
|
1914
|
+
if not conn.api_key_direct and saved is not None and can_edit_connection(request.user, saved):
|
|
1915
|
+
conn.api_key_direct = saved.api_key_direct
|
|
1916
|
+
elif mode == "env":
|
|
1917
|
+
conn.secret_reference = (post.get("conn_secret_ref") or "").strip()
|
|
1918
|
+
try:
|
|
1919
|
+
ids = fetch_remote_model_ids(conn)
|
|
1920
|
+
except Exception as e: # noqa: BLE001 - any failure is the answer the user asked for
|
|
1921
|
+
return JsonResponse({"ok": False, "error": http_error_detail(e)})
|
|
1922
|
+
return JsonResponse({"ok": True, "count": len(ids), "models": ids[:6]})
|
|
1923
|
+
|
|
1924
|
+
|
|
1887
1925
|
class DiscoverModelsView(ProjectMixin, View):
|
|
1888
1926
|
"""List the models a connection's server offers (GET {base_url}/models)."""
|
|
1889
1927
|
|
|
@@ -37,8 +37,8 @@ def models_url(conn) -> str:
|
|
|
37
37
|
def fetch_remote_model_ids(conn, *, timeout: float = 10) -> list[str]:
|
|
38
38
|
"""Model ids the connection's OpenAI-compatible ``/models`` endpoint lists, sorted.
|
|
39
39
|
|
|
40
|
-
Raises ``ValueError`` when the connection has no base URL
|
|
41
|
-
on network or HTTP failures.
|
|
40
|
+
Raises ``ValueError`` when the connection has no base URL or the reply isn't
|
|
41
|
+
a JSON model list, and ``httpx.HTTPError`` on network or HTTP failures.
|
|
42
42
|
"""
|
|
43
43
|
if not (conn.base_url or "").strip():
|
|
44
44
|
raise ValueError("This connection has no base URL.")
|
|
@@ -48,7 +48,13 @@ def fetch_remote_model_ids(conn, *, timeout: float = 10) -> list[str]:
|
|
|
48
48
|
headers["Authorization"] = f"Bearer {key}"
|
|
49
49
|
resp = httpx.get(models_url(conn), headers=headers, timeout=timeout)
|
|
50
50
|
resp.raise_for_status()
|
|
51
|
-
|
|
51
|
+
try:
|
|
52
|
+
data = resp.json()
|
|
53
|
+
except ValueError as exc: # e.g. a web page: the URL isn't the API root
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"{models_url(conn)} didn't return a model list (the reply isn't JSON). "
|
|
56
|
+
"Check the base URL: it usually ends in /v1."
|
|
57
|
+
) from exc
|
|
52
58
|
items = data.get("data", []) if isinstance(data, dict) else data if isinstance(data, list) else []
|
|
53
59
|
ids = {item if isinstance(item, str) else (item.get("id") or item.get("model") or "")
|
|
54
60
|
for item in items if isinstance(item, (str, dict))}
|