from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, TypedDict
from typing_extensions import NotRequired
from mlflow.entities.skill_source import SkillSourceType
from mlflow.exceptions import MlflowException
from mlflow.utils.annotations import experimental
from mlflow.utils.workspace_utils import resolve_entity_workspace_name
class RegistryIcon(TypedDict):
"""Icon descriptor for registry presentation metadata.
Same shape as RFC-0004's ``MCPIcon`` so UIs share one icon renderer
across the MCP and skill registries.
"""
src: str
sizes: NotRequired[list[str]]
mimeType: NotRequired[str]
theme: NotRequired[str]
[docs]class SkillStatus(str, Enum):
DRAFT = "draft"
ACTIVE = "active"
DEPRECATED = "deprecated"
DELETED = "deleted"
def __str__(self):
return self.value
VALID_SKILL_STATUS_TRANSITIONS: dict[SkillStatus, set[SkillStatus]] = {
SkillStatus.DRAFT: {SkillStatus.ACTIVE, SkillStatus.DELETED},
SkillStatus.ACTIVE: {SkillStatus.DRAFT, SkillStatus.DEPRECATED},
SkillStatus.DEPRECATED: {SkillStatus.ACTIVE, SkillStatus.DELETED},
}
def _aliases_to_dict(aliases: Any) -> dict[str, Any]:
"""Normalize an aliases payload to an ``{alias: version}`` dict.
REST responses expose aliases as a list of ``{"alias": ..., "version": ...}``
objects; accept that list (and an already-normalized dict) and return the dict
shape the entity fields use.
"""
if not aliases:
return {}
if isinstance(aliases, dict):
return aliases
return {a["alias"]: a["version"] for a in aliases}
[docs]@experimental(version="3.18.0")
@dataclass
class Skill:
name: str
organization: str = ""
description: str | None = None
# mutable presentation metadata; returned as stored (null when unset)
icons: list[RegistryIcon] | None = None
workspace: str | None = None
status: SkillStatus | None = None # read-only, derived by producers
tags: dict[str, str] = field(default_factory=dict)
aliases: dict[str, int] = field(default_factory=dict) # read-only
latest_version: int | None = None # read-only
source_type: SkillSourceType | None = None # read-only, derived from latest version
created_by: str | None = None
last_updated_by: str | None = None
creation_timestamp: int | None = None
last_updated_timestamp: int | None = None
def __post_init__(self):
self.workspace = resolve_entity_workspace_name(self.workspace)
[docs] @classmethod
def from_dict(cls, data: dict[str, Any]) -> Skill:
if not isinstance(data, dict):
raise MlflowException.invalid_parameter_value(
"Failed to parse Skill response: expected a dictionary"
)
try:
return cls(
name=data["name"],
organization=data.get("organization", ""),
description=data.get("description"),
icons=data.get("icons"),
workspace=data.get("workspace"),
status=SkillStatus(data["status"]) if data.get("status") else None,
tags=data.get("tags") or {},
aliases=_aliases_to_dict(data.get("aliases")),
latest_version=data.get("latest_version"),
source_type=(
SkillSourceType(data["source_type"]) if data.get("source_type") else None
),
created_by=data.get("created_by"),
last_updated_by=data.get("last_updated_by"),
creation_timestamp=data.get("creation_timestamp"),
last_updated_timestamp=data.get("last_updated_timestamp"),
)
except KeyError as e:
raise MlflowException.invalid_parameter_value(
f"Failed to parse Skill response: missing required field {e}"
) from None
except (ValueError, TypeError, MlflowException) as e:
raise MlflowException.invalid_parameter_value(
f"Failed to parse Skill response: {e}"
) from None