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:
2026-08-21 21:48:05 +02:00
co-authored by Claude Opus 5
parent 97785ee590
commit 60d7ec81c0
170 changed files with 629 additions and 619 deletions
+421
View File
@@ -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
)