Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a user's venv, so the package that is about to be published takes the name it is published under. Only the Python package moves; the repo, the Docker WORKDIR and the compose project keep theirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from pydantic import EmailStr
|
||||
from sqlalchemy import JSON, Column, DateTime
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
def get_datetime_utc() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# Shared properties
|
||||
class UserBase(SQLModel):
|
||||
email: EmailStr = Field(unique=True, index=True, max_length=255)
|
||||
is_active: bool = True
|
||||
is_superuser: bool = False
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
class UserRegister(SQLModel):
|
||||
email: EmailStr = Field(max_length=255)
|
||||
password: str = Field(min_length=8, max_length=128)
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
# Properties to receive via API on update, all are optional
|
||||
class UserUpdate(UserBase):
|
||||
email: EmailStr | None = Field(default=None, max_length=255) # type: ignore
|
||||
password: str | None = Field(default=None, min_length=8, max_length=128)
|
||||
|
||||
|
||||
class UserUpdateMe(SQLModel):
|
||||
full_name: str | None = Field(default=None, max_length=255)
|
||||
email: EmailStr | None = Field(default=None, max_length=255)
|
||||
|
||||
|
||||
class UpdatePassword(SQLModel):
|
||||
current_password: str = Field(min_length=8, max_length=128)
|
||||
new_password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
# Database model, database table inferred from class name
|
||||
class User(UserBase, table=True):
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
hashed_password: str
|
||||
#: The portal account this local user stands for, if any. Set for the
|
||||
#: superuser who enrolled this installation and for every remote user one
|
||||
#: of them admitted; None for a purely local account, which is what an
|
||||
#: installation nobody enrolled has only.
|
||||
portal_sub: str | None = Field(default=None, max_length=64, unique=True, index=True)
|
||||
created_at: datetime | None = Field(
|
||||
default_factory=get_datetime_utc,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
# Properties to return via API, id is always required
|
||||
class UserPublic(UserBase):
|
||||
id: uuid.UUID
|
||||
portal_sub: str | None = None
|
||||
created_at: datetime | None = None
|
||||
|
||||
|
||||
class UsersPublic(SQLModel):
|
||||
data: list[UserPublic]
|
||||
count: int
|
||||
|
||||
|
||||
# Generic message
|
||||
class Message(SQLModel):
|
||||
message: str
|
||||
|
||||
|
||||
# JSON payload containing access token
|
||||
class Token(SQLModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
|
||||
|
||||
# Contents of JWT token
|
||||
class TokenPayload(SQLModel):
|
||||
sub: str | None = None
|
||||
#: Set instead of ``sub`` by a token the portal minted for a person: it
|
||||
#: names them on the portal, and the local account it stands for is looked
|
||||
#: up from it. A portal identity nobody here was mapped to resolves to no
|
||||
#: user at all, which is the refusal.
|
||||
portal_sub: str | None = None
|
||||
|
||||
|
||||
class NewPassword(SQLModel):
|
||||
token: str
|
||||
new_password: str = Field(min_length=8, max_length=128)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# OAuth 2.1, for the MCP endpoint
|
||||
#
|
||||
# Agents cannot be handed a password, so they get their own authorization flow:
|
||||
# the client registers itself, a human approves it in the browser, and the code
|
||||
# that comes back is exchanged for a token. Only the hash of a code or a refresh
|
||||
# token is stored, so a copy of this table is not a set of credentials.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class OAuthClient(SQLModel, table=True):
|
||||
"""A registered agent. Registration alone grants nothing."""
|
||||
|
||||
__tablename__ = "oauth_client"
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
client_name: str = Field(max_length=128)
|
||||
redirect_uris: list[str] = Field(sa_column=Column(JSON), default_factory=list)
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
|
||||
|
||||
class OAuthAuthorizationCode(SQLModel, table=True):
|
||||
"""One approved authorization, waiting to be exchanged for a token."""
|
||||
|
||||
__tablename__ = "oauth_authorization_code"
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
code_hash: str = Field(max_length=64, unique=True, index=True)
|
||||
client_id: uuid.UUID = Field(
|
||||
foreign_key="oauth_client.id", nullable=False, ondelete="CASCADE"
|
||||
)
|
||||
user_id: uuid.UUID = Field(
|
||||
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
||||
)
|
||||
redirect_uri: str = Field(max_length=2048)
|
||||
code_challenge: str = Field(max_length=128)
|
||||
resource: str | None = Field(default=None, max_length=2048)
|
||||
expires_at: datetime
|
||||
used_at: datetime | None = None
|
||||
#: The refresh token this code produced, so replaying the code can revoke it.
|
||||
refresh_token_id: uuid.UUID | None = None
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
|
||||
|
||||
class OAuthRefreshToken(SQLModel, table=True):
|
||||
"""A rotating refresh token, one family per authorization."""
|
||||
|
||||
__tablename__ = "oauth_refresh_token"
|
||||
|
||||
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||
token_hash: str = Field(max_length=64, unique=True, index=True)
|
||||
client_id: uuid.UUID = Field(
|
||||
foreign_key="oauth_client.id", nullable=False, ondelete="CASCADE"
|
||||
)
|
||||
user_id: uuid.UUID = Field(
|
||||
foreign_key="user.id", nullable=False, ondelete="CASCADE"
|
||||
)
|
||||
#: Every token rotated out of one authorization shares this, so reusing an
|
||||
#: old one can revoke the whole line rather than just itself.
|
||||
family_id: uuid.UUID
|
||||
expires_at: datetime
|
||||
revoked: bool = False
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc), nullable=False
|
||||
)
|
||||
|
||||
|
||||
class OAuthClientRegister(SQLModel):
|
||||
"""RFC 7591 dynamic client registration request."""
|
||||
|
||||
client_name: str = Field(default="MCP client", max_length=128)
|
||||
redirect_uris: list[str]
|
||||
grant_types: list[str] | None = None
|
||||
response_types: list[str] | None = None
|
||||
token_endpoint_auth_method: str | None = None
|
||||
|
||||
|
||||
class OAuthClientInfo(SQLModel):
|
||||
client_id: str
|
||||
client_name: str
|
||||
redirect_uris: list[str]
|
||||
client_id_issued_at: int
|
||||
grant_types: list[str] = ["authorization_code", "refresh_token"]
|
||||
response_types: list[str] = ["code"]
|
||||
token_endpoint_auth_method: str = "none"
|
||||
|
||||
|
||||
class OAuthAuthorizeInfo(SQLModel):
|
||||
"""What the consent page shows, all of it validated server-side."""
|
||||
|
||||
client_name: str
|
||||
redirect_uri: str
|
||||
scope: str
|
||||
|
||||
|
||||
class OAuthAuthorizeRequest(SQLModel):
|
||||
client_id: str
|
||||
redirect_uri: str
|
||||
code_challenge: str
|
||||
code_challenge_method: str = "S256"
|
||||
state: str | None = None
|
||||
resource: str | None = None
|
||||
scope: str | None = None
|
||||
|
||||
|
||||
class OAuthAuthorizeResponse(SQLModel):
|
||||
redirect_url: str
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Observability
|
||||
#
|
||||
# The engine's own history: what ran, how long it took, and what went wrong.
|
||||
# Rolled up per minute rather than kept per execution — a node firing every
|
||||
# second is 86 400 rows a day raw, and nobody reads a row of that.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class MetricBucket(SQLModel, table=True):
|
||||
"""One node's minute: how much ran, how long it took, how late it was."""
|
||||
|
||||
__tablename__ = "metric_minute"
|
||||
|
||||
flow: str = Field(primary_key=True, max_length=255)
|
||||
node: str = Field(primary_key=True, max_length=255)
|
||||
bucket: datetime = Field(
|
||||
primary_key=True,
|
||||
index=True,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
executions: int = 0
|
||||
errors: int = 0
|
||||
#: Messages emitted, which is not the same as executions: a node can run
|
||||
#: and publish nothing.
|
||||
messages: int = 0
|
||||
duration_sum_ms: float = 0.0
|
||||
duration_max_ms: float = 0.0
|
||||
#: How long queued work waited before it ran.
|
||||
lag_sum_ms: float = 0.0
|
||||
lag_max_ms: float = 0.0
|
||||
#: Items the lag sums are over, so an average can be taken.
|
||||
items: int = 0
|
||||
|
||||
|
||||
class EngineEvent(SQLModel, table=True):
|
||||
"""Something worth keeping after the websocket has forgotten it."""
|
||||
|
||||
__tablename__ = "engine_event"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
ts: datetime = Field(
|
||||
index=True,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
#: node_error, flow_quarantined, engine_degraded, …, or audit.
|
||||
type: str = Field(max_length=32, index=True)
|
||||
flow: str = ""
|
||||
node: str = ""
|
||||
#: The error and its traceback, or what an audit entry says was done.
|
||||
detail: str = ""
|
||||
#: Who did it, on audit rows.
|
||||
actor: str = ""
|
||||
|
||||
|
||||
class FlowRun(SQLModel, table=True):
|
||||
"""One cascade, from the item that started it to the last node in it."""
|
||||
|
||||
__tablename__ = "flow_run"
|
||||
|
||||
#: The queue entry id, or a `manual-` one for a run that never queued.
|
||||
id: str = Field(primary_key=True, max_length=64)
|
||||
flow: str = Field(index=True, max_length=255)
|
||||
#: What caused it: the work item's cause, or "manual".
|
||||
source: str = ""
|
||||
started_at: datetime = Field(
|
||||
index=True,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
finished_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
#: running, ok, error or abandoned.
|
||||
status: str = "running"
|
||||
nodes: int = 0
|
||||
errors: int = 0
|
||||
duration_ms: float = 0.0
|
||||
deliveries: int = 1
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Runs
|
||||
#
|
||||
# A cascade above is what an always-on flow does when a value arrives. A run is
|
||||
# the other shape: a batch flow taken from its inputs to its outputs once, with
|
||||
# parameters that identify it and a result worth keeping. An experiment is a
|
||||
# run, and so is a CI-style job — same entity, different caller.
|
||||
#
|
||||
# Kept apart from the observability tables on purpose: those are rolled up and
|
||||
# pruned on a retention window, and an experiment nobody wants deleted must not
|
||||
# share that fate.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Run(SQLModel, table=True):
|
||||
"""One finite execution of a flow, with what it was asked and what it made."""
|
||||
|
||||
__tablename__ = "run"
|
||||
|
||||
id: str = Field(primary_key=True, max_length=64)
|
||||
flow: str = Field(index=True, max_length=255)
|
||||
#: The flow document this ran, and the commit it was read at, so a result
|
||||
#: can be traced back to the code that produced it.
|
||||
flow_version: int = 1
|
||||
commit: str = Field(default="", max_length=64)
|
||||
params: dict[str, Any] = Field(sa_column=Column(JSON), default_factory=dict)
|
||||
#: sha256 over params and seed. Two runs of the same thing share it, which
|
||||
#: is what makes "have I already run this?" a lookup.
|
||||
params_digest: str = Field(default="", index=True, max_length=64)
|
||||
seed: int | None = None
|
||||
#: Runs submitted together — a sweep, an ensemble.
|
||||
group_id: str | None = Field(default=None, index=True, max_length=64)
|
||||
#: The run this one was made from, on a retry.
|
||||
parent_id: str | None = Field(default=None, max_length=64)
|
||||
#: api, hook, sweep or cli.
|
||||
cause: str = Field(default="api", max_length=32)
|
||||
#: queued, running, ok, error, cancelled or abandoned.
|
||||
status: str = Field(default="queued", index=True, max_length=16)
|
||||
#: Why it is where it is: what it waits for, or what went wrong.
|
||||
status_reason: str = Field(default="", max_length=1024)
|
||||
#: Worker labels its nodes need, so a run with nowhere to go can say so.
|
||||
labels: list[str] = Field(sa_column=Column(JSON), default_factory=list)
|
||||
created_at: datetime = Field(
|
||||
index=True,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
started_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
finished_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
duration_ms: float = 0.0
|
||||
#: The flow's declared outputs once it finished.
|
||||
result: dict[str, Any] = Field(sa_column=Column(JSON), default_factory=dict)
|
||||
#: Which engine holds it, and when it last said so. A run whose lease has
|
||||
#: gone stale is one whose engine died mid-run.
|
||||
engine: str = Field(default="", max_length=64)
|
||||
lease_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
actor: str = Field(default="", max_length=255)
|
||||
|
||||
|
||||
class RunNode(SQLModel, table=True):
|
||||
"""What one node did inside a run: the per-stage view of it."""
|
||||
|
||||
__tablename__ = "run_node"
|
||||
|
||||
run_id: str = Field(primary_key=True, max_length=64)
|
||||
node: str = Field(primary_key=True, max_length=255)
|
||||
#: ok, error, skipped, cached or cancelled.
|
||||
status: str = Field(default="ok", max_length=16)
|
||||
attempt: int = 1
|
||||
started_at: datetime | None = Field(
|
||||
default=None,
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
duration_ms: float = 0.0
|
||||
#: Which worker ran it: "local", or a remote worker's name.
|
||||
worker: str = Field(default="local", max_length=64)
|
||||
error: str = ""
|
||||
logs: str = ""
|
||||
#: Everything this node's output depends on, hashed. Recorded from the
|
||||
#: start so that skipping a stage whose inputs have not changed is later a
|
||||
#: lookup rather than a migration.
|
||||
cache_key: str = Field(default="", index=True, max_length=64)
|
||||
|
||||
|
||||
class RunMetric(SQLModel, table=True):
|
||||
"""One number a run reported, at one step.
|
||||
|
||||
Written by the run's own driver rather than folded off the event bus: the
|
||||
bus drops what it cannot keep up with, which is the right trade for a live
|
||||
canvas and the wrong one for a training curve.
|
||||
"""
|
||||
|
||||
__tablename__ = "run_metric"
|
||||
|
||||
run_id: str = Field(primary_key=True, max_length=64)
|
||||
name: str = Field(primary_key=True, max_length=128)
|
||||
#: -1 for a value with no step of its own — a final score.
|
||||
step: int = Field(primary_key=True)
|
||||
node: str = Field(default="", max_length=255)
|
||||
ts: float = 0.0
|
||||
value: float = 0.0
|
||||
|
||||
|
||||
class RunArtifact(SQLModel, table=True):
|
||||
"""A file a run produced, addressed by the hash of its content."""
|
||||
|
||||
__tablename__ = "run_artifact"
|
||||
|
||||
run_id: str = Field(primary_key=True, max_length=64)
|
||||
name: str = Field(primary_key=True, max_length=255)
|
||||
node: str = Field(default="", max_length=255)
|
||||
digest: str = Field(default="", index=True, max_length=71)
|
||||
size: int = 0
|
||||
media_type: str = Field(default="application/octet-stream", max_length=128)
|
||||
created_at: datetime = Field(
|
||||
default_factory=lambda: datetime.now(timezone.utc),
|
||||
sa_type=DateTime(timezone=True), # type: ignore
|
||||
)
|
||||
Reference in New Issue
Block a user