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
+48 -20
View File
@@ -1,22 +1,22 @@
from pydantic import BaseModel
from pydantic import ConfigDict
from collections.abc import Mapping
from pydantic import Field
from pydantic import field_validator
from .common import SEMVER_RE
from .common import SKILL_ID_RE
from .common import ReferenceEntry
from .common import StrictFrozenModel
from .common import frozen_mapping
class PersonalMcpMetadata(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
class SkillMetadata(StrictFrozenModel):
id: str
version: str
tags: list[str] = Field(default_factory=list)
capabilities: list[str] = Field(min_length=1)
depends_on: list[str] = Field(default_factory=list)
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
tags: tuple[str, ...] = ()
capabilities: tuple[str, ...] = Field(min_length=1)
depends_on: tuple[str, ...] = ()
references: Mapping[str, ReferenceEntry] = Field(default_factory=frozen_mapping)
@field_validator("id")
@classmethod
@@ -34,7 +34,7 @@ class PersonalMcpMetadata(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}")
@@ -42,29 +42,32 @@ class PersonalMcpMetadata(BaseModel):
@field_validator("depends_on")
@classmethod
def validate_depends_on(cls, value: list[str]) -> list[str]:
def validate_depends_on(cls, value: tuple[str, ...]) -> tuple[str, ...]:
for dep in value:
if not SKILL_ID_RE.fullmatch(dep):
raise ValueError(f"invalid depends_on skill id: {dep}")
return value
@field_validator("references", mode="before")
@classmethod
def freeze_references(cls, value: Mapping[str, ReferenceEntry] | None) -> Mapping[str, ReferenceEntry]:
return frozen_mapping(value)
@field_validator("references")
@classmethod
def validate_reference_ids(cls, value: dict[str, ReferenceEntry]) -> dict[str, ReferenceEntry]:
def validate_reference_ids(cls, value: Mapping[str, ReferenceEntry]) -> Mapping[str, ReferenceEntry]:
for ref_id in value:
if not SKILL_ID_RE.fullmatch(ref_id):
raise ValueError(f"invalid reference id: {ref_id}")
return value
class SkillFrontmatter(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
class SkillFrontmatter(StrictFrozenModel):
name: str = Field(min_length=1, max_length=64)
description: str = Field(min_length=1, max_length=1024)
when_to_use: str | None = None
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
disallowed_tools: str | list[str] | None = Field(
allowed_tools: str | tuple[str, ...] | None = Field(default=None, alias="allowed-tools")
disallowed_tools: str | tuple[str, ...] | None = Field(
default=None,
alias="disallowed-tools",
)
@@ -74,11 +77,18 @@ class SkillFrontmatter(BaseModel):
)
user_invocable: bool | None = Field(default=None, alias="user-invocable")
argument_hint: str | None = Field(default=None, alias="argument-hint")
arguments: str | list[str] | None = None
arguments: str | tuple[str, ...] | None = None
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str] | None = None
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
metadata: Mapping[str, str] | None = None
x_personal_mcp: SkillMetadata = Field(alias="x-personal-mcp")
@field_validator("metadata", mode="before")
@classmethod
def freeze_metadata(cls, value: Mapping[str, str] | None) -> Mapping[str, str] | None:
if value is None:
return None
return frozen_mapping(value)
@field_validator("name")
@classmethod
@@ -88,3 +98,21 @@ class SkillFrontmatter(BaseModel):
if "anthropic" in value or "claude" in value:
raise ValueError("name must not contain reserved words anthropic or claude")
return value
class SkillDocumentModel(StrictFrozenModel):
skill_id: str
relpath: str
content: str
frontmatter: SkillFrontmatter
class SkillReferenceDocumentModel(StrictFrozenModel):
ref_id: str
relpath: str
content: str
entry: ReferenceEntry
# Transitional alias while callers move to neutral naming.
PersonalMcpMetadata = SkillMetadata