Spaces:
Running
Running
File size: 9,577 Bytes
d69ace5 4b09d2d d69ace5 4b09d2d bfb6467 4b09d2d bfb6467 5ad96fc bfb6467 d69ace5 4b09d2d d69ace5 4b09d2d d69ace5 4b09d2d d69ace5 4b09d2d 1a28176 d69ace5 a685524 f341328 d69ace5 4b09d2d d69ace5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | """ORM models.
IDs are UUID strings (portable across SQLite/Postgres). JSON-ish payloads
(column lists, construct items, job metadata/summaries) are stored as JSON
text - they are read-mostly blobs, not queried relationally.
"""
import uuid
from datetime import datetime, timezone
from sqlalchemy import Boolean, Float, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from .db import Base
def _uuid() -> str:
return uuid.uuid4().hex
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
name: Mapped[str] = mapped_column(String(120), default="")
password_hash: Mapped[str] = mapped_column(Text) # scrypt$salt$digest (auth.py)
role: Mapped[str] = mapped_column(String(16), default="external")
# external | lab | maintainer | pi (auth.ROLES; legacy "member" reads as
# external). lab+ = unlimited saved runs; maintainer/pi also get /admin.
# Staff roles are grantable only by ADMIN_EMAILS env-allowlisted admins
# (admin.py guards), so the API cannot self-escalate; the env allowlist
# remains the bootstrap and break-glass admin path.
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class RoleAssignment(Base):
"""Pre-provisioned access: a role bound to an email BEFORE the account
exists (PI request 2026-07-22 - e.g. an external collaborator who must
land with full credentials on first sign-in, Google or password).
Unlike invite links (bearer tokens, external/lab only), assignments are
email-bound and may carry staff roles - so creating one for pi/maintainer
requires escalation rights. Claimed rows are kept as history."""
__tablename__ = "role_assignments"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
role: Mapped[str] = mapped_column(String(16))
assigned_by: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
claimed_at: Mapped[str] = mapped_column(String(32), default="") # "" = pending
class Invite(Base):
"""Invite links, stateful so they can be listed, revoked, and traced.
The signed token (auth.py) still proves authenticity, but redemption
requires this row to be live: not revoked, not expired. Revocation is
soft (revoked_at) so history and redemptions stay visible. Redemptions
are a read-mostly JSON list [{email, at}], per the repo's JSON-blob
convention."""
__tablename__ = "invites"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
role: Mapped[str] = mapped_column(String(16))
token: Mapped[str] = mapped_column(Text) # stored so admins can re-copy the link
created_by: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
expires_at: Mapped[str] = mapped_column(String(32)) # ISO date, inclusive
revoked_at: Mapped[str] = mapped_column(String(32), default="") # "" = active
redemptions_json: Mapped[str] = mapped_column(Text, default="[]")
class AdminAudit(Base):
"""Append-only trail of admin actions (who did what to whom, when).
What makes multiple admins trustworthy: role grants, password resets,
deletions, invites, requeues, and verification changes all land here.
Never updated or deleted from the app."""
__tablename__ = "admin_audit"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
at: Mapped[str] = mapped_column(String(32), default=_now)
actor_email: Mapped[str] = mapped_column(String(255))
action: Mapped[str] = mapped_column(String(40)) # e.g. set_role, invite_created
target: Mapped[str] = mapped_column(String(300), default="") # email/name acted on
detail: Mapped[str] = mapped_column(Text, default="")
class Project(Base):
__tablename__ = "projects"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str] = mapped_column(Text, default="")
archived: Mapped[bool] = mapped_column(Boolean, default=False)
owner_user_id: Mapped[str] = mapped_column(String(32), default="") # "" = anonymous
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Corpus(Base):
__tablename__ = "corpora"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"))
filename: Mapped[str] = mapped_column(String(300))
path: Mapped[str] = mapped_column(Text)
n_rows: Mapped[int] = mapped_column()
columns_json: Mapped[str] = mapped_column(Text) # list[str]
parse_info_json: Mapped[str] = mapped_column(Text, default="{}") # how the file was parsed
suggested_text_column: Mapped[str] = mapped_column(String(200), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Construct(Base):
__tablename__ = "constructs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
name: Mapped[str] = mapped_column(String(200))
description: Mapped[str] = mapped_column(Text, default="")
reference: Mapped[str] = mapped_column(Text, default="") # citation
items_json: Mapped[str] = mapped_column(Text) # list[str]
reverse_flags_json: Mapped[str] = mapped_column(Text, default="[]") # list[bool], parallel to items
is_seed: Mapped[bool] = mapped_column(Boolean, default=False)
# Library identity (spec 0004): versioned append-only; hash via reference algorithm.
construct_slug: Mapped[str] = mapped_column(String(120), default="")
version: Mapped[int] = mapped_column(default=1)
item_hash: Mapped[str] = mapped_column(String(64), default="")
verification_status: Mapped[str] = mapped_column(String(24), default="draft")
# draft | needs_verification | verified | archived
language: Mapped[str] = mapped_column(String(12), default="en")
category: Mapped[str] = mapped_column(String(80), default="")
# AI-generated provenance: "" for typed/uploaded/library constructs; else
# {"model", "prompt_version", "generated_at"} (ITEM_GENERATION.md). Set
# when the construct was saved from a generated draft - the label persists
# even after the researcher edits items (the seed was AI; item_hash covers
# as-generated vs edited).
generation_json: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class GenerationEvent(Base):
"""One row per successful LLM item generation - the per-user daily cap
(CCR_USER_MAX_GENERATIONS_PER_DAY) counts today's rows. DB-backed rather
than a cookie counter because generation spends real API money, and
signed-in users could clear cookies; a table survives that (and restarts
on Postgres deployments). Rows are tiny and append-only."""
__tablename__ = "generation_events"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
user_id: Mapped[str] = mapped_column(String(32), index=True)
model: Mapped[str] = mapped_column(String(120), default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
class Job(Base):
__tablename__ = "jobs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=_uuid)
project_id: Mapped[str] = mapped_column(ForeignKey("projects.id"))
corpus_id: Mapped[str] = mapped_column(ForeignKey("corpora.id"))
construct_id: Mapped[str] = mapped_column(ForeignKey("constructs.id"))
# Multi-construct runs: the full ordered id list. construct_id stays the
# first entry (FK + legacy rows, whose "[]" here means "just construct_id").
construct_ids_json: Mapped[str] = mapped_column(Text, default="[]")
# Anchor-vector (bipolar) runs (spec 0006): the contrasting opposite-pole
# construct and the similarity metric. "" on both = a normal, non-anchored
# run. Plain String (not FK) so the additive SQLite auto-migration can add
# it to existing DBs, mirroring construct_ids_json.
opposite_construct_id: Mapped[str] = mapped_column(String(32), default="")
similarity_metric: Mapped[str] = mapped_column(String(12), default="") # "" | "cosine" | "dot"
text_column: Mapped[str] = mapped_column(String(200))
model_name: Mapped[str] = mapped_column(String(200)) # registry id (or test fake)
language: Mapped[str] = mapped_column(String(12), default="en") # selected analysis language
status: Mapped[str] = mapped_column(String(20), default="queued")
# queued -> running -> completed | failed
progress: Mapped[float] = mapped_column(Float, default=0.0) # 0..1
error: Mapped[str] = mapped_column(Text, default="")
metadata_json: Mapped[str] = mapped_column(Text, default="{}") # reproducibility record
summary_json: Mapped[str] = mapped_column(Text, default="{}") # results summary
result_path: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[str] = mapped_column(String(32), default=_now)
started_at: Mapped[str] = mapped_column(String(32), default="")
finished_at: Mapped[str] = mapped_column(String(32), default="")
|