first
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""asf-domain: shared Pydantic models for Azurian Software Factory.
|
||||
|
||||
Every Python service in ASF imports its domain models from this package.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
generate_id,
|
||||
UTCDatetime,
|
||||
ArtifactType,
|
||||
RelationshipType,
|
||||
JobStatus,
|
||||
JobPriority,
|
||||
JobType,
|
||||
AuditAction,
|
||||
)
|
||||
from asf_domain.identity import (
|
||||
Organization,
|
||||
OrganizationCreate,
|
||||
OrganizationUpdate,
|
||||
Project,
|
||||
ProjectCreate,
|
||||
ProjectUpdate,
|
||||
Team,
|
||||
User,
|
||||
)
|
||||
from asf_domain.artifact import (
|
||||
Artifact,
|
||||
ArtifactCreate,
|
||||
ArtifactUpdate,
|
||||
ArtifactRelationship,
|
||||
ArtifactRelationshipCreate,
|
||||
RelationshipDirection,
|
||||
)
|
||||
from asf_domain.job import (
|
||||
Job,
|
||||
JobCreate,
|
||||
JobUpdate,
|
||||
JobUsage,
|
||||
)
|
||||
from asf_domain.ai_execution import (
|
||||
AIExecution,
|
||||
AIExecutionCreate,
|
||||
)
|
||||
from asf_domain.integration import Integration, IntegrationCreate
|
||||
from asf_domain.audit import AuditEvent, AuditEventCreate
|
||||
|
||||
__all__ = [
|
||||
"ASFBaseModel",
|
||||
"TenantScopedModel",
|
||||
"generate_id",
|
||||
"UTCDatetime",
|
||||
"ArtifactType",
|
||||
"RelationshipType",
|
||||
"JobStatus",
|
||||
"JobPriority",
|
||||
"JobType",
|
||||
"AuditAction",
|
||||
"Organization",
|
||||
"OrganizationCreate",
|
||||
"OrganizationUpdate",
|
||||
"Project",
|
||||
"ProjectCreate",
|
||||
"ProjectUpdate",
|
||||
"Team",
|
||||
"User",
|
||||
"Artifact",
|
||||
"ArtifactCreate",
|
||||
"ArtifactUpdate",
|
||||
"ArtifactRelationship",
|
||||
"ArtifactRelationshipCreate",
|
||||
"RelationshipDirection",
|
||||
"Job",
|
||||
"JobCreate",
|
||||
"JobUpdate",
|
||||
"JobUsage",
|
||||
"AIExecution",
|
||||
"AIExecutionCreate",
|
||||
"Integration",
|
||||
"IntegrationCreate",
|
||||
"AuditEvent",
|
||||
"AuditEventCreate",
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
"""AIExecution — immutable record of a single inference call.
|
||||
|
||||
Stored after every real (or mock) AI invocation so that:
|
||||
- usage/cost is attributable to a tenant + project + job
|
||||
- inputs/outputs can be audited / replayed
|
||||
- the AI Gateway can train / evaluate routing policies later
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Dict
|
||||
from pydantic import Field
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
class AIExecutionBase(ASFBaseModel):
|
||||
capability: str = Field(
|
||||
...,
|
||||
max_length=128,
|
||||
description="ASF capability requested (e.g. chat, analyze_code, generate_tests).",
|
||||
)
|
||||
provider: str = Field(
|
||||
..., max_length=64, description="Adapter used (e.g. mock, openai, nvidia, local)."
|
||||
)
|
||||
model: str = Field(
|
||||
..., max_length=128, description="Concrete model identifier reported by the adapter."
|
||||
)
|
||||
success: bool = True
|
||||
|
||||
|
||||
class AIExecutionCreate(AIExecutionBase):
|
||||
job_id: Optional[ASFFieldID] = None
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
input_ref: Optional[str] = Field(
|
||||
None, max_length=512,
|
||||
description="MinIO ref or short fingerprint of the input payload (never raw secrets).",
|
||||
)
|
||||
output_ref: Optional[str] = Field(
|
||||
None, max_length=512, description="MinIO ref / fingerprint of the response."
|
||||
)
|
||||
input_tokens: Optional[int] = Field(None, ge=0)
|
||||
output_tokens: Optional[int] = Field(None, ge=0)
|
||||
duration_ms: Optional[int] = Field(None, ge=0)
|
||||
estimated_cost: Optional[float] = Field(None, ge=0)
|
||||
error_message: Optional[str] = Field(None, max_length=2000)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AIExecution(TenantScopedModel, AIExecutionBase):
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
job_id: Optional[ASFFieldID] = None
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
input_ref: Optional[str] = None
|
||||
output_ref: Optional[str] = None
|
||||
input_tokens: Optional[int] = None
|
||||
output_tokens: Optional[int] = None
|
||||
duration_ms: Optional[int] = None
|
||||
estimated_cost: Optional[float] = None
|
||||
error_message: Optional[str] = None
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: UTCDatetime = ...
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Artifact — the heart of the "Everything is an Artifact" principle.
|
||||
|
||||
One single generic model captures every lifecycle entity (requirement,
|
||||
task, commit, test, deployment, incident, ...). Artifact subtypes are
|
||||
expressed via the ``type`` enum / string and stored in the same MongoDB
|
||||
``artifacts`` collection.
|
||||
|
||||
Relationships between artifacts live in ``artifact_relationships``
|
||||
collection and are modelled as explicit directed edges — this keeps the
|
||||
option open to project the same data onto a graph database later without
|
||||
rewriting domain logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, List, Dict
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
ArtifactType,
|
||||
RelationshipType,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Artifact itself
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class ArtifactBase(ASFBaseModel):
|
||||
type: ArtifactType = Field(
|
||||
..., description="Kind of artifact (requirement | task | commit | …)"
|
||||
)
|
||||
external_id: Optional[str] = Field(
|
||||
None,
|
||||
max_length=255,
|
||||
description="Optional identifier in an external system "
|
||||
"(e.g. GitHub PR id, Jira issue key)",
|
||||
)
|
||||
title: str = Field(..., min_length=1, max_length=500)
|
||||
content: Optional[str] = Field(
|
||||
None,
|
||||
description="Primary human readable body (markdown, description, diff, log body, …). "
|
||||
"Large content should live in MinIO; store a reference here or in metadata.",
|
||||
)
|
||||
source: Optional[str] = Field(
|
||||
None,
|
||||
max_length=255,
|
||||
description="Origin system: jira, github, manual, ai, …",
|
||||
)
|
||||
status: Optional[str] = Field(
|
||||
None, max_length=80, description="Status (e.g. open, done, failed, released)."
|
||||
)
|
||||
|
||||
|
||||
class ArtifactCreate(ArtifactBase):
|
||||
"""Create payload. ``tenant_id`` and ``project_id`` are set server-side."""
|
||||
|
||||
project_id: ASFFieldID = Field(
|
||||
..., description="Owning project (validated against tenant context server side)."
|
||||
)
|
||||
metadata: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Arbitrary extensible fields. Use this for artifact subtype "
|
||||
"specific attributes (e.g. commit.sha, test.outcome, deployment.env).",
|
||||
)
|
||||
|
||||
|
||||
class ArtifactUpdate(ASFBaseModel):
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=500)
|
||||
content: Optional[str] = None
|
||||
type: Optional[ArtifactType] = None
|
||||
external_id: Optional[str] = None
|
||||
source: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
metadata: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="When provided, performs a shallow merge with existing metadata.",
|
||||
)
|
||||
|
||||
|
||||
class Artifact(TenantScopedModel, ArtifactBase):
|
||||
"""Stored Artifact record.
|
||||
|
||||
``relationships`` is a denormalised, always-stale list kept for fast
|
||||
reads in the UI. The authoritative source of truth for edges is the
|
||||
``artifact_relationships`` collection.
|
||||
"""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
project_id: ASFFieldID = ...
|
||||
version: int = Field(
|
||||
1, ge=1, description="Monotonic version bumped on every update."
|
||||
)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
created_by: Optional[ASFFieldID] = Field(
|
||||
None, description="User id that created this artifact (null = system / import)."
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# ArtifactRelationship — the explicit edge
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class RelationshipDirection(str, Enum):
|
||||
FORWARD = "forward"
|
||||
REVERSE = "reverse"
|
||||
|
||||
|
||||
class ArtifactRelationshipBase(ASFBaseModel):
|
||||
relationship_type: RelationshipType = Field(
|
||||
...,
|
||||
description="Semantic predicate: ``source <relationship_type> target``.",
|
||||
)
|
||||
source_artifact_id: ASFFieldID = Field(
|
||||
..., description="Source artifact id (subject)."
|
||||
)
|
||||
target_artifact_id: ASFFieldID = Field(
|
||||
..., description="Target artifact id (object)."
|
||||
)
|
||||
metadata: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Free-form metadata on the edge (confidence, evidence ref, …).",
|
||||
)
|
||||
|
||||
|
||||
class ArtifactRelationshipCreate(ArtifactRelationshipBase):
|
||||
"""Create payload. tenant_id / project_ids are validated server side."""
|
||||
pass
|
||||
|
||||
|
||||
class ArtifactRelationship(TenantScopedModel, ArtifactRelationshipBase):
|
||||
"""Stored edge between two artifacts.
|
||||
|
||||
Invariant: both source and target MUST belong to the same tenant.
|
||||
They MAY live in different projects inside that tenant when an
|
||||
explicit cross-project reference is intentional.
|
||||
"""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
source_project_id: ASFFieldID = ...
|
||||
target_project_id: ASFFieldID = ...
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
@@ -0,0 +1,62 @@
|
||||
"""AuditEvent — append-only security & business audit trail.
|
||||
|
||||
Audit events are never mutated or deleted. They record WHO did WHAT to
|
||||
WHICH entity WHEN and from WHERE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Dict
|
||||
from pydantic import Field
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
AuditAction,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
class AuditEventBase(ASFBaseModel):
|
||||
action: AuditAction = Field(..., description="What happened.")
|
||||
resource_type: str = Field(
|
||||
...,
|
||||
max_length=80,
|
||||
description="Affected entity kind (organization, project, artifact, job, api_key, …).",
|
||||
)
|
||||
resource_id: Optional[ASFFieldID] = Field(None, description="Affected entity id if applicable.")
|
||||
actor_id: Optional[ASFFieldID] = Field(
|
||||
None, description="User id that caused the event, or null for system actions."
|
||||
)
|
||||
actor_type: str = Field(
|
||||
"user",
|
||||
max_length=40,
|
||||
description="user | system | api_key | service — kind of actor that caused the event.",
|
||||
)
|
||||
success: bool = True
|
||||
reason: Optional[str] = Field(
|
||||
None, max_length=500, description="For failed events, why the action was rejected."
|
||||
)
|
||||
|
||||
|
||||
class AuditEventCreate(AuditEventBase):
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
ip_address: Optional[str] = Field(None, max_length=64)
|
||||
user_agent: Optional[str] = Field(None, max_length=512)
|
||||
correlation_id: Optional[str] = Field(None, max_length=128)
|
||||
metadata: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Structured detail of the change. NEVER log secrets here.",
|
||||
)
|
||||
|
||||
|
||||
class AuditEvent(TenantScopedModel, AuditEventBase):
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
ip_address: Optional[str] = None
|
||||
user_agent: Optional[str] = None
|
||||
correlation_id: Optional[str] = None
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: UTCDatetime = ...
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Base types and enums for all ASF domain models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Annotated
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
PlainSerializer,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Identifiers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ASFFieldID = Annotated[
|
||||
UUID,
|
||||
Field(
|
||||
description="ASF unique identifier (UUID v4)",
|
||||
examples=["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def generate_id() -> UUID:
|
||||
"""Return a new UUID v4 identifier for an ASF entity."""
|
||||
return uuid4()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Datetime helpers — force UTC on the wire
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _utc_serialize(dt: datetime) -> str:
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
UTCDatetime = Annotated[
|
||||
datetime,
|
||||
PlainSerializer(_utc_serialize, return_type=str, when_used="json"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Base model configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ASFBaseModel(BaseModel):
|
||||
"""Base Pydantic model used everywhere in ASF."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
populate_by_name=True,
|
||||
use_enum_values=False,
|
||||
from_attributes=True,
|
||||
str_strip_whitespace=True,
|
||||
json_encoders={datetime: _utc_serialize},
|
||||
)
|
||||
|
||||
|
||||
class TenantScopedModel(ASFBaseModel):
|
||||
"""Any record that belongs to a tenant (Organization)."""
|
||||
|
||||
tenant_id: ASFFieldID = Field(
|
||||
description="Owning tenant (Organization) identifier. "
|
||||
"Always validated against the authenticated context — never "
|
||||
"trust the value supplied by an untrusted client."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Artifact type universe (extensible, open)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ArtifactType(str, enum.Enum):
|
||||
"""Canonical set of Artifact subtypes in the initial bootstrap.
|
||||
|
||||
This list is intentionally extensible: adding a new kind of lifecycle
|
||||
entity is a new ArtifactType value, NOT a new MongoDB collection.
|
||||
"""
|
||||
|
||||
REQUIREMENT = "requirement"
|
||||
EPIC = "epic"
|
||||
STORY = "story"
|
||||
TASK = "task"
|
||||
ARCHITECTURE_DECISION = "architecture_decision"
|
||||
DOCUMENT = "document"
|
||||
SOURCE_FILE = "source_file"
|
||||
COMMIT = "commit"
|
||||
PULL_REQUEST = "pull_request"
|
||||
BUILD = "build"
|
||||
TEST = "test"
|
||||
SECURITY_FINDING = "security_finding"
|
||||
DEPLOYMENT = "deployment"
|
||||
RELEASE = "release"
|
||||
RUNTIME_EVENT = "runtime_event"
|
||||
INCIDENT = "incident"
|
||||
AI_ANALYSIS = "ai_analysis"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Relationship types between artifacts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class RelationshipType(str, enum.Enum):
|
||||
"""Canonical relationship predicates between two Artifacts.
|
||||
|
||||
Semantics: ``source <predicate> target``, e.g.
|
||||
``REQ-1 implements TASK-7`` means TASK-7 implements REQ-1.
|
||||
For the inverse use IMPLEMENTED_BY.
|
||||
"""
|
||||
|
||||
IMPLEMENTS = "implements"
|
||||
IMPLEMENTED_BY = "implemented_by"
|
||||
VERIFIES = "verifies"
|
||||
VERIFIED_BY = "verified_by"
|
||||
GENERATED_FROM = "generated_from"
|
||||
DOCUMENTED_BY = "documented_by"
|
||||
DEPENDS_ON = "depends_on"
|
||||
AFFECTS = "affects"
|
||||
DEPLOYED_AS = "deployed_as"
|
||||
CAUSED_BY = "caused_by"
|
||||
SUPERSEDES = "supersedes"
|
||||
RELATED_TO = "related_to"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JobStatus(str, enum.Enum):
|
||||
QUEUED = "queued"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class JobPriority(str, enum.Enum):
|
||||
LOW = "low"
|
||||
NORMAL = "normal"
|
||||
HIGH = "high"
|
||||
URGENT = "urgent"
|
||||
|
||||
|
||||
class JobType(str, enum.Enum):
|
||||
AI_INTERACTIVE = "ai_interactive"
|
||||
AI_BACKGROUND = "ai_background"
|
||||
INDEXING = "indexing"
|
||||
CODE_ANALYSIS = "code_analysis"
|
||||
TESTING = "testing"
|
||||
DEVOPS = "devops"
|
||||
NOTIFICATION = "notification"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Audit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AuditAction(str, enum.Enum):
|
||||
CREATED = "created"
|
||||
UPDATED = "updated"
|
||||
DELETED = "deleted"
|
||||
ACCESSED = "accessed"
|
||||
LOGGED_IN = "logged_in"
|
||||
LOGGED_OUT = "logged_out"
|
||||
PERMISSION_CHANGED = "permission_changed"
|
||||
API_KEY_CREATED = "api_key_created"
|
||||
API_KEY_REVOKED = "api_key_revoked"
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Small shared type aliases & tiny helpers.
|
||||
|
||||
Put things here when they are used by multiple domain modules but don't
|
||||
have a home of their own.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TypeAlias, Any, Dict, Awaitable, Callable
|
||||
|
||||
JSONDict: TypeAlias = Dict[str, Any]
|
||||
"""Any loosely-typed JSON object (metadata, unstructured payloads, …)."""
|
||||
|
||||
AsyncHandler: TypeAlias = Callable[..., Awaitable[Any]]
|
||||
"""Generic async callable type alias."""
|
||||
@@ -0,0 +1,157 @@
|
||||
"""Identity domain: Organization, User, Team, Project.
|
||||
|
||||
SaaS tenancy:
|
||||
Organization == tenant boundary
|
||||
└── Teams
|
||||
└── Projects
|
||||
└── Artifacts + Relationships + Jobs
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Organization (tenant)
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class OrganizationBase(ASFBaseModel):
|
||||
name: str = Field(
|
||||
...,
|
||||
min_length=2,
|
||||
max_length=120,
|
||||
description="Human readable organization / tenant name",
|
||||
examples=["Azurian Labs", "Contoso Retail"],
|
||||
)
|
||||
slug: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=60,
|
||||
pattern=r"^[a-z0-9][a-z0-9\-]{1,58}[a-z0-9]$",
|
||||
description="URL-safe unique identifier for the tenant",
|
||||
examples=["azurian-labs"],
|
||||
)
|
||||
plan: str = Field(
|
||||
"free",
|
||||
description="Subscription tier placeholder",
|
||||
examples=["free", "team", "enterprise"],
|
||||
)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class OrganizationCreate(OrganizationBase):
|
||||
"""Payload used to create a new Organization (tenant)."""
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationUpdate(ASFBaseModel):
|
||||
name: Optional[str] = Field(None, min_length=2, max_length=120)
|
||||
slug: Optional[str] = Field(None, min_length=3, max_length=60)
|
||||
plan: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class Organization(OrganizationBase):
|
||||
"""Stored Organization record — also the tenant boundary record."""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
created_at: UTCDatetime = Field(
|
||||
..., description="Creation timestamp (UTC, written on insert)"
|
||||
)
|
||||
updated_at: UTCDatetime = Field(
|
||||
..., description="Last update timestamp (UTC, written on every write)"
|
||||
)
|
||||
metadata: dict = Field(
|
||||
default_factory=dict,
|
||||
description="Tenant-level extensible metadata (billing id, features, …)",
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# User (bootstrap skeleton; real auth lives outside domain)
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class User(TenantScopedModel):
|
||||
"""Minimal User record used for ownership / audit references."""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
email: str = Field(..., max_length=255)
|
||||
display_name: str = Field(..., max_length=120)
|
||||
roles: List[str] = Field(default_factory=lambda: ["member"])
|
||||
is_active: bool = True
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def _lower_email(cls, v: str) -> str:
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Team
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class Team(TenantScopedModel):
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
name: str = Field(..., min_length=2, max_length=80)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
member_ids: List[ASFFieldID] = Field(default_factory=list)
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Project
|
||||
# ======================================================================
|
||||
|
||||
|
||||
class ProjectBase(ASFBaseModel):
|
||||
name: str = Field(..., min_length=2, max_length=120)
|
||||
key: str = Field(
|
||||
...,
|
||||
min_length=2,
|
||||
max_length=20,
|
||||
pattern=r"^[A-Z][A-Z0-9]{1,19}$",
|
||||
description="Short project key (e.g. ASF, PROJ) used in human readable refs",
|
||||
examples=["ASF", "PRJ"],
|
||||
)
|
||||
description: Optional[str] = Field(None, max_length=1000)
|
||||
default_branch: str = Field("main", max_length=100)
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class ProjectCreate(ProjectBase):
|
||||
"""Create payload; tenant_id is injected server side from auth context."""
|
||||
pass
|
||||
|
||||
|
||||
class ProjectUpdate(ASFBaseModel):
|
||||
name: Optional[str] = Field(None, min_length=2, max_length=120)
|
||||
key: Optional[str] = Field(None, min_length=2, max_length=20)
|
||||
description: Optional[str] = Field(None, max_length=1000)
|
||||
default_branch: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class Project(TenantScopedModel, ProjectBase):
|
||||
"""Stored Project record. Lives inside a tenant."""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
team_ids: List[ASFFieldID] = Field(default_factory=list)
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
metadata: dict = Field(default_factory=dict)
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Integration — registration of a connector bound to a tenant.
|
||||
|
||||
The connector itself lives in ``asf-connectors``; this record stores:
|
||||
* which connector type is in use
|
||||
* tenant-scoped configuration references (secrets live elsewhere)
|
||||
* last health state
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Dict
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
class ConnectorType(str, Enum):
|
||||
GITHUB = "github"
|
||||
GITLAB = "gitlab"
|
||||
BITBUCKET = "bitbucket"
|
||||
JIRA = "jira"
|
||||
CONFLUENCE = "confluence"
|
||||
NOTION = "notion"
|
||||
JENKINS = "jenkins"
|
||||
SONARQUBE = "sonarqube"
|
||||
GRAFANA = "grafana"
|
||||
GENERIC_WEBHOOK = "generic_webhook"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class IntegrationBase(ASFBaseModel):
|
||||
name: str = Field(..., min_length=2, max_length=80)
|
||||
connector: ConnectorType = ConnectorType.CUSTOM
|
||||
is_enabled: bool = True
|
||||
|
||||
|
||||
class IntegrationCreate(IntegrationBase):
|
||||
config_ref: Optional[str] = Field(
|
||||
None,
|
||||
max_length=512,
|
||||
description="Reference to a secure store (e.g. vault path) holding connector secrets. "
|
||||
"NEVER inline credentials here.",
|
||||
)
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class Integration(TenantScopedModel, IntegrationBase):
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
config_ref: Optional[str] = None
|
||||
metadata: Dict[str, Any] = Field(default_factory=dict)
|
||||
last_health_at: Optional[UTCDatetime] = None
|
||||
last_health_status: Optional[str] = Field(None, max_length=40)
|
||||
created_at: UTCDatetime = ...
|
||||
updated_at: UTCDatetime = ...
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Job model — persistent asynchronous job state.
|
||||
|
||||
RabbitMQ delivers work messages. MongoDB is the source of truth for job
|
||||
lifecycle. Worker processes update Job records as they progress through
|
||||
queued → running → completed|failed|cancelled.
|
||||
|
||||
Do not use RabbitMQ queues as the permanent job database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Dict
|
||||
from pydantic import Field
|
||||
|
||||
from asf_domain.base import (
|
||||
ASFBaseModel,
|
||||
TenantScopedModel,
|
||||
ASFFieldID,
|
||||
UTCDatetime,
|
||||
JobStatus,
|
||||
JobPriority,
|
||||
JobType,
|
||||
generate_id,
|
||||
)
|
||||
|
||||
|
||||
class JobUsage(ASFBaseModel):
|
||||
"""Optional resource / cost usage tracked for a job."""
|
||||
|
||||
input_tokens: Optional[int] = Field(None, ge=0)
|
||||
output_tokens: Optional[int] = Field(None, ge=0)
|
||||
gpu_seconds: Optional[float] = Field(None, ge=0)
|
||||
cpu_seconds: Optional[float] = Field(None, ge=0)
|
||||
estimated_cost: Optional[float] = Field(
|
||||
None,
|
||||
ge=0,
|
||||
description="Estimated cost in tenant currency (EUR/USD). Approximate, used for metering only.",
|
||||
)
|
||||
extra: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class JobBase(ASFBaseModel):
|
||||
type: JobType = Field(
|
||||
...,
|
||||
description="Logical job class. Used for routing to the correct RabbitMQ queue.",
|
||||
)
|
||||
priority: JobPriority = JobPriority.NORMAL
|
||||
payload: Dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="The actual work payload (e.g. {artifact_id, prompt, model_preference}).",
|
||||
)
|
||||
correlation_id: Optional[str] = Field(
|
||||
None,
|
||||
max_length=128,
|
||||
description="Optional correlation id propagated across logs, events, child jobs.",
|
||||
)
|
||||
|
||||
|
||||
class JobCreate(JobBase):
|
||||
"""Create payload. tenant_id/project_id/requested_by are injected server-side."""
|
||||
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
model: Optional[str] = Field(
|
||||
None,
|
||||
max_length=128,
|
||||
description="Preferred model / engine, only meaningful for AI job types. "
|
||||
"Ultimate routing decision belongs to the AI Gateway.",
|
||||
)
|
||||
|
||||
|
||||
class JobUpdate(ASFBaseModel):
|
||||
"""Updates issued by workers as the job progresses."""
|
||||
|
||||
status: Optional[JobStatus] = None
|
||||
worker: Optional[str] = Field(
|
||||
None, max_length=255, description="Identifier of the worker process claiming this job."
|
||||
)
|
||||
started_at: Optional[UTCDatetime] = None
|
||||
completed_at: Optional[UTCDatetime] = None
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = Field(None, max_length=2000)
|
||||
error_detail: Optional[Dict[str, Any]] = None
|
||||
usage: Optional[JobUsage] = None
|
||||
progress_percent: Optional[int] = Field(None, ge=0, le=100)
|
||||
|
||||
|
||||
class Job(TenantScopedModel, JobBase):
|
||||
"""Persistent job record — the authoritative source of truth."""
|
||||
|
||||
id: ASFFieldID = Field(default_factory=generate_id)
|
||||
project_id: Optional[ASFFieldID] = None
|
||||
status: JobStatus = JobStatus.QUEUED
|
||||
requested_by: Optional[ASFFieldID] = Field(
|
||||
None, description="User that requested the job, or null for system jobs."
|
||||
)
|
||||
worker: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
created_at: UTCDatetime = ...
|
||||
started_at: Optional[UTCDatetime] = None
|
||||
completed_at: Optional[UTCDatetime] = None
|
||||
result: Optional[Dict[str, Any]] = None
|
||||
error: Optional[str] = None
|
||||
error_detail: Optional[Dict[str, Any]] = None
|
||||
usage: JobUsage = Field(default_factory=JobUsage)
|
||||
progress_percent: Optional[int] = Field(None, ge=0, le=100)
|
||||
retries: int = Field(0, ge=0, description="How many times this job has been retried.")
|
||||
Reference in New Issue
Block a user