secondshift 0.1.0b12__py3-none-any.whl
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.
- secondshift/__init__.py +1 -0
- secondshift/__main__.py +3 -0
- secondshift/cli.py +17 -0
- secondshift/config/__init__.py +0 -0
- secondshift/config/api.py +23 -0
- secondshift/config/app.py +62 -0
- secondshift/config/constants.py +279 -0
- secondshift/config/container_env.py +118 -0
- secondshift/config/first_run.py +40 -0
- secondshift/config/formatting.py +5 -0
- secondshift/config/gunicorn.py +31 -0
- secondshift/config/logging.py +40 -0
- secondshift/config/settings/__init__.py +0 -0
- secondshift/config/settings/base.py +524 -0
- secondshift/config/settings/dev.py +14 -0
- secondshift/config/settings/local.py +23 -0
- secondshift/config/settings/production.py +113 -0
- secondshift/config/settings/runner.py +140 -0
- secondshift/config/settings/test.py +47 -0
- secondshift/config/static_storage.py +45 -0
- secondshift/config/urls.py +42 -0
- secondshift/connections/__init__.py +0 -0
- secondshift/connections/access.py +27 -0
- secondshift/connections/admin.py +25 -0
- secondshift/connections/agents.py +224 -0
- secondshift/connections/api.py +747 -0
- secondshift/connections/apps.py +6 -0
- secondshift/connections/catalog.py +96 -0
- secondshift/connections/checks.py +22 -0
- secondshift/connections/dao.py +305 -0
- secondshift/connections/errors.py +13 -0
- secondshift/connections/execution.py +262 -0
- secondshift/connections/external.py +258 -0
- secondshift/connections/github.py +123 -0
- secondshift/connections/http_api/__init__.py +1 -0
- secondshift/connections/http_api/client.py +161 -0
- secondshift/connections/http_api/connections.py +187 -0
- secondshift/connections/http_api/spec.py +382 -0
- secondshift/connections/mcp/__init__.py +1 -0
- secondshift/connections/mcp/base.py +139 -0
- secondshift/connections/mcp/client.py +353 -0
- secondshift/connections/mcp/custom.py +86 -0
- secondshift/connections/mcp/github.py +256 -0
- secondshift/connections/mcp/oauth.py +352 -0
- secondshift/connections/mcp/providers.py +15 -0
- secondshift/connections/mcp/schemas.py +77 -0
- secondshift/connections/mcp/sentry.py +135 -0
- secondshift/connections/mcp/signin.py +206 -0
- secondshift/connections/migration_helpers.py +28 -0
- secondshift/connections/migrations/0001_initial.py +101 -0
- secondshift/connections/migrations/0002_alter_connection_kind.py +18 -0
- secondshift/connections/migrations/0003_require_approval.py +23 -0
- secondshift/connections/migrations/0004_new_connections_skip_approval.py +18 -0
- secondshift/connections/migrations/0005_agentconnection.py +36 -0
- secondshift/connections/migrations/0006_remove_is_default.py +23 -0
- secondshift/connections/migrations/0007_agent_last_check.py +18 -0
- secondshift/connections/migrations/0008_agentcredentialcleanup.py +25 -0
- secondshift/connections/migrations/0009_release_0_1_0b11.py +52 -0
- secondshift/connections/migrations/0010_connection_attribution.py +18 -0
- secondshift/connections/migrations/__init__.py +0 -0
- secondshift/connections/model_providers.py +441 -0
- secondshift/connections/models.py +246 -0
- secondshift/connections/outbound.py +317 -0
- secondshift/connections/repository_ref.py +19 -0
- secondshift/connections/schemas.py +313 -0
- secondshift/connections/serialization.py +100 -0
- secondshift/connections/services.py +94 -0
- secondshift/connections/tools.py +38 -0
- secondshift/connections/webhooks.py +107 -0
- secondshift/core/__init__.py +0 -0
- secondshift/core/adapters.py +38 -0
- secondshift/core/admin.py +5 -0
- secondshift/core/api.py +104 -0
- secondshift/core/apps.py +20 -0
- secondshift/core/launcher.py +311 -0
- secondshift/core/local_agents.py +90 -0
- secondshift/core/local_output.py +120 -0
- secondshift/core/management/__init__.py +0 -0
- secondshift/core/management/commands/__init__.py +0 -0
- secondshift/core/management/commands/bootstrap_dev.py +33 -0
- secondshift/core/management/commands/dev_reset_db.py +204 -0
- secondshift/core/management/commands/login.py +23 -0
- secondshift/core/management/commands/logs.py +58 -0
- secondshift/core/management/commands/status.py +46 -0
- secondshift/core/management/commands/up.py +244 -0
- secondshift/core/middleware.py +21 -0
- secondshift/core/migrations/__init__.py +0 -0
- secondshift/core/schemas.py +71 -0
- secondshift/core/signin.py +58 -0
- secondshift/core/static/core/account.css +149 -0
- secondshift/core/static/core/admin.css +135 -0
- secondshift/core/templatetags/__init__.py +0 -0
- secondshift/core/templatetags/accounts.py +20 -0
- secondshift/core/terminal.py +27 -0
- secondshift/core/views.py +10 -0
- secondshift/gateway/__init__.py +0 -0
- secondshift/gateway/apps.py +6 -0
- secondshift/gateway/dao.py +137 -0
- secondshift/gateway/metering.py +171 -0
- secondshift/gateway/migrations/0001_initial.py +54 -0
- secondshift/gateway/migrations/0002_modelusage_gateway_usage_created_at_idx.py +19 -0
- secondshift/gateway/migrations/__init__.py +0 -0
- secondshift/gateway/models.py +61 -0
- secondshift/gateway/pricing.py +39 -0
- secondshift/gateway/proxy.py +369 -0
- secondshift/gateway/tokens.py +68 -0
- secondshift/gateway/urls.py +17 -0
- secondshift/gateway/views.py +11 -0
- secondshift/github/__init__.py +6 -0
- secondshift/github/app.py +109 -0
- secondshift/github/authors.py +56 -0
- secondshift/github/credentials.py +59 -0
- secondshift/github/errors.py +2 -0
- secondshift/github/git.py +56 -0
- secondshift/github/identity.py +35 -0
- secondshift/github/installations.py +74 -0
- secondshift/github/issues.py +75 -0
- secondshift/github/permissions.py +49 -0
- secondshift/github/pulls.py +49 -0
- secondshift/github/repos.py +52 -0
- secondshift/github/rest.py +93 -0
- secondshift/github/ruff.toml +16 -0
- secondshift/github/tokens.py +52 -0
- secondshift/jobs/__init__.py +0 -0
- secondshift/jobs/access.py +12 -0
- secondshift/jobs/admin.py +83 -0
- secondshift/jobs/api.py +184 -0
- secondshift/jobs/apps.py +6 -0
- secondshift/jobs/automatic_grants.py +115 -0
- secondshift/jobs/autosync.py +78 -0
- secondshift/jobs/capabilities.py +43 -0
- secondshift/jobs/dao.py +258 -0
- secondshift/jobs/drafts.py +112 -0
- secondshift/jobs/grants.py +116 -0
- secondshift/jobs/limits.py +26 -0
- secondshift/jobs/management/__init__.py +0 -0
- secondshift/jobs/management/commands/__init__.py +0 -0
- secondshift/jobs/manifest.py +364 -0
- secondshift/jobs/migrations/0001_initial.py +87 -0
- secondshift/jobs/migrations/0002_job_history.py +18 -0
- secondshift/jobs/migrations/0003_job_skill_text.py +23 -0
- secondshift/jobs/migrations/0004_jobgrant_tool_rules.py +18 -0
- secondshift/jobs/migrations/0005_job_triggers.py +18 -0
- secondshift/jobs/migrations/0006_job_webhook.py +18 -0
- secondshift/jobs/migrations/0007_job_deliveries.py +23 -0
- secondshift/jobs/migrations/0008_job_removed_at.py +18 -0
- secondshift/jobs/migrations/0009_job_subagents.py +18 -0
- secondshift/jobs/migrations/0010_job_skill_source.py +18 -0
- secondshift/jobs/migrations/__init__.py +0 -0
- secondshift/jobs/models.py +199 -0
- secondshift/jobs/needs.py +154 -0
- secondshift/jobs/plan.py +176 -0
- secondshift/jobs/planning.py +386 -0
- secondshift/jobs/readiness.py +46 -0
- secondshift/jobs/repository_sync.py +199 -0
- secondshift/jobs/scheduling.py +26 -0
- secondshift/jobs/schemas.py +207 -0
- secondshift/jobs/serialization.py +218 -0
- secondshift/jobs/skill_needs.py +189 -0
- secondshift/jobs/sync.py +252 -0
- secondshift/jobs/tool_policy.py +82 -0
- secondshift/jobs/tool_rules.py +200 -0
- secondshift/mcp_server/__init__.py +6 -0
- secondshift/mcp_server/api.py +79 -0
- secondshift/mcp_server/apps.py +7 -0
- secondshift/mcp_server/audit.py +22 -0
- secondshift/mcp_server/core.py +12 -0
- secondshift/mcp_server/dao.py +23 -0
- secondshift/mcp_server/migrations/0001_initial.py +33 -0
- secondshift/mcp_server/migrations/__init__.py +0 -0
- secondshift/mcp_server/models.py +40 -0
- secondshift/mcp_server/schemas.py +204 -0
- secondshift/mcp_server/tools/__init__.py +9 -0
- secondshift/mcp_server/tools/jobs.py +106 -0
- secondshift/mcp_server/tools/manifests.py +127 -0
- secondshift/mcp_server/tools/repositories.py +42 -0
- secondshift/mcp_server/tools/runs.py +86 -0
- secondshift/mcp_server/tools/tasks.py +103 -0
- secondshift/mcp_server/tools_registry.py +81 -0
- secondshift/repositories/__init__.py +0 -0
- secondshift/repositories/access.py +13 -0
- secondshift/repositories/admin.py +19 -0
- secondshift/repositories/api.py +296 -0
- secondshift/repositories/apps.py +6 -0
- secondshift/repositories/dao.py +114 -0
- secondshift/repositories/external_skills.py +224 -0
- secondshift/repositories/github.py +27 -0
- secondshift/repositories/github_access.py +57 -0
- secondshift/repositories/github_check.py +279 -0
- secondshift/repositories/github_sync.py +181 -0
- secondshift/repositories/github_views.py +90 -0
- secondshift/repositories/local.py +198 -0
- secondshift/repositories/management/__init__.py +0 -0
- secondshift/repositories/management/commands/__init__.py +0 -0
- secondshift/repositories/management/commands/prune_source_cache.py +15 -0
- secondshift/repositories/migrations/0001_initial.py +45 -0
- secondshift/repositories/migrations/0002_repository_last_sync_error.py +18 -0
- secondshift/repositories/migrations/0003_repository_checked_sha.py +18 -0
- secondshift/repositories/migrations/0004_pull_request_settings.py +23 -0
- secondshift/repositories/migrations/0005_remove_repository_installation_id.py +17 -0
- secondshift/repositories/migrations/0006_allow_issues.py +18 -0
- secondshift/repositories/migrations/__init__.py +0 -0
- secondshift/repositories/mirrors.py +50 -0
- secondshift/repositories/models.py +104 -0
- secondshift/repositories/pull_requests.py +199 -0
- secondshift/repositories/schemas.py +102 -0
- secondshift/repositories/serialization.py +133 -0
- secondshift/repositories/skill_source.py +64 -0
- secondshift/repositories/snapshots.py +238 -0
- secondshift/repositories/sources.py +256 -0
- secondshift/repositories/status.py +67 -0
- secondshift/runner/README.md +65 -0
- secondshift/runner/__init__.py +1 -0
- secondshift/runner/agent_credentials.py +89 -0
- secondshift/runner/agent_image/Dockerfile +16 -0
- secondshift/runner/agent_image/agent.py +132 -0
- secondshift/runner/agent_image/claude.py +254 -0
- secondshift/runner/agent_image/codex.py +84 -0
- secondshift/runner/agent_image/outcomes.py +120 -0
- secondshift/runner/agent_image/subagent_hook.py +71 -0
- secondshift/runner/agent_protocol.py +41 -0
- secondshift/runner/agents.py +254 -0
- secondshift/runner/api.py +123 -0
- secondshift/runner/apps.py +6 -0
- secondshift/runner/client.py +168 -0
- secondshift/runner/collection.py +276 -0
- secondshift/runner/dao.py +64 -0
- secondshift/runner/dev.py +19 -0
- secondshift/runner/images.py +57 -0
- secondshift/runner/migrations/0001_initial.py +48 -0
- secondshift/runner/migrations/0002_session_source_sha256.py +18 -0
- secondshift/runner/migrations/0003_session_collect_changes.py +18 -0
- secondshift/runner/migrations/0004_session_history_sha256.py +18 -0
- secondshift/runner/migrations/0005_require_source_sha256.py +18 -0
- secondshift/runner/migrations/0006_session_skill_sha256.py +18 -0
- secondshift/runner/migrations/__init__.py +0 -0
- secondshift/runner/models.py +53 -0
- secondshift/runner/protocol.py +97 -0
- secondshift/runner/schemas.py +36 -0
- secondshift/runner/services.py +465 -0
- secondshift/runner/sources.py +130 -0
- secondshift/runner/urls.py +7 -0
- secondshift/runs/__init__.py +0 -0
- secondshift/runs/access.py +13 -0
- secondshift/runs/admin.py +89 -0
- secondshift/runs/api.py +211 -0
- secondshift/runs/apps.py +11 -0
- secondshift/runs/artifacts.py +261 -0
- secondshift/runs/dao.py +640 -0
- secondshift/runs/deliveries.py +80 -0
- secondshift/runs/execution/__init__.py +0 -0
- secondshift/runs/execution/access_needs.py +89 -0
- secondshift/runs/execution/agent.py +91 -0
- secondshift/runs/execution/agent_broker.py +198 -0
- secondshift/runs/execution/budgets.py +177 -0
- secondshift/runs/execution/cli_agent.py +167 -0
- secondshift/runs/execution/consent.py +58 -0
- secondshift/runs/execution/deadlines.py +47 -0
- secondshift/runs/execution/external_tools.py +84 -0
- secondshift/runs/execution/issues.py +99 -0
- secondshift/runs/execution/model_gateway.py +90 -0
- secondshift/runs/execution/payload.py +63 -0
- secondshift/runs/execution/result.py +59 -0
- secondshift/runs/execution/run.py +345 -0
- secondshift/runs/execution/subagents.py +246 -0
- secondshift/runs/execution/tool_broker.py +123 -0
- secondshift/runs/execution/workspace_tools.py +248 -0
- secondshift/runs/history.py +182 -0
- secondshift/runs/inputs.py +49 -0
- secondshift/runs/issue_delivery.py +144 -0
- secondshift/runs/management/__init__.py +0 -0
- secondshift/runs/management/commands/__init__.py +0 -0
- secondshift/runs/management/commands/prune_runs.py +12 -0
- secondshift/runs/management/commands/run_dbos_worker.py +129 -0
- secondshift/runs/migration_helpers.py +313 -0
- secondshift/runs/migrations/0001_initial.py +69 -0
- secondshift/runs/migrations/0002_run_started_by.py +18 -0
- secondshift/runs/migrations/0003_agentlease.py +25 -0
- secondshift/runs/migrations/0004_task_assignment.py +39 -0
- secondshift/runs/migrations/0005_backfill_tasks.py +19 -0
- secondshift/runs/migrations/0006_standalone_tasks.py +50 -0
- secondshift/runs/migrations/0007_run_delivery.py +35 -0
- secondshift/runs/migrations/0008_run_outputs.py +18 -0
- secondshift/runs/migrations/0009_pull_request_delivery.py +28 -0
- secondshift/runs/migrations/0010_run_issue.py +32 -0
- secondshift/runs/migrations/0011_issues_delivery.py +23 -0
- secondshift/runs/migrations/0012_alter_rundelivery_status.py +18 -0
- secondshift/runs/migrations/0013_release_0_1_0b11.py +113 -0
- secondshift/runs/migrations/0014_run_access_need.py +30 -0
- secondshift/runs/migrations/__init__.py +0 -0
- secondshift/runs/models.py +340 -0
- secondshift/runs/orchestration/__init__.py +0 -0
- secondshift/runs/orchestration/admission.py +15 -0
- secondshift/runs/orchestration/dbos_runtime.py +43 -0
- secondshift/runs/orchestration/dispatch.py +130 -0
- secondshift/runs/orchestration/periodic.py +39 -0
- secondshift/runs/orchestration/reconciliation.py +98 -0
- secondshift/runs/orchestration/schedules.py +51 -0
- secondshift/runs/orchestration/workflows.py +480 -0
- secondshift/runs/outputs.py +52 -0
- secondshift/runs/publishing.py +162 -0
- secondshift/runs/queue.py +26 -0
- secondshift/runs/reporting/__init__.py +0 -0
- secondshift/runs/reporting/collection.py +26 -0
- secondshift/runs/reporting/connection_usage.py +45 -0
- secondshift/runs/reporting/failures.py +150 -0
- secondshift/runs/reporting/health.py +158 -0
- secondshift/runs/reporting/notifications.py +258 -0
- secondshift/runs/reporting/queries.py +60 -0
- secondshift/runs/retention.py +35 -0
- secondshift/runs/schemas.py +192 -0
- secondshift/runs/serialization.py +245 -0
- secondshift/runs/services.py +176 -0
- secondshift/runs/signals.py +25 -0
- secondshift/runs/submissions.py +236 -0
- secondshift/runs/transitions.py +84 -0
- secondshift/templates/account/password_reset_help_text.html +1 -0
- secondshift/templates/admin/base_site.html +24 -0
- secondshift/templates/allauth/elements/provider.html +9 -0
- secondshift/templates/allauth/layouts/base.html +67 -0
- secondshift/templates/socialaccount/snippets/login.html +9 -0
- secondshift/templates/workspaces/invite_unusable.html +10 -0
- secondshift/triggers/__init__.py +0 -0
- secondshift/triggers/admin.py +19 -0
- secondshift/triggers/api.py +116 -0
- secondshift/triggers/apps.py +6 -0
- secondshift/triggers/dao.py +66 -0
- secondshift/triggers/filters.py +48 -0
- secondshift/triggers/migrations/0001_initial.py +55 -0
- secondshift/triggers/migrations/0002_ignored_events_and_hourly_limit.py +23 -0
- secondshift/triggers/migrations/__init__.py +0 -0
- secondshift/triggers/models.py +83 -0
- secondshift/triggers/schemas.py +58 -0
- secondshift/triggers/serialization.py +91 -0
- secondshift/triggers/services.py +246 -0
- secondshift/triggers/verification.py +84 -0
- secondshift/web/assets/index-Bwy0hvk_.js +2419 -0
- secondshift/web/assets/index-D27gB9_L.css +1 -0
- secondshift/web/favicon.svg +19 -0
- secondshift/web/index.html +16 -0
- secondshift/web/secondshift-mark.svg +16 -0
- secondshift/workspaces/__init__.py +0 -0
- secondshift/workspaces/access.py +181 -0
- secondshift/workspaces/admin.py +37 -0
- secondshift/workspaces/api.py +264 -0
- secondshift/workspaces/apps.py +6 -0
- secondshift/workspaces/context_processors.py +11 -0
- secondshift/workspaces/dao.py +281 -0
- secondshift/workspaces/invites.py +150 -0
- secondshift/workspaces/limits.py +197 -0
- secondshift/workspaces/management/__init__.py +0 -0
- secondshift/workspaces/management/commands/__init__.py +0 -0
- secondshift/workspaces/management/commands/create_workspace.py +39 -0
- secondshift/workspaces/migrations/0001_initial.py +67 -0
- secondshift/workspaces/migrations/0002_api_token.py +34 -0
- secondshift/workspaces/migrations/0003_execution_default.py +29 -0
- secondshift/workspaces/migrations/0004_copy_default_model_key.py +26 -0
- secondshift/workspaces/migrations/__init__.py +0 -0
- secondshift/workspaces/models.py +169 -0
- secondshift/workspaces/owners.py +58 -0
- secondshift/workspaces/schemas.py +149 -0
- secondshift/workspaces/serialization.py +138 -0
- secondshift/workspaces/server_settings.py +140 -0
- secondshift/workspaces/services.py +94 -0
- secondshift/workspaces/tokens.py +108 -0
- secondshift/workspaces/views.py +32 -0
- secondshift-0.1.0b12.dist-info/METADATA +270 -0
- secondshift-0.1.0b12.dist-info/RECORD +371 -0
- secondshift-0.1.0b12.dist-info/WHEEL +4 -0
- secondshift-0.1.0b12.dist-info/entry_points.txt +2 -0
- secondshift-0.1.0b12.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""One-time sign-in links printed by `secondshift up` and `secondshift login`.
|
|
2
|
+
|
|
3
|
+
A link is signed with SECRET_KEY and holds the user's last sign-in time.
|
|
4
|
+
Signing in changes that time, so each link works once. Links expire after
|
|
5
|
+
SIGN_IN_LINK_SECONDS. Only someone who can read the terminal gets one.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django.contrib.auth import login
|
|
9
|
+
from django.contrib.auth.models import AbstractUser
|
|
10
|
+
from django.core import signing
|
|
11
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|
12
|
+
from django.urls import reverse
|
|
13
|
+
from django.views.decorators.cache import never_cache
|
|
14
|
+
from django.views.decorators.http import require_GET
|
|
15
|
+
|
|
16
|
+
from secondshift.workspaces.dao import active_user
|
|
17
|
+
|
|
18
|
+
SIGN_IN_LINK_SECONDS = 15 * 60
|
|
19
|
+
SIGN_IN_SALT = "secondshift.core.signin"
|
|
20
|
+
MODEL_BACKEND = "django.contrib.auth.backends.ModelBackend"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def last_seen(user: AbstractUser) -> str:
|
|
24
|
+
return user.last_login.isoformat() if user.last_login else ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_token(user: AbstractUser) -> str:
|
|
28
|
+
return signing.dumps({"user": user.pk, "seen": last_seen(user)}, salt=SIGN_IN_SALT)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def user_for_token(token: str) -> AbstractUser | None:
|
|
32
|
+
try:
|
|
33
|
+
data = signing.loads(token, salt=SIGN_IN_SALT, max_age=SIGN_IN_LINK_SECONDS)
|
|
34
|
+
except signing.BadSignature:
|
|
35
|
+
return None
|
|
36
|
+
user = active_user(data.get("user"))
|
|
37
|
+
if user is None or last_seen(user) != data.get("seen"):
|
|
38
|
+
return None
|
|
39
|
+
return user
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def sign_in_url(base_url: str, user: AbstractUser) -> str:
|
|
43
|
+
return base_url.rstrip("/") + reverse("sign-in", args=[make_token(user)])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@never_cache
|
|
47
|
+
@require_GET
|
|
48
|
+
def sign_in(request: HttpRequest, token: str) -> HttpResponse:
|
|
49
|
+
user = user_for_token(token)
|
|
50
|
+
if user is None:
|
|
51
|
+
return HttpResponse(
|
|
52
|
+
"This sign-in link has expired or was already used. "
|
|
53
|
+
"Run `secondshift login` for a new one.",
|
|
54
|
+
status=400,
|
|
55
|
+
content_type="text/plain",
|
|
56
|
+
)
|
|
57
|
+
login(request, user, backend=MODEL_BACKEND)
|
|
58
|
+
return HttpResponseRedirect("/")
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/* SecondShift theme for the sign-in, sign-up and account pages that
|
|
2
|
+
allauth renders. Its elements are plain HTML, so they are styled by tag.
|
|
3
|
+
The tokens and components copy the dashboard (dashboard/src/styles.css):
|
|
4
|
+
neutral greys, IBM Plex, and color only where it carries meaning. */
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: dark;
|
|
7
|
+
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
|
|
8
|
+
--font-mono: 'IBM Plex Mono', ui-monospace, monospace;
|
|
9
|
+
--night-950: #0d0d0d;
|
|
10
|
+
--night-900: #131313;
|
|
11
|
+
--ink: #ededed;
|
|
12
|
+
--muted: #a3a3a3;
|
|
13
|
+
--subtle: #737373;
|
|
14
|
+
--focus: #a3a3a3;
|
|
15
|
+
--card-border: #232323;
|
|
16
|
+
--field: #181818;
|
|
17
|
+
--field-border: #262626;
|
|
18
|
+
--divider: #1c1c1c;
|
|
19
|
+
--mint: #71dbb6;
|
|
20
|
+
--danger: #ff778b;
|
|
21
|
+
--white-3: rgba(255,255,255,.03);
|
|
22
|
+
--white-6: rgba(255,255,255,.06);
|
|
23
|
+
--white-8: rgba(255,255,255,.08);
|
|
24
|
+
--border: var(--white-8);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
|
|
29
|
+
body {
|
|
30
|
+
margin: 0;
|
|
31
|
+
min-height: 100vh;
|
|
32
|
+
color: var(--ink);
|
|
33
|
+
background: var(--night-900);
|
|
34
|
+
font: 14px/1.5 var(--font-sans);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* The form on the left, example shifts on the right. */
|
|
38
|
+
.account-page {
|
|
39
|
+
display: grid;
|
|
40
|
+
grid-template-columns: minmax(380px, 0.82fr) minmax(420px, 1.18fr);
|
|
41
|
+
min-height: 100vh;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.account-card {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-width: 520px;
|
|
50
|
+
margin: 0 auto;
|
|
51
|
+
padding: 48px clamp(28px, 6vw, 72px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.account-brand { display: inline-flex; align-items: center; gap: 10px; margin-bottom: 8px; color: var(--ink); font-size: 16px; font-weight: 600; text-decoration: none; }
|
|
55
|
+
.account-eyebrow { margin-bottom: 32px; color: var(--subtle); font-size: 13px; }
|
|
56
|
+
|
|
57
|
+
h1, h2 { margin: 0 0 8px; font-weight: 600; }
|
|
58
|
+
h1 { font-size: 28px; letter-spacing: -0.02em; }
|
|
59
|
+
h2 { font-size: 15px; }
|
|
60
|
+
p { margin: 0 0 14px; color: var(--muted); }
|
|
61
|
+
a { color: var(--ink); text-decoration-color: var(--subtle); text-underline-offset: 3px; }
|
|
62
|
+
a:hover { text-decoration-color: var(--ink); }
|
|
63
|
+
hr { margin: 22px 0; border: 0; border-top: 1px solid var(--border); }
|
|
64
|
+
|
|
65
|
+
form { display: grid; gap: 4px; margin: 18px 0 0; }
|
|
66
|
+
form p { display: grid; gap: 6px; margin: 0 0 10px; color: var(--ink); }
|
|
67
|
+
label { color: var(--muted); font-size: 12px; font-weight: 500; }
|
|
68
|
+
input[type="email"], input[type="password"], input[type="text"] {
|
|
69
|
+
width: 100%;
|
|
70
|
+
min-height: 40px;
|
|
71
|
+
padding: 8px 12px;
|
|
72
|
+
border: 1px solid var(--field-border);
|
|
73
|
+
border-radius: 8px;
|
|
74
|
+
color: var(--ink);
|
|
75
|
+
background: var(--field);
|
|
76
|
+
font: inherit;
|
|
77
|
+
}
|
|
78
|
+
input::placeholder { color: var(--subtle); }
|
|
79
|
+
input:focus { border-color: var(--focus); outline: none; }
|
|
80
|
+
input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
81
|
+
form p > span, form p ul { color: var(--subtle); font-size: 12px; }
|
|
82
|
+
form p > .helptext a { font-size: 12px; }
|
|
83
|
+
/* Password tips. Browsers move a list out of its paragraph into the form. */
|
|
84
|
+
form > ul { margin: -4px 0 10px; padding-left: 16px; color: var(--subtle); font-size: 12px; }
|
|
85
|
+
form p:has(> input[type="checkbox"]) { display: flex; align-items: center; gap: 8px; }
|
|
86
|
+
input[type="checkbox"] { order: -1; width: 16px; height: 16px; margin: 0; accent-color: var(--ink); }
|
|
87
|
+
form p ul { margin: 0; padding-left: 16px; }
|
|
88
|
+
.errorlist { margin: 0 0 6px; padding: 0; list-style: none; color: var(--danger); font-size: 12px; }
|
|
89
|
+
|
|
90
|
+
/* The dashboard's primary action: a light button on the dark page. */
|
|
91
|
+
button {
|
|
92
|
+
min-height: 40px;
|
|
93
|
+
margin-top: 6px;
|
|
94
|
+
padding: 0 16px;
|
|
95
|
+
border: 1px solid var(--ink);
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
color: var(--night-950);
|
|
98
|
+
background: var(--ink);
|
|
99
|
+
font: 600 13px var(--font-sans);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
button:hover { border-color: #fff; background: #fff; }
|
|
103
|
+
button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
104
|
+
|
|
105
|
+
/* Third-party sign-in, e.g. GitHub. Raised like the dashboard's secondary buttons. */
|
|
106
|
+
.account-card ul:has(> li > .account-provider) { display: grid; gap: 8px; margin: 10px 0 0; padding: 0; list-style: none; }
|
|
107
|
+
.account-provider {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
gap: 8px;
|
|
112
|
+
padding: 10px 12px;
|
|
113
|
+
border: 1px solid #2e2e2e;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
color: var(--ink);
|
|
116
|
+
background: #242424;
|
|
117
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.4);
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
text-decoration: none;
|
|
120
|
+
}
|
|
121
|
+
.account-provider:hover { background: #2a2a2a; }
|
|
122
|
+
.account-provider:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
123
|
+
|
|
124
|
+
/* Notices use the dashboard callout: a faint border in the notice's color. */
|
|
125
|
+
.account-messages, .account-invite { margin: 0 0 18px; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: rgba(255,255,255,.025); color: var(--ink); list-style: none; font-size: 13px; }
|
|
126
|
+
.account-messages { border-color: color-mix(in srgb, var(--mint) 35%, transparent); }
|
|
127
|
+
.account-back { display: inline-block; margin-top: 18px; font-size: 13px; }
|
|
128
|
+
|
|
129
|
+
/* Example shifts, drawn as a schedule in the dashboard's own row style. */
|
|
130
|
+
.account-atmosphere {
|
|
131
|
+
display: grid;
|
|
132
|
+
place-items: center;
|
|
133
|
+
padding: 48px;
|
|
134
|
+
border-left: 1px solid var(--divider);
|
|
135
|
+
background: var(--night-950);
|
|
136
|
+
}
|
|
137
|
+
.shift-schedule { width: min(460px, 100%); margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--divider); }
|
|
138
|
+
.shift-preview { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--divider); }
|
|
139
|
+
.shift-preview > time { padding-top: 2px; color: var(--subtle); font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
140
|
+
.shift-preview strong { display: block; margin-bottom: 4px; font-size: 14px; font-weight: 600; }
|
|
141
|
+
.shift-preview p { margin: 0; color: var(--muted); font-size: 13px; }
|
|
142
|
+
|
|
143
|
+
@media (max-width: 900px) {
|
|
144
|
+
.account-page { grid-template-columns: 1fr; }
|
|
145
|
+
.account-card { padding: 40px 16px; }
|
|
146
|
+
.account-atmosphere { display: none; }
|
|
147
|
+
/* 16px stops iPhone Safari zooming into the field. */
|
|
148
|
+
input[type="email"], input[type="password"], input[type="text"] { font-size: 16px; }
|
|
149
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* SecondShift theme for Django admin, using the dashboard's greys and Geist
|
|
2
|
+
(dashboard/src/styles.css). The dashboard is dark-only, so the palette is
|
|
3
|
+
applied regardless of the admin's light/dark/auto setting. */
|
|
4
|
+
:root,
|
|
5
|
+
html[data-theme] {
|
|
6
|
+
--primary: #1f1f1f;
|
|
7
|
+
--secondary: #191919;
|
|
8
|
+
--accent: #ededed;
|
|
9
|
+
--primary-fg: #ededed;
|
|
10
|
+
|
|
11
|
+
--body-fg: #c4c4c4;
|
|
12
|
+
--body-bg: #131313;
|
|
13
|
+
--body-quiet-color: #737373;
|
|
14
|
+
--body-medium-color: #a3a3a3;
|
|
15
|
+
--body-loud-color: #ededed;
|
|
16
|
+
|
|
17
|
+
--header-color: #ededed;
|
|
18
|
+
--header-branding-color: #ededed;
|
|
19
|
+
--header-bg: #0d0d0d;
|
|
20
|
+
--header-link-color: #ededed;
|
|
21
|
+
|
|
22
|
+
--breadcrumbs-fg: #737373;
|
|
23
|
+
--breadcrumbs-link-fg: #a3a3a3;
|
|
24
|
+
--breadcrumbs-bg: #131313;
|
|
25
|
+
|
|
26
|
+
--link-fg: #ededed;
|
|
27
|
+
--link-hover-color: #ffffff;
|
|
28
|
+
--link-selected-fg: #ededed;
|
|
29
|
+
|
|
30
|
+
--hairline-color: rgba(255,255,255,.06);
|
|
31
|
+
--border-color: rgba(255,255,255,.08);
|
|
32
|
+
|
|
33
|
+
--error-fg: #ff778b;
|
|
34
|
+
|
|
35
|
+
/* Messages sit on the card surface. Their color is the left edge below. */
|
|
36
|
+
--message-debug-bg: #191919;
|
|
37
|
+
--message-debug-icon: url(../admin/img/icon-debug-dark.svg);
|
|
38
|
+
--message-info-bg: #191919;
|
|
39
|
+
--message-info-icon: url(../admin/img/icon-info-dark.svg);
|
|
40
|
+
--message-success-bg: #191919;
|
|
41
|
+
--message-success-icon: url(../admin/img/icon-yes-dark.svg);
|
|
42
|
+
--message-warning-bg: #191919;
|
|
43
|
+
--message-warning-icon: url(../admin/img/icon-alert-dark.svg);
|
|
44
|
+
--message-error-bg: #191919;
|
|
45
|
+
--message-error-icon: url(../admin/img/icon-no-dark.svg);
|
|
46
|
+
|
|
47
|
+
--darkened-bg: #0d0d0d;
|
|
48
|
+
--selected-bg: #1f1f1f;
|
|
49
|
+
--selected-row: rgba(255,255,255,.06);
|
|
50
|
+
|
|
51
|
+
--button-fg: #ededed;
|
|
52
|
+
--button-bg: #262626;
|
|
53
|
+
--button-hover-bg: #313131;
|
|
54
|
+
--default-button-bg: #ededed;
|
|
55
|
+
--default-button-hover-bg: #ffffff;
|
|
56
|
+
--close-button-bg: #262626;
|
|
57
|
+
--close-button-hover-bg: #313131;
|
|
58
|
+
--delete-button-bg: #ff778b;
|
|
59
|
+
--delete-button-hover-bg: #ff8fa0;
|
|
60
|
+
|
|
61
|
+
--object-tools-fg: var(--button-fg);
|
|
62
|
+
--object-tools-bg: var(--close-button-bg);
|
|
63
|
+
--object-tools-hover-bg: var(--close-button-hover-bg);
|
|
64
|
+
|
|
65
|
+
--font-family-primary: 'Geist', system-ui, sans-serif;
|
|
66
|
+
--font-family-monospace: 'Geist Mono', ui-monospace, monospace;
|
|
67
|
+
|
|
68
|
+
color-scheme: dark;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Only one theme, so the toggle has nothing to do. */
|
|
72
|
+
.theme-toggle { display: none; }
|
|
73
|
+
|
|
74
|
+
body { background: var(--body-bg); }
|
|
75
|
+
|
|
76
|
+
h1, h2, #site-name, .module caption { font-weight: 600; letter-spacing: -.01em; }
|
|
77
|
+
|
|
78
|
+
#header { border-bottom: 1px solid #1c1c1c; }
|
|
79
|
+
.ss-brand { display: inline-flex; align-items: center; gap: 10px; }
|
|
80
|
+
.ss-brand img { width: 32px; height: 32px; }
|
|
81
|
+
#site-name .ss-brand span { font-weight: 600; color: var(--header-branding-color); }
|
|
82
|
+
#user-tools a { border-bottom-color: rgba(255,255,255,.18); }
|
|
83
|
+
|
|
84
|
+
div.breadcrumbs { border-bottom: 1px solid var(--hairline-color); }
|
|
85
|
+
|
|
86
|
+
/* Modules are dashboard cards. */
|
|
87
|
+
#content-main .module,
|
|
88
|
+
#content-related .module,
|
|
89
|
+
.dashboard .module,
|
|
90
|
+
fieldset.module {
|
|
91
|
+
border: 1px solid #232323;
|
|
92
|
+
border-radius: 16px;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
background: linear-gradient(180deg, #191919, #161616);
|
|
95
|
+
}
|
|
96
|
+
.module h2, .module caption, .inline-group h2 {
|
|
97
|
+
background: transparent;
|
|
98
|
+
border-bottom: 1px solid var(--hairline-color);
|
|
99
|
+
color: #a3a3a3;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
input[type=text], input[type=password], input[type=email], input[type=url],
|
|
103
|
+
input[type=number], input[type=tel], input[type=search], textarea, select, .vTextField {
|
|
104
|
+
background: #181818;
|
|
105
|
+
color: #ededed;
|
|
106
|
+
border: 1px solid #262626;
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
}
|
|
109
|
+
input:focus, textarea:focus, select:focus { border-color: #a3a3a3; outline: none; }
|
|
110
|
+
input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid #a3a3a3; outline-offset: 2px; }
|
|
111
|
+
|
|
112
|
+
.button, input[type=submit], input[type=button], .submit-row input, a.button, button { border-radius: 8px; }
|
|
113
|
+
/* Save is the dashboard's light primary action, so its text is dark. */
|
|
114
|
+
.button.default, input[type=submit].default, .submit-row input.default { color: #0d0d0d; font-weight: 600; }
|
|
115
|
+
.submit-row a.deletelink, .delete-confirmation form input[type=submit] { color: #2a0a12; }
|
|
116
|
+
|
|
117
|
+
/* The dashboard callout: the card surface with a colored left edge. */
|
|
118
|
+
ul.messagelist li { border: 1px solid var(--border-color); border-left: 2px solid #737373; border-radius: 8px; color: #ededed; }
|
|
119
|
+
ul.messagelist li.success { border-left-color: #71dbb6; }
|
|
120
|
+
ul.messagelist li.warning { border-left-color: #ffb454; }
|
|
121
|
+
ul.messagelist li.error { border-left-color: #ff778b; }
|
|
122
|
+
|
|
123
|
+
/* Login page: a centered card like the dashboard sign-in. */
|
|
124
|
+
body.login #container {
|
|
125
|
+
border: 1px solid #232323;
|
|
126
|
+
border-radius: 16px;
|
|
127
|
+
background: linear-gradient(180deg, #191919, #161616);
|
|
128
|
+
box-shadow: 0 24px 64px rgba(0,0,0,.5);
|
|
129
|
+
}
|
|
130
|
+
body.login #header { padding: 24px 16px 12px; background: transparent; border-bottom: 0; }
|
|
131
|
+
body.login #site-name .ss-brand { flex-direction: column; gap: 12px; }
|
|
132
|
+
body.login .ss-brand img { width: 56px; height: 56px; }
|
|
133
|
+
body.login #site-name .ss-brand span { font-size: 1.35rem; }
|
|
134
|
+
body.login .submit-row input { width: 100%; padding: 11px; color: #0d0d0d; background: #ededed; font-weight: 600; }
|
|
135
|
+
body.login .submit-row input:hover { background: #fff; }
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from django import template
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
from django.utils.html import format_html
|
|
5
|
+
from django.utils.safestring import SafeString
|
|
6
|
+
from django.utils.translation import gettext as _
|
|
7
|
+
|
|
8
|
+
register = template.Library()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register.simple_tag
|
|
12
|
+
def password_reset_link() -> SafeString | str:
|
|
13
|
+
"""The sign-in form's reset link, only when the reset email can arrive."""
|
|
14
|
+
if not settings.EMAIL_DELIVERED:
|
|
15
|
+
return ""
|
|
16
|
+
return format_html(
|
|
17
|
+
'<a href="{}">{}</a>',
|
|
18
|
+
reverse("account_reset_password"),
|
|
19
|
+
_("Forgot your password?"),
|
|
20
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Styles for the messages the local CLI prints itself.
|
|
2
|
+
|
|
3
|
+
Click drops the styles when output is not a terminal.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def heading(text: str) -> None:
|
|
10
|
+
click.secho(text, bold=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def done(text: str, detail: str = "") -> None:
|
|
14
|
+
"""A finished step, with an optional dimmed detail."""
|
|
15
|
+
line = f"{click.style('✓', fg='green', bold=True)} {text}"
|
|
16
|
+
if detail:
|
|
17
|
+
line += f" {click.style(detail, dim=True)}"
|
|
18
|
+
click.echo(line)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def note(text: str) -> None:
|
|
22
|
+
"""Progress and hints that matter less than the steps."""
|
|
23
|
+
click.secho(text, dim=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def link(url: str) -> str:
|
|
27
|
+
return click.style(url, fg="cyan", underline=True)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from allauth.account.views import password_reset as allauth_password_reset
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.http import Http404, HttpRequest, HttpResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def password_reset(request: HttpRequest) -> HttpResponse:
|
|
7
|
+
"""allauth's reset page, unless the reset email could not reach anyone."""
|
|
8
|
+
if not settings.EMAIL_DELIVERED:
|
|
9
|
+
raise Http404("Password resets need email. Set EMAIL_URL.")
|
|
10
|
+
return allauth_password_reset(request)
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Queries and writes on gateway tokens and model usage."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from django.db.models import Count, F, Max, Q, QuerySet, Sum
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
|
|
10
|
+
from secondshift.connections.models import Connection
|
|
11
|
+
from secondshift.gateway.models import ModelUsage, RunToken
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_run_token(
|
|
15
|
+
run_id: int,
|
|
16
|
+
connection: Connection,
|
|
17
|
+
token_hash: str,
|
|
18
|
+
expires_at: datetime,
|
|
19
|
+
budget_tokens: int,
|
|
20
|
+
) -> RunToken:
|
|
21
|
+
"""Store a run's gateway token by its hash."""
|
|
22
|
+
return RunToken.objects.create(
|
|
23
|
+
run_id=run_id,
|
|
24
|
+
connection=connection,
|
|
25
|
+
token_hash=token_hash,
|
|
26
|
+
expires_at=expires_at,
|
|
27
|
+
budget_tokens=budget_tokens,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def revoke_run_tokens(run_id: int) -> int:
|
|
32
|
+
"""Revoke the run's tokens that are still live. Returns how many."""
|
|
33
|
+
return RunToken.objects.filter(run_id=run_id, revoked_at__isnull=True).update(
|
|
34
|
+
revoked_at=timezone.now()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def active_tokens(token_hash: str) -> QuerySet[RunToken]:
|
|
39
|
+
"""Live tokens with this hash, for a connected model.
|
|
40
|
+
|
|
41
|
+
A run's tokens are revoked when it finishes, in the same transaction.
|
|
42
|
+
"""
|
|
43
|
+
return RunToken.objects.select_related(
|
|
44
|
+
"connection", "run__job", "run__task__repository"
|
|
45
|
+
).filter(
|
|
46
|
+
token_hash=token_hash,
|
|
47
|
+
revoked_at__isnull=True,
|
|
48
|
+
expires_at__gt=timezone.now(),
|
|
49
|
+
connection__status=Connection.Status.CONNECTED,
|
|
50
|
+
connection__kind=Connection.Kind.MODEL,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def active_token(token_hash: str) -> RunToken | None:
|
|
55
|
+
return active_tokens(token_hash).first()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def aactive_token(token_hash: str) -> RunToken | None:
|
|
59
|
+
return await active_tokens(token_hash).afirst()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def acreate_model_usage(
|
|
63
|
+
run_token: RunToken,
|
|
64
|
+
provider: str,
|
|
65
|
+
model: str,
|
|
66
|
+
status_code: int,
|
|
67
|
+
tokens: dict[str, int],
|
|
68
|
+
cost_usd: Decimal | None,
|
|
69
|
+
) -> ModelUsage:
|
|
70
|
+
"""Record one model request made with this token.
|
|
71
|
+
|
|
72
|
+
`tokens` holds input_tokens, output_tokens, cache_read_tokens and
|
|
73
|
+
cache_write_tokens.
|
|
74
|
+
"""
|
|
75
|
+
return await ModelUsage.objects.acreate(
|
|
76
|
+
run_id=run_token.run_id,
|
|
77
|
+
connection_id=run_token.connection_id,
|
|
78
|
+
provider=provider,
|
|
79
|
+
model=model,
|
|
80
|
+
status_code=status_code,
|
|
81
|
+
cost_usd=cost_usd,
|
|
82
|
+
**tokens,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def aadd_used_tokens(run_token: RunToken, tokens: int) -> None:
|
|
87
|
+
"""Count tokens against the token's budget."""
|
|
88
|
+
await RunToken.objects.filter(pk=run_token.pk).aupdate(
|
|
89
|
+
used_tokens=F("used_tokens") + tokens
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def usage_by_connection(
|
|
94
|
+
workspace_id: int, since: datetime
|
|
95
|
+
) -> dict[int, dict[str, Any]]:
|
|
96
|
+
"""Per connection: runs, spend and unpriced requests since then, and last use."""
|
|
97
|
+
recent = Q(created_at__gte=since)
|
|
98
|
+
return {
|
|
99
|
+
row["connection_id"]: row
|
|
100
|
+
for row in ModelUsage.objects.filter(connection__workspace_id=workspace_id)
|
|
101
|
+
.values("connection_id")
|
|
102
|
+
.annotate(
|
|
103
|
+
runs=Count("run_id", distinct=True, filter=recent),
|
|
104
|
+
spend=Sum("cost_usd", filter=recent),
|
|
105
|
+
unpriced=Count("pk", filter=recent & Q(cost_usd__isnull=True)),
|
|
106
|
+
last=Max("created_at"),
|
|
107
|
+
)
|
|
108
|
+
.order_by()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def spend_totals(runs: Q, start: datetime, previous_start: datetime) -> dict[str, Any]:
|
|
113
|
+
"""Spend since `start`, spend in the window before it, and unpriced requests.
|
|
114
|
+
|
|
115
|
+
`runs` picks the usage by its run, with lookups that start at `run__`.
|
|
116
|
+
"""
|
|
117
|
+
return ModelUsage.objects.filter(runs, created_at__gte=previous_start).aggregate(
|
|
118
|
+
spend=Sum("cost_usd", filter=Q(created_at__gte=start)),
|
|
119
|
+
previous=Sum("cost_usd", filter=Q(created_at__lt=start)),
|
|
120
|
+
unpriced=Count("pk", filter=Q(created_at__gte=start, cost_usd__isnull=True)),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def spend_per_job(workspace_id: int, since: datetime) -> list[dict[str, Any]]:
|
|
125
|
+
"""Per job: spend, runs and unpriced requests since then."""
|
|
126
|
+
return list(
|
|
127
|
+
ModelUsage.objects.filter(
|
|
128
|
+
run__job__workspace_id=workspace_id, created_at__gte=since
|
|
129
|
+
)
|
|
130
|
+
.values("run__job_id")
|
|
131
|
+
.annotate(
|
|
132
|
+
spend=Sum("cost_usd"),
|
|
133
|
+
runs=Count("run_id", distinct=True),
|
|
134
|
+
unpriced=Count("pk", filter=Q(cost_usd__isnull=True)),
|
|
135
|
+
)
|
|
136
|
+
.order_by()
|
|
137
|
+
)
|