frozen pydantic models
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user