executor abstraction, graphene to strawberry

This commit is contained in:
2026-03-12 23:27:34 -03:00
parent 4e9d731cff
commit eaaf2ad60c
13 changed files with 796 additions and 276 deletions

View File

@@ -1,5 +1,5 @@
"""
GraphQL API using graphene, mounted on FastAPI/Starlette.
GraphQL API using strawberry, served via FastAPI.
Primary API for MPR — all client interactions go through GraphQL.
Uses Django ORM directly for data access.
@@ -7,8 +7,11 @@ Types are generated from schema/ via modelgen — see api/schema/graphql.py.
"""
import os
from typing import List, Optional
from uuid import UUID
import graphene
import strawberry
from strawberry.types import Info
from api.schema.graphql import (
CreateJobInput,
@@ -22,7 +25,6 @@ from api.schema.graphql import (
)
from core.storage import BUCKET_IN, list_objects
# Media extensions (same as assets route)
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"}
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"}
MEDIA_EXTS = VIDEO_EXTS | AUDIO_EXTS
@@ -33,23 +35,15 @@ MEDIA_EXTS = VIDEO_EXTS | AUDIO_EXTS
# ---------------------------------------------------------------------------
class Query(graphene.ObjectType):
assets = graphene.List(
MediaAssetType,
status=graphene.String(),
search=graphene.String(),
)
asset = graphene.Field(MediaAssetType, id=graphene.UUID(required=True))
jobs = graphene.List(
TranscodeJobType,
status=graphene.String(),
source_asset_id=graphene.UUID(),
)
job = graphene.Field(TranscodeJobType, id=graphene.UUID(required=True))
presets = graphene.List(TranscodePresetType)
system_status = graphene.Field(SystemStatusType)
def resolve_assets(self, info, status=None, search=None):
@strawberry.type
class Query:
@strawberry.field
def assets(
self,
info: Info,
status: Optional[str] = None,
search: Optional[str] = None,
) -> List[MediaAssetType]:
from mpr.media_assets.models import MediaAsset
qs = MediaAsset.objects.all()
@@ -57,9 +51,10 @@ class Query(graphene.ObjectType):
qs = qs.filter(status=status)
if search:
qs = qs.filter(filename__icontains=search)
return qs
return list(qs)
def resolve_asset(self, info, id):
@strawberry.field
def asset(self, info: Info, id: UUID) -> Optional[MediaAssetType]:
from mpr.media_assets.models import MediaAsset
try:
@@ -67,7 +62,13 @@ class Query(graphene.ObjectType):
except MediaAsset.DoesNotExist:
return None
def resolve_jobs(self, info, status=None, source_asset_id=None):
@strawberry.field
def jobs(
self,
info: Info,
status: Optional[str] = None,
source_asset_id: Optional[UUID] = None,
) -> List[TranscodeJobType]:
from mpr.media_assets.models import TranscodeJob
qs = TranscodeJob.objects.all()
@@ -75,9 +76,10 @@ class Query(graphene.ObjectType):
qs = qs.filter(status=status)
if source_asset_id:
qs = qs.filter(source_asset_id=source_asset_id)
return qs
return list(qs)
def resolve_job(self, info, id):
@strawberry.field
def job(self, info: Info, id: UUID) -> Optional[TranscodeJobType]:
from mpr.media_assets.models import TranscodeJob
try:
@@ -85,13 +87,15 @@ class Query(graphene.ObjectType):
except TranscodeJob.DoesNotExist:
return None
def resolve_presets(self, info):
@strawberry.field
def presets(self, info: Info) -> List[TranscodePresetType]:
from mpr.media_assets.models import TranscodePreset
return TranscodePreset.objects.all()
return list(TranscodePreset.objects.all())
def resolve_system_status(self, info):
return {"status": "ok", "version": "0.1.0"}
@strawberry.field
def system_status(self, info: Info) -> SystemStatusType:
return SystemStatusType(status="ok", version="0.1.0")
# ---------------------------------------------------------------------------
@@ -99,13 +103,10 @@ class Query(graphene.ObjectType):
# ---------------------------------------------------------------------------
class ScanMediaFolder(graphene.Mutation):
class Arguments:
pass
Output = ScanResultType
def mutate(self, info):
@strawberry.type
class Mutation:
@strawberry.mutation
def scan_media_folder(self, info: Info) -> ScanResultType:
from mpr.media_assets.models import MediaAsset
objects = list_objects(BUCKET_IN, extensions=MEDIA_EXTS)
@@ -135,14 +136,8 @@ class ScanMediaFolder(graphene.Mutation):
files=registered,
)
class CreateJob(graphene.Mutation):
class Arguments:
input = CreateJobInput(required=True)
Output = TranscodeJobType
def mutate(self, info, input):
@strawberry.mutation
def create_job(self, info: Info, input: CreateJobInput) -> TranscodeJobType:
from pathlib import Path
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
@@ -186,9 +181,8 @@ class CreateJob(graphene.Mutation):
priority=input.priority or 0,
)
# Dispatch
executor_mode = os.environ.get("MPR_EXECUTOR", "local")
if executor_mode == "lambda":
if executor_mode in ("lambda", "gcp"):
from task.executor import get_executor
get_executor().run(
@@ -217,14 +211,8 @@ class CreateJob(graphene.Mutation):
return job
class CancelJob(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
Output = TranscodeJobType
def mutate(self, info, id):
@strawberry.mutation
def cancel_job(self, info: Info, id: UUID) -> TranscodeJobType:
from mpr.media_assets.models import TranscodeJob
try:
@@ -239,14 +227,8 @@ class CancelJob(graphene.Mutation):
job.save(update_fields=["status"])
return job
class RetryJob(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
Output = TranscodeJobType
def mutate(self, info, id):
@strawberry.mutation
def retry_job(self, info: Info, id: UUID) -> TranscodeJobType:
from mpr.media_assets.models import TranscodeJob
try:
@@ -263,15 +245,8 @@ class RetryJob(graphene.Mutation):
job.save(update_fields=["status", "progress", "error_message"])
return job
class UpdateAsset(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
input = UpdateAssetInput(required=True)
Output = MediaAssetType
def mutate(self, info, id, input):
@strawberry.mutation
def update_asset(self, info: Info, id: UUID, input: UpdateAssetInput) -> MediaAssetType:
from mpr.media_assets.models import MediaAsset
try:
@@ -292,14 +267,8 @@ class UpdateAsset(graphene.Mutation):
return asset
class DeleteAsset(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
Output = DeleteResultType
def mutate(self, info, id):
@strawberry.mutation
def delete_asset(self, info: Info, id: UUID) -> DeleteResultType:
from mpr.media_assets.models import MediaAsset
try:
@@ -310,17 +279,8 @@ class DeleteAsset(graphene.Mutation):
raise Exception("Asset not found")
class Mutation(graphene.ObjectType):
scan_media_folder = ScanMediaFolder.Field()
create_job = CreateJob.Field()
cancel_job = CancelJob.Field()
retry_job = RetryJob.Field()
update_asset = UpdateAsset.Field()
delete_asset = DeleteAsset.Field()
# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------
schema = graphene.Schema(query=Query, mutation=Mutation)
schema = strawberry.Schema(query=Query, mutation=Mutation)

View File

@@ -21,7 +21,7 @@ django.setup()
from fastapi import FastAPI, Header, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from starlette_graphene3 import GraphQLApp, make_graphiql_handler
from strawberry.fastapi import GraphQLRouter
from api.graphql import schema as graphql_schema
@@ -45,7 +45,8 @@ app.add_middleware(
)
# GraphQL
app.mount("/graphql", GraphQLApp(schema=graphql_schema, on_get=make_graphiql_handler()))
graphql_router = GraphQLRouter(schema=graphql_schema, graphql_ide="graphiql")
app.include_router(graphql_router, prefix="/graphql")
@app.get("/")

View File

@@ -1,19 +1,26 @@
"""
Graphene Types - GENERATED FILE
Strawberry Types - GENERATED FILE
Do not edit directly. Regenerate using modelgen.
"""
import graphene
import strawberry
from enum import Enum
from typing import List, Optional
from uuid import UUID
from datetime import datetime
from strawberry.scalars import JSON
class AssetStatus(graphene.Enum):
@strawberry.enum
class AssetStatus(Enum):
PENDING = "pending"
READY = "ready"
ERROR = "error"
class JobStatus(graphene.Enum):
@strawberry.enum
class JobStatus(Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
@@ -21,122 +28,131 @@ class JobStatus(graphene.Enum):
CANCELLED = "cancelled"
class MediaAssetType(graphene.ObjectType):
@strawberry.type
class MediaAssetType:
"""A video/audio file registered in the system."""
id = graphene.UUID()
filename = graphene.String()
file_path = graphene.String()
status = graphene.String()
error_message = graphene.String()
file_size = graphene.Int()
duration = graphene.Float()
video_codec = graphene.String()
audio_codec = graphene.String()
width = graphene.Int()
height = graphene.Int()
framerate = graphene.Float()
bitrate = graphene.Int()
properties = graphene.JSONString()
comments = graphene.String()
tags = graphene.List(graphene.String)
created_at = graphene.DateTime()
updated_at = graphene.DateTime()
id: Optional[UUID] = None
filename: Optional[str] = None
file_path: Optional[str] = None
status: Optional[str] = None
error_message: Optional[str] = None
file_size: Optional[int] = None
duration: Optional[float] = None
video_codec: Optional[str] = None
audio_codec: Optional[str] = None
width: Optional[int] = None
height: Optional[int] = None
framerate: Optional[float] = None
bitrate: Optional[int] = None
properties: Optional[JSON] = None
comments: Optional[str] = None
tags: Optional[List[str]] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class TranscodePresetType(graphene.ObjectType):
@strawberry.type
class TranscodePresetType:
"""A reusable transcoding configuration (like Handbrake presets)."""
id = graphene.UUID()
name = graphene.String()
description = graphene.String()
is_builtin = graphene.Boolean()
container = graphene.String()
video_codec = graphene.String()
video_bitrate = graphene.String()
video_crf = graphene.Int()
video_preset = graphene.String()
resolution = graphene.String()
framerate = graphene.Float()
audio_codec = graphene.String()
audio_bitrate = graphene.String()
audio_channels = graphene.Int()
audio_samplerate = graphene.Int()
extra_args = graphene.List(graphene.String)
created_at = graphene.DateTime()
updated_at = graphene.DateTime()
id: Optional[UUID] = None
name: Optional[str] = None
description: Optional[str] = None
is_builtin: Optional[bool] = None
container: Optional[str] = None
video_codec: Optional[str] = None
video_bitrate: Optional[str] = None
video_crf: Optional[int] = None
video_preset: Optional[str] = None
resolution: Optional[str] = None
framerate: Optional[float] = None
audio_codec: Optional[str] = None
audio_bitrate: Optional[str] = None
audio_channels: Optional[int] = None
audio_samplerate: Optional[int] = None
extra_args: Optional[List[str]] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class TranscodeJobType(graphene.ObjectType):
@strawberry.type
class TranscodeJobType:
"""A transcoding or trimming job in the queue."""
id = graphene.UUID()
source_asset_id = graphene.UUID()
preset_id = graphene.UUID()
preset_snapshot = graphene.JSONString()
trim_start = graphene.Float()
trim_end = graphene.Float()
output_filename = graphene.String()
output_path = graphene.String()
output_asset_id = graphene.UUID()
status = graphene.String()
progress = graphene.Float()
current_frame = graphene.Int()
current_time = graphene.Float()
speed = graphene.String()
error_message = graphene.String()
celery_task_id = graphene.String()
execution_arn = graphene.String()
priority = graphene.Int()
created_at = graphene.DateTime()
started_at = graphene.DateTime()
completed_at = graphene.DateTime()
id: Optional[UUID] = None
source_asset_id: Optional[UUID] = None
preset_id: Optional[UUID] = None
preset_snapshot: Optional[JSON] = None
trim_start: Optional[float] = None
trim_end: Optional[float] = None
output_filename: Optional[str] = None
output_path: Optional[str] = None
output_asset_id: Optional[UUID] = None
status: Optional[str] = None
progress: Optional[float] = None
current_frame: Optional[int] = None
current_time: Optional[float] = None
speed: Optional[str] = None
error_message: Optional[str] = None
celery_task_id: Optional[str] = None
execution_arn: Optional[str] = None
priority: Optional[int] = None
created_at: Optional[datetime] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class CreateJobInput(graphene.InputObjectType):
@strawberry.input
class CreateJobInput:
"""Request body for creating a transcode/trim job."""
source_asset_id = graphene.UUID(required=True)
preset_id = graphene.UUID()
trim_start = graphene.Float()
trim_end = graphene.Float()
output_filename = graphene.String()
priority = graphene.Int(default_value=0)
source_asset_id: UUID
preset_id: Optional[UUID] = None
trim_start: Optional[float] = None
trim_end: Optional[float] = None
output_filename: Optional[str] = None
priority: int = 0
class UpdateAssetInput(graphene.InputObjectType):
@strawberry.input
class UpdateAssetInput:
"""Request body for updating asset metadata."""
comments = graphene.String()
tags = graphene.List(graphene.String)
comments: Optional[str] = None
tags: Optional[List[str]] = None
class SystemStatusType(graphene.ObjectType):
@strawberry.type
class SystemStatusType:
"""System status response."""
status = graphene.String()
version = graphene.String()
status: Optional[str] = None
version: Optional[str] = None
class ScanResultType(graphene.ObjectType):
@strawberry.type
class ScanResultType:
"""Result of scanning the media input bucket."""
found = graphene.Int()
registered = graphene.Int()
skipped = graphene.Int()
files = graphene.List(graphene.String)
found: Optional[int] = None
registered: Optional[int] = None
skipped: Optional[int] = None
files: Optional[List[str]] = None
class DeleteResultType(graphene.ObjectType):
@strawberry.type
class DeleteResultType:
"""Result of a delete operation."""
ok = graphene.Boolean()
ok: Optional[bool] = None
class WorkerStatusType(graphene.ObjectType):
@strawberry.type
class WorkerStatusType:
"""Worker health and capabilities."""
available = graphene.Boolean()
active_jobs = graphene.Int()
supported_codecs = graphene.List(graphene.String)
gpu_available = graphene.Boolean()
available: Optional[bool] = None
active_jobs: Optional[int] = None
supported_codecs: Optional[List[str]] = None
gpu_available: Optional[bool] = None