ref #8: [feat] add http endpoint
This commit is contained in:
parent
bae540427a
commit
d2506e0c63
7 changed files with 87 additions and 20 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
from collections.abc import Callable
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -11,25 +10,22 @@ from domain.error import SandboxError, SandboxStartError
|
||||||
from domain.sandbox import SandboxSession, SandboxStatus
|
from domain.sandbox import SandboxSession, SandboxStatus
|
||||||
from usecase.interface import SandboxRuntime
|
from usecase.interface import SandboxRuntime
|
||||||
|
|
||||||
type NowFactory = Callable[[datetime], datetime]
|
|
||||||
|
|
||||||
|
|
||||||
class DockerSandboxRuntime(SandboxRuntime):
|
class DockerSandboxRuntime(SandboxRuntime):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: SandboxConfig,
|
config: SandboxConfig,
|
||||||
client: DockerClient,
|
client: DockerClient,
|
||||||
now: NowFactory | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
self._client = client
|
self._client = client
|
||||||
self._now = _current_time if now is None else now
|
|
||||||
|
|
||||||
def create(
|
def create(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
|
created_at: datetime,
|
||||||
expires_at: datetime,
|
expires_at: datetime,
|
||||||
) -> SandboxSession:
|
) -> SandboxSession:
|
||||||
try:
|
try:
|
||||||
|
|
@ -59,7 +55,7 @@ class DockerSandboxRuntime(SandboxRuntime):
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
container_id=container_id,
|
container_id=container_id,
|
||||||
status=SandboxStatus.RUNNING,
|
status=SandboxStatus.RUNNING,
|
||||||
created_at=self._now(expires_at),
|
created_at=created_at,
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -128,7 +124,3 @@ class DockerSandboxRuntime(SandboxRuntime):
|
||||||
|
|
||||||
def _host_path(self, path_value: str) -> Path:
|
def _host_path(self, path_value: str) -> Path:
|
||||||
return Path(path_value).expanduser().resolve(strict=False)
|
return Path(path_value).expanduser().resolve(strict=False)
|
||||||
|
|
||||||
|
|
||||||
def _current_time(expires_at: datetime) -> datetime:
|
|
||||||
return datetime.now(tz=expires_at.tzinfo)
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from fastapi import Depends, Request
|
from fastapi import Depends, Request
|
||||||
|
|
||||||
from adapter.di.container import AppContainer
|
from adapter.di.container import AppContainer
|
||||||
|
from usecase.sandbox import CreateSandbox
|
||||||
from usecase.user import GetUser
|
from usecase.user import GetUser
|
||||||
|
|
||||||
APP_CONTAINER_STATE = 'container'
|
APP_CONTAINER_STATE = 'container'
|
||||||
|
|
@ -11,10 +10,16 @@ APP_CONFIG_STATE = 'config'
|
||||||
|
|
||||||
def get_container(request: Request) -> AppContainer:
|
def get_container(request: Request) -> AppContainer:
|
||||||
container = getattr(request.app.state, APP_CONTAINER_STATE, None)
|
container = getattr(request.app.state, APP_CONTAINER_STATE, None)
|
||||||
if container is None:
|
if not isinstance(container, AppContainer):
|
||||||
raise RuntimeError('container unavailable')
|
raise RuntimeError('container unavailable')
|
||||||
return cast(AppContainer, container)
|
return container
|
||||||
|
|
||||||
|
|
||||||
def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser:
|
def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser:
|
||||||
return container.usecases.get_user
|
return container.usecases.get_user
|
||||||
|
|
||||||
|
|
||||||
|
def get_create_sandbox(
|
||||||
|
container: AppContainer = Depends(get_container),
|
||||||
|
) -> CreateSandbox:
|
||||||
|
return container.usecases.create_sandbox
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,21 @@
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from adapter.di.container import AppContainer
|
from adapter.di.container import AppContainer
|
||||||
from adapter.http.fastapi.dependencies import get_container, get_get_user
|
from adapter.http.fastapi.dependencies import (
|
||||||
from adapter.http.fastapi.schemas import ErrorResponse, HealthResponse, UserResponse
|
get_container,
|
||||||
from domain.error import UserNotFoundError
|
get_create_sandbox,
|
||||||
|
get_get_user,
|
||||||
|
)
|
||||||
|
from adapter.http.fastapi.schemas import (
|
||||||
|
CreateSandboxRequest,
|
||||||
|
ErrorResponse,
|
||||||
|
HealthResponse,
|
||||||
|
SandboxSessionResponse,
|
||||||
|
UserResponse,
|
||||||
|
)
|
||||||
|
from domain.error import SandboxError, SandboxStartError, UserNotFoundError
|
||||||
|
from domain.sandbox import SandboxSession
|
||||||
|
from usecase.sandbox import CreateSandbox, CreateSandboxCommand
|
||||||
from usecase.user import GetUser, GetUserQuery
|
from usecase.user import GetUser, GetUserQuery
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -38,3 +50,42 @@ def get_user(user_id: str, usecase: GetUser = Depends(get_get_user)) -> UserResp
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
return UserResponse(id=user.id, email=user.email, name=user.name)
|
return UserResponse(id=user.id, email=user.email, name=user.name)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
'/create',
|
||||||
|
response_model=SandboxSessionResponse,
|
||||||
|
responses={
|
||||||
|
status.HTTP_503_SERVICE_UNAVAILABLE: {'model': ErrorResponse},
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': ErrorResponse},
|
||||||
|
},
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
def create_sandbox(
|
||||||
|
request: CreateSandboxRequest,
|
||||||
|
usecase: CreateSandbox = Depends(get_create_sandbox),
|
||||||
|
) -> SandboxSessionResponse:
|
||||||
|
try:
|
||||||
|
session = usecase.execute(CreateSandboxCommand(chat_id=request.chat_id))
|
||||||
|
except SandboxStartError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail=str(exc),
|
||||||
|
) from exc
|
||||||
|
except SandboxError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(exc),
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
return _to_sandbox_session_response(session)
|
||||||
|
|
||||||
|
|
||||||
|
def _to_sandbox_session_response(session: SandboxSession) -> SandboxSessionResponse:
|
||||||
|
return SandboxSessionResponse(
|
||||||
|
session_id=session.session_id,
|
||||||
|
chat_id=session.chat_id,
|
||||||
|
container_id=session.container_id,
|
||||||
|
status=session.status.value,
|
||||||
|
expires_at=session.expires_at,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from pydantic import BaseModel
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
class HealthResponse(BaseModel):
|
class HealthResponse(BaseModel):
|
||||||
|
|
@ -7,6 +9,20 @@ class HealthResponse(BaseModel):
|
||||||
env: str
|
env: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSandboxRequest(BaseModel):
|
||||||
|
model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
|
||||||
|
|
||||||
|
chat_id: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class SandboxSessionResponse(BaseModel):
|
||||||
|
session_id: str
|
||||||
|
chat_id: str
|
||||||
|
container_id: str
|
||||||
|
status: str
|
||||||
|
expires_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
email: str
|
email: str
|
||||||
|
|
|
||||||
2
tasks.md
2
tasks.md
|
|
@ -86,7 +86,7 @@
|
||||||
### M06. HTTP endpoint `POST /api/v1/create`
|
### M06. HTTP endpoint `POST /api/v1/create`
|
||||||
|
|
||||||
- Субагент: `feature-developer`
|
- Субагент: `feature-developer`
|
||||||
- Статус: pending
|
- Статус: completed
|
||||||
- Зависимости: `M04`
|
- Зависимости: `M04`
|
||||||
- Commit required: no
|
- Commit required: no
|
||||||
- Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth
|
- Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class SandboxRuntime(Protocol):
|
||||||
*,
|
*,
|
||||||
session_id: str,
|
session_id: str,
|
||||||
chat_id: str,
|
chat_id: str,
|
||||||
|
created_at: datetime,
|
||||||
expires_at: datetime,
|
expires_at: datetime,
|
||||||
) -> SandboxSession: ...
|
) -> SandboxSession: ...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,12 @@ class CreateSandbox:
|
||||||
self._runtime.stop(session.container_id)
|
self._runtime.stop(session.container_id)
|
||||||
self._repository.delete(session.session_id)
|
self._repository.delete(session.session_id)
|
||||||
|
|
||||||
|
expires_at = now + self._ttl
|
||||||
new_session = self._runtime.create(
|
new_session = self._runtime.create(
|
||||||
session_id=_new_session_id(),
|
session_id=_new_session_id(),
|
||||||
chat_id=command.chat_id,
|
chat_id=command.chat_id,
|
||||||
expires_at=now + self._ttl,
|
created_at=now,
|
||||||
|
expires_at=expires_at,
|
||||||
)
|
)
|
||||||
self._repository.save(new_session)
|
self._repository.save(new_session)
|
||||||
self._logger.info(
|
self._logger.info(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue