@weirdfingers/baseboards 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +887 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/templates/README.md +120 -0
- package/templates/api/.env.example +62 -0
- package/templates/api/Dockerfile +32 -0
- package/templates/api/README.md +132 -0
- package/templates/api/alembic/env.py +106 -0
- package/templates/api/alembic/script.py.mako +28 -0
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
- package/templates/api/alembic.ini +36 -0
- package/templates/api/config/generators.yaml +25 -0
- package/templates/api/config/storage_config.yaml +26 -0
- package/templates/api/docs/ADDING_GENERATORS.md +409 -0
- package/templates/api/docs/GENERATORS_API.md +502 -0
- package/templates/api/docs/MIGRATIONS.md +472 -0
- package/templates/api/docs/storage_providers.md +337 -0
- package/templates/api/pyproject.toml +165 -0
- package/templates/api/src/boards/__init__.py +10 -0
- package/templates/api/src/boards/api/app.py +171 -0
- package/templates/api/src/boards/api/auth.py +75 -0
- package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
- package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
- package/templates/api/src/boards/api/endpoints/setup.py +505 -0
- package/templates/api/src/boards/api/endpoints/sse.py +129 -0
- package/templates/api/src/boards/api/endpoints/storage.py +74 -0
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
- package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
- package/templates/api/src/boards/auth/__init__.py +15 -0
- package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
- package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
- package/templates/api/src/boards/auth/adapters/base.py +73 -0
- package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
- package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
- package/templates/api/src/boards/auth/adapters/none.py +102 -0
- package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
- package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
- package/templates/api/src/boards/auth/context.py +35 -0
- package/templates/api/src/boards/auth/factory.py +115 -0
- package/templates/api/src/boards/auth/middleware.py +221 -0
- package/templates/api/src/boards/auth/provisioning.py +129 -0
- package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
- package/templates/api/src/boards/cli.py +354 -0
- package/templates/api/src/boards/config.py +116 -0
- package/templates/api/src/boards/database/__init__.py +7 -0
- package/templates/api/src/boards/database/cli.py +110 -0
- package/templates/api/src/boards/database/connection.py +252 -0
- package/templates/api/src/boards/database/models.py +19 -0
- package/templates/api/src/boards/database/seed_data.py +182 -0
- package/templates/api/src/boards/dbmodels/__init__.py +455 -0
- package/templates/api/src/boards/generators/__init__.py +57 -0
- package/templates/api/src/boards/generators/artifacts.py +53 -0
- package/templates/api/src/boards/generators/base.py +140 -0
- package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
- package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
- package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
- package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
- package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
- package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
- package/templates/api/src/boards/generators/loader.py +253 -0
- package/templates/api/src/boards/generators/registry.py +114 -0
- package/templates/api/src/boards/generators/resolution.py +515 -0
- package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
- package/templates/api/src/boards/graphql/__init__.py +7 -0
- package/templates/api/src/boards/graphql/access_control.py +136 -0
- package/templates/api/src/boards/graphql/mutations/root.py +136 -0
- package/templates/api/src/boards/graphql/queries/root.py +116 -0
- package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
- package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
- package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
- package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
- package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
- package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
- package/templates/api/src/boards/graphql/schema.py +81 -0
- package/templates/api/src/boards/graphql/types/board.py +102 -0
- package/templates/api/src/boards/graphql/types/generation.py +130 -0
- package/templates/api/src/boards/graphql/types/generator.py +17 -0
- package/templates/api/src/boards/graphql/types/user.py +47 -0
- package/templates/api/src/boards/jobs/repository.py +104 -0
- package/templates/api/src/boards/logging.py +195 -0
- package/templates/api/src/boards/middleware.py +339 -0
- package/templates/api/src/boards/progress/__init__.py +4 -0
- package/templates/api/src/boards/progress/models.py +25 -0
- package/templates/api/src/boards/progress/publisher.py +64 -0
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +118 -0
- package/templates/api/src/boards/storage/__init__.py +52 -0
- package/templates/api/src/boards/storage/base.py +363 -0
- package/templates/api/src/boards/storage/config.py +187 -0
- package/templates/api/src/boards/storage/factory.py +278 -0
- package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
- package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
- package/templates/api/src/boards/storage/implementations/local.py +201 -0
- package/templates/api/src/boards/storage/implementations/s3.py +294 -0
- package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
- package/templates/api/src/boards/tenant_isolation.py +446 -0
- package/templates/api/src/boards/validation.py +262 -0
- package/templates/api/src/boards/workers/__init__.py +1 -0
- package/templates/api/src/boards/workers/actors.py +201 -0
- package/templates/api/src/boards/workers/cli.py +125 -0
- package/templates/api/src/boards/workers/context.py +188 -0
- package/templates/api/src/boards/workers/middleware.py +58 -0
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.dev.yaml +39 -0
- package/templates/compose.yaml +109 -0
- package/templates/docker/env.example +23 -0
- package/templates/web/.env.example +28 -0
- package/templates/web/Dockerfile +51 -0
- package/templates/web/components.json +22 -0
- package/templates/web/imageLoader.js +18 -0
- package/templates/web/next-env.d.ts +5 -0
- package/templates/web/next.config.js +36 -0
- package/templates/web/package.json +37 -0
- package/templates/web/postcss.config.mjs +7 -0
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
- package/templates/web/src/app/globals.css +120 -0
- package/templates/web/src/app/layout.tsx +21 -0
- package/templates/web/src/app/page.tsx +35 -0
- package/templates/web/src/app/providers.tsx +18 -0
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
- package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
- package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
- package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
- package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
- package/templates/web/src/components/header.tsx +30 -0
- package/templates/web/src/components/ui/button.tsx +58 -0
- package/templates/web/src/components/ui/card.tsx +92 -0
- package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
- package/templates/web/src/lib/utils.ts +6 -0
- package/templates/web/tsconfig.json +47 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generator resolvers for GraphQL API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
|
|
7
|
+
from ...generators.registry import registry
|
|
8
|
+
from ..types.generation import ArtifactType
|
|
9
|
+
from ..types.generator import GeneratorInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def resolve_generators(
|
|
13
|
+
info: strawberry.Info,
|
|
14
|
+
artifact_type: str | None = None,
|
|
15
|
+
) -> list[GeneratorInfo]:
|
|
16
|
+
"""Get all available generators, optionally filtered by artifact type.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
info: GraphQL info context
|
|
20
|
+
artifact_type: Optional filter by artifact type (image, video, audio, text)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of generator information
|
|
24
|
+
"""
|
|
25
|
+
_ = info # Unused but required by GraphQL interface
|
|
26
|
+
|
|
27
|
+
# Get generators from registry
|
|
28
|
+
if artifact_type:
|
|
29
|
+
generators = registry.list_by_artifact_type(artifact_type)
|
|
30
|
+
else:
|
|
31
|
+
generators = registry.list_all()
|
|
32
|
+
|
|
33
|
+
# Convert to GraphQL types
|
|
34
|
+
result = []
|
|
35
|
+
for gen in generators:
|
|
36
|
+
input_schema_class = gen.get_input_schema()
|
|
37
|
+
|
|
38
|
+
# Convert string artifact_type to enum
|
|
39
|
+
artifact_type_enum = ArtifactType(gen.artifact_type)
|
|
40
|
+
|
|
41
|
+
result.append(
|
|
42
|
+
GeneratorInfo(
|
|
43
|
+
name=gen.name,
|
|
44
|
+
description=gen.description,
|
|
45
|
+
artifact_type=artifact_type_enum,
|
|
46
|
+
input_schema=input_schema_class.model_json_schema(),
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..types.board import Board
|
|
9
|
+
from ..types.user import User
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def resolve_current_user(info: strawberry.Info) -> User | None:
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def resolve_user_by_id(info: strawberry.Info, id: str) -> User | None:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def resolve_user_boards(user: User, info: strawberry.Info) -> list[Board]:
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def resolve_user_member_boards(user: User, info: strawberry.Info) -> list[Board]:
|
|
25
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main GraphQL schema definition using Strawberry
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import strawberry
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
from graphql import validate_schema as gql_validate_schema
|
|
10
|
+
from strawberry.fastapi import GraphQLRouter
|
|
11
|
+
|
|
12
|
+
from ..logging import get_logger
|
|
13
|
+
from .mutations.root import Mutation
|
|
14
|
+
from .queries.root import Query
|
|
15
|
+
|
|
16
|
+
# Import types to ensure they're registered with Strawberry
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
# Create the GraphQL schema
|
|
21
|
+
schema = strawberry.Schema(
|
|
22
|
+
query=Query,
|
|
23
|
+
mutation=Mutation,
|
|
24
|
+
# Note: Introspection is enabled by default in strawberry
|
|
25
|
+
# TODO: Disable in production for security by using extensions
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_schema() -> None:
|
|
30
|
+
"""Validate the GraphQL schema at startup.
|
|
31
|
+
|
|
32
|
+
This ensures that all type references can be resolved and catches
|
|
33
|
+
circular reference errors early, causing the server to fail fast
|
|
34
|
+
rather than returning 404s at runtime.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
Exception: If the schema is invalid or has unresolved types
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Convert to GraphQL core schema to trigger full validation
|
|
41
|
+
graphql_schema = schema._schema
|
|
42
|
+
|
|
43
|
+
# Validate the schema structure
|
|
44
|
+
errors = gql_validate_schema(graphql_schema)
|
|
45
|
+
if errors:
|
|
46
|
+
error_messages = [str(e) for e in errors]
|
|
47
|
+
raise Exception(f"GraphQL schema validation failed: {'; '.join(error_messages)}")
|
|
48
|
+
|
|
49
|
+
# Check that introspection query works (catches most resolution issues)
|
|
50
|
+
from graphql import get_introspection_query, graphql_sync
|
|
51
|
+
|
|
52
|
+
introspection_query = get_introspection_query()
|
|
53
|
+
result = graphql_sync(graphql_schema, introspection_query)
|
|
54
|
+
|
|
55
|
+
if result.errors:
|
|
56
|
+
error_messages = [str(e) for e in result.errors]
|
|
57
|
+
raise Exception(f"GraphQL introspection failed: {'; '.join(error_messages)}")
|
|
58
|
+
|
|
59
|
+
logger.info("GraphQL schema validation successful")
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error("GraphQL schema validation failed", error=str(e))
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Create the GraphQL router for FastAPI integration
|
|
67
|
+
def create_graphql_router() -> GraphQLRouter[dict[str, Any], None]:
|
|
68
|
+
"""Create a GraphQL router for FastAPI."""
|
|
69
|
+
|
|
70
|
+
async def get_context(request: Request) -> dict[str, Any]:
|
|
71
|
+
"""Get the context for GraphQL resolvers."""
|
|
72
|
+
return {
|
|
73
|
+
"request": request,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return GraphQLRouter(
|
|
77
|
+
schema,
|
|
78
|
+
path="/graphql",
|
|
79
|
+
graphiql=True, # Enable GraphiQL IDE in development
|
|
80
|
+
context_getter=get_context,
|
|
81
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Board GraphQL type definitions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import strawberry
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .generation import Generation
|
|
14
|
+
from .user import User
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@strawberry.enum
|
|
18
|
+
class BoardRole(Enum):
|
|
19
|
+
"""Board member role enumeration."""
|
|
20
|
+
|
|
21
|
+
VIEWER = "viewer"
|
|
22
|
+
EDITOR = "editor"
|
|
23
|
+
ADMIN = "admin"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@strawberry.type
|
|
27
|
+
class BoardMember:
|
|
28
|
+
"""Board member type for GraphQL API."""
|
|
29
|
+
|
|
30
|
+
id: UUID
|
|
31
|
+
board_id: UUID
|
|
32
|
+
user_id: UUID
|
|
33
|
+
role: BoardRole
|
|
34
|
+
invited_by: UUID | None
|
|
35
|
+
joined_at: datetime
|
|
36
|
+
|
|
37
|
+
@strawberry.field
|
|
38
|
+
async def user(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
|
|
39
|
+
"""Get the user for this board member."""
|
|
40
|
+
from ..resolvers.board import resolve_board_member_user
|
|
41
|
+
|
|
42
|
+
return await resolve_board_member_user(self, info)
|
|
43
|
+
|
|
44
|
+
@strawberry.field
|
|
45
|
+
async def inviter(
|
|
46
|
+
self, info: strawberry.Info
|
|
47
|
+
) -> Annotated["User", strawberry.lazy(".user")] | None: # noqa: E501
|
|
48
|
+
"""Get the user who invited this member."""
|
|
49
|
+
if not self.invited_by:
|
|
50
|
+
return None
|
|
51
|
+
from ..resolvers.board import resolve_board_member_inviter
|
|
52
|
+
|
|
53
|
+
return await resolve_board_member_inviter(self, info)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@strawberry.type
|
|
57
|
+
class Board:
|
|
58
|
+
"""Board type for GraphQL API."""
|
|
59
|
+
|
|
60
|
+
id: UUID
|
|
61
|
+
tenant_id: UUID
|
|
62
|
+
owner_id: UUID
|
|
63
|
+
title: str
|
|
64
|
+
description: str | None
|
|
65
|
+
is_public: bool
|
|
66
|
+
settings: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
67
|
+
metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
68
|
+
created_at: datetime
|
|
69
|
+
updated_at: datetime
|
|
70
|
+
|
|
71
|
+
@strawberry.field
|
|
72
|
+
async def owner(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
|
|
73
|
+
"""Get the owner of this board."""
|
|
74
|
+
from ..resolvers.board import resolve_board_owner
|
|
75
|
+
|
|
76
|
+
return await resolve_board_owner(self, info)
|
|
77
|
+
|
|
78
|
+
@strawberry.field
|
|
79
|
+
async def members(self, info: strawberry.Info) -> list[BoardMember]:
|
|
80
|
+
"""Get members of this board."""
|
|
81
|
+
from ..resolvers.board import resolve_board_members
|
|
82
|
+
|
|
83
|
+
return await resolve_board_members(self, info)
|
|
84
|
+
|
|
85
|
+
@strawberry.field
|
|
86
|
+
async def generations(
|
|
87
|
+
self,
|
|
88
|
+
info: strawberry.Info,
|
|
89
|
+
limit: int | None = 50,
|
|
90
|
+
offset: int | None = 0,
|
|
91
|
+
) -> list[Annotated["Generation", strawberry.lazy(".generation")]]:
|
|
92
|
+
"""Get generations in this board."""
|
|
93
|
+
from ..resolvers.board import resolve_board_generations
|
|
94
|
+
|
|
95
|
+
return await resolve_board_generations(self, info, limit or 50, offset or 0)
|
|
96
|
+
|
|
97
|
+
@strawberry.field
|
|
98
|
+
async def generation_count(self, info: strawberry.Info) -> int:
|
|
99
|
+
"""Get total number of generations in this board."""
|
|
100
|
+
from ..resolvers.board import resolve_board_generation_count
|
|
101
|
+
|
|
102
|
+
return await resolve_board_generation_count(self, info)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generation GraphQL type definitions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING, Annotated
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import strawberry
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .board import Board
|
|
14
|
+
from .user import User
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@strawberry.enum
|
|
18
|
+
class ArtifactType(Enum):
|
|
19
|
+
"""Artifact type enumeration."""
|
|
20
|
+
|
|
21
|
+
IMAGE = "image"
|
|
22
|
+
VIDEO = "video"
|
|
23
|
+
AUDIO = "audio"
|
|
24
|
+
TEXT = "text"
|
|
25
|
+
LORA = "lora"
|
|
26
|
+
MODEL = "model"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@strawberry.enum
|
|
30
|
+
class GenerationStatus(Enum):
|
|
31
|
+
"""Generation status enumeration."""
|
|
32
|
+
|
|
33
|
+
PENDING = "pending"
|
|
34
|
+
PROCESSING = "processing"
|
|
35
|
+
COMPLETED = "completed"
|
|
36
|
+
FAILED = "failed"
|
|
37
|
+
CANCELLED = "cancelled"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@strawberry.type
|
|
41
|
+
class AdditionalFile:
|
|
42
|
+
"""Additional file associated with a generation."""
|
|
43
|
+
|
|
44
|
+
url: str
|
|
45
|
+
type: str
|
|
46
|
+
metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@strawberry.type
|
|
50
|
+
class Generation:
|
|
51
|
+
"""Generation type for GraphQL API."""
|
|
52
|
+
|
|
53
|
+
id: UUID
|
|
54
|
+
tenant_id: UUID
|
|
55
|
+
board_id: UUID
|
|
56
|
+
user_id: UUID
|
|
57
|
+
|
|
58
|
+
# Generation details
|
|
59
|
+
generator_name: str
|
|
60
|
+
artifact_type: ArtifactType
|
|
61
|
+
|
|
62
|
+
# Storage
|
|
63
|
+
storage_url: str | None
|
|
64
|
+
thumbnail_url: str | None
|
|
65
|
+
additional_files: list[AdditionalFile]
|
|
66
|
+
|
|
67
|
+
# Parameters and metadata
|
|
68
|
+
input_params: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
69
|
+
output_metadata: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
70
|
+
|
|
71
|
+
# Lineage
|
|
72
|
+
parent_generation_id: UUID | None
|
|
73
|
+
input_generation_ids: list[UUID]
|
|
74
|
+
|
|
75
|
+
# Job tracking
|
|
76
|
+
external_job_id: str | None
|
|
77
|
+
status: GenerationStatus
|
|
78
|
+
progress: float
|
|
79
|
+
error_message: str | None
|
|
80
|
+
|
|
81
|
+
# Timestamps
|
|
82
|
+
started_at: datetime | None
|
|
83
|
+
completed_at: datetime | None
|
|
84
|
+
created_at: datetime
|
|
85
|
+
updated_at: datetime
|
|
86
|
+
|
|
87
|
+
@strawberry.field
|
|
88
|
+
async def board(self, info: strawberry.Info) -> Annotated["Board", strawberry.lazy(".board")]:
|
|
89
|
+
"""Get the board this generation belongs to."""
|
|
90
|
+
from ..resolvers.generation import resolve_generation_board
|
|
91
|
+
|
|
92
|
+
return await resolve_generation_board(self, info)
|
|
93
|
+
|
|
94
|
+
@strawberry.field
|
|
95
|
+
async def user(self, info: strawberry.Info) -> Annotated["User", strawberry.lazy(".user")]:
|
|
96
|
+
"""Get the user who created this generation."""
|
|
97
|
+
from ..resolvers.generation import resolve_generation_user
|
|
98
|
+
|
|
99
|
+
return await resolve_generation_user(self, info)
|
|
100
|
+
|
|
101
|
+
@strawberry.field
|
|
102
|
+
async def parent(
|
|
103
|
+
self, info: strawberry.Info
|
|
104
|
+
) -> Annotated["Generation", strawberry.lazy(".generation")] | None: # noqa: E501
|
|
105
|
+
"""Get the parent generation if any."""
|
|
106
|
+
if not self.parent_generation_id:
|
|
107
|
+
return None
|
|
108
|
+
from ..resolvers.generation import resolve_generation_parent
|
|
109
|
+
|
|
110
|
+
return await resolve_generation_parent(self, info)
|
|
111
|
+
|
|
112
|
+
@strawberry.field
|
|
113
|
+
async def inputs(
|
|
114
|
+
self, info: strawberry.Info
|
|
115
|
+
) -> list[Annotated["Generation", strawberry.lazy(".generation")]]: # noqa: E501
|
|
116
|
+
"""Get input generations used for this generation."""
|
|
117
|
+
if not self.input_generation_ids:
|
|
118
|
+
return []
|
|
119
|
+
from ..resolvers.generation import resolve_generation_inputs
|
|
120
|
+
|
|
121
|
+
return await resolve_generation_inputs(self, info)
|
|
122
|
+
|
|
123
|
+
@strawberry.field
|
|
124
|
+
async def children(
|
|
125
|
+
self, info: strawberry.Info
|
|
126
|
+
) -> list[Annotated["Generation", strawberry.lazy(".generation")]]: # noqa: E501
|
|
127
|
+
"""Get child generations derived from this one."""
|
|
128
|
+
from ..resolvers.generation import resolve_generation_children
|
|
129
|
+
|
|
130
|
+
return await resolve_generation_children(self, info)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generator GraphQL type definitions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import strawberry
|
|
6
|
+
|
|
7
|
+
from .generation import ArtifactType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@strawberry.type
|
|
11
|
+
class GeneratorInfo:
|
|
12
|
+
"""Information about an available generator."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
description: str
|
|
16
|
+
artifact_type: ArtifactType
|
|
17
|
+
input_schema: strawberry.scalars.JSON # type: ignore[reportInvalidTypeForm]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User GraphQL type definitions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
import strawberry
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .board import Board
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@strawberry.type
|
|
16
|
+
class User:
|
|
17
|
+
"""User type for GraphQL API."""
|
|
18
|
+
|
|
19
|
+
id: UUID
|
|
20
|
+
tenant_id: UUID
|
|
21
|
+
auth_provider: str
|
|
22
|
+
auth_subject: str
|
|
23
|
+
email: str | None
|
|
24
|
+
display_name: str | None
|
|
25
|
+
avatar_url: str | None
|
|
26
|
+
created_at: datetime
|
|
27
|
+
updated_at: datetime
|
|
28
|
+
|
|
29
|
+
@strawberry.field
|
|
30
|
+
async def boards(
|
|
31
|
+
self, info: strawberry.Info
|
|
32
|
+
) -> list[Annotated["Board", strawberry.lazy(".board")]]: # noqa: E501
|
|
33
|
+
"""Get boards owned by this user."""
|
|
34
|
+
# TODO: Implement data loader
|
|
35
|
+
from ..resolvers.user import resolve_user_boards
|
|
36
|
+
|
|
37
|
+
return await resolve_user_boards(self, info)
|
|
38
|
+
|
|
39
|
+
@strawberry.field
|
|
40
|
+
async def member_boards(
|
|
41
|
+
self, info: strawberry.Info
|
|
42
|
+
) -> list[Annotated["Board", strawberry.lazy(".board")]]: # noqa: E501
|
|
43
|
+
"""Get boards where user is a member."""
|
|
44
|
+
# TODO: Implement data loader
|
|
45
|
+
from ..resolvers.user import resolve_user_member_boards
|
|
46
|
+
|
|
47
|
+
return await resolve_user_member_boards(self, info)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Repository helpers for Generations job lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import select, update
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
from ..dbmodels import Generations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def get_generation(session: AsyncSession, generation_id: str | UUID) -> Generations:
|
|
17
|
+
stmt = select(Generations).where(Generations.id == str(generation_id))
|
|
18
|
+
res = await session.execute(stmt)
|
|
19
|
+
row = res.scalar_one()
|
|
20
|
+
return row
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def update_progress(
|
|
24
|
+
session: AsyncSession,
|
|
25
|
+
generation_id: str | UUID,
|
|
26
|
+
*,
|
|
27
|
+
status: str,
|
|
28
|
+
progress: float,
|
|
29
|
+
error_message: str | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
now = datetime.now(UTC)
|
|
32
|
+
stmt = (
|
|
33
|
+
update(Generations)
|
|
34
|
+
.where(Generations.id == str(generation_id))
|
|
35
|
+
.values(
|
|
36
|
+
status=status,
|
|
37
|
+
progress=progress,
|
|
38
|
+
error_message=error_message,
|
|
39
|
+
updated_at=now,
|
|
40
|
+
started_at=now if status == "processing" else Generations.started_at,
|
|
41
|
+
completed_at=(now if status in {"completed", "failed", "cancelled"} else None),
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
await session.execute(stmt)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def create_generation(
|
|
48
|
+
session: AsyncSession,
|
|
49
|
+
*,
|
|
50
|
+
tenant_id: UUID,
|
|
51
|
+
board_id: UUID,
|
|
52
|
+
user_id: UUID,
|
|
53
|
+
generator_name: str,
|
|
54
|
+
artifact_type: str,
|
|
55
|
+
input_params: dict,
|
|
56
|
+
) -> Generations:
|
|
57
|
+
gen = Generations()
|
|
58
|
+
gen.tenant_id = tenant_id
|
|
59
|
+
gen.board_id = board_id
|
|
60
|
+
gen.user_id = user_id
|
|
61
|
+
gen.generator_name = generator_name
|
|
62
|
+
gen.artifact_type = artifact_type
|
|
63
|
+
gen.input_params = input_params
|
|
64
|
+
gen.status = "pending"
|
|
65
|
+
gen.progress = Decimal(0.0)
|
|
66
|
+
session.add(gen)
|
|
67
|
+
await session.flush()
|
|
68
|
+
return gen
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def set_external_job_id(
|
|
72
|
+
session: AsyncSession, generation_id: str | UUID, external_job_id: str
|
|
73
|
+
) -> None:
|
|
74
|
+
stmt = (
|
|
75
|
+
update(Generations)
|
|
76
|
+
.where(Generations.id == str(generation_id))
|
|
77
|
+
.values(external_job_id=external_job_id)
|
|
78
|
+
)
|
|
79
|
+
await session.execute(stmt)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def finalize_success(
|
|
83
|
+
session: AsyncSession,
|
|
84
|
+
generation_id: str | UUID,
|
|
85
|
+
*,
|
|
86
|
+
storage_url: str | None = None,
|
|
87
|
+
thumbnail_url: str | None = None,
|
|
88
|
+
output_metadata: dict[str, Any] | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
now = datetime.now(UTC)
|
|
91
|
+
stmt = (
|
|
92
|
+
update(Generations)
|
|
93
|
+
.where(Generations.id == str(generation_id))
|
|
94
|
+
.values(
|
|
95
|
+
status="completed",
|
|
96
|
+
progress=100.0,
|
|
97
|
+
storage_url=storage_url,
|
|
98
|
+
thumbnail_url=thumbnail_url,
|
|
99
|
+
output_metadata=output_metadata or {},
|
|
100
|
+
updated_at=now,
|
|
101
|
+
completed_at=now,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
await session.execute(stmt)
|