frozen pydantic models
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import field_validator
|
||||
|
||||
from .common import SEMVER_RE
|
||||
from .common import SKILL_ID_RE
|
||||
from .common import StrictFrozenModel
|
||||
from .common import frozen_mapping
|
||||
|
||||
|
||||
class PromptArgumentEntry(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
class PromptArgumentEntry(StrictFrozenModel):
|
||||
type: str = Field(min_length=1)
|
||||
description: str | None = None
|
||||
required: bool = False
|
||||
default: Any | None = None
|
||||
enum: list[str] | None = None
|
||||
enum: tuple[str, ...] | None = None
|
||||
|
||||
@field_validator("type")
|
||||
@classmethod
|
||||
@@ -36,20 +35,18 @@ class PromptArgumentEntry(BaseModel):
|
||||
|
||||
@field_validator("enum")
|
||||
@classmethod
|
||||
def validate_enum(cls, value: list[str] | None) -> list[str] | None:
|
||||
def validate_enum(cls, value: tuple[str, ...] | None) -> tuple[str, ...] | None:
|
||||
if value is not None and not value:
|
||||
raise ValueError("enum must contain at least one value when provided")
|
||||
return value
|
||||
|
||||
|
||||
class PromptMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
class PromptMetadata(StrictFrozenModel):
|
||||
id: str
|
||||
version: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
capabilities: list[str] = Field(min_length=1)
|
||||
arguments: dict[str, PromptArgumentEntry] = Field(default_factory=dict)
|
||||
tags: tuple[str, ...] = ()
|
||||
capabilities: tuple[str, ...] = Field(min_length=1)
|
||||
arguments: Mapping[str, PromptArgumentEntry] = Field(default_factory=frozen_mapping)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
@@ -67,24 +64,27 @@ class PromptMetadata(BaseModel):
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def validate_tags(cls, value: list[str]) -> list[str]:
|
||||
def validate_tags(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
||||
for tag in value:
|
||||
if not SKILL_ID_RE.fullmatch(tag):
|
||||
raise ValueError(f"invalid tag: {tag}")
|
||||
return value
|
||||
|
||||
@field_validator("arguments", mode="before")
|
||||
@classmethod
|
||||
def freeze_arguments(cls, value: Mapping[str, PromptArgumentEntry] | None) -> Mapping[str, PromptArgumentEntry]:
|
||||
return frozen_mapping(value)
|
||||
|
||||
@field_validator("arguments")
|
||||
@classmethod
|
||||
def validate_argument_names(cls, value: dict[str, PromptArgumentEntry]) -> dict[str, PromptArgumentEntry]:
|
||||
def validate_argument_names(cls, value: Mapping[str, PromptArgumentEntry]) -> Mapping[str, PromptArgumentEntry]:
|
||||
for name in value:
|
||||
if not re.fullmatch(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
|
||||
raise ValueError(f"invalid prompt argument name: {name}")
|
||||
return value
|
||||
|
||||
|
||||
class PromptFrontmatter(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
class PromptFrontmatter(StrictFrozenModel):
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
description: str = Field(min_length=1, max_length=1024)
|
||||
x_personal_mcp: PromptMetadata = Field(alias="x-personal-mcp")
|
||||
@@ -97,3 +97,10 @@ class PromptFrontmatter(BaseModel):
|
||||
if "anthropic" in value or "claude" in value:
|
||||
raise ValueError("name must not contain reserved words anthropic or claude")
|
||||
return value
|
||||
|
||||
|
||||
class PromptDocumentModel(StrictFrozenModel):
|
||||
prompt_id: str
|
||||
relpath: str
|
||||
content: str
|
||||
frontmatter: PromptFrontmatter
|
||||
|
||||
Reference in New Issue
Block a user