diff --git a/griptapecli/commands/skatepark.py b/griptapecli/commands/skatepark.py index 3477602..8e7a295 100644 --- a/griptapecli/commands/skatepark.py +++ b/griptapecli/commands/skatepark.py @@ -65,7 +65,13 @@ def start( port: int, ) -> None: """Starts the Griptape server.""" - uvicorn.run("griptapecli.core.skatepark:app", host=host, port=port, reload=False) + uvicorn.run( + "griptapecli.core.skatepark:app", + host=host, + port=port, + reload=False, + workers=1, # Skatepark only supports 1 worker. We're explictly setting it here to avoid inheriting it from the environment. + ) @skatepark.command(name="register") diff --git a/griptapecli/core/models.py b/griptapecli/core/models.py index 3957086..f9e2ee3 100644 --- a/griptapecli/core/models.py +++ b/griptapecli/core/models.py @@ -12,7 +12,7 @@ class Event(BaseModel): value: dict = Field() -class Run(BaseModel): +class StructureRun(BaseModel): class Status(Enum): RUNNING = "RUNNING" COMPLETED = "COMPLETED" @@ -40,3 +40,15 @@ def structure_id(self) -> str: path = f"{self.directory}/{self.main_file}" return uuid.uuid5(uuid.NAMESPACE_URL, path).hex + + +class ListStructuresResponseModel(BaseModel): + structures: list[Structure] = Field(default_factory=lambda: []) + + +class ListStructureRunsResponseModel(BaseModel): + structure_runs: list[StructureRun] = Field(default_factory=lambda: []) + + +class ListStructureRunEventsResponseModel(BaseModel): + events: list[Event] = Field(default_factory=lambda: []) diff --git a/griptapecli/core/skatepark.py b/griptapecli/core/skatepark.py index 262e280..c5a6b6f 100644 --- a/griptapecli/core/skatepark.py +++ b/griptapecli/core/skatepark.py @@ -5,13 +5,17 @@ import subprocess from dotenv import dotenv_values -from fastapi import FastAPI, HTTPException - -from .models import Event, Run, Structure +from fastapi import FastAPI, HTTPException, status +from .models import ( + Event, + StructureRun, + Structure, + ListStructuresResponseModel, + ListStructureRunsResponseModel, + ListStructureRunEventsResponseModel, +) from .state import State, RunProcess -import asyncio - app = FastAPI() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -20,7 +24,7 @@ state = State() -@app.post("/api/structures") +@app.post("/api/structures", status_code=status.HTTP_201_CREATED) def create_structure(structure: Structure) -> Structure: logger.info(f"Creating structure: {structure}") @@ -30,26 +34,30 @@ def create_structure(structure: Structure) -> Structure: except HTTPException as e: state.remove_structure(structure.structure_id) - raise HTTPException(status_code=400, detail=str(e)) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) return structure -@app.get("/api/structures") -def list_structures() -> list[Structure]: +@app.get( + "/api/structures", + response_model=ListStructuresResponseModel, + status_code=status.HTTP_200_OK, +) +def list_structures(): logger.info("Listing structures") - return list(state.structures.values()) + return {"structures": list(state.structures.values())} -@app.delete("/api/structures/{structure_id}") -def delete_structure(structure_id: str) -> str: +@app.delete("/api/structures/{structure_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_structure(structure_id: str): logger.info(f"Deleting structure: {structure_id}") - return state.remove_structure(structure_id) + state.remove_structure(structure_id) -@app.post("/api/structures/{structure_id}/build") +@app.post("/api/structures/{structure_id}/build", status_code=status.HTTP_201_CREATED) def build_structure(structure_id: str) -> Structure: logger.info(f"Building structure: {structure_id}") structure = state.get_structure(structure_id) @@ -69,8 +77,8 @@ def build_structure(structure_id: str) -> Structure: return structure -@app.post("/api/structures/{structure_id}/runs") -def create_structure_run(structure_id: str, run: Run) -> Run: +@app.post("/api/structures/{structure_id}/runs", status_code=status.HTTP_201_CREATED) +def create_structure_run(structure_id: str, run: StructureRun) -> StructureRun: logger.info(f"Creating run for structure: {structure_id}") structure = state.get_structure(structure_id) @@ -93,28 +101,35 @@ def create_structure_run(structure_id: str, run: Run) -> Run: return run -@app.get("/api/structures/{structure_id}/runs") -def list_structure_runs(structure_id: str) -> list[Run]: +@app.get( + "/api/structures/{structure_id}/runs", + response_model=ListStructureRunsResponseModel, + status_code=status.HTTP_200_OK, +) +def list_structure_runs(structure_id: str): logger.info(f"Listing runs for structure: {structure_id}") - return [ - run.run - for run in state.runs.values() - if run.run.structure.structure_id == structure_id - ] + return { + "structure_runs": [ + run.run + for run in state.runs.values() + if run.run.structure.structure_id == structure_id + ] + } -@app.patch("/api/structure-runs/{run_id}") -def patch_run(run_id: str, values: dict) -> Run: + +@app.patch("/api/structure-runs/{run_id}", status_code=status.HTTP_200_OK) +def patch_run(run_id: str, values: dict) -> StructureRun: logger.info(f"Patching run: {run_id}") cur_run = state.runs[run_id].run.model_dump() - new_run = Run(**(cur_run | values)) + new_run = StructureRun(**(cur_run | values)) state.runs[run_id].run = new_run return new_run -@app.get("/api/structure-runs/{run_id}") -def get_run(run_id: str) -> Run: +@app.get("/api/structure-runs/{run_id}", status_code=status.HTTP_200_OK) +def get_run(run_id: str) -> StructureRun: logger.info(f"Getting run: {run_id}") run = state.runs[run_id] @@ -124,7 +139,7 @@ def get_run(run_id: str) -> Run: return state.runs[run_id].run -@app.post("/api/structure-runs/{run_id}/events") +@app.post("/api/structure-runs/{run_id}/events", status_code=status.HTTP_201_CREATED) def create_run_event(run_id: str, event_value: dict) -> Event: logger.info(f"Creating event for run: {run_id}") event = Event(value=event_value) @@ -138,15 +153,24 @@ def create_run_event(run_id: str, event_value: dict) -> Event: return event -@app.get("/api/structure-runs/{run_id}/events") -def get_run_events(run_id: str) -> list[Event]: +@app.get( + "/api/structure-runs/{run_id}/events", + status_code=status.HTTP_200_OK, + response_model=ListStructureRunEventsResponseModel, +) +def list_run_events(run_id: str): logger.info(f"Getting events for run: {run_id}") - return sorted( - state.runs[run_id].run.events, key=lambda event: event.value["timestamp"] - ) + + events = state.runs[run_id].run.events + + sorted_events = sorted(events, key=lambda event: event.value["timestamp"]) + + return { + "events": sorted_events, + } -def _validate_files(structure: Structure): +def _validate_files(structure: Structure) -> None: if not os.path.exists(structure.directory): raise HTTPException(status_code=400, detail="Directory does not exist") @@ -171,9 +195,9 @@ def _check_run_process(run_process: RunProcess) -> RunProcess: if return_code is not None: stdout, stderr = run_process.process.communicate() if return_code == 0: - run_process.run.status = Run.Status.COMPLETED + run_process.run.status = StructureRun.Status.COMPLETED else: - run_process.run.status = Run.Status.FAILED + run_process.run.status = StructureRun.Status.FAILED run_process.run.stdout = stdout run_process.run.stderr = stderr diff --git a/griptapecli/core/state.py b/griptapecli/core/state.py index 16c95b5..cdfd6b1 100644 --- a/griptapecli/core/state.py +++ b/griptapecli/core/state.py @@ -4,12 +4,12 @@ from fastapi import HTTPException from subprocess import Popen -from .models import Run, Structure +from .models import StructureRun, Structure @define class RunProcess: - run: Run = field() + run: StructureRun = field() process: Popen = field() diff --git a/tests/unit/core/test_models.py b/tests/unit/core/test_models.py index cc6282e..556ddaf 100644 --- a/tests/unit/core/test_models.py +++ b/tests/unit/core/test_models.py @@ -1,4 +1,4 @@ -from griptapecli.core.models import Event, Run, Structure +from griptapecli.core.models import Event, StructureRun, Structure class TestModels: @@ -6,7 +6,7 @@ def test_event_model_init(self): assert Event(value={}) def test_run_model_init(self): - assert Run() + assert StructureRun() def test_structure_model_init(self): assert Structure(