Source code for ralph.pipeline.work_unit
"""Single planning work unit declaration."""
from __future__ import annotations
import re
from pathlib import PurePosixPath
from pydantic import ConfigDict, Field, field_validator
from ralph.pydantic_compat import RalphBaseModel
_UNIT_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
_UNIT_ID_MAX_LEN = 64
MAX_DESCRIPTION_CHARS = 4096
def _validate_relative_subpath(path: str) -> str:
if not path:
raise ValueError("allowed_directories entries must be non-empty")
if "\\" in path:
raise ValueError("allowed_directories entries must use '/' separators")
parsed = PurePosixPath(path)
if parsed.is_absolute():
raise ValueError("allowed_directories entries must be relative paths")
if ".." in parsed.parts:
raise ValueError("allowed_directories entries must not contain '..'")
return path
[docs]
class WorkUnit(RalphBaseModel):
"""Single planning work unit declaration."""
model_config = ConfigDict(frozen=True)
unit_id: str = Field(..., min_length=1)
description: str = Field(..., min_length=1, max_length=MAX_DESCRIPTION_CHARS)
allowed_directories: list[str] = Field(default_factory=list)
dependencies: list[str] = Field(default_factory=list)
@field_validator("unit_id")
@classmethod
def _validate_unit_id(cls, v: str) -> str:
if _UNIT_ID_RE.fullmatch(v):
return v
if not v:
raise ValueError("unit_id must be 1-64 chars from [a-zA-Z0-9_-] (got empty string)")
if len(v) > _UNIT_ID_MAX_LEN:
raise ValueError(
f"unit_id must be at most {_UNIT_ID_MAX_LEN} chars (got length {len(v)}: {v!r})"
)
invalid_char = next((ch for ch in v if not re.fullmatch(r"[a-zA-Z0-9_-]", ch)), None)
if invalid_char is not None:
raise ValueError(
f"unit_id contains invalid character {invalid_char!r}; allowed: [a-zA-Z0-9_-]"
)
raise ValueError(f"unit_id must match ^[a-zA-Z0-9_-]{{1,64}}$ (got: {v!r})")
@field_validator("allowed_directories")
@classmethod
def _validate_allowed_directories(cls, v: list[str]) -> list[str]:
return [_validate_relative_subpath(path) for path in v]