board CRUD

This commit is contained in:
maryhipp 2023-06-13 11:51:20 -07:00 committed by psychedelicious
parent 257e972599
commit a1671519d5
6 changed files with 262 additions and 2 deletions

View File

@ -2,6 +2,7 @@
from logging import Logger from logging import Logger
import os import os
from invokeai.app.services import boards
from invokeai.app.services.image_record_storage import SqliteImageRecordStorage from invokeai.app.services.image_record_storage import SqliteImageRecordStorage
from invokeai.app.services.images import ImageService from invokeai.app.services.images import ImageService
from invokeai.app.services.metadata import CoreMetadataService from invokeai.app.services.metadata import CoreMetadataService
@ -20,6 +21,7 @@ from ..services.invoker import Invoker
from ..services.processor import DefaultInvocationProcessor from ..services.processor import DefaultInvocationProcessor
from ..services.sqlite import SqliteItemStorage from ..services.sqlite import SqliteItemStorage
from ..services.model_manager_service import ModelManagerService from ..services.model_manager_service import ModelManagerService
from ..services.boards import SqliteBoardStorage
from .events import FastAPIEventService from .events import FastAPIEventService
@ -71,6 +73,7 @@ class ApiDependencies:
latents = ForwardCacheLatentsStorage( latents = ForwardCacheLatentsStorage(
DiskLatentsStorage(f"{output_folder}/latents") DiskLatentsStorage(f"{output_folder}/latents")
) )
boards = SqliteBoardStorage(db_location)
images = ImageService( images = ImageService(
image_record_storage=image_record_storage, image_record_storage=image_record_storage,
@ -96,6 +99,7 @@ class ApiDependencies:
restoration=RestorationServices(config, logger), restoration=RestorationServices(config, logger),
configuration=config, configuration=config,
logger=logger, logger=logger,
boards=boards
) )
create_system_graphs(services.graph_library) create_system_graphs(services.graph_library)

View File

@ -0,0 +1,77 @@
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from invokeai.app.services.boards import BoardRecord, BoardRecordChanges
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from ..dependencies import ApiDependencies
boards_router = APIRouter(prefix="/v1/boards", tags=["boards"])
@boards_router.post(
"/",
operation_id="create_board",
responses={
201: {"description": "The board was created successfully"},
},
status_code=201,
)
async def create_board(
board_name: str = Body(description="The name of the board to create"),
):
"""Creates a board"""
try:
ApiDependencies.invoker.services.boards.save(board_name=board_name)
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to create board")
@boards_router.delete("/{board_id}", operation_id="delete_board")
async def delete_board(
board_id: str = Path(description="The id of board to delete"),
) -> None:
"""Deletes a board"""
try:
ApiDependencies.invoker.services.boards.delete(board_id=board_id)
except Exception as e:
# TODO: Does this need any exception handling at all?
pass
@boards_router.patch(
"/{board_id}",
operation_id="update_baord"
)
async def update_baord(
id: str = Path(description="The id of the board to update"),
board_changes: BoardRecordChanges = Body(
description="The changes to apply to the board"
),
):
"""Updates a board"""
try:
return ApiDependencies.invoker.services.boards.update(
id, board_changes
)
except Exception as e:
raise HTTPException(status_code=400, detail="Failed to update board")
@boards_router.get(
"/",
operation_id="list_boards",
response_model=OffsetPaginatedResults[BoardRecord],
)
async def list_boards(
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of boards per page"),
) -> OffsetPaginatedResults[BoardRecord]:
"""Gets a list of boards"""
boards = ApiDependencies.invoker.services.boards.get_many(
offset,
limit,
)
return boards

View File

@ -24,7 +24,7 @@ logger = InvokeAILogger.getLogger(config=app_config)
import invokeai.frontend.web as web_dir import invokeai.frontend.web as web_dir
from .api.dependencies import ApiDependencies from .api.dependencies import ApiDependencies
from .api.routers import sessions, models, images from .api.routers import sessions, models, images, boards
from .api.sockets import SocketIO from .api.sockets import SocketIO
from .invocations.baseinvocation import BaseInvocation from .invocations.baseinvocation import BaseInvocation
@ -78,6 +78,8 @@ app.include_router(models.models_router, prefix="/api")
app.include_router(images.images_router, prefix="/api") app.include_router(images.images_router, prefix="/api")
app.include_router(boards.boards_router, prefix="/api")
# Build a custom OpenAPI to include all outputs # Build a custom OpenAPI to include all outputs
# TODO: can outputs be included on metadata of invocation schemas somehow? # TODO: can outputs be included on metadata of invocation schemas somehow?
def custom_openapi(): def custom_openapi():

View File

