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.
Files changed (231) hide show
  1. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/PKG-INFO +1 -1
  2. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/comparison.py +3 -3
  3. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/urls.py +6 -4
  4. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/architecture.md +1 -1
  5. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/domain-model.md +1 -1
  6. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/context_processors.py +1 -1
  7. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/seo.py +1 -1
  8. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_connection_delete.py +87 -12
  9. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_connection_sharing.py +6 -6
  10. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_smoke_all_pages.py +1 -1
  11. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_write_guard.py +14 -14
  12. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/ui.py +44 -6
  13. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/services.py +9 -3
  14. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/pyproject.toml +1 -1
  15. simpleaudit_studio-0.4.2/templates/models.html → simpleaudit_studio-0.5.0/templates/connections.html +40 -4
  16. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/model_pickers.html +2 -2
  17. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/run_detail.html +1 -1
  18. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/uv.lock +1 -1
  19. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.dockerignore +0 -0
  20. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.env.example +0 -0
  21. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.env.local.example +0 -0
  22. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/ci.yml +0 -0
  23. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/docker.yml +0 -0
  24. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.github/workflows/publish.yml +0 -0
  25. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/.gitignore +0 -0
  26. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/Dockerfile +0 -0
  27. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/LICENSE +0 -0
  28. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/README.md +0 -0
  29. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/__init__.py +0 -0
  30. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/admin.py +0 -0
  31. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/admin_urls.py +0 -0
  32. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/apps.py +0 -0
  33. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/auth_urls.py +0 -0
  34. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/0001_initial.py +0 -0
  35. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/0002_user_preferences.py +0 -0
  36. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/migrations/__init__.py +0 -0
  37. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/models.py +0 -0
  38. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/project_urls.py +0 -0
  39. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/serializers.py +0 -0
  40. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/services.py +0 -0
  41. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/views.py +0 -0
  42. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/accounts/workos_auth.py +0 -0
  43. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/__init__.py +0 -0
  44. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/admin.py +0 -0
  45. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/apps.py +0 -0
  46. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/events.py +0 -0
  47. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/experiments.py +0 -0
  48. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0001_initial.py +0 -0
  49. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0002_strip_api_keys_from_snapshots.py +0 -0
  50. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0003_judges.py +0 -0
  51. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/0004_judge_model_per_run.py +0 -0
  52. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/migrations/__init__.py +0 -0
  53. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/models.py +0 -0
  54. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/monitors.py +0 -0
  55. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/serializers.py +0 -0
  56. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/services.py +0 -0
  57. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/urls.py +0 -0
  58. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/audits/views.py +0 -0
  59. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/__init__.py +0 -0
  60. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/asgi.py +0 -0
  61. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/settings.py +0 -0
  62. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/config/wsgi.py +0 -0
  63. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/compose/Dockerfile +0 -0
  64. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/mock_openai_server.py +0 -0
  65. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/deploy/postgres-init.sql +0 -0
  66. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docker-compose.yml +0 -0
  67. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/docs/deployment.md +0 -0
  68. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/e2e/test_ui.py +0 -0
  69. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/__init__.py +0 -0
  70. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/admin.py +0 -0
  71. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/apps.py +0 -0
  72. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/codegen.py +0 -0
  73. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/db.py +0 -0
  74. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/engine.py +0 -0
  75. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/exceptions.py +0 -0
  76. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/fixtures/demo_audit_results.json +0 -0
  77. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/hashing.py +0 -0
  78. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/health.py +0 -0
  79. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/health_api.py +0 -0
  80. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/logging.py +0 -0
  81. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/__init__.py +0 -0
  82. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/__init__.py +0 -0
  83. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/bootstrap_platform.py +0 -0
  84. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/dev_server.py +0 -0
  85. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/purge_test_data.py +0 -0
  86. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/run_monitors.py +0 -0
  87. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/run_worker.py +0 -0
  88. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/seed_demo_audits.py +0 -0
  89. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/seed_platform.py +0 -0
  90. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/management/commands/setup_local.py +0 -0
  91. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/middleware.py +0 -0
  92. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/minimal_config.py +0 -0
  93. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/readiness.py +0 -0
  94. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/runs_table.py +0 -0
  95. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/seed.py +0 -0
  96. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/simpleaudit_package.py +0 -0
  97. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/startup_checks.py +0 -0
  98. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/templatetags/__init__.py +0 -0
  99. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/templatetags/json_filters.py +0 -0
  100. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/__init__.py +0 -0
  101. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/factories.py +0 -0
  102. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_admin_api.py +0 -0
  103. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_admin_page.py +0 -0
  104. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_api_e2e.py +0 -0
  105. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_archive.py +0 -0
  106. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_clone.py +0 -0
  107. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_events.py +0 -0
  108. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_poll_results.py +0 -0
  109. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_rename.py +0 -0
  110. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_results_live.py +0 -0
  111. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_run_freeze.py +0 -0
  112. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_sse.py +0 -0
  113. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_submission.py +0 -0
  114. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_audit_timing.py +0 -0
  115. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_auth_projects.py +0 -0
  116. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_bootstrap.py +0 -0
  117. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_codegen.py +0 -0
  118. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_comparison.py +0 -0
  119. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_db_resilience.py +0 -0
  120. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_engine_integration.py +0 -0
  121. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_experiments.py +0 -0
  122. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_health.py +0 -0
  123. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_health_panel.py +0 -0
  124. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_judges.py +0 -0
  125. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_middleware.py +0 -0
  126. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_minimal_config.py +0 -0
  127. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_model_registry.py +0 -0
  128. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_monitors.py +0 -0
  129. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_profile.py +0 -0
  130. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_profile_ui.py +0 -0
  131. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_purge_test_data.py +0 -0
  132. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_runs_table.py +0 -0
  133. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_scenario_import_export.py +0 -0
  134. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_scenarios.py +0 -0
  135. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_seed_platform.py +0 -0
  136. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_snapshot_secrets.py +0 -0
  137. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspace_archive.py +0 -0
  138. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspaces.py +0 -0
  139. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/test_workspaces_ui.py +0 -0
  140. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/tests/utils.py +0 -0
  141. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/infra/worker.py +0 -0
  142. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/__init__.py +0 -0
  143. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/admin.py +0 -0
  144. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/apps.py +0 -0
  145. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0001_judges.py +0 -0
  146. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0002_judge_model_per_run.py +0 -0
  147. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/0003_composable_judges.py +0 -0
  148. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/migrations/__init__.py +0 -0
  149. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/models.py +0 -0
  150. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/services.py +0 -0
  151. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/judges/views.py +0 -0
  152. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/manage.py +0 -0
  153. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/__init__.py +0 -0
  154. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/admin.py +0 -0
  155. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/apps.py +0 -0
  156. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0001_initial.py +0 -0
  157. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0002_descriptions.py +0 -0
  158. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/0003_modelconnection_shared_with_and_more.py +0 -0
  159. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/migrations/__init__.py +0 -0
  160. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/models.py +0 -0
  161. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/serializers.py +0 -0
  162. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/urls.py +0 -0
  163. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/model_registry/views.py +0 -0
  164. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/__init__.py +0 -0
  165. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/admin.py +0 -0
  166. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/apps.py +0 -0
  167. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/0001_initial.py +0 -0
  168. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/0002_add_severity_ceiling_documents_file_uri.py +0 -0
  169. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/migrations/__init__.py +0 -0
  170. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/models.py +0 -0
  171. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/serializers.py +0 -0
  172. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/services.py +0 -0
  173. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/urls.py +0 -0
  174. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/scenarios/views.py +0 -0
  175. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/simpleaudit_studio/__init__.py +0 -0
  176. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/simpleaudit_studio/cli.py +0 -0
  177. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon-16x16.svg +0 -0
  178. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon-32x32.svg +0 -0
  179. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/favicon.svg +0 -0
  180. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/js/diff.min.js +0 -0
  181. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/js/version-diff.js +0 -0
  182. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/logo.png +0 -0
  183. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/logo.svg +0 -0
  184. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/static/og-image.png +0 -0
  185. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/404.html +0 -0
  186. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/admin.html +0 -0
  187. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/login.html +0 -0
  188. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/register.html +0 -0
  189. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/workos_login.html +0 -0
  190. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/auth/workos_verify.html +0 -0
  191. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/base.html +0 -0
  192. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/compare.html +0 -0
  193. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/dashboard.html +0 -0
  194. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_detail.html +0 -0
  195. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_new.html +0 -0
  196. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiment_review.html +0 -0
  197. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/experiments.html +0 -0
  198. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/health.html +0 -0
  199. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/judge_detail.html +0 -0
  200. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/judges.html +0 -0
  201. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/monitor_detail.html +0 -0
  202. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/monitors.html +0 -0
  203. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/auth_header.html +0 -0
  204. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/chip_input.html +0 -0
  205. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/compare_run_header.html +0 -0
  206. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/footer.html +0 -0
  207. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/judge_picker.html +0 -0
  208. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/local_time_js.html +0 -0
  209. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/multi_filter.html +0 -0
  210. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/need_help.html +0 -0
  211. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/not_found.html +0 -0
  212. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/pinned_version.html +0 -0
  213. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/result_rep_panel.html +0 -0
  214. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_results.html +0 -0
  215. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_settings.html +0 -0
  216. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/run_settings_js.html +0 -0
  217. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/severity_badge.html +0 -0
  218. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/sidebar.html +0 -0
  219. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/text_file_modal.html +0 -0
  220. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/tooltips.html +0 -0
  221. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/ui_js.html +0 -0
  222. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_badge.html +0 -0
  223. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_diff_js.html +0 -0
  224. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/partials/version_diff_modal.html +0 -0
  225. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/profile.html +0 -0
  226. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/public/base_public.html +0 -0
  227. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/public/landing.html +0 -0
  228. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/run_result.html +0 -0
  229. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/scenarios.html +0 -0
  230. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/seo/head.html +0 -0
  231. {simpleaudit_studio-0.4.2 → simpleaudit_studio-0.5.0}/templates/workspaces.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simpleaudit-studio
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Production AI model audit platform — one-liner demo mode via uvx
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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"/models/#conn-{r.target_model.connection_id}"
159
+ url = f"/connections/#conn-{r.target_model.connection_id}"
160
160
  elif label == "Auditor model" and r.auditor_model_id:
