Skip to content

raplan.utils

RaPlan utilities.

convert_pascal_str

convert_pascal_str(
    input: str, space: str = "_", lowercase: bool = True
) -> str

Convert cased input string to a uniform representation.

Parameters:

Name Type Description Default
input str

Input string to convert.

required
space str

Character to use as a space.

'_'
lowercase bool

Whether to enforce lowercase output.

True
Source code in src/raplan/utils.py
def convert_pascal_str(input: str, space: str = "_", lowercase: bool = True) -> str:
    """Convert cased input string to a uniform representation.

    Arguments:
        input: Input string to convert.
        space: Character to use as a space.
        lowercase: Whether to enforce lowercase output.
    """

    def convert(input) -> Generator[str, None, None]:
        caps = ""
        was_lower = False

        for c in input:
            if c.isupper():
                if was_lower:
                    yield space
                caps += c.lower() if lowercase else c

                was_lower = False
            else:
                if not was_lower and caps:
                    yield caps[:-1]
                    if len(caps) > 1:
                        yield space
                    yield caps[-1]
                    caps = ""

                yield c

                was_lower = True

        yield caps

    return "".join(convert(input))

datetime_to_float_years

datetime_to_float_years(dt: datetime) -> float

Convert a datetime to a float.

Source code in src/raplan/utils.py
def datetime_to_float_years(dt: datetime) -> float:
    """Convert a datetime to a float."""

    year = dt.year

    delta = dt - datetime(year=year, month=1, day=1)
    delta_days = delta.days + delta.seconds / 86400 + delta.microseconds / 86400000
    frac = delta_days / days_in_year(year)

    return year + frac

days_in_year

days_in_year(year: int) -> int

Return the number of days in a year.

Source code in src/raplan/utils.py
def days_in_year(year: int) -> int:
    """Return the number of days in a year."""
    is_leap = year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

    return 365 + int(is_leap)

deterministic_uuidv4_from_set

deterministic_uuidv4_from_set(uuids: list[UUID]) -> UUID

Calculate a deterministic UUIDv4 for a collection of UUID's.

Source code in src/raplan/utils.py
def deterministic_uuidv4_from_set(uuids: list[UUID]) -> UUID:
    """Calculate a deterministic UUIDv4 for a collection of UUID's."""
    items = sorted(uuids)
    data = b"".join(item.bytes for item in items)
    h = sha256(data).digest()
    b = bytearray(h[:16])
    b[6] = (b[6] & 0x0F) | (4 << 4)  # set version to 4
    b[8] = (b[8] & 0x3F) | 0x80  # set RFC4122 variant
    return UUID(bytes=bytes(b))

deterministic_uuidv5_from_hashes

deterministic_uuidv5_from_hashes(
    *objects: Hashable,
) -> UUID

Calclate a deterministic UUIDv5 for hashable objects.

Source code in src/raplan/utils.py
def deterministic_uuidv5_from_hashes(*objects: Hashable) -> UUID:
    """Calclate a deterministic UUIDv5 for hashable objects."""
    hashes: list[int] = sorted({hash(obj) for obj in objects})
    enc = b"".join(h.to_bytes(8, byteorder="big", signed=True) for h in hashes)
    name = base64.urlsafe_b64encode(enc).decode("ascii")
    return uuid5(NAMESPACE_DNS, name)

float_years_to_datetime

float_years_to_datetime(
    years: float, precision: YearPrecision = "exact"
) -> datetime

Convert a float of years into a datetime object (rounded down to given precision).

Source code in src/raplan/utils.py
def float_years_to_datetime(years: float, precision: YearPrecision = "exact") -> datetime:
    """Convert a float of years into a datetime object (rounded down to given precision)."""
    year = int(years)
    frac = years - year

    if precision == "year":
        return datetime(year=year, month=1, day=1)

    days = frac * days_in_year(year)
    time = datetime(year=year, month=1, day=1) + timedelta(days=days)

    if precision == "month":
        return datetime(year=time.year, month=time.month, day=1)

    if precision == "day":
        return datetime(year=time.year, month=time.month, day=time.day)

    if precision == "hour":
        return datetime(year=time.year, month=time.month, day=time.day, hour=time.hour)

    return time

gen_dataclass_columns

gen_dataclass_columns(cls) -> Generator[str, None, None]

Yield fields and subfields from a class or object.

