Source code for bluebase.core.domain

import struct

from typing import ClassVar

from abc import (
    ABC,
    abstractmethod,
)
from dataclasses import dataclass

from .util import dump
from .value import Value


__all__ = (
    'Domain',
    'BoolDomain',
    'IntDomain',
    'FloatDomain',
    'StringDomain',
)


[docs] class Domain(ABC): """ An abstract base class for domains. """ ENDIAN: ClassVar[str] = '<' """The endian format used by the `struct` module.""" enum: ClassVar[int] """The enum value of the domain."""
[docs] @classmethod def restore(cls, enum: int, size: int | None = None) -> 'Domain': """ Restore a domain from its enum value and size. The size is required only for `StringDomain`. """ match enum: case 0: return BoolDomain() case 1: return IntDomain() case 2: return FloatDomain() case 3: assert size is not None return StringDomain(size) case _: # pragma: no cover assert False
[docs] @classmethod def parse(cls, format: str) -> 'Domain': """ Restore a domain from its format. Remove the endian prefix, if present, and use the default endian. """ format = format.lstrip(Domain.ENDIAN) match format: case 'B': return BoolDomain() case 'i': return IntDomain() case 'f': return FloatDomain() case _: if format.endswith('s'): return StringDomain(int(format.rstrip('s'))) assert False # pragma: no cover
@property @abstractmethod def format(self) -> str: # pragma: no cover """ The format of the domain. This format is used by the `struct` module. """ pass @property def size(self) -> int: """ The size of the domain. """ return struct.calcsize(self.format)
[docs] @abstractmethod def validate(self, value: Value) -> None: # pragma: no cover """ Validate the type of a value. Raises: TypeError: if the value has an invalid type """ pass
[docs] @abstractmethod def encode(self, value: Value) -> bytes: # pragma: no cover """ Encode a value into data. """ pass
[docs] @abstractmethod def decode(self, data: bytes) -> Value: # pragma: no cover """ Decode data into a value. """ pass
@abstractmethod def verbose(self) -> str: # pragma: no cover pass
[docs] @dataclass(frozen=True, slots=True) class BoolDomain(Domain): """ A domain for `bool` values. """ enum = 0 def __repr__(self) -> str: # pragma: no cover return "BoolDomain()" @property def format(self) -> str: return Domain.ENDIAN + 'B'
[docs] def validate(self, value: Value) -> None: if type(value) is not bool: raise TypeError(f"BoolDomain value must be of type bool ({dump( value=type(value), )}).")
[docs] def encode(self, value: Value) -> bytes: self.validate(value) data = struct.pack(self.format, bool(value)) return data
[docs] def decode(self, data: bytes) -> bool: (value,) = struct.unpack(self.format, data) value = bool(value) return value
def verbose(self) -> str: # pragma: no cover return "BOOL"
[docs] @dataclass(frozen=True, slots=True) class IntDomain(Domain): """ A domain for `int` values. """ enum = 1 def __repr__(self) -> str: # pragma: no cover return "IntDomain()" @property def format(self) -> str: return Domain.ENDIAN + 'i'
[docs] def validate(self, value: Value) -> None: if type(value) is not int: raise TypeError(f"IntDomain value must be of type int ({dump( value=type(value), )}).")
[docs] def encode(self, value: Value) -> bytes: self.validate(value) data = struct.pack(self.format, value) return data
[docs] def decode(self, data: bytes) -> int: (value,) = struct.unpack(self.format, data) return value
def verbose(self) -> str: # pragma: no cover return "INT"
[docs] @dataclass(frozen=True, slots=True) class FloatDomain(Domain): """ A domain for `float` values. """ enum = 2 def __repr__(self) -> str: # pragma: no cover return "FloatDomain()" @property def format(self) -> str: return Domain.ENDIAN + 'f'
[docs] def validate(self, value: Value) -> None: if type(value) is not float: raise TypeError(f"FloatDomain value must be of type float ({dump( value=type(value), )}).")
[docs] def encode(self, value: Value) -> bytes: self.validate(value) data = struct.pack(self.format, value) return data
[docs] def decode(self, data: bytes) -> float: (value,) = struct.unpack(self.format, data) return value
def verbose(self) -> str: # pragma: no cover return "FLOAT"
[docs] @dataclass(frozen=True, slots=True) class StringDomain(Domain): """ A domain for `str` values. Attributes: length: the length of a string Raises: ValueError: if the string length is invalid """ ENCODING: ClassVar[str] = 'utf-8' """The encoding used for strings.""" MAX_LENGTH: ClassVar[int] = 255 """The maximum length of a string.""" enum = 3 length: int def __repr__(self) -> str: # pragma: no cover return f"StringDomain({self.length})" def __post_init__(self) -> None: if self.length < 1 or self.length > StringDomain.MAX_LENGTH: raise ValueError(f"Invalid StringDomain length ({dump( length=self.length, min=1, max=StringDomain.MAX_LENGTH, )}).") @property def format(self) -> str: return Domain.ENDIAN + f'{self.length}s'
[docs] def validate(self, value: Value) -> None: """ Validate the type and length of a value. Raises: TypeError: if the value has an invalid type ValueError: if the value length exceeds the domain length """ if type(value) is not str: raise TypeError(f"StringDomain value must be of type str ({dump( value=type(value), )}).") if len(value) > self.length: raise ValueError(f"Value length exceeds the domain length ({dump( value=len(value), domain=self.length, )}).")
[docs] def encode(self, value: Value) -> bytes: self.validate(value) assert type(value) is str data = value.encode(StringDomain.ENCODING) data = data[:self.length] data = data.ljust(self.length, b'\x00') return data
[docs] def decode(self, data: bytes) -> str: """ Decode data into a value. Raises: ValueError: if the data length does not match the domain length """ if len(data) != self.length: raise ValueError(f"Data length does not match the domain length ({dump( data=len(data), domain=self.length, )}).") data = data[:self.length] value = data.decode(StringDomain.ENCODING) value = value.rstrip('\x00') return value
def verbose(self) -> str: # pragma: no cover return f"STRING({self.length})"