gruyere 0.0.1__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 @@
1
+ 3.14
gruyere-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: gruyere
3
+ Version: 0.0.1
4
+ Summary: A tiny program for viewing and killing processes on ports
5
+ Project-URL: Homepage, https://github.com/yourusername/gruyere
6
+ Project-URL: Repository, https://github.com/yourusername/gruyere
7
+ Project-URL: Issues, https://github.com/yourusername/gruyere/issues
8
+ Author-email: Savannah Ostrowski <savannah@python.org>
9
+ License: MIT
10
+ Keywords: kill,lsof,ports,processes,tui
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: System :: Monitoring
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: readchar>=4.2.1
23
+ Requires-Dist: rich>=14.2.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Gruyere
27
+
28
+ A tiny, beautiful TUI program for viewing and killing processes listening on ports.
29
+
30
+ ## Features
31
+
32
+ - Beautiful gradient UI with rich colors
33
+ - Filter processes by command name
34
+ - Vim-style navigation (j/k) or arrow keys
35
+ - Kill processes with confirmation dialog
36
+ - Pagination for many processes
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ uv tool install gruyere
42
+ ```
43
+
44
+ Or with pipx:
45
+ ```bash
46
+ pipx install gruyere
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Simply run:
52
+
53
+ ```bash
54
+ gruyere
55
+ ```
56
+
57
+ ### Controls
58
+
59
+ - Up/k: Move up
60
+ - Down/j: Move down
61
+ - /: Filter processes
62
+ - ENTER: Kill selected process
63
+ - q: Quit
64
+
65
+ ## Requirements
66
+
67
+ - Python 3.12+
68
+ - macOS or Linux (uses lsof)
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,47 @@
1
+ # Gruyere
2
+
3
+ A tiny, beautiful TUI program for viewing and killing processes listening on ports.
4
+
5
+ ## Features
6
+
7
+ - Beautiful gradient UI with rich colors
8
+ - Filter processes by command name
9
+ - Vim-style navigation (j/k) or arrow keys
10
+ - Kill processes with confirmation dialog
11
+ - Pagination for many processes
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ uv tool install gruyere
17
+ ```
18
+
19
+ Or with pipx:
20
+ ```bash
21
+ pipx install gruyere
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ Simply run:
27
+
28
+ ```bash
29
+ gruyere
30
+ ```
31
+
32
+ ### Controls
33
+
34
+ - Up/k: Move up
35
+ - Down/j: Move down
36
+ - /: Filter processes
37
+ - ENTER: Kill selected process
38
+ - q: Quit
39
+
40
+ ## Requirements
41
+
42
+ - Python 3.12+
43
+ - macOS or Linux (uses lsof)
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1,3 @@
1
+ """Gruyere - A tiny program for viewing and killing processes on ports."""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,331 @@
1
+ from dataclasses import dataclass
2
+ import signal
3
+ import subprocess
4
+ import sys
5
+ from typing import List, Optional
6
+
7
+ from readchar import readkey, key
8
+ from rich.color import Color
9
+ from rich.console import Console, Group
10
+ from rich.live import Live
11
+ from rich.style import Style
12
+ from rich.panel import Panel
13
+ from rich.text import Text
14
+ from rich import box
15
+
16
+
17
+ @dataclass
18
+ class Process:
19
+ pid: int
20
+ port: int | str
21
+ user: str
22
+ command: str
23
+
24
+
25
+ SELECTED_COLOR = Style(color="#EE6FF8", bold=True)
26
+
27
+ signal.signal(signal.SIGINT, lambda _, __: sys.exit(0))
28
+
29
+
30
+ def _clear_screen():
31
+ """Clear the entire screen and scroll buffer using ANSI escape codes."""
32
+ sys.stdout.write("\033[2J\033[3J\033[H")
33
+ sys.stdout.flush()
34
+
35
+
36
+ def _parse_port(port_str: str) -> int | str:
37
+ try:
38
+ return int(port_str)
39
+ except (ValueError, IndexError):
40
+ return port_str
41
+
42
+
43
+ def _get_processes() -> list[Process]:
44
+ raw_processes = subprocess.run(
45
+ ["lsof", "-i", "-P", "-n", "-sTCP:LISTEN"], capture_output=True, text=True
46
+ )
47
+ processes: list[Process] = []
48
+
49
+ for line in raw_processes.stdout.splitlines()[1:]:
50
+ parts = line.split()
51
+ if len(parts) >= 9:
52
+ pid = int(parts[1])
53
+ user = parts[2]
54
+ command = parts[0]
55
+ port = _parse_port(parts[8].split(":")[-1])
56
+ process = Process(pid=pid, port=port, user=user, command=command)
57
+ processes.append(process)
58
+
59
+ return processes
60
+
61
+
62
+ def _kill_process(pid: int):
63
+ subprocess.run(["kill", "-9", str(pid)])
64
+
65
+
66
+ def _show_pagination_indicator(total: int, selected: int, panels: list[Panel | str]):
67
+ """Add pagination indicator dots to the panels list if needed."""
68
+ if total <= len(panels):
69
+ return
70
+
71
+ page = selected // len(panels) % 4
72
+ indicator = ""
73
+ for i in range((total + len(panels) - 1) // len(panels)):
74
+ if i == page:
75
+ indicator += "● "
76
+ else:
77
+ indicator += "○ "
78
+
79
+ panels.append(f" [dim]{indicator}[/dim]")
80
+
81
+
82
+ def _render_processes_table(processes: List[Process], selected: int):
83
+ max_display = 4
84
+
85
+ if len(processes) <= max_display:
86
+ display_processes = processes
87
+ display_selected = selected
88
+ else:
89
+ if selected < max_display // 2:
90
+ display_processes = processes[:max_display]
91
+ display_selected = selected
92
+ elif selected >= len(processes) - max_display // 2:
93
+ display_processes = processes[-max_display:]
94
+ display_selected = selected - (len(processes) - max_display)
95
+ else:
96
+ start = selected - max_display // 2
97
+ display_processes = processes[start : start + max_display]
98
+ display_selected = max_display // 2
99
+
100
+ panels: list[Panel | str] = []
101
+ for i, process in enumerate(display_processes):
102
+ # Add vertical indicator for selected item
103
+ indicator = "▐ " if i == display_selected else " "
104
+
105
+ port_line = f"{indicator}[bold]Port: {process.port} (PID: {process.pid})[/bold]"
106
+ user_line = (
107
+ f"{indicator}[dim]User: {process.user}, Command: {process.command}[/dim]"
108
+ )
109
+ content = f"{port_line}\n{user_line}"
110
+
111
+ # All items get a panel with no border
112
+ panel = Panel(
113
+ content,
114
+ style=SELECTED_COLOR if i == display_selected else "",
115
+ box=box.SIMPLE,
116
+ )
117
+ panels.append(panel)
118
+
119
+ # Add empty panels to always show 4 slots
120
+ while len(panels) < max_display:
121
+ empty_panel = Panel("\n", box=box.SIMPLE, style="")
122
+ panels.append(empty_panel)
123
+
124
+ _show_pagination_indicator(len(processes), selected, panels)
125
+
126
+ help_text = "[bold dim]Commands: ↑/k: Up | ↓/j: Down | ENTER: Kill | /: Filter | q: Quit [/bold dim]"
127
+ return Group(*panels, Panel(help_text, box=box.SIMPLE))
128
+
129
+
130
+ def _colorGrid(x_steps: int, y_steps: int) -> List[List[Style]]:
131
+ """
132
+ Generate a 2D grid of gradient colors using bilinear interpolation.
133
+ Via https://github.com/charmbracelet/lipgloss/blob/776c15f0da16d2b1058a079ec6a08a2e1170d721/examples/layout/main.go#L338
134
+ """
135
+ x0y0 = Color.parse("#EE6FF8")
136
+ x1y0 = Color.parse("#EDFF82")
137
+ x0y1 = Color.parse("#643AFF")
138
+ x1y1 = Color.parse("#14F9D5")
139
+
140
+ # Get RGB triplets
141
+ x0y0_rgb = x0y0.get_truecolor()
142
+ x1y0_rgb = x1y0.get_truecolor()
143
+ x0y1_rgb = x0y1.get_truecolor()
144
+ x1y1_rgb = x1y1.get_truecolor()
145
+
146
+ grid: List[List[Style]] = []
147
+ for y in range(y_steps):
148
+ row: List[Style] = []
149
+ for x in range(x_steps):
150
+ rx = x / (x_steps - 1) if x_steps > 1 else 0
151
+ ry = y / (y_steps - 1) if y_steps > 1 else 0
152
+
153
+ r = int(
154
+ (1 - rx) * (1 - ry) * x0y0_rgb.red
155
+ + rx * (1 - ry) * x1y0_rgb.red
156
+ + (1 - rx) * ry * x0y1_rgb.red
157
+ + rx * ry * x1y1_rgb.red
158
+ )
159
+ g = int(
160
+ (1 - rx) * (1 - ry) * x0y0_rgb.green
161
+ + rx * (1 - ry) * x1y0_rgb.green
162
+ + (1 - rx) * ry * x0y1_rgb.green
163
+ + rx * ry * x1y1_rgb.green
164
+ )
165
+ b = int(
166
+ (1 - rx) * (1 - ry) * x0y0_rgb.blue
167
+ + rx * (1 - ry) * x1y0_rgb.blue
168
+ + (1 - rx) * ry * x0y1_rgb.blue
169
+ + rx * ry * x1y1_rgb.blue
170
+ )
171
+
172
+ row.append(Style(color=Color.from_rgb(r, g, b)))
173
+ grid.append(row)
174
+
175
+ return grid
176
+
177
+
178
+ def _render_title():
179
+ colors = _colorGrid(1, 5)
180
+ title = "Gruyere"
181
+
182
+ desc = "A tiny program for viewing + killing ports"
183
+ divider = "─" * len(desc)
184
+ subtext = "Here's what's running..."
185
+
186
+ base_spacing = " " # Space between title and description
187
+ desc_start_col = len(title) + len(base_spacing) # Column where description starts
188
+
189
+ # Create layered text effect with white text on colored backgrounds
190
+ text = Text()
191
+
192
+ right_texts: List[Optional[str]] = [desc, divider, subtext, None, None]
193
+
194
+ for i in range(5):
195
+ indent = " " * (i * 2)
196
+ text.append(indent)
197
+ text.append(title, style=Style(color="white", bgcolor=colors[i][0].color))
198
+
199
+ right_text = right_texts[i]
200
+ if right_text is not None:
201
+ text.append(" " * (desc_start_col - len(indent) - len(title)))
202
+ text.append(right_text, style=Style(dim=True))
203
+
204
+ text.append("\n")
205
+
206
+ return text
207
+
208
+ def _show_confirmation_view(console: Console, process: Process, title: Text) -> bool:
209
+ # Screen should already be cleared before calling this function
210
+ console.print(Panel(title, box=box.SIMPLE))
211
+ console.print(
212
+ Panel(
213
+ f"[bold]Are you sure you want to kill the process?[/bold]\n\n"
214
+ f"[bold]PID:[/bold] {process.pid}\n"
215
+ f"[bold]Port:[/bold] {process.port}\n"
216
+ f"[bold]User:[/bold] {process.user}\n"
217
+ f"[bold]Command:[/bold] {process.command}\n\n"
218
+ f"[dim]Press Y to confirm, N to cancel.[/dim]",
219
+ border_style="#EE6FF8",
220
+ expand=False,
221
+ )
222
+ )
223
+ while True:
224
+ ch = readkey()
225
+ if ch.lower() == "y":
226
+ return True
227
+ elif ch.lower() == "n":
228
+ return False
229
+
230
+
231
+
232
+ def main():
233
+ console = Console()
234
+ text = _render_title()
235
+ processes = _get_processes()
236
+ selected = 0
237
+ filter_text = ""
238
+ is_filtering = False
239
+ running = True
240
+
241
+ console.clear()
242
+ console.print(Panel(text, box=box.SIMPLE))
243
+
244
+ while running:
245
+ process_to_kill = None
246
+
247
+ with Live(
248
+ _render_processes_table(processes, selected),
249
+ console=console,
250
+ refresh_per_second=10,
251
+ ) as live:
252
+ while ch := readkey():
253
+ if is_filtering:
254
+ if ch == "/":
255
+ is_filtering = False
256
+ filter_text = ""
257
+ processes = _get_processes()
258
+ live.update(_render_processes_table(processes, selected))
259
+ continue
260
+ elif ch == key.UP or ch == "k":
261
+ selected = max(0, selected - 1)
262
+ elif ch == key.DOWN or ch == "j":
263
+ selected = min(len(processes) - 1, selected + 1)
264
+ elif ch == key.BACKSPACE:
265
+ filter_text = filter_text[:-1]
266
+ processes = [
267
+ p
268
+ for p in _get_processes()
269
+ if filter_text.lower() in p.command.lower()
270
+ ]
271
+ selected = 0
272
+ elif ch == key.ENTER:
273
+ is_filtering = False
274
+ process_to_kill = processes[selected] if processes else None
275
+ break
276
+ elif len(ch) == 1 and ch.isprintable():
277
+ filter_text += ch
278
+ processes = [
279
+ p
280
+ for p in _get_processes()
281
+ if filter_text.lower() in p.command.lower()
282
+ ]
283
+ selected = 0
284
+
285
+ filter_panel = Panel(
286
+ f"[bold magenta]Filter:[/bold magenta] {filter_text}[blink]_[/blink]",
287
+ title="Press / to cancel, ENTER to apply",
288
+ border_style="magenta",
289
+ )
290
+ display = Group(
291
+ filter_panel, _render_processes_table(processes, selected)
292
+ )
293
+ live.update(display)
294
+ else:
295
+ if ch == key.UP or ch == "k":
296
+ selected = max(0, selected - 1)
297
+ elif ch == key.DOWN or ch == "j":
298
+ selected = min(len(processes) - 1, selected + 1)
299
+ elif ch == "/":
300
+ is_filtering = True
301
+ filter_text = ""
302
+ filter_panel = Panel(
303
+ f"[bold magenta]Filter:[/bold magenta] {filter_text}[blink]_[/blink]",
304
+ title="Press / to cancel, ENTER to apply",
305
+ border_style="magenta",
306
+ )
307
+ display = Group(
308
+ filter_panel, _render_processes_table(processes, selected)
309
+ )
310
+ live.update(display)
311
+ continue # Skip the update at the end of the loop
312
+ elif ch == "q":
313
+ running = False
314
+ break
315
+ elif ch == key.ENTER:
316
+ # Exit live context to show confirmation view
317
+ process_to_kill = processes[selected]
318
+ break
319
+ live.update(_render_processes_table(processes, selected))
320
+
321
+ if process_to_kill is not None:
322
+ _clear_screen()
323
+
324
+ if running and process_to_kill is not None:
325
+ if _show_confirmation_view(console, process_to_kill, text):
326
+ _kill_process(process_to_kill.pid)
327
+ processes = _get_processes()
328
+ selected = min(selected, len(processes) - 1)
329
+
330
+ _clear_screen()
331
+ console.print(Panel(text, box=box.SIMPLE))
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: gruyere
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: readchar>=4.2.1
8
+ Requires-Dist: rich>=14.2.0
9
+ Requires-Dist: typer>=0.20.0
@@ -0,0 +1,9 @@
1
+ README.md
2
+ main.py
3
+ pyproject.toml
4
+ gruyere.egg-info/PKG-INFO
5
+ gruyere.egg-info/SOURCES.txt
6
+ gruyere.egg-info/dependency_links.txt
7
+ gruyere.egg-info/entry_points.txt
8
+ gruyere.egg-info/requires.txt
9
+ gruyere.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gruyere = gruyere.main:main
@@ -0,0 +1,3 @@
1
+ readchar>=4.2.1
2
+ rich>=14.2.0
3
+ typer>=0.20.0
@@ -0,0 +1 @@
1
+ main
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: gruyere-py
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: readchar>=4.2.1
8
+ Requires-Dist: rich>=14.2.0
9
+ Requires-Dist: typer>=0.20.0
@@ -0,0 +1,9 @@
1
+ README.md
2
+ main.py
3
+ pyproject.toml
4
+ gruyere_py.egg-info/PKG-INFO
5
+ gruyere_py.egg-info/SOURCES.txt
6
+ gruyere_py.egg-info/dependency_links.txt
7
+ gruyere_py.egg-info/entry_points.txt
8
+ gruyere_py.egg-info/requires.txt
9
+ gruyere_py.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gruyere-py = main:main
@@ -0,0 +1,3 @@
1
+ readchar>=4.2.1
2
+ rich>=14.2.0
3
+ typer>=0.20.0
@@ -0,0 +1 @@
1
+ main
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "gruyere"
3
+ version = "0.0.1"
4
+ description = "A tiny program for viewing and killing processes on ports"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Savannah Ostrowski", email = "savannah@python.org" }
8
+ ]
9
+ license = { text = "MIT" }
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "readchar>=4.2.1",
13
+ "rich>=14.2.0",
14
+ ]
15
+ keywords = ["ports", "processes", "kill", "lsof", "tui"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: POSIX :: Linux",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: System :: Monitoring",
26
+ "Topic :: Utilities",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/yourusername/gruyere"
31
+ Repository = "https://github.com/yourusername/gruyere"
32
+ Issues = "https://github.com/yourusername/gruyere/issues"
33
+
34
+ [project.scripts]
35
+ gruyere = "gruyere.main:main"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatchling.build.targets.wheel]
42
+ # Only include the gruyere package
43
+ packages = ["gruyere"]
44
+
45
+ [tool.hatchling.build.targets.sdist]
46
+ exclude = [
47
+ "/.python-version",
48
+ "/uv.lock",
49
+ ]
50
+
51
+ [dependency-groups]
52
+ dev = [
53
+ "pre-commit>=4.3.0",
54
+ ]
gruyere-0.0.1/uv.lock ADDED
@@ -0,0 +1,208 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "cfgv"
7
+ version = "3.4.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "distlib"
16
+ version = "0.4.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "filelock"
25
+ version = "3.20.0"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "gruyere"
34
+ version = "0.0.1"
35
+ source = { editable = "." }
36
+ dependencies = [
37
+ { name = "readchar" },
38
+ { name = "rich" },
39
+ ]
40
+
41
+ [package.dev-dependencies]
42
+ dev = [
43
+ { name = "pre-commit" },
44
+ ]
45
+
46
+ [package.metadata]
47
+ requires-dist = [
48
+ { name = "readchar", specifier = ">=4.2.1" },
49
+ { name = "rich", specifier = ">=14.2.0" },
50
+ ]
51
+
52
+ [package.metadata.requires-dev]
53
+ dev = [{ name = "pre-commit", specifier = ">=4.3.0" }]
54
+
55
+ [[package]]
56
+ name = "identify"
57
+ version = "2.6.15"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "markdown-it-py"
66
+ version = "4.0.0"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ dependencies = [
69
+ { name = "mdurl" },
70
+ ]
71
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
72
+ wheels = [
73
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
74
+ ]
75
+
76
+ [[package]]
77
+ name = "mdurl"
78
+ version = "0.1.2"
79
+ source = { registry = "https://pypi.org/simple" }
80
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
81
+ wheels = [
82
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
83
+ ]
84
+
85
+ [[package]]
86
+ name = "nodeenv"
87
+ version = "1.9.1"
88
+ source = { registry = "https://pypi.org/simple" }
89
+ sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
90
+ wheels = [
91
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "platformdirs"
96
+ version = "4.5.0"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
99
+ wheels = [
100
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
101
+ ]
102
+
103
+ [[package]]
104
+ name = "pre-commit"
105
+ version = "4.3.0"
106
+ source = { registry = "https://pypi.org/simple" }
107
+ dependencies = [
108
+ { name = "cfgv" },
109
+ { name = "identify" },
110
+ { name = "nodeenv" },
111
+ { name = "pyyaml" },
112
+ { name = "virtualenv" },
113
+ ]
114
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
115
+ wheels = [
116
+ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
117
+ ]
118
+
119
+ [[package]]
120
+ name = "pygments"
121
+ version = "2.19.2"
122
+ source = { registry = "https://pypi.org/simple" }
123
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
124
+ wheels = [
125
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
126
+ ]
127
+
128
+ [[package]]
129
+ name = "pyyaml"
130
+ version = "6.0.3"
131
+ source = { registry = "https://pypi.org/simple" }
132
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
133
+ wheels = [
134
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
135
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
136
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
137
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
138
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
139
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
140
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
141
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
142
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
143
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
144
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
145
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
146
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
147
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
148
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
149
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
150
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
151
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
152
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
153
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
154
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
155
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
156
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
157
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
158
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
159
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
160
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
161
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
162
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
163
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
164
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
165
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
166
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
167
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
168
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
169
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
170
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
171
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
172
+ ]
173
+
174
+ [[package]]
175
+ name = "readchar"
176
+ version = "4.2.1"
177
+ source = { registry = "https://pypi.org/simple" }
178
+ sdist = { url = "https://files.pythonhosted.org/packages/dd/f8/8657b8cbb4ebeabfbdf991ac40eca8a1d1bd012011bd44ad1ed10f5cb494/readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb", size = 9685, upload-time = "2024-11-04T18:28:07.757Z" }
179
+ wheels = [
180
+ { url = "https://files.pythonhosted.org/packages/a9/10/e4b1e0e5b6b6745c8098c275b69bc9d73e9542d5c7da4f137542b499ed44/readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77", size = 9350, upload-time = "2024-11-04T18:28:02.859Z" },
181
+ ]
182
+
183
+ [[package]]
184
+ name = "rich"
185
+ version = "14.2.0"
186
+ source = { registry = "https://pypi.org/simple" }
187
+ dependencies = [
188
+ { name = "markdown-it-py" },
189
+ { name = "pygments" },
190
+ ]
191
+ sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
192
+ wheels = [
193
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
194
+ ]
195
+
196
+ [[package]]
197
+ name = "virtualenv"
198
+ version = "20.35.3"
199
+ source = { registry = "https://pypi.org/simple" }
200
+ dependencies = [
201
+ { name = "distlib" },
202
+ { name = "filelock" },
203
+ { name = "platformdirs" },
204
+ ]
205
+ sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" }
206
+ wheels = [
207
+ { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" },
208
+ ]