refactor: extract Pydantic schemas into schemas/ subpackages
Extract inline Pydantic models from 9 API routers into dedicated schema modules under schemas/: - schemas/tool/tool_type.py — ToolTypeCreate, ToolTypeUpdate, etc. - schemas/tool/tool_instance.py — CreateInstanceRequest, StartInstanceRequest - schemas/config/config_profile.py — ConfigProfileCreate, ConfigProfileUpdate, ConfigProfileResponse, DefaultProfilesUpdate, ValidateGitUrlRequest, etc. - schemas/system/health.py — DatabaseHealth, DiskHealth, HealthResponse, etc. - schemas/user/user.py — UserProfileResponse, UserProfileUpdate - schemas/user/user_config.py — UserConfigResponse, UserConfigUpdate - schemas/project/project.py — ProjectCreate, ProjectUpdate, etc. - schemas/project/ssh_key.py — SSHKeyCreate, SSHKeyResponse, etc. - schemas/project/git_repository.py — GitRepositoryCreate, etc. API routers now import from src.schemas.* instead of defining inline. Net change: -548 lines across 16 files. Quality gates: py_compile passed, ruff passed on all 18 files.
This commit is contained in:
@@ -7,17 +7,24 @@ import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.api.shared_validators import validate_env_vars as _validate_env_vars
|
||||
from src.auth.dependencies import get_current_user_id, get_db_session
|
||||
from src.models import ConfigProfile, ConfigProfileInclude
|
||||
from src.models.project import Project
|
||||
from src.models import ToolType
|
||||
from src.models import UserConfig
|
||||
from src.schemas.config import (
|
||||
ConfigProfileCreate,
|
||||
ConfigProfileIncludeUpdate,
|
||||
ConfigProfileResponse,
|
||||
ConfigProfileUpdate,
|
||||
DefaultProfilesUpdate,
|
||||
ValidateGitUrlRequest,
|
||||
ValidateGitUrlResponse,
|
||||
)
|
||||
from src.services.config_profile_resolver import (
|
||||
ConfigProfileCycleError,
|
||||
check_include_cycle,
|
||||
@@ -34,16 +41,6 @@ MAX_PROFILE_SIZE_MB = 10
|
||||
MAX_PROFILE_SIZE_BYTES = MAX_PROFILE_SIZE_MB * 1024 * 1024
|
||||
|
||||
|
||||
def _validate_uuid(v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
try:
|
||||
uuid.UUID(v)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid UUID: {v}")
|
||||
return v
|
||||
|
||||
|
||||
def _calculate_profile_size(data: dict) -> int:
|
||||
"""Calculate approximate serialized size of profile data."""
|
||||
total = 0
|
||||
@@ -61,247 +58,6 @@ def _calculate_profile_size(data: dict) -> int:
|
||||
return total
|
||||
|
||||
|
||||
class GitMountMapping(BaseModel):
|
||||
source_path: str = Field(
|
||||
description="Path within repository (supports glob patterns)"
|
||||
)
|
||||
target_path: str = Field(description="Absolute path inside container")
|
||||
|
||||
@field_validator("source_path")
|
||||
@classmethod
|
||||
def validate_source_path(cls, v: str) -> str:
|
||||
if v.startswith("/"):
|
||||
raise ValueError("source_path must be relative (no leading /)")
|
||||
if ".." in v:
|
||||
raise ValueError("source_path cannot contain path traversal (..)")
|
||||
return v
|
||||
|
||||
@field_validator("target_path")
|
||||
@classmethod
|
||||
def validate_target_path(cls, v: str) -> str:
|
||||
if ".." in v:
|
||||
raise ValueError("target_path cannot contain path traversal (..)")
|
||||
return v
|
||||
|
||||
|
||||
class GitMountItem(BaseModel):
|
||||
remote_url: str = Field(description="Git remote URL (HTTPS or SSH)")
|
||||
source_path: str | None = Field(
|
||||
default=None, description="Path within repository (legacy single mapping)"
|
||||
)
|
||||
target_path: str | None = Field(
|
||||
default=None,
|
||||
description="Absolute path inside container (legacy single mapping)",
|
||||
)
|
||||
branch: str | None = Field(default=None, description="Optional branch or tag name")
|
||||
mappings: list[GitMountMapping] | None = Field(
|
||||
default=None, description="Multiple source/target mappings from the same repo"
|
||||
)
|
||||
|
||||
@field_validator("remote_url")
|
||||
@classmethod
|
||||
def validate_remote_url(cls, v: str) -> str:
|
||||
if not v.startswith(("http://", "https://", "git@", "ssh://")):
|
||||
raise ValueError(
|
||||
"remote_url must be a valid git URL (https://, git@, or ssh://)"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("source_path")
|
||||
@classmethod
|
||||
def validate_source_path(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v.startswith("/"):
|
||||
raise ValueError("source_path must be relative (no leading /)")
|
||||
if ".." in v:
|
||||
raise ValueError("source_path cannot contain path traversal (..)")
|
||||
return v
|
||||
|
||||
@field_validator("target_path")
|
||||
@classmethod
|
||||
def validate_target_path(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if ".." in v:
|
||||
raise ValueError("target_path cannot contain path traversal (..)")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_mappings_or_legacy(self):
|
||||
has_legacy = self.source_path is not None and self.target_path is not None
|
||||
has_mappings = self.mappings is not None and len(self.mappings) > 0
|
||||
if not has_legacy and not has_mappings:
|
||||
raise ValueError(
|
||||
"Git mount must have either 'mappings' (non-empty array) or both 'source_path' and 'target_path'"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class MountItem(BaseModel):
|
||||
target: str = Field(description="Absolute mount target path")
|
||||
mode: str = Field(default="rw", description="Mount mode: ro or rw")
|
||||
files: dict = Field(
|
||||
default_factory=dict, description="Files as {relative_path: content}"
|
||||
)
|
||||
|
||||
@field_validator("target")
|
||||
@classmethod
|
||||
def validate_target(cls, v: str) -> str:
|
||||
if not v.startswith("/"):
|
||||
raise ValueError("Mount target must be absolute (start with /)")
|
||||
return v
|
||||
|
||||
@field_validator("mode")
|
||||
@classmethod
|
||||
def validate_mode(cls, v: str) -> str:
|
||||
if v not in ("ro", "rw"):
|
||||
raise ValueError("Mount mode must be 'ro' or 'rw'")
|
||||
return v
|
||||
|
||||
@field_validator("files")
|
||||
@classmethod
|
||||
def validate_files(cls, v: dict) -> dict:
|
||||
for path in v.keys():
|
||||
if ".." in path or not path:
|
||||
raise ValueError(f"Invalid file path: {path}")
|
||||
if path.startswith("/"):
|
||||
raise ValueError(
|
||||
f"Mount file paths must be relative (got: {path}). "
|
||||
f"The mount target defines the absolute container path."
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class ConfigProfileCreate(BaseModel):
|
||||
name: str = Field(description="Profile name (unique per user)")
|
||||
description: str | None = Field(default=None, description="Optional description")
|
||||
project_id: str | None = Field(default=None, description="Optional project ID")
|
||||
tool_type_id: str | None = Field(default=None, description="Optional tool type ID")
|
||||
env_vars: dict = Field(default_factory=dict, description="Environment variables")
|
||||
runtime_hints: dict = Field(default_factory=dict, description="Runtime hints")
|
||||
mounts: list[MountItem] = Field(
|
||||
default_factory=list, description="Mount definitions"
|
||||
)
|
||||
files: dict = Field(
|
||||
default_factory=dict, description="Files as {relative_path: content}"
|
||||
)
|
||||
git_mounts: list[GitMountItem] = Field(
|
||||
default_factory=list, description="Git repository mounts"
|
||||
)
|
||||
is_default: bool = Field(
|
||||
default=False, description="Whether this is the default profile for its scope"
|
||||
)
|
||||
|
||||
@field_validator("project_id", "tool_type_id")
|
||||
@classmethod
|
||||
def validate_uuids(cls, v: str | None) -> str | None:
|
||||
return _validate_uuid(v)
|
||||
|
||||
@field_validator("files")
|
||||
@classmethod
|
||||
def validate_files(cls, v: dict) -> dict:
|
||||
for path in v.keys():
|
||||
if ".." in path or not path:
|
||||
raise ValueError(f"Invalid file path: {path}")
|
||||
if path.startswith("/"):
|
||||
raise ValueError(
|
||||
f"File paths must be relative (got: {path}). "
|
||||
f"Use Mounts for absolute container paths."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("env_vars")
|
||||
@classmethod
|
||||
def validate_env_vars(cls, v: dict) -> dict:
|
||||
result = _validate_env_vars(v)
|
||||
if result is None:
|
||||
raise ValueError("env_vars must be a JSON object")
|
||||
return result
|
||||
|
||||
@field_validator("runtime_hints")
|
||||
@classmethod
|
||||
def validate_runtime_hints(cls, v: dict) -> dict:
|
||||
if not isinstance(v, dict):
|
||||
raise ValueError("runtime_hints must be a JSON object")
|
||||
return v
|
||||
|
||||
@field_validator("mounts")
|
||||
@classmethod
|
||||
def validate_mounts(cls, v: list) -> list:
|
||||
if not isinstance(v, list):
|
||||
raise ValueError("mounts must be a JSON array")
|
||||
return v
|
||||
|
||||
|
||||
class ConfigProfileUpdate(BaseModel):
|
||||
name: str | None = Field(default=None, description="Profile name")
|
||||
description: str | None = Field(default=None, description="Optional description")
|
||||
project_id: str | None = Field(default=None, description="Optional project ID")
|
||||
tool_type_id: str | None = Field(default=None, description="Optional tool type ID")
|
||||
env_vars: dict | None = Field(default=None, description="Environment variables")
|
||||
runtime_hints: dict | None = Field(default=None, description="Runtime hints")
|
||||
mounts: list[MountItem] | None = Field(
|
||||
default=None, description="Mount definitions"
|
||||
)
|
||||
files: dict | None = Field(
|
||||
default=None, description="Files as {relative_path: content}"
|
||||
)
|
||||
git_mounts: list[GitMountItem] | None = Field(
|
||||
default=None, description="Git repository mounts"
|
||||
)
|
||||
is_default: bool | None = Field(
|
||||
default=None, description="Whether this is the default profile"
|
||||
)
|
||||
|
||||
@field_validator("project_id", "tool_type_id")
|
||||
@classmethod
|
||||
def validate_uuids(cls, v: str | None) -> str | None:
|
||||
return _validate_uuid(v)
|
||||
|
||||
@field_validator("files")
|
||||
@classmethod
|
||||
def validate_files(cls, v: dict | None) -> dict | None:
|
||||
if v is None:
|
||||
return v
|
||||
for path in v.keys():
|
||||
if ".." in path or path.startswith("/") or not path:
|
||||
raise ValueError(f"Invalid file path: {path}")
|
||||
return v
|
||||
|
||||
|
||||
class ConfigProfileIncludeUpdate(BaseModel):
|
||||
includes: list[str] = Field(description="Ordered list of included profile IDs")
|
||||
|
||||
@field_validator("includes")
|
||||
@classmethod
|
||||
def validate_includes(cls, v: list) -> list:
|
||||
for item in v:
|
||||
try:
|
||||
uuid.UUID(item)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid UUID in includes: {item}")
|
||||
return v
|
||||
|
||||
|
||||
class ConfigProfileResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
name: str
|
||||
description: str | None
|
||||
project_id: str | None
|
||||
tool_type_id: str | None
|
||||
env_vars: dict
|
||||
runtime_hints: dict
|
||||
mounts: list
|
||||
files: dict
|
||||
git_mounts: list
|
||||
is_default: bool
|
||||
includes: list[dict]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
async def _get_profile_with_includes(
|
||||
session: AsyncSession, profile_id: uuid.UUID
|
||||
) -> ConfigProfile | None:
|
||||
@@ -846,12 +602,6 @@ async def resolve_default_profile(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DefaultProfilesUpdate(BaseModel):
|
||||
default_profiles: dict[str, str] = Field(
|
||||
description="Mapping of tool_type_id -> profile_id for default profiles"
|
||||
)
|
||||
|
||||
|
||||
async def _get_or_create_user_config(
|
||||
session: AsyncSession,
|
||||
user_id: uuid.UUID,
|
||||
@@ -940,22 +690,6 @@ async def get_default_profile_for_tool_type_endpoint(
|
||||
return {"tool_type_id": tool_type_id, "profile_id": profile_id}
|
||||
|
||||
|
||||
class ValidateGitUrlRequest(BaseModel):
|
||||
url: str = Field(description="Git remote URL to validate")
|
||||
ssh_key_id: str | None = Field(
|
||||
default=None, description="Optional SSH key ID for private repos"
|
||||
)
|
||||
|
||||
|
||||
class ValidateGitUrlResponse(BaseModel):
|
||||
valid: bool
|
||||
suggested_url: str | None = None
|
||||
branches: list[str] | None = None
|
||||
default_branch: str | None = None
|
||||
error: str | None = None
|
||||
error_code: str | None = None
|
||||
|
||||
|
||||
@router.post("/validate-git-url", response_model=ValidateGitUrlResponse)
|
||||
async def validate_git_url(
|
||||
data: ValidateGitUrlRequest,
|
||||
|
||||
@@ -3,10 +3,9 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -19,6 +18,13 @@ from src.auth.dependencies import (
|
||||
from src.config import Settings
|
||||
from src.models import GitRepository
|
||||
from src.models import SSHKey
|
||||
from src.schemas.project import (
|
||||
GitRepositoryCreate,
|
||||
GitRepositoryResponse,
|
||||
URLParseRequest,
|
||||
URLParseResponse,
|
||||
UpdateSSHKeyRequest,
|
||||
)
|
||||
from src.utils.git_files import (
|
||||
commit_file,
|
||||
get_file_content,
|
||||
@@ -218,43 +224,6 @@ def _init_working_repository(repo_path: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
class GitRepositoryCreate(BaseModel):
|
||||
name: str
|
||||
remote_url: str | None = None
|
||||
force_original_url: bool = False
|
||||
ssh_key_id: str | None = None
|
||||
|
||||
|
||||
class URLParseRequest(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
class URLParseResponse(BaseModel):
|
||||
original_url: str
|
||||
base_url: str | None
|
||||
is_valid_clone_url: bool
|
||||
needs_parsing: bool
|
||||
host: str | None
|
||||
message: str
|
||||
error_code: str | None
|
||||
|
||||
|
||||
class GitRepositoryResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
path: str
|
||||
project_id: uuid.UUID | None
|
||||
owner_id: uuid.UUID
|
||||
is_mirror: bool
|
||||
remote_url: str | None
|
||||
last_push: datetime | None
|
||||
ssh_key_id: uuid.UUID | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@router.get(
|
||||
"/repositories",
|
||||
response_model=list[GitRepositoryResponse],
|
||||
@@ -599,10 +568,6 @@ async def create_repository(
|
||||
return repo
|
||||
|
||||
|
||||
class UpdateSSHKeyRequest(BaseModel):
|
||||
ssh_key_id: str | None = None
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{project_id}/repositories/{repo_id}/ssh-key",
|
||||
response_model=GitRepositoryResponse,
|
||||
|
||||
@@ -5,10 +5,16 @@ from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.database import SessionLocal
|
||||
from src.schemas.system import (
|
||||
DatabaseHealth,
|
||||
DatabaseHealthResponse,
|
||||
DiskHealth,
|
||||
HealthChecks,
|
||||
HealthResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -16,45 +22,6 @@ router = APIRouter()
|
||||
_start_time = time.time()
|
||||
|
||||
|
||||
class DatabaseHealth(BaseModel):
|
||||
"""Database health check result."""
|
||||
|
||||
status: str = Field(description="Database health status", examples=["healthy"])
|
||||
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
||||
|
||||
|
||||
class DiskHealth(BaseModel):
|
||||
"""Disk space health check result."""
|
||||
|
||||
status: str = Field(description="Disk health status", examples=["healthy"])
|
||||
free_gb: float = Field(description="Free disk space in GB", examples=[45.2])
|
||||
total_gb: float = Field(description="Total disk space in GB", examples=[100.0])
|
||||
|
||||
|
||||
class HealthChecks(BaseModel):
|
||||
"""Individual health checks."""
|
||||
|
||||
database: DatabaseHealth | None = None
|
||||
disk: DiskHealth | None = None
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Overall health check response."""
|
||||
|
||||
status: str = Field(description="Overall health status", examples=["healthy"])
|
||||
timestamp: str = Field(description="ISO 8601 timestamp", examples=["2026-05-19T12:00:00Z"])
|
||||
version: str = Field(description="API version", examples=["0.1.0"])
|
||||
checks: HealthChecks = Field(description="Individual health checks")
|
||||
uptime_seconds: float = Field(description="Server uptime in seconds", examples=[3600.0])
|
||||
|
||||
|
||||
class DatabaseHealthResponse(BaseModel):
|
||||
"""Database-specific health check response."""
|
||||
|
||||
status: str = Field(description="Database health status", examples=["healthy"])
|
||||
response_time_ms: float = Field(description="Query response time in milliseconds", examples=[5.2])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
response_model=HealthResponse,
|
||||
|
||||
@@ -3,7 +3,6 @@ import shutil
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -17,34 +16,16 @@ from src.models import GitRepository
|
||||
from src.models.project import Project
|
||||
from src.models import SSHKey
|
||||
from src.models import ToolInstance
|
||||
from src.schemas.project import (
|
||||
ProjectCreate,
|
||||
ProjectResponse,
|
||||
ProjectUpdate,
|
||||
SetDefaultSSHKeyRequest,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||
|
||||
|
||||
class ProjectCreate(BaseModel):
|
||||
name: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
description: str | None
|
||||
owner_id: uuid.UUID
|
||||
default_ssh_key_id: uuid.UUID | None
|
||||
|
||||
|
||||
class SetDefaultSSHKeyRequest(BaseModel):
|
||||
ssh_key_id: uuid.UUID
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=ProjectResponse,
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import base64
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||
from src.config import Settings
|
||||
from src.models import SSHKey
|
||||
from src.schemas.project import (
|
||||
SSHKeyCreate,
|
||||
SSHKeyResponse,
|
||||
SignPayloadRequest,
|
||||
SignatureResponse,
|
||||
VerifySignatureRequest,
|
||||
VerifySignatureResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/ssh-keys", tags=["ssh-keys"])
|
||||
|
||||
@@ -21,7 +27,7 @@ def _get_fernet() -> Fernet:
|
||||
"""Generate a valid Fernet key from the session secret."""
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
|
||||
settings = Settings()
|
||||
# Derive a 32-byte key from the session secret using SHA256
|
||||
key_bytes = hashlib.sha256(settings.session_secret.encode()).digest()
|
||||
@@ -53,36 +59,6 @@ def generate_ssh_key_pair() -> tuple[str, str]:
|
||||
return private_bytes.decode("utf-8"), public_bytes.decode("utf-8")
|
||||
|
||||
|
||||
class SSHKeyCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class SSHKeyResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
public_key: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class SignPayloadRequest(BaseModel):
|
||||
payload: str
|
||||
|
||||
|
||||
class SignatureResponse(BaseModel):
|
||||
signature: str
|
||||
|
||||
|
||||
class VerifySignatureRequest(BaseModel):
|
||||
payload: str
|
||||
signature: str
|
||||
|
||||
|
||||
class VerifySignatureResponse(BaseModel):
|
||||
valid: bool
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=SSHKeyResponse,
|
||||
@@ -171,7 +147,9 @@ async def delete_ssh_key(
|
||||
user = await _get_user(session, user_id)
|
||||
ssh_key = await session.get(SSHKey, key_id)
|
||||
if ssh_key is None or ssh_key.user_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
|
||||
)
|
||||
|
||||
await session.delete(ssh_key)
|
||||
await session.commit()
|
||||
@@ -203,7 +181,9 @@ async def sign_payload(
|
||||
user = await _get_user(session, user_id)
|
||||
ssh_key = await session.get(SSHKey, key_id)
|
||||
if ssh_key is None or ssh_key.user_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
|
||||
)
|
||||
|
||||
fernet = _get_fernet()
|
||||
private_key_pem = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
|
||||
@@ -242,7 +222,9 @@ async def verify_signature(
|
||||
user = await _get_user(session, user_id)
|
||||
ssh_key = await session.get(SSHKey, key_id)
|
||||
if ssh_key is None or ssh_key.user_id != user.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
|
||||
)
|
||||
|
||||
public_key = serialization.load_ssh_public_key(ssh_key.public_key.encode())
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from fastapi import (
|
||||
Response,
|
||||
status,
|
||||
)
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -82,6 +81,7 @@ from src.services.manifest_compiler import (
|
||||
from src.services.permission_fixer import apply_mount_permissions, apply_ssh_permissions
|
||||
from src.services.readiness_probe import execute_probe
|
||||
from src.services.ssh_keys import cleanup_ssh_key_files, prepare_ssh_key_files
|
||||
from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_event_bus = InstanceEventBus()
|
||||
@@ -418,48 +418,6 @@ def _expand_glob_source(source_path: str, repo_path: str) -> list[str]:
|
||||
router = APIRouter(prefix="/projects", tags=["tool-instances"])
|
||||
|
||||
|
||||
class CreateInstanceRequest(BaseModel):
|
||||
"""Request body for creating a tool instance."""
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
tool_type_id: str = Field(description="UUID of the tool type to instantiate")
|
||||
display_name: str | None = Field(
|
||||
default=None, description="Optional display name for the instance"
|
||||
)
|
||||
workspace_id: str | None = Field(
|
||||
default=None, description="UUID of workspace to mount (replaces clone_mode)"
|
||||
)
|
||||
clone_mode: str = Field(
|
||||
default="mount", description="Repository access mode: 'mount' or 'clone'"
|
||||
)
|
||||
branch: str | None = Field(
|
||||
default="main", description="Branch to clone (when clone_mode='clone')"
|
||||
)
|
||||
new_branch: str | None = Field(
|
||||
default=None, description="Create a new local branch after cloning"
|
||||
)
|
||||
config_profile_id: str | None = Field(
|
||||
default=None, description="Optional config profile ID for launch"
|
||||
)
|
||||
ssh_key_ids: list[str] = Field(
|
||||
default_factory=list, description="SSH key IDs to mount into container ~/.ssh"
|
||||
)
|
||||
|
||||
|
||||
class StartInstanceRequest(BaseModel):
|
||||
"""Request body for starting a tool instance."""
|
||||
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
config_profile_id: str | None = Field(
|
||||
default=None, description="Config profile ID to apply, or null for none"
|
||||
)
|
||||
ssh_key_ids: list[str] = Field(
|
||||
default_factory=list, description="SSH key IDs to mount into container ~/.ssh"
|
||||
)
|
||||
|
||||
|
||||
async def _validate_config_profile(
|
||||
session: AsyncSession,
|
||||
profile_id: str | None,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -14,6 +12,12 @@ from src.api.tool_types_validation import (
|
||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||
from src.models import ToolType
|
||||
from src.models.user import User
|
||||
from src.schemas.tool import (
|
||||
ToolTypeCreate,
|
||||
ToolTypeResponse,
|
||||
ToolTypeUpdate,
|
||||
ToolTypeValidateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/tool-types", tags=["tool-types"])
|
||||
|
||||
@@ -29,237 +33,6 @@ async def _require_admin(user: User) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class ToolTypeCreate(BaseModel):
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None = None
|
||||
default_port: int = 0
|
||||
definition_type: str = "compose"
|
||||
manifest_id: uuid.UUID | None = None
|
||||
compose_template: str | None = None
|
||||
dockerfile_template: str | None = None
|
||||
build_context: dict | None = None
|
||||
readiness_probe: dict | None = None
|
||||
startup_command: str | None = None
|
||||
required_variables: list[str] = []
|
||||
category: str = "other"
|
||||
interface_type: str = "web"
|
||||
requires_port: bool = True
|
||||
|
||||
@field_validator("definition_type")
|
||||
@classmethod
|
||||
def validate_definition_type(cls, v: str) -> str:
|
||||
if v not in ("compose", "dockerfile", "manifest"):
|
||||
raise ValueError(
|
||||
"definition_type must be 'compose', 'dockerfile', or 'manifest'"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("compose_template")
|
||||
@classmethod
|
||||
def validate_compose_template(cls, v: str | None, info) -> str | None:
|
||||
data = info.data
|
||||
if data.get("definition_type") != "compose":
|
||||
return v
|
||||
|
||||
if v is None or not v.strip():
|
||||
raise ValueError(
|
||||
"compose_template is required when definition_type is 'compose'"
|
||||
)
|
||||
|
||||
validate_compose_yaml(v)
|
||||
return v
|
||||
|
||||
@field_validator("dockerfile_template")
|
||||
@classmethod
|
||||
def validate_dockerfile_template(cls, v: str | None, info) -> str | None:
|
||||
data = info.data
|
||||
if data.get("definition_type") != "dockerfile":
|
||||
return v
|
||||
|
||||
if v is None or not v.strip():
|
||||
raise ValueError(
|
||||
"dockerfile_template is required when definition_type is 'dockerfile'"
|
||||
)
|
||||
|
||||
if not v.strip().startswith("FROM"):
|
||||
raise ValueError("Dockerfile must start with a FROM instruction")
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("interface_type")
|
||||
@classmethod
|
||||
def validate_interface_type(cls, v: str) -> str:
|
||||
if v not in ("web", "terminal"):
|
||||
raise ValueError("interface_type must be 'web' or 'terminal'")
|
||||
return v
|
||||
|
||||
@field_validator("default_port")
|
||||
@classmethod
|
||||
def validate_default_port(cls, v: int, info) -> int:
|
||||
data = info.data
|
||||
requires_port = data.get("requires_port", True)
|
||||
if not requires_port:
|
||||
return v
|
||||
if v <= 0 or v > 65535:
|
||||
raise ValueError("Port must be between 1 and 65535")
|
||||
return v
|
||||
|
||||
@field_validator("required_variables")
|
||||
@classmethod
|
||||
def validate_required_variables(cls, v: list[str], info) -> list[str]:
|
||||
if not v:
|
||||
return v
|
||||
|
||||
data = info.data
|
||||
if data.get("definition_type") != "compose":
|
||||
return v
|
||||
|
||||
template = data.get("compose_template")
|
||||
if not template:
|
||||
return v
|
||||
|
||||
for var in v:
|
||||
placeholder = f"{{{{{var}}}}}"
|
||||
if placeholder not in template:
|
||||
raise ValueError(
|
||||
f"Required variable '{var}' not found in compose template"
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_templates(self) -> "ToolTypeCreate":
|
||||
if self.definition_type == "manifest":
|
||||
if self.manifest_id is None:
|
||||
raise ValueError(
|
||||
"manifest_id is required when definition_type is 'manifest'"
|
||||
)
|
||||
return self
|
||||
|
||||
if self.definition_type == "dockerfile" and (
|
||||
self.dockerfile_template is None or not self.dockerfile_template.strip()
|
||||
):
|
||||
raise ValueError(
|
||||
"dockerfile_template is required when definition_type is 'dockerfile'"
|
||||
)
|
||||
if self.definition_type == "compose" and (
|
||||
self.compose_template is None or not self.compose_template.strip()
|
||||
):
|
||||
raise ValueError(
|
||||
"compose_template is required when definition_type is 'compose'"
|
||||
)
|
||||
|
||||
# Validate that default_port is exposed in compose template (only if requires_port)
|
||||
if (
|
||||
self.requires_port
|
||||
and self.definition_type == "compose"
|
||||
and self.compose_template
|
||||
):
|
||||
try:
|
||||
parsed = validate_compose_yaml(self.compose_template)
|
||||
except ValueError:
|
||||
return self
|
||||
|
||||
if not check_port_exposed(parsed, self.default_port):
|
||||
raise ValueError(
|
||||
f"Port {self.default_port} is not exposed in the compose template. Add it to the 'ports' section."
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class ToolTypeUpdate(BaseModel):
|
||||
display_name: str | None = None
|
||||
description: str | None = None
|
||||
default_port: int | None = None
|
||||
definition_type: str | None = None
|
||||
manifest_id: uuid.UUID | None = None
|
||||
compose_template: str | None = None
|
||||
dockerfile_template: str | None = None
|
||||
build_context: dict | None = None
|
||||
readiness_probe: dict | None = None
|
||||
startup_command: str | None = None
|
||||
required_variables: list[str] | None = None
|
||||
category: str | None = None
|
||||
interface_type: str | None = None
|
||||
requires_port: bool | None = None
|
||||
|
||||
@field_validator("definition_type")
|
||||
@classmethod
|
||||
def validate_definition_type(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v not in ("compose", "dockerfile", "manifest"):
|
||||
raise ValueError(
|
||||
"definition_type must be 'compose', 'dockerfile', or 'manifest'"
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("interface_type")
|
||||
@classmethod
|
||||
def validate_interface_type(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v not in ("web", "terminal"):
|
||||
raise ValueError("interface_type must be 'web' or 'terminal'")
|
||||
return v
|
||||
|
||||
@field_validator("compose_template")
|
||||
@classmethod
|
||||
def validate_compose_template(cls, v: str | None, info) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
data = info.data
|
||||
definition_type = data.get("definition_type")
|
||||
if definition_type and definition_type != "compose":
|
||||
return v
|
||||
|
||||
validate_compose_yaml(v)
|
||||
return v
|
||||
|
||||
@field_validator("dockerfile_template")
|
||||
@classmethod
|
||||
def validate_dockerfile_template(cls, v: str | None, info) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
|
||||
data = info.data
|
||||
definition_type = data.get("definition_type")
|
||||
if definition_type and definition_type != "dockerfile":
|
||||
return v
|
||||
|
||||
if not v.strip().startswith("FROM"):
|
||||
raise ValueError("Dockerfile must start with a FROM instruction")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class ToolTypeResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None
|
||||
category: str
|
||||
interface_type: str
|
||||
requires_port: bool
|
||||
default_port: int
|
||||
definition_type: str
|
||||
manifest_id: uuid.UUID | None
|
||||
compose_template: str | None
|
||||
dockerfile_template: str | None
|
||||
build_context: dict | None
|
||||
readiness_probe: dict | None
|
||||
startup_command: str | None
|
||||
required_variables: list[str]
|
||||
created_by_id: uuid.UUID | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=ToolTypeResponse,
|
||||
@@ -461,12 +234,6 @@ async def update_tool_type(
|
||||
return tool_type
|
||||
|
||||
|
||||
class ToolTypeValidateRequest(BaseModel):
|
||||
definition_type: str
|
||||
compose_template: str | None = None
|
||||
dockerfile_template: str | None = None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/validate",
|
||||
summary="Validate tool type template",
|
||||
|
||||
@@ -2,12 +2,12 @@ import logging
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||
from src.models import UserConfig
|
||||
from src.schemas.user import UserConfigResponse, UserConfigUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,28 +38,6 @@ async def _get_or_create_config(
|
||||
return config
|
||||
|
||||
|
||||
class UserConfigResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
default_editor: str | None = None
|
||||
theme: str = "system"
|
||||
git_user_name: str | None = None
|
||||
git_user_email: str | None = None
|
||||
last_session_id: str | None = None
|
||||
notification_mute_categories: list[str] | None = None
|
||||
notification_toast_level: str | None = None
|
||||
|
||||
|
||||
class UserConfigUpdate(BaseModel):
|
||||
default_editor: str | None = None
|
||||
theme: str | None = None
|
||||
git_user_name: str | None = None
|
||||
git_user_email: str | None = None
|
||||
last_session_id: str | None = None
|
||||
notification_mute_categories: list[str] | None = None
|
||||
notification_toast_level: str | None = None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/config",
|
||||
response_model=UserConfigResponse,
|
||||
|
||||
@@ -2,11 +2,11 @@ import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
|
||||
from src.models.user import User
|
||||
from src.schemas.user import UserProfileResponse, UserProfileUpdate
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
@@ -16,20 +16,6 @@ ALLOWED_CONTENT_TYPES = {"image/png", "image/jpeg", "image/jpg"}
|
||||
MAX_AVATAR_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
|
||||
class UserProfileResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
email: str
|
||||
name: str
|
||||
avatar_url: str | None
|
||||
|
||||
|
||||
class UserProfileUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserProfileResponse,
|
||||
@@ -77,12 +63,16 @@ async def update_profile(
|
||||
|
||||
if data.name is not None:
|
||||
if len(data.name.strip()) == 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="name cannot be empty")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="name cannot be empty"
|
||||
)
|
||||
user.name = data.name.strip()
|
||||
|
||||
if data.email is not None:
|
||||
if "@" not in data.email:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid email")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="invalid email"
|
||||
)
|
||||
user.email = data.email.strip()
|
||||
|
||||
await session.commit()
|
||||
|
||||
@@ -1 +1,27 @@
|
||||
"""Config module."""
|
||||
"""Config schemas module."""
|
||||
|
||||
from src.schemas.config.config_profile import (
|
||||
ConfigProfileCreate,
|
||||
ConfigProfileIncludeUpdate,
|
||||
ConfigProfileResponse,
|
||||
ConfigProfileUpdate,
|
||||
DefaultProfilesUpdate,
|
||||
GitMountItem,
|
||||
GitMountMapping,
|
||||
MountItem,
|
||||
ValidateGitUrlRequest,
|
||||
ValidateGitUrlResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ConfigProfileCreate",
|
||||
"ConfigProfileIncludeUpdate",
|
||||
"ConfigProfileResponse",
|
||||
"ConfigProfileUpdate",
|
||||
"DefaultProfilesUpdate",
|
||||
"GitMountItem",
|
||||
"GitMountMapping",
|
||||
"MountItem",
|
||||
"ValidateGitUrlRequest",
|
||||
"ValidateGitUrlResponse",
|
||||
]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Config profile request/response schemas."""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
@@ -13,8 +12,8 @@ def _validate_uuid(v: str | None) -> str | None:
|
||||
return v
|
||||
try:
|
||||
uuid.UUID(v)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid UUID: {v}")
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid UUID: {v}") from exc
|
||||
return v
|
||||
|
||||
|
||||
@@ -236,8 +235,8 @@ class ConfigProfileIncludeUpdate(BaseModel):
|
||||
for item in v:
|
||||
try:
|
||||
uuid.UUID(item)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid UUID in includes: {item}")
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Invalid UUID in includes: {item}") from exc
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,41 @@
|
||||
"""Project module."""
|
||||
"""Project schemas module."""
|
||||
|
||||
from src.schemas.project.git_repository import (
|
||||
GitRepositoryCreate,
|
||||
GitRepositoryResponse,
|
||||
UpdateSSHKeyRequest,
|
||||
URLParseRequest,
|
||||
URLParseResponse,
|
||||
)
|
||||
from src.schemas.project.project import (
|
||||
ProjectCreate,
|
||||
ProjectResponse,
|
||||
ProjectUpdate,
|
||||
SetDefaultSSHKeyRequest,
|
||||
)
|
||||
from src.schemas.project.ssh_key import (
|
||||
SSHKeyCreate,
|
||||
SSHKeyResponse,
|
||||
SignPayloadRequest,
|
||||
SignatureResponse,
|
||||
VerifySignatureRequest,
|
||||
VerifySignatureResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"GitRepositoryCreate",
|
||||
"GitRepositoryResponse",
|
||||
"ProjectCreate",
|
||||
"ProjectResponse",
|
||||
"ProjectUpdate",
|
||||
"SSHKeyCreate",
|
||||
"SSHKeyResponse",
|
||||
"SetDefaultSSHKeyRequest",
|
||||
"SignPayloadRequest",
|
||||
"SignatureResponse",
|
||||
"URLParseRequest",
|
||||
"URLParseResponse",
|
||||
"UpdateSSHKeyRequest",
|
||||
"VerifySignatureRequest",
|
||||
"VerifySignatureResponse",
|
||||
]
|
||||
|
||||
@@ -1 +1,17 @@
|
||||
"""System module."""
|
||||
"""System schemas module."""
|
||||
|
||||
from src.schemas.system.health import (
|
||||
DatabaseHealth,
|
||||
DatabaseHealthResponse,
|
||||
DiskHealth,
|
||||
HealthChecks,
|
||||
HealthResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DatabaseHealth",
|
||||
"DatabaseHealthResponse",
|
||||
"DiskHealth",
|
||||
"HealthChecks",
|
||||
"HealthResponse",
|
||||
]
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
"""Tool module."""
|
||||
"""Tool schemas module."""
|
||||
|
||||
from src.schemas.tool.tool_instance import CreateInstanceRequest, StartInstanceRequest
|
||||
from src.schemas.tool.tool_type import (
|
||||
ToolTypeCreate,
|
||||
ToolTypeResponse,
|
||||
ToolTypeUpdate,
|
||||
ToolTypeValidateRequest,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CreateInstanceRequest",
|
||||
"StartInstanceRequest",
|
||||
"ToolTypeCreate",
|
||||
"ToolTypeResponse",
|
||||
"ToolTypeUpdate",
|
||||
"ToolTypeValidateRequest",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, field_validator, model_validator
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||
|
||||
from src.api.tool_types_validation import (
|
||||
check_port_exposed,
|
||||
@@ -196,3 +198,33 @@ class ToolTypeUpdate(BaseModel):
|
||||
if not v.strip().startswith("FROM"):
|
||||
raise ValueError("Dockerfile must start with a FROM instruction")
|
||||
return v
|
||||
|
||||
|
||||
class ToolTypeResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
display_name: str
|
||||
description: str | None
|
||||
category: str
|
||||
interface_type: str
|
||||
requires_port: bool
|
||||
default_port: int
|
||||
definition_type: str
|
||||
manifest_id: uuid.UUID | None
|
||||
compose_template: str | None
|
||||
dockerfile_template: str | None
|
||||
build_context: dict | None
|
||||
readiness_probe: dict | None
|
||||
startup_command: str | None
|
||||
required_variables: list[str]
|
||||
created_by_id: uuid.UUID | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ToolTypeValidateRequest(BaseModel):
|
||||
definition_type: str
|
||||
compose_template: str | None = None
|
||||
dockerfile_template: str | None = None
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
"""User module."""
|
||||
"""User schemas module."""
|
||||
|
||||
from src.schemas.user.user import UserProfileResponse, UserProfileUpdate
|
||||
from src.schemas.user.user_config import UserConfigResponse, UserConfigUpdate
|
||||
|
||||
__all__ = [
|
||||
"UserConfigResponse",
|
||||
"UserConfigUpdate",
|
||||
"UserProfileResponse",
|
||||
"UserProfileUpdate",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user