frozen pydantic models

This commit is contained in:
John Lancaster
2026-06-21 12:26:12 -05:00
parent 4320a251f5
commit 9a9432cc55
10 changed files with 309 additions and 119 deletions
+25 -18
View File
@@ -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