@ -0,0 +1,172 @@
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Generic, Optional, TypeVar, cast
import sqlite3
import threading
from typing import Optional, Union
import uuid
from invokeai.app.services.image_record_storage import OffsetPaginatedResults
from pydantic import BaseModel, Field, Extra
from pydantic.generics import GenericModel
T = TypeVar("T", bound=BaseModel)
class BoardRecord(BaseModel):
"""Deserialized board record."""
id: str = Field(description="The unique ID of the board.")
name: str = Field(description="The name of the board.")
"""The name of the board."""
created_at: Union[datetime, str] = Field(
description="The created timestamp of the board."
)
"""The created timestamp of the image."""
updated_at: Union[datetime, str] = Field(
description="The updated timestamp of the board."
)
class BoardRecordChanges(BaseModel, extra=Extra.forbid):
name: Optional[str] = Field(
description="The board's new name."
)
class BoardRecordNotFoundException(Exception):
"""Raised when an board record is not found."""
def __init__(self, message="Board record not found"):
super().__init__(message)
class BoardRecordSaveException(Exception):
"""Raised when an board record cannot be saved."""
def __init__(self, message="Board record not saved"):
super().__init__(message)
class BoardRecordDeleteException(Exception):
"""Raised when an board record cannot be deleted."""
def __init__(self, message="Board record not deleted"):
super().__init__(message)
class BoardStorageBase(ABC):
"""Low-level service responsible for interfacing with the board record store."""
@abstractmethod
def get(self, board_id: str) -> BoardRecord:
"""Gets an board record."""
pass
@abstractmethod
def delete(self, board_id: str) -> None:
"""Deletes a board record."""
pass
@abstractmethod
def save(
self,
board_name: str,
):
"""Saves a board record."""
pass
class SqliteBoardStorage(BoardStorageBase):
_filename: str
_conn: sqlite3.Connection
_cursor: sqlite3.Cursor
_lock: threading.Lock
def __init__(self, filename: str) -> None:
super().__init__()
self._filename = filename
self._conn = sqlite3.connect(filename, check_same_thread=False)
# Enable row factory to get rows as dictionaries (must be done before making the cursor!)
self._conn.row_factory = sqlite3.Row
self._cursor = self._conn.cursor()
self._lock = threading.Lock()
try:
self._lock.acquire()
# Enable foreign keys
self._conn.execute("PRAGMA foreign_keys = ON;")
self._create_tables()
self._conn.commit()
finally:
self._lock.release()
def _create_tables(self) -> None:
"""Creates the `board` table."""
# Create the `images` table.
self._cursor.execute(
"""--sql
CREATE TABLE IF NOT EXISTS boards (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
-- Updated via trigger
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
);
"""
)
self._cursor.execute(
"""--sql
CREATE INDEX IF NOT EXISTS idx_boards_created_at ON boards(created_at);
"""
)
# Add trigger for `updated_at`.
self._cursor.execute(
"""--sql
CREATE TRIGGER IF NOT EXISTS tg_boards_updated_at
AFTER UPDATE
ON boards FOR EACH ROW
BEGIN
UPDATE boards SET updated_at = current_timestamp
WHERE board_name = old.board_name;
END;
"""
)
def delete(self, board_id: str) -> None:
try:
self._lock.acquire()
self._cursor.execute(
"""--sql
DELETE FROM boards
WHERE id = ?;
""",
(board_id),
)
self._conn.commit()
except sqlite3.Error as e:
self._conn.rollback()
raise BoardRecordDeleteException from e
finally:
self._lock.release()
def save(
self,
board_name: str,
):
try:
board_id = str(uuid.uuid4())
self._lock.acquire()
self._cursor.execute(
"""--sql
INSERT OR IGNORE INTO boards (id, name)
VALUES (?, ?);
""",
(board_id, board_name),
)
self._conn.commit()
except sqlite3.Error as e:
self._conn.rollback()
raise BoardRecordSaveException from e
finally:
self._lock.release()

View File

@ -135,7 +135,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
self._lock.release() self._lock.release()
def _create_tables(self) -> None: def _create_tables(self) -> None:
"""Creates the tables for the `images` database.""" """Creates the `images` table."""
# Create the `images` table. # Create the `images` table.
self._cursor.execute( self._cursor.execute(
@ -152,6 +152,7 @@ class SqliteImageRecordStorage(ImageRecordStorageBase):
node_id TEXT, node_id TEXT,
metadata TEXT, metadata TEXT,
is_intermediate BOOLEAN DEFAULT FALSE, is_intermediate BOOLEAN DEFAULT FALSE,
board_id TEXT,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
-- Updated via trigger -- Updated via trigger
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),

View File

@ -14,6 +14,7 @@ if TYPE_CHECKING:
from invokeai.app.services.config import InvokeAISettings from invokeai.app.services.config import InvokeAISettings
from invokeai.app.services.graph import GraphExecutionState, LibraryGraph from invokeai.app.services.graph import GraphExecutionState, LibraryGraph
from invokeai.app.services.invoker import InvocationProcessorABC from invokeai.app.services.invoker import InvocationProcessorABC
from invokeai.app.services.boards import BoardStorageBase
class InvocationServices: class InvocationServices:
@ -27,6 +28,7 @@ class InvocationServices:
restoration: "RestorationServices" restoration: "RestorationServices"
configuration: "InvokeAISettings" configuration: "InvokeAISettings"
images: "ImageService" images: "ImageService"
boards: "BoardStorageBase"
# NOTE: we must forward-declare any types that include invocations, since invocations can use services # NOTE: we must forward-declare any types that include invocations, since invocations can use services
graph_library: "ItemStorageABC"["LibraryGraph"] graph_library: "ItemStorageABC"["LibraryGraph"]
@ -46,6 +48,7 @@ class InvocationServices:
processor: "InvocationProcessorABC", processor: "InvocationProcessorABC",
restoration: "RestorationServices", restoration: "RestorationServices",
configuration: "InvokeAISettings", configuration: "InvokeAISettings",
boards: "BoardStorageBase",
): ):
self.model_manager = model_manager self.model_manager = model_manager
self.events = events self.events = events
@ -58,3 +61,4 @@ class InvocationServices:
self.processor = processor self.processor = processor
self.restoration = restoration self.restoration = restoration
self.configuration = configuration self.configuration = configuration
self.boards = boards