This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
@@ -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 = ...
+154
View File
@@ -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 = ...
+62
View File
@@ -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 = ...
+184
View File
@@ -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"
+15
View File
@@ -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."""
+157
View File
@@ -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 = ...
+106
View File
@@ -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.")