Skip to content

Validator Registry

The registry holds the parsers and checks a schema resolves by name.

Registry

Registry is the public entry point. It groups four ValidatorRegistry instances, one per validator kind.

nyctea.validators.registry.Registry

Bases: BaseModel

Registry containing all validator types.

This Pydantic model manages separate registries for each validator type, providing type-safe registration methods and centralized validator management.

Attributes:

Name Type Description
column_parsers ValidatorRegistry[ColumnParser]

Registry for column parser validators.

column_checks ValidatorRegistry[ColumnCheck]

Registry for column check validators.

frame_parsers ValidatorRegistry[FrameParser]

Registry for frame parser validators.

frame_checks ValidatorRegistry[FrameCheck]

Registry for frame check validators.

Source code in src/nyctea/validators/registry.py
class Registry(BaseModel):
    """Registry containing all validator types.

    This Pydantic model manages separate registries for each validator type,
    providing type-safe registration methods and centralized validator management.

    Attributes:
        column_parsers: Registry for column parser validators.
        column_checks: Registry for column check validators.
        frame_parsers: Registry for frame parser validators.
        frame_checks: Registry for frame check validators.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    column_parsers: ValidatorRegistry[ColumnParser]
    column_checks: ValidatorRegistry[ColumnCheck]
    frame_parsers: ValidatorRegistry[FrameParser]
    frame_checks: ValidatorRegistry[FrameCheck]

    @model_validator(mode="before")
    @classmethod
    def _init_registries(cls, data: Any) -> Any:
        """Provide empty sub-registries when not supplied."""
        if not isinstance(data, dict):
            return data
        if "column_parsers" not in data:
            data["column_parsers"] = ValidatorRegistry(ColumnParser)
        if "column_checks" not in data:
            data["column_checks"] = ValidatorRegistry(ColumnCheck)
        if "frame_parsers" not in data:
            data["frame_parsers"] = ValidatorRegistry(FrameParser)
        if "frame_checks" not in data:
            data["frame_checks"] = ValidatorRegistry(FrameCheck)
        return data

    def register_column_parser(self, validator: ColumnParser) -> None:
        """Register a column parser validator.

        Args:
            validator: Column parser to register.
        """
        self.column_parsers.register(validator)

    def register_column_check(self, validator: ColumnCheck) -> None:
        """Register a column check validator.

        Args:
            validator: Column check to register.
        """
        self.column_checks.register(validator)

    def register_frame_parser(self, validator: FrameParser) -> None:
        """Register a frame parser validator.

        Args:
            validator: Frame parser to register.
        """
        self.frame_parsers.register(validator)

    def register_frame_check(self, validator: FrameCheck) -> None:
        """Register a frame check validator.

        Args:
            validator: Frame check to register.
        """
        self.frame_checks.register(validator)

    def get_validator_counts(self) -> dict[str, int]:
        """Get count of validators in each registry.

        Returns:
            Dictionary mapping registry name to validator count.
        """
        return {
            "column_parsers": len(self.column_parsers),
            "column_checks": len(self.column_checks),
            "frame_parsers": len(self.frame_parsers),
            "frame_checks": len(self.frame_checks),
        }

    def __repr__(self) -> str:
        """Return string representation of registry."""
        counts = self.get_validator_counts()
        return (
            f"Registry("
            f"column_parsers={counts['column_parsers']}, "
            f"column_checks={counts['column_checks']}, "
            f"frame_parsers={counts['frame_parsers']}, "
            f"frame_checks={counts['frame_checks']})"
        )

__repr__()

Return string representation of registry.

Source code in src/nyctea/validators/registry.py
def __repr__(self) -> str:
    """Return string representation of registry."""
    counts = self.get_validator_counts()
    return (
        f"Registry("
        f"column_parsers={counts['column_parsers']}, "
        f"column_checks={counts['column_checks']}, "
        f"frame_parsers={counts['frame_parsers']}, "
        f"frame_checks={counts['frame_checks']})"
    )

get_validator_counts()

Get count of validators in each registry.

Returns:

Type Description
dict[str, int]

Dictionary mapping registry name to validator count.

Source code in src/nyctea/validators/registry.py
def get_validator_counts(self) -> dict[str, int]:
    """Get count of validators in each registry.

    Returns:
        Dictionary mapping registry name to validator count.
    """
    return {
        "column_parsers": len(self.column_parsers),
        "column_checks": len(self.column_checks),
        "frame_parsers": len(self.frame_parsers),
        "frame_checks": len(self.frame_checks),
    }

register_column_check(validator)

Register a column check validator.

Parameters:

Name Type Description Default
validator ColumnCheck

Column check to register.

required
Source code in src/nyctea/validators/registry.py
def register_column_check(self, validator: ColumnCheck) -> None:
    """Register a column check validator.

    Args:
        validator: Column check to register.
    """
    self.column_checks.register(validator)

register_column_parser(validator)

Register a column parser validator.

Parameters:

Name Type Description Default
validator ColumnParser

Column parser to register.

required
Source code in src/nyctea/validators/registry.py
def register_column_parser(self, validator: ColumnParser) -> None:
    """Register a column parser validator.

    Args:
        validator: Column parser to register.
    """
    self.column_parsers.register(validator)

register_frame_check(validator)

Register a frame check validator.

Parameters:

Name Type Description Default
validator FrameCheck

Frame check to register.

required
Source code in src/nyctea/validators/registry.py
def register_frame_check(self, validator: FrameCheck) -> None:
    """Register a frame check validator.

    Args:
        validator: Frame check to register.
    """
    self.frame_checks.register(validator)

register_frame_parser(validator)

Register a frame parser validator.

Parameters:

Name Type Description Default
validator FrameParser

Frame parser to register.

required
Source code in src/nyctea/validators/registry.py
def register_frame_parser(self, validator: FrameParser) -> None:
    """Register a frame parser validator.

    Args:
        validator: Frame parser to register.
    """
    self.frame_parsers.register(validator)

ValidatorRegistry

nyctea.validators.registry.ValidatorRegistry

Bases: Generic[T]

Type-safe registry for a specific validator type.

This generic class manages a collection of validators of a single type, providing name-based lookup, tag-based discovery, and collision detection.

Class Type Parameters:

Name Bound or Constraints Description Default
T

The validator type this registry manages (must extend Validator).

required

Attributes:

Name Type Description
validator_type

The class of validators this registry accepts.

Source code in src/nyctea/validators/registry.py
class ValidatorRegistry(Generic[T]):
    """Type-safe registry for a specific validator type.

    This generic class manages a collection of validators of a single type,
    providing name-based lookup, tag-based discovery, and collision detection.

    Type Parameters:
        T: The validator type this registry manages (must extend Validator).

    Attributes:
        validator_type: The class of validators this registry accepts.
    """

    def __init__(self, validator_type: type[T]) -> None:
        """Initialize a validator registry for a specific type.

        Args:
            validator_type: The class of validators this registry will accept.
        """
        self.validator_type = validator_type
        self._validators: dict[str, T] = {}
        self._tags: dict[str, list[T]] = {}

    def register(self, validator: T) -> None:
        """Register a validator instance.

        Args:
            validator: The validator to register.

        Raises:
            TypeError: If validator is not of the correct type.
            RegistrationError: If a validator with the same name is already registered.
        """
        # Type validation
        if not isinstance(validator, self.validator_type):
            raise TypeError(f"Registry expects {self.validator_type.__name__}, got {type(validator).__name__}")

        # Name collision check
        if validator.name in self._validators:
            existing = self._validators[validator.name]
            raise RegistrationError(
                f"Validator '{validator.name}' is already registered as "
                f"{existing.__class__.__name__} (version {existing.metadata.version})",
                validator_name=validator.name,
                validator_type=self.validator_type.__name__,
            )

        # Register validator
        self._validators[validator.name] = validator

        # Index by tags
        for tag in validator.metadata.tags:
            if tag not in self._tags:
                self._tags[tag] = []
            self._tags[tag].append(validator)

    def get(self, name: str) -> T:
        """Get a validator by name.

        Args:
            name: Validator name to lookup.

        Returns:
            The validator instance.

        Raises:
            KeyError: If no validator with that name is registered.
        """
        if name not in self._validators:
            raise KeyError(
                f"No validator named '{name}' registered in "
                f"{self.validator_type.__name__} registry. "
                f"Available: {sorted(self._validators.keys())}"
            )
        return self._validators[name]

    def get_by_tag(self, tag: str) -> list[T]:
        """Get all validators with a specific tag.

        Args:
            tag: Tag to search for.

        Returns:
            List of validators with that tag (empty if none found).
        """
        return self._tags.get(tag, [])

    def list_all(self) -> list[T]:
        """Get all registered validators.

        Returns:
            List of all registered validators.
        """
        return list(self._validators.values())

    def list_names(self) -> list[str]:
        """Get names of all registered validators.

        Returns:
            Sorted list of validator names.
        """
        return sorted(self._validators.keys())

    def has(self, name: str) -> bool:
        """Check if a validator is registered.

        Args:
            name: Validator name to check.

        Returns:
            True if validator is registered, False otherwise.
        """
        return name in self._validators

    def __len__(self) -> int:
        """Get number of registered validators."""
        return len(self._validators)

    def __repr__(self) -> str:
        """Return string representation of registry."""
        return f"ValidatorRegistry[{self.validator_type.__name__}]({len(self._validators)} validators)"

__init__(validator_type)

Initialize a validator registry for a specific type.

Parameters:

Name Type Description Default
validator_type type[T]

The class of validators this registry will accept.

required
Source code in src/nyctea/validators/registry.py
def __init__(self, validator_type: type[T]) -> None:
    """Initialize a validator registry for a specific type.

    Args:
        validator_type: The class of validators this registry will accept.
    """
    self.validator_type = validator_type
    self._validators: dict[str, T] = {}
    self._tags: dict[str, list[T]] = {}

__len__()

Get number of registered validators.

Source code in src/nyctea/validators/registry.py
def __len__(self) -> int:
    """Get number of registered validators."""
    return len(self._validators)

__repr__()

Return string representation of registry.

Source code in src/nyctea/validators/registry.py
def __repr__(self) -> str:
    """Return string representation of registry."""
    return f"ValidatorRegistry[{self.validator_type.__name__}]({len(self._validators)} validators)"

get(name)

Get a validator by name.

Parameters:

Name Type Description Default
name str

Validator name to lookup.

required

Returns:

Type Description
T

The validator instance.

Raises:

Type Description
KeyError

If no validator with that name is registered.

Source code in src/nyctea/validators/registry.py
def get(self, name: str) -> T:
    """Get a validator by name.

    Args:
        name: Validator name to lookup.

    Returns:
        The validator instance.

    Raises:
        KeyError: If no validator with that name is registered.
    """
    if name not in self._validators:
        raise KeyError(
            f"No validator named '{name}' registered in "
            f"{self.validator_type.__name__} registry. "
            f"Available: {sorted(self._validators.keys())}"
        )
    return self._validators[name]

get_by_tag(tag)

Get all validators with a specific tag.

Parameters:

Name Type Description Default
tag str

Tag to search for.

required

Returns:

Type Description
list[T]

List of validators with that tag (empty if none found).

Source code in src/nyctea/validators/registry.py
def get_by_tag(self, tag: str) -> list[T]:
    """Get all validators with a specific tag.

    Args:
        tag: Tag to search for.

    Returns:
        List of validators with that tag (empty if none found).
    """
    return self._tags.get(tag, [])

has(name)

Check if a validator is registered.

Parameters:

Name Type Description Default
name str

Validator name to check.

required

Returns:

Type Description
bool

True if validator is registered, False otherwise.

Source code in src/nyctea/validators/registry.py
def has(self, name: str) -> bool:
    """Check if a validator is registered.

    Args:
        name: Validator name to check.

    Returns:
        True if validator is registered, False otherwise.
    """
    return name in self._validators

list_all()

Get all registered validators.

Returns:

Type Description
list[T]

List of all registered validators.

Source code in src/nyctea/validators/registry.py
def list_all(self) -> list[T]:
    """Get all registered validators.

    Returns:
        List of all registered validators.
    """
    return list(self._validators.values())

list_names()

Get names of all registered validators.

Returns:

Type Description
list[str]

Sorted list of validator names.

Source code in src/nyctea/validators/registry.py
def list_names(self) -> list[str]:
    """Get names of all registered validators.

    Returns:
        Sorted list of validator names.
    """
    return sorted(self._validators.keys())

register(validator)

Register a validator instance.

Parameters:

Name Type Description Default
validator T

The validator to register.

required

Raises:

Type Description
TypeError

If validator is not of the correct type.

RegistrationError

If a validator with the same name is already registered.

Source code in src/nyctea/validators/registry.py
def register(self, validator: T) -> None:
    """Register a validator instance.

    Args:
        validator: The validator to register.

    Raises:
        TypeError: If validator is not of the correct type.
        RegistrationError: If a validator with the same name is already registered.
    """
    # Type validation
    if not isinstance(validator, self.validator_type):
        raise TypeError(f"Registry expects {self.validator_type.__name__}, got {type(validator).__name__}")

    # Name collision check
    if validator.name in self._validators:
        existing = self._validators[validator.name]
        raise RegistrationError(
            f"Validator '{validator.name}' is already registered as "
            f"{existing.__class__.__name__} (version {existing.metadata.version})",
            validator_name=validator.name,
            validator_type=self.validator_type.__name__,
        )

    # Register validator
    self._validators[validator.name] = validator

    # Index by tags
    for tag in validator.metadata.tags:
        if tag not in self._tags:
            self._tags[tag] = []
        self._tags[tag].append(validator)

Decorators

nyctea.validators.decorators.ValidatorDecorator

Decorator factory for functional-style validator registration.

This class provides decorators that wrap functions in anonymous validator classes and register them automatically.

Example

from nyctea.validators.registry import Registry import polars as pl

registry = Registry() decorators = ValidatorDecorator(registry)

@decorators.column_parser(name="trim") def trim(column: pl.Expr) -> pl.Expr: ... return column.str.strip_chars()

@decorators.column_check(name="positive", tags=["numeric"]) def is_positive(column: pl.Expr) -> pl.Expr: ... return column > 0

Source code in src/nyctea/validators/decorators.py
class ValidatorDecorator:
    """Decorator factory for functional-style validator registration.

    This class provides decorators that wrap functions in anonymous validator
    classes and register them automatically.

    Example:
        >>> from nyctea.validators.registry import Registry
        >>> import polars as pl
        >>>
        >>> registry = Registry()
        >>> decorators = ValidatorDecorator(registry)
        >>>
        >>> @decorators.column_parser(name="trim")
        >>> def trim(column: pl.Expr) -> pl.Expr:
        ...     return column.str.strip_chars()
        >>>
        >>> @decorators.column_check(name="positive", tags=["numeric"])
        >>> def is_positive(column: pl.Expr) -> pl.Expr:
        ...     return column > 0
    """

    def __init__(self, registry: Registry) -> None:
        """Initialize decorator factory with a registry.

        Args:
            registry: Registry where validators will be registered.
        """
        self.registry = registry

    def column_parser(
        self,
        name: str,
        description: str = "",
        version: str = "1.0.0",
        tags: Sequence[str] | None = None,
        author: str = "",
    ) -> Callable[[Callable[[pl.Expr], pl.Expr]], Callable[[pl.Expr], pl.Expr]]:
        """Decorator to register a function as a column parser.

        Args:
            name: Unique validator name.
            description: Human-readable description.
            version: Validator version.
            tags: Optional tags for discovery.
            author: Validator author.

        Returns:
            Decorator function.

        Example:
            >>> @decorators.column_parser(name="uppercase")
            >>> def to_upper(column: pl.Expr) -> pl.Expr:
            ...     return column.str.to_uppercase()
        """

        def decorator(func: Callable[[pl.Expr], pl.Expr]) -> Callable[[pl.Expr], pl.Expr]:
            # Create anonymous validator class wrapping the function
            class FunctionColumnParser(ColumnParser):
                def __init__(self) -> None:
                    metadata = ValidatorMetadata(
                        name=name,
                        description=description or func.__doc__ or "",
                        version=version,
                        tags=list(tags) if tags else [],
                        author=author,
                    )
                    super().__init__(metadata)

                def execute(self, column: pl.Expr, **kwargs: Any) -> pl.Expr:
                    return func(column, **kwargs)

                def validate_args(self, **kwargs: Any) -> None:
                    # No additional validation for function-based validators
                    pass

            # Create instance and register
            validator = FunctionColumnParser()
            self.registry.register_column_parser(validator)

            # Return original function for use
            return func

        return decorator

    def column_check(
        self,
        name: str,
        description: str = "",
        version: str = "1.0.0",
        tags: Sequence[str] | None = None,
        author: str = "",
    ) -> Callable[[Callable[[pl.Expr], pl.Expr]], Callable[[pl.Expr], pl.Expr]]:
        """Decorator to register a function as a column check.

        Args:
            name: Unique validator name.
            description: Human-readable description.
            version: Validator version.
            tags: Optional tags for discovery.
            author: Validator author.

        Returns:
            Decorator function.

        Example:
            >>> @decorators.column_check(name="not_empty")
            >>> def check_not_empty(column: pl.Expr) -> pl.Expr:
            ...     return column.str.len_chars() > 0
        """

        def decorator(func: Callable[[pl.Expr], pl.Expr]) -> Callable[[pl.Expr], pl.Expr]:
            # Create anonymous validator class wrapping the function
            class FunctionColumnCheck(ColumnCheck):
                def __init__(self) -> None:
                    metadata = ValidatorMetadata(
                        name=name,
                        description=description or func.__doc__ or "",
                        version=version,
                        tags=list(tags) if tags else [],
                        author=author,
                    )
                    super().__init__(metadata)

                def execute(self, column: pl.Expr, **kwargs: Any) -> pl.Expr:
                    return func(column, **kwargs)

                def validate_args(self, **kwargs: Any) -> None:
                    # No additional validation for function-based validators
                    pass

            # Create instance and register
            validator = FunctionColumnCheck()
            self.registry.register_column_check(validator)

            # Return original function for use
            return func

        return decorator

    def frame_parser(
        self,
        name: str,
        description: str = "",
        version: str = "1.0.0",
        tags: Sequence[str] | None = None,
        author: str = "",
        preserve_columns: bool = True,
        preserve_rows: bool = False,
    ) -> Callable[[Callable[[pl.LazyFrame], pl.LazyFrame]], Callable[[pl.LazyFrame], pl.LazyFrame]]:
        """Decorator to register a function as a frame parser.

        Args:
            name: Unique validator name.
            description: Human-readable description.
            version: Validator version.
            tags: Optional tags for discovery.
            author: Validator author.
            preserve_columns: If True, enforce column preservation.
            preserve_rows: If True, enforce row preservation.

        Returns:
            Decorator function.

        Example:
            >>> @decorators.frame_parser(name="sort_by_age", preserve_rows=True)
            >>> def sort_age(frame: pl.LazyFrame) -> pl.LazyFrame:
            ...     return frame.sort("age")
        """

        def decorator(
            func: Callable[[pl.LazyFrame], pl.LazyFrame],
        ) -> Callable[[pl.LazyFrame], pl.LazyFrame]:
            # Create anonymous validator class wrapping the function
            class FunctionFrameParser(FrameParser):
                def __init__(self) -> None:
                    metadata = ValidatorMetadata(
                        name=name,
                        description=description or func.__doc__ or "",
                        version=version,
                        tags=list(tags) if tags else [],
                        author=author,
                    )
                    super().__init__(
                        metadata,
                        preserve_columns=preserve_columns,
                        preserve_rows=preserve_rows,
                    )

                def execute(self, frame: pl.LazyFrame, **kwargs: Any) -> pl.LazyFrame:
                    return func(frame, **kwargs)

                def validate_args(self, **kwargs: Any) -> None:
                    # No additional validation for function-based validators
                    pass

            # Create instance and register
            validator = FunctionFrameParser()
            self.registry.register_frame_parser(validator)

            # Return original function for use
            return func

        return decorator

    def frame_check(
        self,
        name: str,
        description: str = "",
        version: str = "1.0.0",
        tags: Sequence[str] | None = None,
        author: str = "",
    ) -> Callable[[Callable[[pl.LazyFrame], pl.LazyFrame]], Callable[[pl.LazyFrame], pl.LazyFrame]]:
        """Decorator to register a function as a frame check.

        Args:
            name: Unique validator name.
            description: Human-readable description.
            version: Validator version.
            tags: Optional tags for discovery.
            author: Validator author.

        Returns:
            Decorator function.

        Example:
            >>> @decorators.frame_check(name="min_rows")
            >>> def check_min_rows(frame: pl.LazyFrame, min_rows: int = 1) -> pl.LazyFrame:
            ...     count = frame.select(pl.len()).collect().item()
            ...     if count < min_rows:
            ...         raise ValueError(f"Expected >= {min_rows} rows, got {count}")
            ...     return frame
        """

        def decorator(
            func: Callable[[pl.LazyFrame], pl.LazyFrame],
        ) -> Callable[[pl.LazyFrame], pl.LazyFrame]:
            # Create anonymous validator class wrapping the function
            class FunctionFrameCheck(FrameCheck):
                def __init__(self) -> None:
                    metadata = ValidatorMetadata(
                        name=name,
                        description=description or func.__doc__ or "",
                        version=version,
                        tags=list(tags) if tags else [],
                        author=author,
                    )
                    super().__init__(metadata)

                def execute(self, frame: pl.LazyFrame, **kwargs: Any) -> pl.LazyFrame:
                    return func(frame, **kwargs)

                def validate_args(self, **kwargs: Any) -> None:
                    # No additional validation for function-based validators
                    pass

            # Create instance and register
            validator = FunctionFrameCheck()
            self.registry.register_frame_check(validator)

            # Return original function for use
            return func

        return decorator

__init__(registry)

Initialize decorator factory with a registry.

Parameters:

Name Type Description Default
registry Registry

Registry where validators will be registered.

required
Source code in src/nyctea/validators/decorators.py
def __init__(self, registry: Registry) -> None:
    """Initialize decorator factory with a registry.

    Args:
        registry: Registry where validators will be registered.
    """
    self.registry = registry

column_check(name, description='', version='1.0.0', tags=None, author='')

Decorator to register a function as a column check.

Parameters:

Name Type Description Default
name str

Unique validator name.

required
description str

Human-readable description.

''
version str

Validator version.

'1.0.0'
tags Sequence[str] | None

Optional tags for discovery.

None
author str

Validator author.

''

Returns:

Type Description
Callable[[Callable[[Expr], Expr]], Callable[[Expr], Expr]]

Decorator function.

Example

@decorators.column_check(name="not_empty") def check_not_empty(column: pl.Expr) -> pl.Expr: ... return column.str.len_chars() > 0

Source code in src/nyctea/validators/decorators.py
def column_check(
    self,
    name: str,
    description: str = "",
    version: str = "1.0.0",
    tags: Sequence[str] | None = None,
    author: str = "",
) -> Callable[[Callable[[pl.Expr], pl.Expr]], Callable[[pl.Expr], pl.Expr]]:
    """Decorator to register a function as a column check.

    Args:
        name: Unique validator name.
        description: Human-readable description.
        version: Validator version.
        tags: Optional tags for discovery.
        author: Validator author.

    Returns:
        Decorator function.

    Example:
        >>> @decorators.column_check(name="not_empty")
        >>> def check_not_empty(column: pl.Expr) -> pl.Expr:
        ...     return column.str.len_chars() > 0
    """

    def decorator(func: Callable[[pl.Expr], pl.Expr]) -> Callable[[pl.Expr], pl.Expr]:
        # Create anonymous validator class wrapping the function
        class FunctionColumnCheck(ColumnCheck):
            def __init__(self) -> None:
                metadata = ValidatorMetadata(
                    name=name,
                    description=description or func.__doc__ or "",
                    version=version,
                    tags=list(tags) if tags else [],
                    author=author,
                )
                super().__init__(metadata)

            def execute(self, column: pl.Expr, **kwargs: Any) -> pl.Expr:
                return func(column, **kwargs)

            def validate_args(self, **kwargs: Any) -> None:
                # No additional validation for function-based validators
                pass

        # Create instance and register
        validator = FunctionColumnCheck()
        self.registry.register_column_check(validator)

        # Return original function for use
        return func

    return decorator

column_parser(name, description='', version='1.0.0', tags=None, author='')

Decorator to register a function as a column parser.

Parameters:

Name Type Description Default
name str

Unique validator name.

required
description str

Human-readable description.

''
version str

Validator version.

'1.0.0'
tags Sequence[str] | None

Optional tags for discovery.

None
author str

Validator author.

''

Returns:

Type Description
Callable[[Callable[[Expr], Expr]], Callable[[Expr], Expr]]

Decorator function.

Example

@decorators.column_parser(name="uppercase") def to_upper(column: pl.Expr) -> pl.Expr: ... return column.str.to_uppercase()

Source code in src/nyctea/validators/decorators.py
def column_parser(
    self,
    name: str,
    description: str = "",
    version: str = "1.0.0",
    tags: Sequence[str] | None = None,
    author: str = "",
) -> Callable[[Callable[[pl.Expr], pl.Expr]], Callable[[pl.Expr], pl.Expr]]:
    """Decorator to register a function as a column parser.

    Args:
        name: Unique validator name.
        description: Human-readable description.
        version: Validator version.
        tags: Optional tags for discovery.
        author: Validator author.

    Returns:
        Decorator function.

    Example:
        >>> @decorators.column_parser(name="uppercase")
        >>> def to_upper(column: pl.Expr) -> pl.Expr:
        ...     return column.str.to_uppercase()
    """

    def decorator(func: Callable[[pl.Expr], pl.Expr]) -> Callable[[pl.Expr], pl.Expr]:
        # Create anonymous validator class wrapping the function
        class FunctionColumnParser(ColumnParser):
            def __init__(self) -> None:
                metadata = ValidatorMetadata(
                    name=name,
                    description=description or func.__doc__ or "",
                    version=version,
                    tags=list(tags) if tags else [],
                    author=author,
                )
                super().__init__(metadata)

            def execute(self, column: pl.Expr, **kwargs: Any) -> pl.Expr:
                return func(column, **kwargs)

            def validate_args(self, **kwargs: Any) -> None:
                # No additional validation for function-based validators
                pass

        # Create instance and register
        validator = FunctionColumnParser()
        self.registry.register_column_parser(validator)

        # Return original function for use
        return func

    return decorator

frame_check(name, description='', version='1.0.0', tags=None, author='')

Decorator to register a function as a frame check.

Parameters:

Name Type Description Default
name str

Unique validator name.

required
description str

Human-readable description.

''
version str

Validator version.

'1.0.0'
tags Sequence[str] | None

Optional tags for discovery.

None
author str

Validator author.

''

Returns:

Type Description
Callable[[Callable[[LazyFrame], LazyFrame]], Callable[[LazyFrame], LazyFrame]]

Decorator function.

Example

@decorators.frame_check(name="min_rows") def check_min_rows(frame: pl.LazyFrame, min_rows: int = 1) -> pl.LazyFrame: ... count = frame.select(pl.len()).collect().item() ... if count < min_rows: ... raise ValueError(f"Expected >= {min_rows} rows, got {count}") ... return frame

Source code in src/nyctea/validators/decorators.py
def frame_check(
    self,
    name: str,
    description: str = "",
    version: str = "1.0.0",
    tags: Sequence[str] | None = None,
    author: str = "",
) -> Callable[[Callable[[pl.LazyFrame], pl.LazyFrame]], Callable[[pl.LazyFrame], pl.LazyFrame]]:
    """Decorator to register a function as a frame check.

    Args:
        name: Unique validator name.
        description: Human-readable description.
        version: Validator version.
        tags: Optional tags for discovery.
        author: Validator author.

    Returns:
        Decorator function.

    Example:
        >>> @decorators.frame_check(name="min_rows")
        >>> def check_min_rows(frame: pl.LazyFrame, min_rows: int = 1) -> pl.LazyFrame:
        ...     count = frame.select(pl.len()).collect().item()
        ...     if count < min_rows:
        ...         raise ValueError(f"Expected >= {min_rows} rows, got {count}")
        ...     return frame
    """

    def decorator(
        func: Callable[[pl.LazyFrame], pl.LazyFrame],
    ) -> Callable[[pl.LazyFrame], pl.LazyFrame]:
        # Create anonymous validator class wrapping the function
        class FunctionFrameCheck(FrameCheck):
            def __init__(self) -> None:
                metadata = ValidatorMetadata(
                    name=name,
                    description=description or func.__doc__ or "",
                    version=version,
                    tags=list(tags) if tags else [],
                    author=author,
                )
                super().__init__(metadata)

            def execute(self, frame: pl.LazyFrame, **kwargs: Any) -> pl.LazyFrame:
                return func(frame, **kwargs)

            def validate_args(self, **kwargs: Any) -> None:
                # No additional validation for function-based validators
                pass

        # Create instance and register
        validator = FunctionFrameCheck()
        self.registry.register_frame_check(validator)

        # Return original function for use
        return func

    return decorator

frame_parser(name, description='', version='1.0.0', tags=None, author='', preserve_columns=True, preserve_rows=False)

Decorator to register a function as a frame parser.

Parameters:

Name Type Description Default
name str

Unique validator name.

required
description str

Human-readable description.

''
version str

Validator version.

'1.0.0'
tags Sequence[str] | None

Optional tags for discovery.

None
author str

Validator author.

''
preserve_columns bool

If True, enforce column preservation.

True
preserve_rows bool

If True, enforce row preservation.

False

Returns:

Type Description
Callable[[Callable[[LazyFrame], LazyFrame]], Callable[[LazyFrame], LazyFrame]]

Decorator function.

Example

@decorators.frame_parser(name="sort_by_age", preserve_rows=True) def sort_age(frame: pl.LazyFrame) -> pl.LazyFrame: ... return frame.sort("age")

Source code in src/nyctea/validators/decorators.py
def frame_parser(
    self,
    name: str,
    description: str = "",
    version: str = "1.0.0",
    tags: Sequence[str] | None = None,
    author: str = "",
    preserve_columns: bool = True,
    preserve_rows: bool = False,
) -> Callable[[Callable[[pl.LazyFrame], pl.LazyFrame]], Callable[[pl.LazyFrame], pl.LazyFrame]]:
    """Decorator to register a function as a frame parser.

    Args:
        name: Unique validator name.
        description: Human-readable description.
        version: Validator version.
        tags: Optional tags for discovery.
        author: Validator author.
        preserve_columns: If True, enforce column preservation.
        preserve_rows: If True, enforce row preservation.

    Returns:
        Decorator function.

    Example:
        >>> @decorators.frame_parser(name="sort_by_age", preserve_rows=True)
        >>> def sort_age(frame: pl.LazyFrame) -> pl.LazyFrame:
        ...     return frame.sort("age")
    """

    def decorator(
        func: Callable[[pl.LazyFrame], pl.LazyFrame],
    ) -> Callable[[pl.LazyFrame], pl.LazyFrame]:
        # Create anonymous validator class wrapping the function
        class FunctionFrameParser(FrameParser):
            def __init__(self) -> None:
                metadata = ValidatorMetadata(
                    name=name,
                    description=description or func.__doc__ or "",
                    version=version,
                    tags=list(tags) if tags else [],
                    author=author,
                )
                super().__init__(
                    metadata,
                    preserve_columns=preserve_columns,
                    preserve_rows=preserve_rows,
                )

            def execute(self, frame: pl.LazyFrame, **kwargs: Any) -> pl.LazyFrame:
                return func(frame, **kwargs)

            def validate_args(self, **kwargs: Any) -> None:
                # No additional validation for function-based validators
                pass

        # Create instance and register
        validator = FunctionFrameParser()
        self.registry.register_frame_parser(validator)

        # Return original function for use
        return func

    return decorator

Legacy: FunctionRegistry

nyctea.functions.registry is the pre-Registry system. It is retained for compatibility and scheduled for removal. New code should use Registry above.

FunctionRegistry

nyctea.functions.registry.FunctionRegistry

Bases: BaseModel

Holds all registered functions with strict validation.

Source code in src/nyctea/functions/registry.py
class FunctionRegistry(BaseModel):
    """Holds all registered functions with strict validation."""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    column_parsers: dict[str, ColumnFunctionWrapper] = Field(default_factory=dict)
    column_checks: dict[str, ColumnFunctionWrapper] = Field(default_factory=dict)
    frame_parsers: dict[str, FrameFunctionWrapper] = Field(default_factory=dict)
    frame_checks: dict[str, FrameFunctionWrapper] = Field(default_factory=dict)

    @property
    def column_parser(self) -> DecoratorAdapter[ColumnFunction, ColumnFunctionWrapper]:
        """Decorator for registering column parsers."""
        return DecoratorAdapter(self.register_column_parser)

    @property
    def column_check(self) -> DecoratorAdapter[ColumnFunction, ColumnFunctionWrapper]:
        """Decorator for registering column checks."""
        return DecoratorAdapter(self.register_column_check)

    @property
    def frame_parser(self) -> DecoratorAdapter[FrameParserFunction, FrameFunctionWrapper]:
        """Decorator for registering frame parsers."""
        return DecoratorAdapter(self.register_frame_parser)

    @property
    def frame_check(self) -> DecoratorAdapter[FrameCheckFunction, FrameFunctionWrapper]:
        """Decorator for registering frame checks."""
        return DecoratorAdapter(self.register_frame_check)

    def register_column_parser(
        self,
        func: ColumnFunction,
        *,
        name: str | None = None,
    ) -> ColumnFunctionWrapper:
        """Register a column parser."""
        return self._register_column(func, name=name, store=self.column_parsers, kind="column parser")

    def register_column_check(
        self,
        func: ColumnFunction,
        *,
        name: str | None = None,
    ) -> ColumnFunctionWrapper:
        """Register a column check."""
        return self._register_column(func, name=name, store=self.column_checks, kind="column check")

    def register_frame_parser(
        self,
        func: FrameParserFunction,
        *,
        name: str | None = None,
    ) -> FrameFunctionWrapper:
        """Register a frame parser."""
        return self._register_frame(
            func,
            name=name,
            store=self.frame_parsers,
            kind="frame parser",
            enforce_rows=True,
        )

    def register_frame_check(
        self,
        func: FrameCheckFunction,
        *,
        name: str | None = None,
    ) -> FrameFunctionWrapper:
        """Register a frame check."""
        return self._register_frame(
            func,
            name=name,
            store=self.frame_checks,
            kind="frame check",
            enforce_rows=True,
        )

    def _register_column(
        self,
        func: ColumnFunction,
        *,
        name: str | None,
        store: dict[str, ColumnFunctionWrapper],
        kind: str,
    ) -> ColumnFunctionWrapper:
        """Register a column-level function after validation."""
        SignatureValidator.validate(
            func,
            expected_first=pl.Expr,
            expected_return=pl.Expr,
            kind=kind,
        )
        wrapped = ColumnFunctionWrapper(func, kind=kind)
        self._insert(name=name, func=wrapped, store=store, kind=kind)
        return wrapped

    def _register_frame(
        self,
        func: F,
        *,
        name: str | None,
        store: dict[str, FrameFunctionWrapper],
        kind: str,
        enforce_rows: bool,
    ) -> FrameFunctionWrapper:
        """Register a frame-level function after validation."""
        SignatureValidator.validate(
            func,
            expected_first=pl.LazyFrame,
            expected_return=pl.LazyFrame,
            kind=kind,
        )
        wrapped = FrameFunctionWrapper(func, kind=kind, enforce_row_count=enforce_rows)
        self._insert(name=name, func=wrapped, store=store, kind=kind)
        return wrapped

    def _insert(self, *, name: str | None, func: F, store: dict[str, F], kind: str) -> None:
        """Insert a validated function into its registry."""
        func_name = name or getattr(func, "__name__", "")
        if not func_name:
            raise RegistryError(f"{kind} functions must have a name")
        if func_name in store:
            raise RegistryError(f"{kind} '{func_name}' is already registered")
        store[func_name] = func

column_check property

Decorator for registering column checks.

column_parser property

Decorator for registering column parsers.

frame_check property

Decorator for registering frame checks.

frame_parser property

Decorator for registering frame parsers.

register_column_check(func, *, name=None)

Register a column check.

Source code in src/nyctea/functions/registry.py
def register_column_check(
    self,
    func: ColumnFunction,
    *,
    name: str | None = None,
) -> ColumnFunctionWrapper:
    """Register a column check."""
    return self._register_column(func, name=name, store=self.column_checks, kind="column check")

register_column_parser(func, *, name=None)

Register a column parser.

Source code in src/nyctea/functions/registry.py
def register_column_parser(
    self,
    func: ColumnFunction,
    *,
    name: str | None = None,
) -> ColumnFunctionWrapper:
    """Register a column parser."""
    return self._register_column(func, name=name, store=self.column_parsers, kind="column parser")

register_frame_check(func, *, name=None)

Register a frame check.

Source code in src/nyctea/functions/registry.py
def register_frame_check(
    self,
    func: FrameCheckFunction,
    *,
    name: str | None = None,
) -> FrameFunctionWrapper:
    """Register a frame check."""
    return self._register_frame(
        func,
        name=name,
        store=self.frame_checks,
        kind="frame check",
        enforce_rows=True,
    )

register_frame_parser(func, *, name=None)

Register a frame parser.

Source code in src/nyctea/functions/registry.py
def register_frame_parser(
    self,
    func: FrameParserFunction,
    *,
    name: str | None = None,
) -> FrameFunctionWrapper:
    """Register a frame parser."""
    return self._register_frame(
        func,
        name=name,
        store=self.frame_parsers,
        kind="frame parser",
        enforce_rows=True,
    )

Wrappers

ColumnFunctionWrapper

nyctea.functions.registry.ColumnFunctionWrapper

Callable wrapper that enforces column purity at invocation time.

Source code in src/nyctea/functions/registry.py
class ColumnFunctionWrapper:
    """Callable wrapper that enforces column purity at invocation time."""

    def __init__(self, func: ColumnFunction, *, kind: str) -> None:
        self.func = func
        self.kind = kind
        update_wrapper(self, func)

    def __call__(self, column: pl.Expr, *args: Any, **kwargs: Any) -> pl.Expr:
        """Invoke the wrapped function and enforce purity.

        Args:
            column: Source column expression.
            *args: Positional arguments passed to the wrapped function.
            **kwargs: Keyword arguments passed to the wrapped function.

        Returns:
            pl.Expr: Resulting expression from the wrapped function.

        Raises:
            ColumnPurityError: If the input or output violates purity rules.
            RegistryError: If the wrapped function returns an invalid type.
        """
        if not isinstance(column, pl.Expr):
            raise ColumnPurityError(
                f"{self.kind} '{self.func.__name__}' received non-expression input of type '{type(column).__name__}'"
            )

        source_columns = column.meta.root_names()
        if len(source_columns) != 1:
            raise ColumnPurityError(
                f"{self.kind} '{self.func.__name__}' can only operate on a single column expression"
            )

        result = self.func(column, *args, **kwargs)
        if not isinstance(result, pl.Expr):
            raise RegistryError(
                f"{self.kind} '{self.func.__name__}' must return a pl.Expr, got '{type(result).__name__}'"
            )

        referenced = set(result.meta.root_names())
        allowed = set(source_columns)
        if not referenced:
            raise ColumnPurityError(
                f"{self.kind} '{self.func.__name__}' must reference the source column '{next(iter(allowed))}'"
            )
        if referenced != allowed:
            bad = ", ".join(sorted(referenced - allowed))
            raise ColumnPurityError(
                f"{self.kind} '{self.func.__name__}' referenced disallowed columns: {bad or 'unknown'}"
            )
        return result

__call__(column, *args, **kwargs)

Invoke the wrapped function and enforce purity.

Parameters:

Name Type Description Default
column Expr

Source column expression.

required
*args Any

Positional arguments passed to the wrapped function.

()
**kwargs Any

Keyword arguments passed to the wrapped function.

{}

Returns:

Type Description
Expr

pl.Expr: Resulting expression from the wrapped function.

Raises:

Type Description
ColumnPurityError

If the input or output violates purity rules.

RegistryError

If the wrapped function returns an invalid type.

Source code in src/nyctea/functions/registry.py
def __call__(self, column: pl.Expr, *args: Any, **kwargs: Any) -> pl.Expr:
    """Invoke the wrapped function and enforce purity.

    Args:
        column: Source column expression.
        *args: Positional arguments passed to the wrapped function.
        **kwargs: Keyword arguments passed to the wrapped function.

    Returns:
        pl.Expr: Resulting expression from the wrapped function.

    Raises:
        ColumnPurityError: If the input or output violates purity rules.
        RegistryError: If the wrapped function returns an invalid type.
    """
    if not isinstance(column, pl.Expr):
        raise ColumnPurityError(
            f"{self.kind} '{self.func.__name__}' received non-expression input of type '{type(column).__name__}'"
        )

    source_columns = column.meta.root_names()
    if len(source_columns) != 1:
        raise ColumnPurityError(
            f"{self.kind} '{self.func.__name__}' can only operate on a single column expression"
        )

    result = self.func(column, *args, **kwargs)
    if not isinstance(result, pl.Expr):
        raise RegistryError(
            f"{self.kind} '{self.func.__name__}' must return a pl.Expr, got '{type(result).__name__}'"
        )

    referenced = set(result.meta.root_names())
    allowed = set(source_columns)
    if not referenced:
        raise ColumnPurityError(
            f"{self.kind} '{self.func.__name__}' must reference the source column '{next(iter(allowed))}'"
        )
    if referenced != allowed:
        bad = ", ".join(sorted(referenced - allowed))
        raise ColumnPurityError(
            f"{self.kind} '{self.func.__name__}' referenced disallowed columns: {bad or 'unknown'}"
        )
    return result

FrameFunctionWrapper

nyctea.functions.registry.FrameFunctionWrapper

Callable wrapper that enforces frame output type and shape.

Source code in src/nyctea/functions/registry.py
class FrameFunctionWrapper:
    """Callable wrapper that enforces frame output type and shape."""

    def __init__(self, func: FrameFunction, *, kind: str, enforce_row_count: bool) -> None:
        self.func = func
        self.kind = kind
        self.enforce_row_count = enforce_row_count
        update_wrapper(self, func)

    def __call__(self, frame: pl.LazyFrame, *args: Any, **kwargs: Any) -> pl.LazyFrame:
        """Invoke the wrapped function and enforce shape constraints.

        Args:
            frame: Input lazy frame.
            *args: Positional arguments passed to the wrapped function.
            **kwargs: Keyword arguments passed to the wrapped function.

        Returns:
            pl.LazyFrame: Output frame from the wrapped function.

        Raises:
            FrameShapeError: If type, column set, or row count are altered.
        """
        if not isinstance(frame, pl.LazyFrame):
            raise FrameShapeError(
                f"{self.kind} '{self.func.__name__}' received non-lazy input of type '{type(frame).__name__}'"
            )

        input_columns = tuple(frame.collect_schema().names())
        input_rows = self._row_count(frame) if self.enforce_row_count else None
        result = self.func(frame, *args, **kwargs)
        if not isinstance(result, pl.LazyFrame):
            raise FrameShapeError(
                f"{self.kind} '{self.func.__name__}' must return a pl.LazyFrame, got '{type(result).__name__}'"
            )

        output_columns = tuple(result.collect_schema().names())
        if input_columns != output_columns:
            raise FrameShapeError(
                f"{self.kind} '{self.func.__name__}' must preserve columns. "
                f"Before: {input_columns} After: {output_columns}"
            )

        if self.enforce_row_count:
            output_rows = self._row_count(result)
            if input_rows != output_rows:
                raise FrameShapeError(
                    f"{self.kind} '{self.func.__name__}' must preserve row count. "
                    f"Before: {input_rows} After: {output_rows}"
                )

        return result

    @staticmethod
    def _row_count(frame: pl.LazyFrame) -> int:
        """Collect row count for a lazy frame."""
        count_df = frame.select(pl.len().alias("row_count")).collect()
        return int(count_df.to_series(0).item())

__call__(frame, *args, **kwargs)

Invoke the wrapped function and enforce shape constraints.

Parameters:

Name Type Description Default
frame LazyFrame

Input lazy frame.

required
*args Any

Positional arguments passed to the wrapped function.

()
**kwargs Any

Keyword arguments passed to the wrapped function.

{}

Returns:

Type Description
LazyFrame

pl.LazyFrame: Output frame from the wrapped function.

Raises:

Type Description
FrameShapeError

If type, column set, or row count are altered.

Source code in src/nyctea/functions/registry.py
def __call__(self, frame: pl.LazyFrame, *args: Any, **kwargs: Any) -> pl.LazyFrame:
    """Invoke the wrapped function and enforce shape constraints.

    Args:
        frame: Input lazy frame.
        *args: Positional arguments passed to the wrapped function.
        **kwargs: Keyword arguments passed to the wrapped function.

    Returns:
        pl.LazyFrame: Output frame from the wrapped function.

    Raises:
        FrameShapeError: If type, column set, or row count are altered.
    """
    if not isinstance(frame, pl.LazyFrame):
        raise FrameShapeError(
            f"{self.kind} '{self.func.__name__}' received non-lazy input of type '{type(frame).__name__}'"
        )

    input_columns = tuple(frame.collect_schema().names())
    input_rows = self._row_count(frame) if self.enforce_row_count else None
    result = self.func(frame, *args, **kwargs)
    if not isinstance(result, pl.LazyFrame):
        raise FrameShapeError(
            f"{self.kind} '{self.func.__name__}' must return a pl.LazyFrame, got '{type(result).__name__}'"
        )

    output_columns = tuple(result.collect_schema().names())
    if input_columns != output_columns:
        raise FrameShapeError(
            f"{self.kind} '{self.func.__name__}' must preserve columns. "
            f"Before: {input_columns} After: {output_columns}"
        )

    if self.enforce_row_count:
        output_rows = self._row_count(result)
        if input_rows != output_rows:
            raise FrameShapeError(
                f"{self.kind} '{self.func.__name__}' must preserve row count. "
                f"Before: {input_rows} After: {output_rows}"
            )

    return result

DecoratorAdapter

nyctea.functions.registry.DecoratorAdapter

Bases: Generic[InFunc, OutFunc]

Decorator-like helper that avoids nested functions.

Source code in src/nyctea/functions/registry.py
class DecoratorAdapter(Generic[InFunc, OutFunc]):
    """Decorator-like helper that avoids nested functions."""

    def __init__(
        self,
        registrar: Callable[[InFunc, str | None], OutFunc],
        name: str | None = None,
    ) -> None:
        self._registrar = registrar
        self._name = name

    def __call__(
        self,
        func: InFunc | None = None,
        *,
        name: str | None = None,
    ) -> "OutFunc | DecoratorAdapter[InFunc, OutFunc]":
        """Apply registration or return a configured adapter."""
        chosen = name or self._name
        if func is None:
            return DecoratorAdapter(self._registrar, chosen)
        return self._registrar(func, name=chosen)

__call__(func=None, *, name=None)

Apply registration or return a configured adapter.

Source code in src/nyctea/functions/registry.py
def __call__(
    self,
    func: InFunc | None = None,
    *,
    name: str | None = None,
) -> "OutFunc | DecoratorAdapter[InFunc, OutFunc]":
    """Apply registration or return a configured adapter."""
    chosen = name or self._name
    if func is None:
        return DecoratorAdapter(self._registrar, chosen)
    return self._registrar(func, name=chosen)

Signature validation

SignatureValidator

nyctea.functions.registry.SignatureValidator

Utility class to enforce function signatures at registration time.

Source code in src/nyctea/functions/registry.py
class SignatureValidator:
    """Utility class to enforce function signatures at registration time."""

    @staticmethod
    def _strip_annotated(annotation: Any) -> Any:
        origin = get_origin(annotation)
        if origin is Annotated:
            args = get_args(annotation)
            if args:
                return args[0]
        return annotation

    @staticmethod
    def validate(
        func: Callable[..., Any],
        *,
        expected_first: type,
        expected_return: type,
        kind: str,
    ) -> None:
        """Validate callable signature and return type.

        Args:
            func: Function to validate.
            expected_first: Expected annotation for the first argument.
            expected_return: Expected return annotation.
            kind: Human-readable function kind for error messages.

        Raises:
            RegistryError: If any signature constraint is violated.
        """
        if not callable(func):
            raise RegistryError(f"{kind} must be callable")

        signature = inspect.signature(func)
        try:
            hints = get_type_hints(func, globalns=getattr(func, "__globals__", {}), localns=None)
        except Exception as err:
            raise RegistryError(
                f"{kind} '{func.__name__}' annotations could not be resolved; "
                "ensure all annotations are valid types (not unresolved forward references)"
            ) from err

        parameters = list(signature.parameters.values())
        if not parameters:
            raise RegistryError(f"{kind} '{func.__name__}' must accept at least one argument")

        first = parameters[0]
        if first.kind not in (
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        ):
            raise RegistryError(
                f"{kind} '{func.__name__}' must declare the first parameter as positional "
                f"and annotated as {expected_first.__name__}"
            )

        first_hint = hints.get(first.name)
        if first_hint is None:
            raise RegistryError(
                f"{kind} '{func.__name__}' must annotate the first parameter as {expected_first.__name__}"
            )
        first_hint = SignatureValidator._strip_annotated(first_hint)
        if first_hint is not expected_first:
            raise RegistryError(
                f"{kind} '{func.__name__}' must annotate the first parameter as {expected_first.__name__}"
            )

        return_hint = hints.get("return")
        if return_hint is None:
            raise RegistryError(f"{kind} '{func.__name__}' must return {expected_return.__name__}")
        return_hint = SignatureValidator._strip_annotated(return_hint)
        if return_hint is not expected_return:
            raise RegistryError(f"{kind} '{func.__name__}' must return {expected_return.__name__}")

        for param in parameters[1:]:
            if param.kind in (
                inspect.Parameter.VAR_POSITIONAL,
                inspect.Parameter.VAR_KEYWORD,
            ):
                raise RegistryError(
                    f"{kind} '{func.__name__}' may not use *args or **kwargs to avoid silent argument handling"
                )
            param_hint = hints.get(param.name)
            if param_hint is not None:
                param_hint = SignatureValidator._strip_annotated(param_hint)
            if param_hint is expected_first:
                raise RegistryError(
                    f"{kind} '{func.__name__}' may not accept multiple {expected_first.__name__} parameters"
                )
            if param.default is inspect._empty:
                continue
            try:
                json.dumps(param.default)  # type: ignore[arg-type]
            except TypeError as err:
                raise RegistryError(
                    f"{kind} '{func.__name__}' default for parameter '{param.name}' must be JSON-serializable"
                ) from err

validate(func, *, expected_first, expected_return, kind) staticmethod

Validate callable signature and return type.

Parameters:

Name Type Description Default
func Callable[..., Any]

Function to validate.

required
expected_first type

Expected annotation for the first argument.

required
expected_return type

Expected return annotation.

required
kind str

Human-readable function kind for error messages.

required

Raises:

Type Description
RegistryError

If any signature constraint is violated.

Source code in src/nyctea/functions/registry.py
@staticmethod
def validate(
    func: Callable[..., Any],
    *,
    expected_first: type,
    expected_return: type,
    kind: str,
) -> None:
    """Validate callable signature and return type.

    Args:
        func: Function to validate.
        expected_first: Expected annotation for the first argument.
        expected_return: Expected return annotation.
        kind: Human-readable function kind for error messages.

    Raises:
        RegistryError: If any signature constraint is violated.
    """
    if not callable(func):
        raise RegistryError(f"{kind} must be callable")

    signature = inspect.signature(func)
    try:
        hints = get_type_hints(func, globalns=getattr(func, "__globals__", {}), localns=None)
    except Exception as err:
        raise RegistryError(
            f"{kind} '{func.__name__}' annotations could not be resolved; "
            "ensure all annotations are valid types (not unresolved forward references)"
        ) from err

    parameters = list(signature.parameters.values())
    if not parameters:
        raise RegistryError(f"{kind} '{func.__name__}' must accept at least one argument")

    first = parameters[0]
    if first.kind not in (
        inspect.Parameter.POSITIONAL_ONLY,
        inspect.Parameter.POSITIONAL_OR_KEYWORD,
    ):
        raise RegistryError(
            f"{kind} '{func.__name__}' must declare the first parameter as positional "
            f"and annotated as {expected_first.__name__}"
        )

    first_hint = hints.get(first.name)
    if first_hint is None:
        raise RegistryError(
            f"{kind} '{func.__name__}' must annotate the first parameter as {expected_first.__name__}"
        )
    first_hint = SignatureValidator._strip_annotated(first_hint)
    if first_hint is not expected_first:
        raise RegistryError(
            f"{kind} '{func.__name__}' must annotate the first parameter as {expected_first.__name__}"
        )

    return_hint = hints.get("return")
    if return_hint is None:
        raise RegistryError(f"{kind} '{func.__name__}' must return {expected_return.__name__}")
    return_hint = SignatureValidator._strip_annotated(return_hint)
    if return_hint is not expected_return:
        raise RegistryError(f"{kind} '{func.__name__}' must return {expected_return.__name__}")

    for param in parameters[1:]:
        if param.kind in (
            inspect.Parameter.VAR_POSITIONAL,
            inspect.Parameter.VAR_KEYWORD,
        ):
            raise RegistryError(
                f"{kind} '{func.__name__}' may not use *args or **kwargs to avoid silent argument handling"
            )
        param_hint = hints.get(param.name)
        if param_hint is not None:
            param_hint = SignatureValidator._strip_annotated(param_hint)
        if param_hint is expected_first:
            raise RegistryError(
                f"{kind} '{func.__name__}' may not accept multiple {expected_first.__name__} parameters"
            )
        if param.default is inspect._empty:
            continue
        try:
            json.dumps(param.default)  # type: ignore[arg-type]
        except TypeError as err:
            raise RegistryError(
                f"{kind} '{func.__name__}' default for parameter '{param.name}' must be JSON-serializable"
            ) from err

Exceptions

RegistryError

nyctea.functions.registry.RegistryError

Bases: ValueError

Raised when a function cannot be registered.

Source code in src/nyctea/functions/registry.py
class RegistryError(ValueError):
    """Raised when a function cannot be registered."""

ColumnPurityError

nyctea.functions.registry.ColumnPurityError

Bases: RegistryError

Raised when a column function touches disallowed columns.

Source code in src/nyctea/functions/registry.py
class ColumnPurityError(RegistryError):
    """Raised when a column function touches disallowed columns."""

FrameShapeError

nyctea.functions.registry.FrameShapeError

Bases: RegistryError

Raised when a frame function alters shape unexpectedly.

Source code in src/nyctea/functions/registry.py
class FrameShapeError(RegistryError):
    """Raised when a frame function alters shape unexpectedly."""