Skip to content

Utils

Utilities.

UniqueIdGenerator

Generate unique IDs using provided function.

Source code in src/snailz/utils.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class UniqueIdGenerator:
    """Generate unique IDs using provided function."""

    def __init__(self, name: str, func: callable, limit: int = 10000) -> None:
        """Initialize the UniqueIdGenerator.

        Parameters:
            name: A name for this generator (used in error messages)
            func: Function that creates IDs when called
            limit: Maximum number of attempts to find a unique ID
        """
        self._name = name
        self._func = func
        self._seen = set()
        self._limit = limit

    def next(self, *args: object) -> str:
        """Get next unique ID.

        Parameters:
            *args: Arguments to pass to the ID-generating function

        Returns:
            A unique identifier that hasn't been returned before

        Raises:
            RuntimeError: If unable to generate a unique ID within limit attempts
        """
        for i in range(self._limit):
            ident = self._func(*args)
            if ident in self._seen:
                continue
            self._seen.add(ident)
            return ident
        raise RuntimeError(f"failed to find unique ID for {self._name}")

__init__(name, func, limit=10000)

Initialize the UniqueIdGenerator.

Parameters:

Name Type Description Default
name str

A name for this generator (used in error messages)

required
func callable

Function that creates IDs when called

required
limit int

Maximum number of attempts to find a unique ID

10000
Source code in src/snailz/utils.py
18
19
20
21
22
23
24
25
26
27
28
29
def __init__(self, name: str, func: callable, limit: int = 10000) -> None:
    """Initialize the UniqueIdGenerator.

    Parameters:
        name: A name for this generator (used in error messages)
        func: Function that creates IDs when called
        limit: Maximum number of attempts to find a unique ID
    """
    self._name = name
    self._func = func
    self._seen = set()
    self._limit = limit

next(*args)

Get next unique ID.

Parameters:

Name Type Description Default
*args object

Arguments to pass to the ID-generating function

()

Returns:

Type Description
str

A unique identifier that hasn't been returned before

Raises:

Type Description
RuntimeError

If unable to generate a unique ID within limit attempts

Source code in src/snailz/utils.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def next(self, *args: object) -> str:
    """Get next unique ID.

    Parameters:
        *args: Arguments to pass to the ID-generating function

    Returns:
        A unique identifier that hasn't been returned before

    Raises:
        RuntimeError: If unable to generate a unique ID within limit attempts
    """
    for i in range(self._limit):
        ident = self._func(*args)
        if ident in self._seen:
            continue
        self._seen.add(ident)
        return ident
    raise RuntimeError(f"failed to find unique ID for {self._name}")

check_keys_and_types(spec, actual)

Check that parameters have all and only required keys with correct types.

Parameters:

Name Type Description Default
spec dict[str, type]

Dictionary with parameter names as keys and expected types as values

required
actual dict[str, object]

Dictionary with parameter names and their actual values

required

Raises:

Type Description
ValueError

If any parameters are missing, extra parameters are present, or parameters have the wrong type

Source code in src/snailz/utils.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def check_keys_and_types(spec: dict[str, type], actual: dict[str, object]) -> None:
    """Check that parameters have all and only required keys with correct types.

    Parameters:
        spec: Dictionary with parameter names as keys and expected types as values
        actual: Dictionary with parameter names and their actual values

    Raises:
        ValueError: If any parameters are missing, extra parameters are present,
                   or parameters have the wrong type
    """

    missing = set(spec.keys()) - set(actual.keys())
    require(not missing, f"Missing parameter(s): {list(sorted(missing))}")

    extra = set(actual.keys()) - set(spec.keys())
    require(not extra, f"Extra parameter(s): {list(sorted(extra))}")

    for key, required_type in spec.items():
        require(
            isinstance(actual[key], required_type),
            f"Parameter {key} has wrong type: got {type(actual[key])} expected {required_type}",
        )

fail(msg)

Print message to standard error and exit with status 1.

Parameters:

Name Type Description Default
msg str

The error message to display

required

Note: This function does not return as it exits the program

