unicode-animations 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:raw.githubusercontent.com)",
5
+ "WebFetch(domain:api.github.com)",
6
+ "Bash(python -m pytest:*)",
7
+ "Bash(python3 -m pytest:*)",
8
+ "Bash(uv run pytest:*)",
9
+ "Bash(uv pip install:*)",
10
+ "Bash(uv run:*)",
11
+ "Bash(cd:*)"
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ _reference/
9
+ .env
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: unicode-animations
3
+ Version: 0.1.0
4
+ Summary: Pre-built Unicode braille spinner animations as raw frame data
5
+ License-Expression: MIT
6
+ Keywords: animation,braille,cli,spinner,unicode
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == 'dev'
@@ -0,0 +1,48 @@
1
+ # unicode-animations
2
+
3
+ Python port of [gunnargray-dev/unicode-animations](https://github.com/gunnargray-dev/unicode-animations).
4
+
5
+ 18 pre-built Unicode braille spinner animations as raw frame data, plus grid utilities for building custom spinners. Zero dependencies.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ uv add unicode-animations
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from unicode_animations import spinners
17
+
18
+ spinner = spinners["helix"]
19
+ print(spinner.frames) # tuple of animation frames
20
+ print(spinner.interval) # ms between frames
21
+ ```
22
+
23
+ ### Custom spinners with the grid API
24
+
25
+ ```python
26
+ from unicode_animations import make_grid, grid_to_braille
27
+
28
+ grid = make_grid(4, 2) # 4 rows × 2 cols (one braille char)
29
+ grid[0][0] = True
30
+ grid[3][1] = True
31
+ print(grid_to_braille(grid)) # ⡁
32
+ ```
33
+
34
+ ### CLI demo
35
+
36
+ ```
37
+ uv run python -m unicode_animations # cycle through all spinners
38
+ uv run python -m unicode_animations helix # preview one
39
+ uv run python -m unicode_animations --list # list all names
40
+ ```
41
+
42
+ ## Available spinners
43
+
44
+ `braille` · `braillewave` · `dna` · `scan` · `rain` · `scanline` · `pulse` · `snake` · `sparkle` · `cascade` · `columns` · `orbit` · `breathe` · `waverows` · `checkerboard` · `helix` · `fillsweep` · `diagswipe`
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "unicode-animations"
7
+ version = "0.1.0"
8
+ description = "Pre-built Unicode braille spinner animations as raw frame data"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ keywords = ["spinner", "braille", "unicode", "animation", "cli"]
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = ["pytest"]
File without changes
@@ -0,0 +1,125 @@
1
+ import pytest
2
+
3
+ from unicode_animations.braille import spinners, grid_to_braille, make_grid, BrailleSpinnerName
4
+
5
+ ALL_NAMES: list[str] = [
6
+ "braille", "braillewave", "dna",
7
+ "scan", "rain", "scanline", "pulse", "snake",
8
+ "sparkle", "cascade", "columns", "orbit", "breathe",
9
+ "waverows", "checkerboard", "helix", "fillsweep", "diagswipe",
10
+ ]
11
+
12
+
13
+ # ── make_grid ──────────────────────────────────────────────────────────
14
+
15
+
16
+ class TestMakeGrid:
17
+ def test_correct_dimensions(self):
18
+ g = make_grid(4, 8)
19
+ assert len(g) == 4
20
+ assert len(g[0]) == 8
21
+ assert all(cell is False for row in g for cell in row)
22
+
23
+ def test_zero_dimensions(self):
24
+ assert make_grid(0, 5) == []
25
+ assert make_grid(5, 0) == []
26
+
27
+ def test_negative_dimensions(self):
28
+ assert make_grid(-1, 5) == []
29
+ assert make_grid(5, -1) == []
30
+
31
+
32
+ # ── grid_to_braille ───────────────────────────────────────────────────
33
+
34
+
35
+ class TestGridToBraille:
36
+ def test_empty_grid(self):
37
+ assert grid_to_braille([]) == ""
38
+
39
+ def test_blank_braille_char(self):
40
+ g = make_grid(4, 2)
41
+ assert grid_to_braille(g) == "\u2800"
42
+
43
+ def test_full_braille_char(self):
44
+ g = make_grid(4, 2)
45
+ for r in range(4):
46
+ for c in range(2):
47
+ g[r][c] = True
48
+ assert grid_to_braille(g) == "\u28FF"
49
+
50
+ def test_individual_dots(self):
51
+ # dot1 (row0, col0) = 0x01
52
+ g1 = make_grid(4, 2)
53
+ g1[0][0] = True
54
+ assert grid_to_braille(g1) == "\u2801"
55
+
56
+ # dot4 (row0, col1) = 0x08
57
+ g2 = make_grid(4, 2)
58
+ g2[0][1] = True
59
+ assert grid_to_braille(g2) == "\u2808"
60
+
61
+ # dot2 (row1, col0) = 0x02
62
+ g3 = make_grid(4, 2)
63
+ g3[1][0] = True
64
+ assert grid_to_braille(g3) == "\u2802"
65
+
66
+ # dot5 (row1, col1) = 0x10
67
+ g4 = make_grid(4, 2)
68
+ g4[1][1] = True
69
+ assert grid_to_braille(g4) == "\u2810"
70
+
71
+ # dot3 (row2, col0) = 0x04
72
+ g5 = make_grid(4, 2)
73
+ g5[2][0] = True
74
+ assert grid_to_braille(g5) == "\u2804"
75
+
76
+ # dot6 (row2, col1) = 0x20
77
+ g6 = make_grid(4, 2)
78
+ g6[2][1] = True
79
+ assert grid_to_braille(g6) == "\u2820"
80
+
81
+ # dot7 (row3, col0) = 0x40
82
+ g7 = make_grid(4, 2)
83
+ g7[3][0] = True
84
+ assert grid_to_braille(g7) == "\u2840"
85
+
86
+ # dot8 (row3, col1) = 0x80
87
+ g8 = make_grid(4, 2)
88
+ g8[3][1] = True
89
+ assert grid_to_braille(g8) == "\u2880"
90
+
91
+ def test_multiple_characters(self):
92
+ g = make_grid(4, 4)
93
+ g[0][0] = True
94
+ g[0][2] = True
95
+ result = grid_to_braille(g)
96
+ assert len(result) == 2
97
+ assert result == "\u2801\u2801"
98
+
99
+ def test_odd_width(self):
100
+ g = make_grid(4, 3)
101
+ g[0][0] = True
102
+ g[0][2] = True
103
+ result = grid_to_braille(g)
104
+ assert len(result) == 2
105
+
106
+
107
+ # ── Spinners ──────────────────────────────────────────────────────────
108
+
109
+
110
+ class TestSpinners:
111
+ def test_exports_all_18(self):
112
+ assert sorted(spinners.keys()) == sorted(ALL_NAMES)
113
+
114
+ @pytest.mark.parametrize("name", ALL_NAMES)
115
+ def test_non_empty_frames(self, name: str):
116
+ assert len(spinners[name].frames) > 0
117
+
118
+ @pytest.mark.parametrize("name", ALL_NAMES)
119
+ def test_positive_interval(self, name: str):
120
+ assert spinners[name].interval > 0
121
+
122
+ @pytest.mark.parametrize("name", ALL_NAMES)
123
+ def test_consistent_frame_widths(self, name: str):
124
+ widths = [len(list(f)) for f in spinners[name].frames]
125
+ assert len(set(widths)) == 1
@@ -0,0 +1 @@
1
+ from .braille import spinners, grid_to_braille, make_grid, Spinner, BrailleSpinnerName
@@ -0,0 +1,59 @@
1
+ """CLI demo: python -m unicode_animations [name | --list]"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+
8
+ from .braille import spinners
9
+
10
+
11
+ def _preview(name: str, duration: float = 3.0) -> None:
12
+ spinner = spinners[name] # type: ignore[index]
13
+ frames = spinner.frames
14
+ interval = spinner.interval / 1000
15
+ end = time.monotonic() + duration
16
+ idx = 0
17
+ sys.stdout.write(f"\033[?25l {name}: ")
18
+ sys.stdout.flush()
19
+ try:
20
+ while time.monotonic() < end:
21
+ sys.stdout.write(f"\r {name}: {frames[idx % len(frames)]}")
22
+ sys.stdout.flush()
23
+ time.sleep(interval)
24
+ idx += 1
25
+ finally:
26
+ sys.stdout.write("\033[?25h\n")
27
+ sys.stdout.flush()
28
+
29
+
30
+ def main() -> None:
31
+ args = sys.argv[1:]
32
+
33
+ if "--list" in args or "-l" in args:
34
+ for name in spinners:
35
+ print(name)
36
+ return
37
+
38
+ if args:
39
+ name = args[0]
40
+ if name not in spinners:
41
+ print(f"Unknown spinner: {name}")
42
+ print(f"Available: {', '.join(spinners)}")
43
+ sys.exit(1)
44
+ try:
45
+ _preview(name, duration=5.0)
46
+ except KeyboardInterrupt:
47
+ sys.stdout.write("\033[?25h\n")
48
+ return
49
+
50
+ # Cycle through all spinners
51
+ try:
52
+ for name in spinners:
53
+ _preview(name)
54
+ except KeyboardInterrupt:
55
+ sys.stdout.write("\033[?25h\n")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ main()
@@ -0,0 +1,403 @@
1
+ """
2
+ Unicode Braille Spinners
3
+
4
+ A collection of animated unicode spinners built on braille characters (U+2800 block).
5
+ Each braille char is a 2x4 dot grid — these generators compose them into
6
+ multi-character animated frames for use as loading indicators.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ from typing import Literal, NamedTuple
13
+
14
+ # ── Types ──────────────────────────────────────────────────────────────
15
+
16
+
17
+ class Spinner(NamedTuple):
18
+ frames: tuple[str, ...]
19
+ interval: int
20
+
21
+
22
+ BrailleSpinnerName = Literal[
23
+ "braille", "braillewave", "dna",
24
+ "scan", "rain", "scanline", "pulse", "snake",
25
+ "sparkle", "cascade", "columns", "orbit", "breathe",
26
+ "waverows", "checkerboard", "helix", "fillsweep", "diagswipe",
27
+ ]
28
+
29
+ # ── Braille Grid Utility ──────────────────────────────────────────────
30
+ #
31
+ # Each braille char is a 2-col × 4-row dot grid.
32
+ # Dot numbering & bit values:
33
+ # Row 0: dot1 (0x01) dot4 (0x08)
34
+ # Row 1: dot2 (0x02) dot5 (0x10)
35
+ # Row 2: dot3 (0x04) dot6 (0x20)
36
+ # Row 3: dot7 (0x40) dot8 (0x80)
37
+ #
38
+ # Base codepoint: U+2800
39
+
40
+ BRAILLE_DOT_MAP = [
41
+ [0x01, 0x08], # row 0
42
+ [0x02, 0x10], # row 1
43
+ [0x04, 0x20], # row 2
44
+ [0x40, 0x80], # row 3
45
+ ]
46
+
47
+
48
+ def grid_to_braille(grid: list[list[bool]]) -> str:
49
+ """Convert a 2D boolean grid into a braille string.
50
+
51
+ grid[row][col] = True means dot is raised.
52
+ Width must be even (2 dot-columns per braille char).
53
+ """
54
+ rows = len(grid)
55
+ cols = len(grid[0]) if grid else 0
56
+ char_count = math.ceil(cols / 2)
57
+ result: list[str] = []
58
+ for c in range(char_count):
59
+ code = 0x2800
60
+ for r in range(min(4, rows)):
61
+ for d in range(2):
62
+ col = c * 2 + d
63
+ if col < cols and r < len(grid) and grid[r][col]:
64
+ code |= BRAILLE_DOT_MAP[r][d]
65
+ result.append(chr(code))
66
+ return "".join(result)
67
+
68
+
69
+ def make_grid(rows: int, cols: int) -> list[list[bool]]:
70
+ """Create an empty grid of given dimensions."""
71
+ if rows <= 0 or cols <= 0:
72
+ return []
73
+ return [[False] * cols for _ in range(rows)]
74
+
75
+
76
+ # ── Frame Generators ──────────────────────────────────────────────────
77
+
78
+
79
+ def _gen_scan() -> tuple[str, ...]:
80
+ W, H = 8, 4
81
+ frames: list[str] = []
82
+ for pos in range(-1, W + 1):
83
+ g = make_grid(H, W)
84
+ for r in range(H):
85
+ for c in range(W):
86
+ if c == pos or c == pos - 1:
87
+ g[r][c] = True
88
+ frames.append(grid_to_braille(g))
89
+ return tuple(frames)
90
+
91
+
92
+ def _gen_rain() -> tuple[str, ...]:
93
+ W, H, total_frames = 8, 4, 12
94
+ offsets = [0, 3, 1, 5, 2, 7, 4, 6]
95
+ frames: list[str] = []
96
+ for f in range(total_frames):
97
+ g = make_grid(H, W)
98
+ for c in range(W):
99
+ row = (f + offsets[c]) % (H + 2)
100
+ if row < H:
101
+ g[row][c] = True
102
+ frames.append(grid_to_braille(g))
103
+ return tuple(frames)
104
+
105
+
106
+ def _gen_scan_line() -> tuple[str, ...]:
107
+ W, H = 6, 4
108
+ positions = [0, 1, 2, 3, 2, 1]
109
+ frames: list[str] = []
110
+ for row in positions:
111
+ g = make_grid(H, W)
112
+ for c in range(W):
113
+ g[row][c] = True
114
+ if row > 0:
115
+ g[row - 1][c] = c % 2 == 0
116
+ frames.append(grid_to_braille(g))
117
+ return tuple(frames)
118
+
119
+
120
+ def _gen_pulse() -> tuple[str, ...]:
121
+ W, H = 6, 4
122
+ cx = W / 2 - 0.5
123
+ cy = H / 2 - 0.5
124
+ radii = [0.5, 1.2, 2, 3, 3.5]
125
+ frames: list[str] = []
126
+ for radius in radii:
127
+ g = make_grid(H, W)
128
+ for row in range(H):
129
+ for col in range(W):
130
+ dist = math.sqrt((col - cx) ** 2 + (row - cy) ** 2)
131
+ if abs(dist - radius) < 0.9:
132
+ g[row][col] = True
133
+ frames.append(grid_to_braille(g))
134
+ return tuple(frames)
135
+
136
+
137
+ def _gen_snake() -> tuple[str, ...]:
138
+ W, H = 4, 4
139
+ path: list[tuple[int, int]] = []
140
+ for r in range(H):
141
+ if r % 2 == 0:
142
+ for c in range(W):
143
+ path.append((r, c))
144
+ else:
145
+ for c in range(W - 1, -1, -1):
146
+ path.append((r, c))
147
+ frames: list[str] = []
148
+ for i in range(len(path)):
149
+ g = make_grid(H, W)
150
+ for t in range(4):
151
+ idx = (i - t + len(path)) % len(path)
152
+ g[path[idx][0]][path[idx][1]] = True
153
+ frames.append(grid_to_braille(g))
154
+ return tuple(frames)
155
+
156
+
157
+ def _gen_sparkle() -> tuple[str, ...]:
158
+ patterns = [
159
+ [1,0,0,1,0,0,1,0, 0,0,1,0,0,1,0,0, 0,1,0,0,1,0,0,1, 1,0,0,0,0,1,0,0],
160
+ [0,1,0,0,1,0,0,1, 1,0,0,1,0,0,0,1, 0,0,0,1,0,1,0,0, 0,0,1,0,1,0,1,0],
161
+ [0,0,1,0,0,1,0,0, 0,1,0,0,0,0,1,0, 1,0,1,0,0,0,0,1, 0,1,0,1,0,0,0,1],
162
+ [1,0,0,0,0,0,1,1, 0,0,1,0,1,0,0,0, 0,0,0,0,1,0,1,0, 1,0,0,1,0,0,1,0],
163
+ [0,0,0,1,1,0,0,0, 0,1,0,0,0,1,0,1, 1,0,0,1,0,0,0,0, 0,1,0,0,0,1,0,1],
164
+ [0,1,1,0,0,0,0,1, 0,0,0,1,0,0,1,0, 0,1,0,0,0,1,0,0, 0,0,1,0,1,0,0,0],
165
+ ]
166
+ W, H = 8, 4
167
+ frames: list[str] = []
168
+ for pat in patterns:
169
+ g = make_grid(H, W)
170
+ for r in range(H):
171
+ for c in range(W):
172
+ g[r][c] = bool(pat[r * W + c])
173
+ frames.append(grid_to_braille(g))
174
+ return tuple(frames)
175
+
176
+
177
+ def _gen_cascade() -> tuple[str, ...]:
178
+ W, H = 8, 4
179
+ frames: list[str] = []
180
+ for offset in range(-2, W + H):
181
+ g = make_grid(H, W)
182
+ for r in range(H):
183
+ for c in range(W):
184
+ diag = c + r
185
+ if diag == offset or diag == offset - 1:
186
+ g[r][c] = True
187
+ frames.append(grid_to_braille(g))
188
+ return tuple(frames)
189
+
190
+
191
+ def _gen_columns() -> tuple[str, ...]:
192
+ W, H = 6, 4
193
+ frames: list[str] = []
194
+ for col in range(W):
195
+ for fill_to in range(H - 1, -1, -1):
196
+ g = make_grid(H, W)
197
+ for pc in range(col):
198
+ for r in range(H):
199
+ g[r][pc] = True
200
+ for r in range(fill_to, H):
201
+ g[r][col] = True
202
+ frames.append(grid_to_braille(g))
203
+ full = make_grid(H, W)
204
+ for r in range(H):
205
+ for c in range(W):
206
+ full[r][c] = True
207
+ frames.append(grid_to_braille(full))
208
+ frames.append(grid_to_braille(make_grid(H, W)))
209
+ return tuple(frames)
210
+
211
+
212
+ def _gen_orbit() -> tuple[str, ...]:
213
+ W, H = 2, 4
214
+ path: list[tuple[int, int]] = [
215
+ (0, 0), (0, 1),
216
+ (1, 1), (2, 1), (3, 1),
217
+ (3, 0),
218
+ (2, 0), (1, 0),
219
+ ]
220
+ frames: list[str] = []
221
+ for i in range(len(path)):
222
+ g = make_grid(H, W)
223
+ g[path[i][0]][path[i][1]] = True
224
+ t1 = (i - 1 + len(path)) % len(path)
225
+ g[path[t1][0]][path[t1][1]] = True
226
+ frames.append(grid_to_braille(g))
227
+ return tuple(frames)
228
+
229
+
230
+ def _gen_breathe() -> tuple[str, ...]:
231
+ stages: list[list[tuple[int, int]]] = [
232
+ [],
233
+ [(1, 0)],
234
+ [(0, 1), (2, 0)],
235
+ [(0, 0), (1, 1), (3, 0)],
236
+ [(0, 0), (1, 1), (2, 0), (3, 1)],
237
+ [(0, 0), (0, 1), (1, 1), (2, 0), (3, 1)],
238
+ [(0, 0), (0, 1), (1, 0), (2, 1), (3, 0), (3, 1)],
239
+ [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (3, 0), (3, 1)],
240
+ [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (3, 0), (3, 1)],
241
+ ]
242
+ sequence = stages + list(reversed(stages))[1:]
243
+ frames: list[str] = []
244
+ for dots in sequence:
245
+ g = make_grid(4, 2)
246
+ for r, c in dots:
247
+ g[r][c] = True
248
+ frames.append(grid_to_braille(g))
249
+ return tuple(frames)
250
+
251
+
252
+ def _gen_wave_rows() -> tuple[str, ...]:
253
+ W, H, total_frames = 8, 4, 16
254
+ frames: list[str] = []
255
+ for f in range(total_frames):
256
+ g = make_grid(H, W)
257
+ for c in range(W):
258
+ phase = f - c * 0.5
259
+ row = round((math.sin(phase * 0.8) + 1) / 2 * (H - 1))
260
+ g[row][c] = True
261
+ if row > 0:
262
+ g[row - 1][c] = (f + c) % 3 == 0
263
+ frames.append(grid_to_braille(g))
264
+ return tuple(frames)
265
+
266
+
267
+ def _gen_checkerboard() -> tuple[str, ...]:
268
+ W, H = 6, 4
269
+ frames: list[str] = []
270
+ for phase in range(4):
271
+ g = make_grid(H, W)
272
+ for r in range(H):
273
+ for c in range(W):
274
+ if phase < 2:
275
+ g[r][c] = (r + c + phase) % 2 == 0
276
+ else:
277
+ g[r][c] = (r + c + phase) % 3 == 0
278
+ frames.append(grid_to_braille(g))
279
+ return tuple(frames)
280
+
281
+
282
+ def _gen_helix() -> tuple[str, ...]:
283
+ W, H, total_frames = 8, 4, 16
284
+ frames: list[str] = []
285
+ for f in range(total_frames):
286
+ g = make_grid(H, W)
287
+ for c in range(W):
288
+ phase = (f + c) * (math.pi / 4)
289
+ y1 = round((math.sin(phase) + 1) / 2 * (H - 1))
290
+ y2 = round((math.sin(phase + math.pi) + 1) / 2 * (H - 1))
291
+ g[y1][c] = True
292
+ g[y2][c] = True
293
+ frames.append(grid_to_braille(g))
294
+ return tuple(frames)
295
+
296
+
297
+ def _gen_fill_sweep() -> tuple[str, ...]:
298
+ W, H = 4, 4
299
+ frames: list[str] = []
300
+ for row in range(H - 1, -1, -1):
301
+ g = make_grid(H, W)
302
+ for r in range(row, H):
303
+ for c in range(W):
304
+ g[r][c] = True
305
+ frames.append(grid_to_braille(g))
306
+ full = make_grid(H, W)
307
+ for r in range(H):
308
+ for c in range(W):
309
+ full[r][c] = True
310
+ frames.append(grid_to_braille(full))
311
+ frames.append(grid_to_braille(full))
312
+ for row in range(H):
313
+ g = make_grid(H, W)
314
+ for r in range(row + 1, H):
315
+ for c in range(W):
316
+ g[r][c] = True
317
+ frames.append(grid_to_braille(g))
318
+ frames.append(grid_to_braille(make_grid(H, W)))
319
+ return tuple(frames)
320
+
321
+
322
+ def _gen_diagonal_swipe() -> tuple[str, ...]:
323
+ W, H = 4, 4
324
+ max_diag = W + H - 2
325
+ frames: list[str] = []
326
+ for d in range(max_diag + 1):
327
+ g = make_grid(H, W)
328
+ for r in range(H):
329
+ for c in range(W):
330
+ if r + c <= d:
331
+ g[r][c] = True
332
+ frames.append(grid_to_braille(g))
333
+ full = make_grid(H, W)
334
+ for r in range(H):
335
+ for c in range(W):
336
+ full[r][c] = True
337
+ frames.append(grid_to_braille(full))
338
+ for d in range(max_diag + 1):
339
+ g = make_grid(H, W)
340
+ for r in range(H):
341
+ for c in range(W):
342
+ if r + c > d:
343
+ g[r][c] = True
344
+ frames.append(grid_to_braille(g))
345
+ frames.append(grid_to_braille(make_grid(H, W)))
346
+ return tuple(frames)
347
+
348
+
349
+ # ── Spinner Registry ──────────────────────────────────────────────────
350
+
351
+ spinners: dict[BrailleSpinnerName, Spinner] = {
352
+ # Classic braille single-char
353
+ "braille": Spinner(
354
+ frames=("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"),
355
+ interval=80,
356
+ ),
357
+ "braillewave": Spinner(
358
+ frames=(
359
+ "⠁⠂⠄⡀",
360
+ "⠂⠄⡀⢀",
361
+ "⠄⡀⢀⠠",
362
+ "⡀⢀⠠⠐",
363
+ "⢀⠠⠐⠈",
364
+ "⠠⠐⠈⠁",
365
+ "⠐⠈⠁⠂",
366
+ "⠈⠁⠂⠄",
367
+ ),
368
+ interval=100,
369
+ ),
370
+ "dna": Spinner(
371
+ frames=(
372
+ "⠋⠉⠙⠚",
373
+ "⠉⠙⠚⠒",
374
+ "⠙⠚⠒⠂",
375
+ "⠚⠒⠂⠂",
376
+ "⠒⠂⠂⠒",
377
+ "⠂⠂⠒⠲",
378
+ "⠂⠒⠲⠴",
379
+ "⠒⠲⠴⠤",
380
+ "⠲⠴⠤⠄",
381
+ "⠴⠤⠄⠋",
382
+ "⠤⠄⠋⠉",
383
+ "⠄⠋⠉⠙",
384
+ ),
385
+ interval=80,
386
+ ),
387
+ # Generated braille grid animations
388
+ "scan": Spinner(frames=_gen_scan(), interval=70),
389
+ "rain": Spinner(frames=_gen_rain(), interval=100),
390
+ "scanline": Spinner(frames=_gen_scan_line(), interval=120),
391
+ "pulse": Spinner(frames=_gen_pulse(), interval=180),
392
+ "snake": Spinner(frames=_gen_snake(), interval=80),
393
+ "sparkle": Spinner(frames=_gen_sparkle(), interval=150),
394
+ "cascade": Spinner(frames=_gen_cascade(), interval=60),
395
+ "columns": Spinner(frames=_gen_columns(), interval=60),
396
+ "orbit": Spinner(frames=_gen_orbit(), interval=100),
397
+ "breathe": Spinner(frames=_gen_breathe(), interval=100),
398
+ "waverows": Spinner(frames=_gen_wave_rows(), interval=90),
399
+ "checkerboard": Spinner(frames=_gen_checkerboard(), interval=250),
400
+ "helix": Spinner(frames=_gen_helix(), interval=80),
401
+ "fillsweep": Spinner(frames=_gen_fill_sweep(), interval=100),
402
+ "diagswipe": Spinner(frames=_gen_diagonal_swipe(), interval=60),
403
+ }
@@ -0,0 +1,153 @@
1
+ version = 1
2
+ requires-python = ">=3.10"
3
+
4
+ [[package]]
5
+ name = "colorama"
6
+ version = "0.4.6"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
9
+ wheels = [
10
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "exceptiongroup"
15
+ version = "1.3.1"
16
+ source = { registry = "https://pypi.org/simple" }
17
+ dependencies = [
18
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
19
+ ]
20
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 }
21
+ wheels = [
22
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 },
23
+ ]
24
+
25
+ [[package]]
26
+ name = "iniconfig"
27
+ version = "2.3.0"
28
+ source = { registry = "https://pypi.org/simple" }
29
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
30
+ wheels = [
31
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
32
+ ]
33
+
34
+ [[package]]
35
+ name = "packaging"
36
+ version = "26.0"
37
+ source = { registry = "https://pypi.org/simple" }
38
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "pluggy"
45
+ version = "1.6.0"
46
+ source = { registry = "https://pypi.org/simple" }
47
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
48
+ wheels = [
49
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
50
+ ]
51
+
52
+ [[package]]
53
+ name = "pygments"
54
+ version = "2.19.2"
55
+ source = { registry = "https://pypi.org/simple" }
56
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
57
+ wheels = [
58
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
59
+ ]
60
+
61
+ [[package]]
62
+ name = "pytest"
63
+ version = "9.0.2"
64
+ source = { registry = "https://pypi.org/simple" }
65
+ dependencies = [
66
+ { name = "colorama", marker = "sys_platform == 'win32'" },
67
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
68
+ { name = "iniconfig" },
69
+ { name = "packaging" },
70
+ { name = "pluggy" },
71
+ { name = "pygments" },
72
+ { name = "tomli", marker = "python_full_version < '3.11'" },
73
+ ]
74
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 }
75
+ wheels = [
76
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 },
77
+ ]
78
+
79
+ [[package]]
80
+ name = "tomli"
81
+ version = "2.4.0"
82
+ source = { registry = "https://pypi.org/simple" }
83
+ sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477 }
84
+ wheels = [
85
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663 },
86
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469 },
87
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039 },
88
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007 },
89
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875 },
90
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271 },
91
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770 },
92
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626 },
93
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842 },
94
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894 },
95
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053 },
96
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481 },
97
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720 },
98
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014 },
99
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820 },
100
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712 },
101
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296 },
102
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553 },
103
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915 },
104
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038 },
105
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245 },
106
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335 },
107
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962 },
108
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396 },
109
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530 },
110
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227 },
111
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748 },
112
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725 },
113
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901 },
114
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375 },
115
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639 },
116
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897 },
117
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697 },
118
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567 },
119
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556 },
120
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014 },
121
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339 },
122
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490 },
123
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398 },
124
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515 },
125
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806 },
126
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340 },
127
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106 },
128
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504 },
129
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561 },
130
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 },
131
+ ]
132
+
133
+ [[package]]
134
+ name = "typing-extensions"
135
+ version = "4.15.0"
136
+ source = { registry = "https://pypi.org/simple" }
137
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
138
+ wheels = [
139
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
140
+ ]
141
+
142
+ [[package]]
143
+ name = "unicode-animations"
144
+ version = "0.1.0"
145
+ source = { editable = "." }
146
+
147
+ [package.optional-dependencies]
148
+ dev = [
149
+ { name = "pytest" },
150
+ ]
151
+
152
+ [package.metadata]
153
+ requires-dist = [{ name = "pytest", marker = "extra == 'dev'" }]