Source code in src/raplan/utils.py
def gen_dataclass_columns(cls) -> Generator[str, None, None]:
    """Yield fields and subfields from a class or object."""
    if not isclass(cls):
        cls = cls.__class__
    if issubclass(cls, Enum):
        raise TypeError("Enum should not be treated as a dataclass with fields.")

    for f in fields(cls):
        field_name = convert_pascal_str(f.name)

        # Extract the type's origin of this field (such as list or set)
        type_origin = get_origin(f.type)

        # Attempt to find sub-columns and fall back to a plain value if that fails.
        try:
            # Handle lists and sets and prepare boolean columns.
            if type_origin is list or type_origin is set:
                sub_type = get_args(f.type)[0]
                for sub in gen_enum_collection_columns(sub_type):
                    yield f"{field_name}_{sub}"
            else:
                # Handle as if there are could be sub columns.
                for sub in gen_dataclass_columns(f.type):
                    yield f"{field_name}_{sub}"

        # No sub-columns, assume a plain value.
        except TypeError:
            yield field_name

gen_dataclass_types

gen_dataclass_types(
    cls,
) -> Generator[tuple[str, type], None, None]

Yield fields and subfields from a class or object.

Source code in src/raplan/utils.py
def gen_dataclass_types(cls) -> Generator[tuple[str, type], None, None]:
    """Yield fields and subfields from a class or object."""
    if not isclass(cls):
        cls = cls.__class__
    if issubclass(cls, Enum):
        raise TypeError("Enum should not be treated as a dataclass with fields.")

    for f in fields(cls):
        field_name = convert_pascal_str(f.name)

        # Extract the type's origin of this field (such as list or set)
        type_origin = get_origin(f.type)

        # Attempt to find sub-columns and fall back to a plain value if that fails.
        try:
            # Handle lists and sets and prepare boolean columns.
            if type_origin is list or type_origin is set:
                sub_type = get_args(f.type)[0]
                for sub_name in gen_enum_collection_columns(sub_type):
                    yield (f"{field_name}_{sub_name}", bool)
            else:
                # Handle as if there are could be sub columns.
                for sub_name, sub_type in gen_dataclass_types(f.type):
                    yield (f"{field_name}_{sub_name}", sub_type)

        # No sub-columns, assume a plain value.
        except TypeError:
            yield (field_name, cls)

gen_dataclass_values

gen_dataclass_values(
    obj,
) -> Generator[str | bool, None, None]

Yield all field and sub-field values in order of their dataclass field definition.

Source code in src/raplan/utils.py
def gen_dataclass_values(obj) -> Generator[str | bool, None, None]:
    """Yield all field and sub-field values in order of their dataclass field definition."""
    if issubclass(obj.__class__, Enum):
        raise TypeError("Enum should not be treated as a dataclass.")

    for f in fields(obj):
        value = getattr(obj, f.name)

        # Catch the error when that doesn't work and proceed to check alternatives.
        type_origin = get_origin(f.type)

        # Attempt to find sub-columns and fall back to a plain value if that fails.
        try:
            # Handle lists and sets and prepare boolean columns.
            if type_origin is list or type_origin is set:
                yield from gen_enum_collection_toggles(value, get_args(f.type)[0])
            else:
                # Handle as if there are could be sub columns.
                yield from gen_dataclass_values(value)
        except TypeError:
            yield value.name if isinstance(value, StrEnum) else value

gen_enum_collection_columns

gen_enum_collection_columns(
    cls,
) -> Generator[str, None, None]

Column names for toggles for an Enum.

Source code in src/raplan/utils.py
def gen_enum_collection_columns(cls) -> Generator[str, None, None]:
    """Column names for toggles for an Enum."""
    if not isclass(cls):
        cls = cls.__class__
    if issubclass(cls, Enum):
        for mn in cls:
            yield convert_pascal_str(mn.name)
    else:
        raise TypeError("Not an enumeration.")

gen_enum_collection_toggles

gen_enum_collection_toggles(
    collection, cls
) -> Generator[bool, None, None]

Column boolean values for an Enum.

Source code in src/raplan/utils.py
def gen_enum_collection_toggles(collection, cls) -> Generator[bool, None, None]:
    """Column boolean values for an Enum."""
    if issubclass(cls, Enum):
        for mn in cls:
            yield mn in collection
    else:
        raise TypeError("Not an enumeration.")