Source code in src/snailz/utils.py
77
78
79
80
81
82
83
84
85
86
def fail(msg: str) -> None:
    """Print message to standard error and exit with status 1.

    Parameters:
        msg: The error message to display

    Note: This function does not return as it exits the program
    """
    print(msg, file=sys.stderr)
    sys.exit(1)

load_data(parameter_name, filename, cls)

Construct a dataclass from serialized JSON.

Parameters:

Name Type Description Default
parameter_name str

Name of the parameter requiring this file (for error messages)

required
filename str | None

Path to the JSON file to load

required
cls type[T]

The dataclass to instantiate with the loaded data

required

Returns:

Type Description
T

An instance of cls constructed from the JSON data

Raises:

Type Description
ValueError

If filename is None or empty

IOError

If the file cannot be read

Source code in src/snailz/utils.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def load_data[T](parameter_name: str, filename: str | None, cls: type[T]) -> T:
    """Construct a dataclass from serialized JSON.

    Parameters:
        parameter_name: Name of the parameter requiring this file (for error messages)
        filename: Path to the JSON file to load
        cls: The dataclass to instantiate with the loaded data

    Returns:
        An instance of cls constructed from the JSON data

    Raises:
        ValueError: If filename is None or empty
        IOError: If the file cannot be read
    """
    require(bool(filename), f"--{parameter_name} is required")
    with open(filename, "r") as reader:
        return from_dict(data_class=cls, data=json.load(reader))

report_result(output, result)

Save or display result as JSON.

Parameters:

Name Type Description Default
output str | None

Path to output file, or None to print to stdout

required
result object

The dataclass object to serialize as JSON

required
Side effects

Either writes to the specified output file or prints to stdout

Source code in src/snailz/utils.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def report_result(output: str | None, result: object) -> None:
    """Save or display result as JSON.

    Parameters:
        output: Path to output file, or None to print to stdout
        result: The dataclass object to serialize as JSON

    Side effects:
        Either writes to the specified output file or prints to stdout
    """
    result_json = json.dumps(asdict(result), default=serialize_values)
    if output:
        with open(output, "w") as writer:
            writer.write(result_json)
    else:
        print(result_json)

require(cond, msg, cls=ValueError)

Raise exception if condition not satisfied.

Parameters:

Name Type Description Default
cond bool

The condition to check

required
msg str

The error message to use if condition is not met

required
cls type

The exception class to raise (default: ValueError)

ValueError

Raises:

Type Description
cls

If cond is False or evaluates to a falsy value

Source code in src/snailz/utils.py
127
128
129
130
131
132
133
134
135
136
137
138
139
def require(cond: bool, msg: str, cls: type = ValueError) -> None:
    """Raise exception if condition not satisfied.

    Parameters:
        cond: The condition to check
        msg: The error message to use if condition is not met
        cls: The exception class to raise (default: ValueError)

    Raises:
        cls: If cond is False or evaluates to a falsy value
    """
    if not cond:
        raise cls(msg)

serialize_values(obj)

Custom JSON serializer for dates.

Parameters:

Name Type Description Default
obj object

The object to serialize

required

Returns:

Type Description
str

String representation of date objects

Raises:

Type Description
TypeError

If the object type is not supported for serialization

Source code in src/snailz/utils.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def serialize_values(obj: object) -> str:
    """Custom JSON serializer for dates.

    Parameters:
        obj: The object to serialize

    Returns:
        String representation of date objects

    Raises:
        TypeError: If the object type is not supported for serialization
    """
    if isinstance(obj, date):
        return obj.isoformat()
    raise TypeError(f"Type {type(obj)} not serializable")

validate_date(ctx, param, value)

Validate and convert date string to date object.

Parameters:

Name Type Description Default
ctx object

Click context object

required
param object

Click parameter being processed

required
value str | None

The value to validate

required

Returns:

Type Description
date | None

None if value is None, otherwise a date object

Source code in src/snailz/utils.py
159
160
161
162
163
164
165
166
167
168
169
170
def validate_date(ctx: object, param: object, value: str | None) -> date | None:
    """Validate and convert date string to date object.

    Parameters:
        ctx: Click context object
        param: Click parameter being processed
        value: The value to validate

    Returns:
        None if value is None, otherwise a date object
    """
    return None if value is None else date.fromisoformat(value)