161
- url = f"/models/#conn-{r.auditor_model.connection_id}"
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"/models/#conn-{r.judge_model.connection_id}"
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("models/", ModelsView.as_view(), name="models"),
180
- path("models/discover/", DiscoverModelsView.as_view(), name="models_discover"),
181
- path("models/connection-delete/<int:conn_id>/", ConnectionDeleteView.as_view(), name="connection_delete"),
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/`, `/models/` | Scenario library and model registry |
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 models page and model discovery look 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
- ("models", "Models", "⬡", ("/models/",), False),
95
+ ("connections", "Connections", "⬡", ("/connections/",), False),
96
96
  ("judges", "Judges", "⚖", ("/judges/",), False),
97
97
  ("compare", "Compare", "⇄", ("/compare/",), False),
98
98
  ("health", "Health", "⚕", ("/health/",), True),
@@ -175,7 +175,7 @@ def robots_txt(request):
175
175
  "Disallow: /health/",
176
176
  "Disallow: /runs/",
177
177
  "Disallow: /scenarios/",
178
- "Disallow: /models/",
178
+ "Disallow: /connections/",
179
179
  "Disallow: /compare/",
180
180
  "Disallow: /auth/",
181
181
  "Disallow: /static/",
@@ -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"/models/connection-delete/{conn.id}/")
37
+ resp = self.client.post(f"/connections/{conn.id}/delete/")
38
38
 
39
39
  assert resp.status_code == 302
40
- assert resp.url == "/models/"
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"/models/connection-delete/{conn.id}/")
48
+ resp = self.client.post(f"/connections/{conn.id}/delete/")
49
49
 
50
50
  assert resp.status_code == 302
51
- assert resp.url == "/models/"
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("/models/")
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"/models/connection-delete/{conn.id}/")
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("/models/", {"action": "delete_model", "rm_id": rm.id}, follow=True)
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("/models/", {"action": "delete_model", "rm_id": rm.id}, follow=True)
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 Models page must never render API keys; discover resolves them server-side."""
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("/models/")
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("/models/discover/", {"connection_id": self.conn.id})
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("/models/discover/", {"connection_id": other.id}).status_code, 404)
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
@@ -111,8 +111,8 @@ class VisibilityHelperTests(TestCase):
111
111
  self.assertEqual(names, {"Owner WS", "Other WS"})
112
112
 
113
113
 
114
- class ModelsPageSharingTests(TestCase):
115
- """The Models page shows shared connections read-only to consumers."""
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("/models/")
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("/models/", {
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"/models/connection-delete/{self.conn.id}/")
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("/models/", {
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("/models/", "Models")
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("/models/", {
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("/models/", {"action": "add_connection", "conn_name": "Bad", "conn_base_url": "not a url"}, follow=True)
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("/models/", {"action": "add_connection", "conn_name": "Ok", "conn_base_url": "http://mock-model:8080/v1"})
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("/models/", {"action": "add_models", "conn_id": conn.id, "model_id": ["a", "b", "c"]})
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("/models/", {**data, "key_mode": "stored", "conn_api_key": ""})
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("/models/", {**data, "key_mode": "env", "conn_secret_ref": ""}, follow=True)
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("/models/", {**data, "key_mode": "env", "conn_secret_ref": "MY_KEY"})
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("/models/", {**data, "key_mode": "none"})
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("/models/", {"action": "edit_connection", "conn_id": conn.id, "conn_name": conn.name,
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("/models/", {"action": "edit_model", "rm_id": rm.id, "model_id_new": rm.model_id,
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("/models/", {"action": "add_models", "conn_id": conn.id, "model_id": ["solo"],
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("/models/")
124
+ page = self.client.get("/connections/")
125
125
  self.assertContains(page, "Team account")
126
126
  self.assertContains(page, "Cheap &amp; quick")
127
127
  # Bulk adds don't copy one description onto every model.
128
- self.client.post("/models/", {"action": "add_models", "conn_id": conn.id, "model_id": ["x", "y"],
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("/models/").content.decode()
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 ModelsView(ProjectMixin, TemplateView):
1678
- """Model registry: connections (server + key) and the models each one serves."""
1678
+ class ConnectionsView(ProjectMixin, TemplateView):
1679
+ """Connections (a server and its API key) and the models registered on each."""
1679
1680
 
1680
- template_name = "models.html"
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(f"/models/{anchor}")
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(f"/models/{anchor}")
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("/models/")
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 and ``httpx.HTTPError``
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
- data = resp.json()
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))}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simpleaudit-studio"
7
- version = "0.4.2"
7
+ version = "0.5.0"
8
8
  description = "Production AI model audit platform — one-liner demo mode via uvx"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"