batchedllm 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,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "uv"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+
8
+ - package-ecosystem: "github-actions"
9
+ directory: "/"
10
+ schedule:
11
+ interval: "weekly"
12
+
13
+ - package-ecosystem: "pre-commit"
14
+ directory: "/"
15
+ schedule:
16
+ interval: "weekly"
@@ -0,0 +1,49 @@
1
+ name: Ruff and Ty and PyTest checks
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ ruff_check:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v6
10
+
11
+ - uses: actions/setup-python@v6
12
+ with:
13
+ python-version-file: "pyproject.toml"
14
+
15
+ - uses: astral-sh/setup-uv@v7
16
+
17
+ - run: uv sync --locked --all-extras --dev
18
+
19
+ - run: uv run ruff check
20
+
21
+ ty_check:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v6
25
+
26
+ - uses: actions/setup-python@v6
27
+ with:
28
+ python-version-file: "pyproject.toml"
29
+
30
+ - uses: astral-sh/setup-uv@v7
31
+
32
+ - run: uv sync --locked --all-extras --dev
33
+
34
+ - run: uv run ty check
35
+
36
+ pytest:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v6
40
+
41
+ - uses: actions/setup-python@v6
42
+ with:
43
+ python-version-file: "pyproject.toml"
44
+
45
+ - uses: astral-sh/setup-uv@v7
46
+
47
+ - run: uv sync --locked --all-extras --dev
48
+
49
+ - run: uv run pytest
@@ -0,0 +1,51 @@
1
+ name: Upload to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version-file: "pyproject.toml"
20
+
21
+ - uses: astral-sh/setup-uv@v7
22
+
23
+ - run: uv sync --locked --all-extras --dev
24
+
25
+ - run: uv build
26
+
27
+ - uses: actions/upload-artifact@v4
28
+ with:
29
+ name: release-dist
30
+ path: dist/
31
+
32
+ pypi-publish:
33
+ runs-on: ubuntu-latest
34
+ needs:
35
+ - build
36
+ permissions:
37
+ id-token: write # for trusted publishing
38
+
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/p/batchedllm
42
+
43
+ steps:
44
+ - uses: actions/download-artifact@v4
45
+ with:
46
+ name: release-dist
47
+ path: dist/
48
+
49
+ - uses: pypa/gh-action-pypi-publish@release/v1
50
+ with:
51
+ packages-dir: dist/
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ dist/
3
+
4
+ **/.pytest_cache/
5
+ **/.ruff_cache/
6
+ **/__pycache__/
@@ -0,0 +1,17 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: check-yaml
6
+ - id: end-of-file-fixer
7
+ - id: trailing-whitespace
8
+ args: [--markdown-linebreak-ext=md]
9
+ - repo: https://github.com/astral-sh/ruff-pre-commit
10
+ rev: v0.15.8
11
+ hooks:
12
+ - id: ruff-check
13
+ types_or: [ python, pyproject ]
14
+ - repo: https://github.com/allganize/ty-pre-commit
15
+ rev: v0.0.27
16
+ hooks:
17
+ - id: ty-check
@@ -0,0 +1,7 @@
1
+ {
2
+ "recommendations": [
3
+ "ms-python.python",
4
+ "tamasfe.even-better-toml",
5
+ "github.vscode-github-actions"
6
+ ]
7
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "python.analysis.typeCheckingMode": "standard"
3
+ }
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: batchedllm
3
+ Version: 0.1.0
4
+ Summary: like itertools.batched but for calling LLM
5
+ Project-URL: repository, https://github.com/batchedllm/batchedllm
6
+ Author-email: Danil Kireev <nenil@nenil.dev>, Daniil Larionov <rexhaif.io@gmail.com>
7
+ License-Expression: MIT
8
+ Keywords: LLM,batched
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Topic :: Utilities
13
+ Requires-Python: >=3.13
14
+ Requires-Dist: tqdm>=4.67.3
15
+ Description-Content-Type: text/markdown
16
+
17
+ # BatchedLLM
18
+ like itertools.batched but for calling LLM
19
+
20
+ ## Example
21
+ Run the OpenAI speed comparison example with `uv --script`:
22
+
23
+ ```sh
24
+ OPENAI_BASE_URL=<url or https://api.openai.com/v1> OPENAI_MODEL=<model or gpt-5-nano> OPENAI_API_KEY=sk-... uv run --script examples/openai_speed.py
25
+ ```
@@ -0,0 +1,9 @@
1
+ # BatchedLLM
2
+ like itertools.batched but for calling LLM
3
+
4
+ ## Example
5
+ Run the OpenAI speed comparison example with `uv --script`:
6
+
7
+ ```sh
8
+ OPENAI_BASE_URL=<url or https://api.openai.com/v1> OPENAI_MODEL=<model or gpt-5-nano> OPENAI_API_KEY=sk-... uv run --script examples/openai_speed.py
9
+ ```
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.13"
4
+ # dependencies = [
5
+ # "openai",
6
+ # "batchedllm @ git+https://github.com/batchedllm/batchedllm.git@main",
7
+ # ]
8
+ # ///
9
+
10
+ import os
11
+ import random
12
+ import asyncio
13
+ from time import perf_counter
14
+
15
+ from openai import AsyncOpenAI # ty: ignore[unresolved-import]
16
+ from batchedllm import Manager
17
+
18
+ PROMPTS = [
19
+ "Come up with 4 words describing rain.",
20
+ "Come up with 4 words describing space.",
21
+ "Come up with 4 words describing plants.",
22
+ "Come up with 4 words describing coffee.",
23
+ ]
24
+ CONCURRENCY = 4
25
+
26
+
27
+ async def main() -> None:
28
+ client = AsyncOpenAI(
29
+ api_key=os.environ["OPENAI_API_KEY"],
30
+ base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
31
+ )
32
+ model = os.environ.get("OPENAI_MODEL", "gpt-5-nano")
33
+
34
+ # using openAI directly
35
+ sequential_started = perf_counter()
36
+ sequential_outputs = []
37
+ for prompt in PROMPTS:
38
+ # call 1 by 1
39
+ response = await client.chat.completions.create(
40
+ model=model,
41
+ # random.randint to prevent caching
42
+ messages=[
43
+ {"role": "user", "content": f"{random.randint(0, 99)}. {prompt}"}
44
+ ],
45
+ )
46
+ # process separetly
47
+ try:
48
+ sequential_outputs.append(response.choices[0].message.content.strip())
49
+ except (AttributeError, IndexError, TypeError):
50
+ sequential_outputs.append(repr(response))
51
+ sequential_elapsed = perf_counter() - sequential_started
52
+
53
+ # using Manager
54
+ batched_started = perf_counter()
55
+ manager = Manager(client, concurrency=CONCURRENCY, progress_bar=True)
56
+ for prompt in PROMPTS:
57
+ # same call name and parameters
58
+ manager.chat.completions.create(
59
+ model=model,
60
+ # random.randint to prevent caching
61
+ messages=[
62
+ {"role": "user", "content": f"{random.randint(0, 99)}. {prompt}"}
63
+ ],
64
+ )
65
+
66
+ # now we collect results
67
+ batched_raw = await manager.process()
68
+
69
+ # and process them already in list
70
+ batched_outputs = []
71
+ for response in batched_raw:
72
+ try:
73
+ batched_outputs.append(response.choices[0].message.content.strip())
74
+ except (AttributeError, IndexError, TypeError):
75
+ batched_outputs.append(repr(response))
76
+ batched_elapsed = perf_counter() - batched_started
77
+
78
+ print(f"Model : {model}")
79
+ print(f"Prompts : {len(PROMPTS)}")
80
+ print(f"Batched concurrency : {CONCURRENCY}")
81
+
82
+ print()
83
+ print(f"Sequential async OpenAI: {sequential_elapsed:.2f}s")
84
+ print(f"BatchedLLM manager : {batched_elapsed:.2f}s")
85
+ if batched_elapsed < sequential_elapsed:
86
+ print(f"Speedup : {sequential_elapsed / batched_elapsed:.2f}x")
87
+
88
+ print()
89
+ print("Results")
90
+ for prompt, sequential, batched in zip(
91
+ PROMPTS, sequential_outputs, batched_outputs
92
+ ):
93
+ print(f"- Prompt : {prompt}")
94
+ print(f" Sequential : {sequential}")
95
+ print(f" Batched : {batched}")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ asyncio.run(main())
@@ -0,0 +1,27 @@
1
+ {
2
+ "nodes": {
3
+ "nixpkgs": {
4
+ "locked": {
5
+ "lastModified": 1773821835,
6
+ "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
7
+ "owner": "NixOS",
8
+ "repo": "nixpkgs",
9
+ "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "NixOS",
14
+ "ref": "nixos-unstable",
15
+ "repo": "nixpkgs",
16
+ "type": "github"
17
+ }
18
+ },
19
+ "root": {
20
+ "inputs": {
21
+ "nixpkgs": "nixpkgs"
22
+ }
23
+ }
24
+ },
25
+ "root": "root",
26
+ "version": 7
27
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ description = "Flake for batchedllm";
3
+
4
+ inputs = {
5
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6
+ };
7
+
8
+ outputs = {
9
+ self,
10
+ nixpkgs,
11
+ ...
12
+ } @ inputs: let
13
+ inherit (nixpkgs) lib;
14
+ forAllSystems = lib.genAttrs lib.systems.flakeExposed;
15
+ in {
16
+ devShells = forAllSystems (
17
+ system: let
18
+ pkgs = nixpkgs.legacyPackages.${system};
19
+ in {
20
+ default = pkgs.mkShell {
21
+ buildInputs = with pkgs; [
22
+ git
23
+
24
+ act
25
+
26
+ python313 # TODO: keep in sync with pyproject.toml
27
+ ruff
28
+ ty
29
+ pre-commit
30
+ uv
31
+ ];
32
+
33
+
34
+ env = lib.optionalAttrs pkgs.stdenv.isLinux {
35
+ LD_LIBRARY_PATH = lib.makeLibraryPath pkgs.pythonManylinuxPackages.manylinux1;
36
+ };
37
+
38
+ shellHook = ''
39
+ unset PYTHONPATH
40
+ uv sync
41
+ . .venv/bin/activate
42
+ export PATH="${pkgs.ruff}/bin:${pkgs.ty}/bin:$PATH"
43
+ '';
44
+ };
45
+ }
46
+ );
47
+
48
+ formatter = forAllSystems (system: inputs.nixpkgs.legacyPackages.${system}.alejandra);
49
+ };
50
+ }
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "batchedllm"
3
+ readme = "README.md"
4
+ keywords = ["batched", "LLM"]
5
+ authors = [
6
+ {name = "Danil Kireev", email="nenil@nenil.dev"},
7
+ {name = "Daniil Larionov", email="rexhaif.io@gmail.com"},
8
+ ]
9
+ license = "MIT"
10
+ dynamic = ["version", "description"]
11
+ classifiers = [
12
+ "License :: OSI Approved :: MIT License",
13
+ "Topic :: Utilities",
14
+ "Development Status :: 3 - Alpha",
15
+ "Programming Language :: Python :: 3.13",
16
+ ]
17
+ requires-python = ">=3.13"
18
+ dependencies = [
19
+ "tqdm>=4.67.3",
20
+ ]
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pre-commit>=4.5.1",
25
+ "pytest>=8.4.2",
26
+ "pytest-asyncio>=1.2.0",
27
+ "ruff>=0.15.7",
28
+ "ty>=0.0.24",
29
+ ]
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src"]
33
+ asyncio_mode = "auto"
34
+
35
+ [project.urls]
36
+ repository = "https://github.com/batchedllm/batchedllm"
37
+
38
+ [build-system]
39
+ requires = ["hatchling", "hatch-docstring-description"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/batchedllm/__init__.py"
44
+
45
+ [tool.hatch.metadata.hooks.docstring-description]
@@ -0,0 +1,8 @@
1
+ """like itertools.batched but for calling LLM"""
2
+
3
+ from .manager import Manager
4
+ from .cached_manager import CachedManager
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["Manager", "CachedManager"]
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ from collections.abc import MutableMapping
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, ClassVar
7
+
8
+ from tqdm.asyncio import tqdm
9
+
10
+ from .manager import Manager, QueuedCall
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class CachedManager(Manager):
17
+ """version of Manager with a minimal cache for succesfull calls"""
18
+
19
+ # TODO: assumes same client
20
+ cacher: MutableMapping[Any, Any] = field(default_factory=dict)
21
+
22
+ _logger: ClassVar[logging.Logger] = logger
23
+
24
+ async def process(self) -> list[Any | Exception]:
25
+ queue, self._queue = self._queue, []
26
+
27
+ if not queue:
28
+ return []
29
+
30
+ pbar = tqdm(total=len(queue)) if self.progress_bar else None
31
+ semaphore = asyncio.Semaphore(self.concurrency)
32
+
33
+ async def semaphore_wrapper(task: QueuedCall) -> Any:
34
+ self._logger.debug(
35
+ "executing `%s.%s(*%s, **%s)",
36
+ self.client,
37
+ ".".join(task.path),
38
+ task.args or tuple(),
39
+ task.kwargs or dict(),
40
+ )
41
+
42
+ try:
43
+ async with semaphore:
44
+ try:
45
+ result = task.func(
46
+ *(task.args or tuple()), **(task.kwargs or dict())
47
+ )
48
+ if inspect.isawaitable(result):
49
+ return await result
50
+ return result
51
+ except Exception as exc:
52
+ self._logger.debug(
53
+ "executing `%s.%s(*%s, **%s) failed",
54
+ self.client,
55
+ ".".join(task.path),
56
+ task.args or tuple(),
57
+ task.kwargs or dict(),
58
+ exc_info=exc,
59
+ )
60
+ if self.error_behavior == "raise":
61
+ raise exc
62
+ elif self.error_behavior == "ignore":
63
+ return None
64
+ elif self.error_behavior == "forward":
65
+ return exc
66
+ finally:
67
+ if pbar:
68
+ pbar.update()
69
+
70
+ async def cached_semaphore_wrapper(task: QueuedCall) -> Any:
71
+ key = task.as_cache_key()
72
+
73
+ if key not in self.cacher:
74
+ self.cacher[key] = semaphore_wrapper(task)
75
+ else:
76
+ if pbar:
77
+ pbar.update()
78
+
79
+ return self.cacher[key]
80
+
81
+ try:
82
+ return await asyncio.gather(*map(cached_semaphore_wrapper, queue))
83
+ finally:
84
+ if pbar:
85
+ pbar.close()
@@ -0,0 +1,142 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, ClassVar, Literal, cast, get_args
7
+
8
+ from tqdm.asyncio import tqdm
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ type QueuedCallable = Callable[..., Awaitable[Any] | Any]
13
+ type ErrorBehavior = Literal["raise", "ignore", "forward"]
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class QueuedCall:
18
+ path: tuple[str, ...]
19
+ func: QueuedCallable
20
+ args: tuple[Any, ...] | None
21
+ kwargs: dict[str, Any] | None
22
+
23
+ def as_cache_key(self) -> str:
24
+ # TODO: maybe sort args/kwargs
25
+ return f"{'.'.join(self.path)}.{self.func}(*({self.args or tuple()}), **{{{self.kwargs or dict()}}})"
26
+
27
+
28
+ @dataclass(slots=True, frozen=True)
29
+ class _PathBuilder:
30
+ """immutable path builder to avoid path leakage and allow for path reuse"""
31
+
32
+ manager: "Manager"
33
+ path: tuple[str, ...]
34
+
35
+ def __getattr__(self, name: str) -> "_PathBuilder":
36
+ return _PathBuilder(self.manager, (*self.path, name))
37
+
38
+ def __call__(self, *args: Any | None, **kwargs: Any | None) -> "Manager":
39
+ target = self.manager.client
40
+ for part in self.path:
41
+ target = getattr(target, part)
42
+
43
+ # if not isinstance(target, QueuedCallable):
44
+ if not callable(target):
45
+ raise TypeError(f"Resolved target `{'.'.join(self.path)}` is not callable")
46
+
47
+ self.manager._queue.append(
48
+ QueuedCall(
49
+ path=self.path,
50
+ func=cast(QueuedCallable, target),
51
+ args=args or None,
52
+ kwargs=dict(kwargs) or None,
53
+ )
54
+ )
55
+ return self.manager
56
+
57
+
58
+ @dataclass(slots=True)
59
+ class Manager:
60
+ """basic implementation of an async batch manager"""
61
+
62
+ client: object
63
+ concurrency: int = 1
64
+ error_behavior: ErrorBehavior = "raise"
65
+ progress_bar: bool = False
66
+
67
+ _queue: list[QueuedCall] = field(default_factory=list, init=False, repr=False)
68
+
69
+ _logger: ClassVar[logging.Logger] = logger
70
+
71
+ def __post_init__(self) -> None:
72
+ #
73
+ if not (isinstance(self.concurrency, int) and self.concurrency >= 1):
74
+ raise ValueError("concurrency must be a positive integer")
75
+
76
+ error_behavior_acceptable_values = get_args(ErrorBehavior.__value__)
77
+ if self.error_behavior not in error_behavior_acceptable_values:
78
+ raise ValueError(
79
+ f"error_behavior must be one of {', '.join(map('`{}`'.format, error_behavior_acceptable_values))}"
80
+ )
81
+
82
+ def __getattr__(self, name: str) -> _PathBuilder:
83
+ return _PathBuilder(self, (name,))
84
+
85
+ async def process(self) -> list[Any]:
86
+ queue, self._queue = self._queue, []
87
+
88
+ if not queue:
89
+ return []
90
+
91
+ pbar = tqdm(total=len(queue)) if self.progress_bar else None
92
+ semaphore = asyncio.Semaphore(self.concurrency)
93
+
94
+ async def semaphore_wrapper(task: QueuedCall) -> Any:
95
+ self._logger.debug(
96
+ "executing `%s.%s(*%s, **%s)",
97
+ self.client,
98
+ ".".join(task.path),
99
+ task.args or tuple(),
100
+ task.kwargs or dict(),
101
+ )
102
+
103
+ try:
104
+ async with semaphore:
105
+ try:
106
+ result = task.func(
107
+ *(task.args or tuple()), **(task.kwargs or dict())
108
+ )
109
+ if inspect.isawaitable(result):
110
+ return await result
111
+ return result
112
+ except Exception as exc:
113
+ self._logger.debug(
114
+ "executing `%s.%s(*%s, **%s) failed",
115
+ self.client,
116
+ ".".join(task.path),
117
+ task.args or tuple(),
118
+ task.kwargs or dict(),
119
+ exc_info=exc,
120
+ )
121
+ if self.error_behavior == "raise":
122
+ raise exc
123
+ elif self.error_behavior == "ignore":
124
+ return None
125
+ elif self.error_behavior == "forward":
126
+ return exc
127
+ finally:
128
+ if pbar:
129
+ pbar.update()
130
+
131
+ try:
132
+ return await asyncio.gather(*map(semaphore_wrapper, queue))
133
+ finally:
134
+ if pbar:
135
+ pbar.close()
136
+
137
+ def sync_process(self) -> list[Any | Exception]:
138
+ """best effort to run async from sync, async version should be prefered"""
139
+ try:
140
+ return asyncio.get_running_loop().run_until_complete(self.process())
141
+ except RuntimeError: # no loop running
142
+ return asyncio.run(self.process())
@@ -0,0 +1,197 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Any, cast
4
+ from dataclasses import dataclass, field
5
+
6
+ import pytest
7
+
8
+ from batchedllm import Manager
9
+
10
+
11
+ @dataclass
12
+ class MockAI:
13
+ history: list = field(default_factory=list)
14
+ active: int = 0
15
+ max_active: int = 0
16
+
17
+ chat: "Chat" = field(init=False)
18
+
19
+ def __post_init__(self):
20
+ self.chat = Chat(self)
21
+
22
+
23
+ @dataclass
24
+ class Chat:
25
+ parent: MockAI
26
+
27
+ completions: "Completions" = field(init=False)
28
+
29
+ def __post_init__(self):
30
+ self.completions = Completions(self)
31
+
32
+
33
+ @dataclass
34
+ class Completions:
35
+ parent: Chat
36
+
37
+ async def create(self, value, *, delay: int = 0, fail: bool = False):
38
+ self.parent.parent.active += 1
39
+ self.parent.parent.max_active = max(
40
+ self.parent.parent.max_active, self.parent.parent.active
41
+ )
42
+ self.parent.parent.history.append(("chat.completions.create", value))
43
+
44
+ try:
45
+ await asyncio.sleep(delay)
46
+ if fail:
47
+ raise ValueError(value)
48
+ return value
49
+ finally:
50
+ self.parent.parent.active -= 1
51
+
52
+ def sync_create(self, value):
53
+ self.parent.parent.history.append(("chat.completions.sync_create", value))
54
+ return value
55
+
56
+
57
+ def test_generally_works():
58
+ manager = Manager(MockAI())
59
+
60
+ partial = manager.chat.completions
61
+
62
+ partial.create("hello")
63
+ returned = partial.create("world", delay=1).chat.completions.create(value="!")
64
+
65
+ assert returned is manager
66
+ assert len(manager._queue) == 3
67
+ assert manager._queue[0].path == ("chat", "completions", "create")
68
+ assert manager._queue[0].args == ("hello",)
69
+ assert manager._queue[0].kwargs is None
70
+ assert manager._queue[1].path == ("chat", "completions", "create")
71
+ assert manager._queue[1].args == ("world",)
72
+ assert manager._queue[1].kwargs == {"delay": 1}
73
+ assert manager._queue[2].path == ("chat", "completions", "create")
74
+ assert manager._queue[2].args is None
75
+ assert manager._queue[2].kwargs == {"value": "!"}
76
+
77
+
78
+ def test_paths_dont_cross():
79
+ manager = Manager(MockAI())
80
+
81
+ manager.chat
82
+ manager.chat.completions
83
+ manager.chat.completions.create
84
+ manager.this.can.be.any.path_we.dont.care.until.you.call.process
85
+ manager.chat.completions.create("only one")
86
+
87
+ assert len(manager._queue) == 1
88
+ assert manager._queue[0].path == ("chat", "completions", "create")
89
+
90
+
91
+ def test_paths_dont_cross_even_when_error():
92
+ manager = Manager(MockAI())
93
+
94
+ with pytest.raises(TypeError, match="is not callable"):
95
+ manager.history()
96
+
97
+ manager.chat.completions.create("only one even if previous errors")
98
+
99
+ assert len(manager._queue) == 1
100
+ assert manager._queue[0].path == ("chat", "completions", "create")
101
+
102
+
103
+ def test_sync_works():
104
+ manager = Manager(MockAI())
105
+ manager.chat.completions.sync_create("sync")
106
+
107
+ result = manager.sync_process()
108
+
109
+ assert result == ["sync"]
110
+ assert len(manager._queue) == 0
111
+
112
+
113
+ @pytest.mark.asyncio
114
+ async def test_async_works():
115
+ client = MockAI()
116
+ manager = Manager(client)
117
+
118
+ manager.chat.completions.create("first")
119
+ manager.chat.completions.sync_create("second")
120
+
121
+ result = await manager.process()
122
+
123
+ assert result == [
124
+ "first",
125
+ "second",
126
+ ]
127
+ assert client.history == [
128
+ ("chat.completions.create", "first"),
129
+ ("chat.completions.sync_create", "second"),
130
+ ]
131
+ assert len(manager._queue) == 0
132
+
133
+
134
+ @pytest.mark.asyncio
135
+ async def test_error_behavior_is_raise():
136
+ manager = Manager(MockAI(), error_behavior="raise")
137
+
138
+ manager.chat.completions.create("fail", fail=True)
139
+
140
+ with pytest.raises(ValueError, match="fail"):
141
+ await manager.process()
142
+
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_error_behavior_is_ignore(caplog):
146
+ manager = Manager(MockAI(), error_behavior="ignore")
147
+ manager.chat.completions.create("ok")
148
+ manager.chat.completions.create("fail", fail=True)
149
+
150
+ with caplog.at_level(logging.DEBUG):
151
+ result = await manager.process()
152
+
153
+ assert result == ["ok", None]
154
+ assert any(record.exc_info for record in caplog.records)
155
+
156
+
157
+ @pytest.mark.asyncio
158
+ async def test_error_behavior_is_forward():
159
+ manager = Manager(MockAI(), error_behavior="forward")
160
+ manager.chat.completions.create("ok")
161
+ manager.chat.completions.create("fail", fail=True)
162
+
163
+ result = await manager.process()
164
+
165
+ assert result[0] == "ok"
166
+ assert isinstance(result[1], ValueError)
167
+ assert str(result[1]) == "fail"
168
+
169
+
170
+ @pytest.mark.asyncio
171
+ async def test_concurency_respected():
172
+ client = MockAI()
173
+ manager = Manager(client, concurrency=2)
174
+
175
+ for value in range(5):
176
+ manager.chat.completions.create(f"task-{value}")
177
+
178
+ result = await manager.process()
179
+
180
+ assert result == [
181
+ "task-0",
182
+ "task-1",
183
+ "task-2",
184
+ "task-3",
185
+ "task-4",
186
+ ]
187
+ assert client.max_active == 2
188
+
189
+
190
+ def test_typechecks_concurrency():
191
+ with pytest.raises(ValueError, match="positive integer"):
192
+ Manager(MockAI(), concurrency=0)
193
+
194
+
195
+ def test_typechecks_error_behavior():
196
+ with pytest.raises(ValueError, match="error_behavior"):
197
+ Manager(MockAI(), error_behavior=cast(Any, "nope"))
@@ -0,0 +1,299 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "batchedllm"
7
+ source = { editable = "." }
8
+ dependencies = [
9
+ { name = "tqdm" },
10
+ ]
11
+
12
+ [package.dev-dependencies]
13
+ dev = [
14
+ { name = "pre-commit" },
15
+ { name = "pytest" },
16
+ { name = "pytest-asyncio" },
17
+ { name = "ruff" },
18
+ { name = "ty" },
19
+ ]
20
+
21
+ [package.metadata]
22
+ requires-dist = [{ name = "tqdm", specifier = ">=4.67.3" }]
23
+
24
+ [package.metadata.requires-dev]
25
+ dev = [
26
+ { name = "pre-commit", specifier = ">=4.5.1" },
27
+ { name = "pytest", specifier = ">=8.4.2" },
28
+ { name = "pytest-asyncio", specifier = ">=1.2.0" },
29
+ { name = "ruff", specifier = ">=0.15.7" },
30
+ { name = "ty", specifier = ">=0.0.24" },
31
+ ]
32
+
33
+ [[package]]
34
+ name = "cfgv"
35
+ version = "3.5.0"
36
+ source = { registry = "https://pypi.org/simple" }
37
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
38
+ wheels = [
39
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
40
+ ]
41
+
42
+ [[package]]
43
+ name = "colorama"
44
+ version = "0.4.6"
45
+ source = { registry = "https://pypi.org/simple" }
46
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
47
+ wheels = [
48
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
49
+ ]
50
+
51
+ [[package]]
52
+ name = "distlib"
53
+ version = "0.4.0"
54
+ source = { registry = "https://pypi.org/simple" }
55
+ 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" }
56
+ wheels = [
57
+ { 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" },
58
+ ]
59
+
60
+ [[package]]
61
+ name = "filelock"
62
+ version = "3.25.2"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "identify"
71
+ version = "2.6.18"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "iniconfig"
80
+ version = "2.3.0"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "nodeenv"
89
+ version = "1.10.0"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
92
+ wheels = [
93
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
94
+ ]
95
+
96
+ [[package]]
97
+ name = "packaging"
98
+ version = "26.0"
99
+ source = { registry = "https://pypi.org/simple" }
100
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "platformdirs"
107
+ version = "4.9.4"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
110
+ wheels = [
111
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
112
+ ]
113
+
114
+ [[package]]
115
+ name = "pluggy"
116
+ version = "1.6.0"
117
+ source = { registry = "https://pypi.org/simple" }
118
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
119
+ wheels = [
120
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
121
+ ]
122
+
123
+ [[package]]
124
+ name = "pre-commit"
125
+ version = "4.5.1"
126
+ source = { registry = "https://pypi.org/simple" }
127
+ dependencies = [
128
+ { name = "cfgv" },
129
+ { name = "identify" },
130
+ { name = "nodeenv" },
131
+ { name = "pyyaml" },
132
+ { name = "virtualenv" },
133
+ ]
134
+ sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
135
+ wheels = [
136
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
137
+ ]
138
+
139
+ [[package]]
140
+ name = "pygments"
141
+ version = "2.19.2"
142
+ source = { registry = "https://pypi.org/simple" }
143
+ 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" }
144
+ wheels = [
145
+ { 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" },
146
+ ]
147
+
148
+ [[package]]
149
+ name = "pytest"
150
+ version = "9.0.2"
151
+ source = { registry = "https://pypi.org/simple" }
152
+ dependencies = [
153
+ { name = "colorama", marker = "sys_platform == 'win32'" },
154
+ { name = "iniconfig" },
155
+ { name = "packaging" },
156
+ { name = "pluggy" },
157
+ { name = "pygments" },
158
+ ]
159
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
160
+ wheels = [
161
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
162
+ ]
163
+
164
+ [[package]]
165
+ name = "pytest-asyncio"
166
+ version = "1.3.0"
167
+ source = { registry = "https://pypi.org/simple" }
168
+ dependencies = [
169
+ { name = "pytest" },
170
+ ]
171
+ sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
174
+ ]
175
+
176
+ [[package]]
177
+ name = "python-discovery"
178
+ version = "1.2.1"
179
+ source = { registry = "https://pypi.org/simple" }
180
+ dependencies = [
181
+ { name = "filelock" },
182
+ { name = "platformdirs" },
183
+ ]
184
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" },
187
+ ]
188
+
189
+ [[package]]
190
+ name = "pyyaml"
191
+ version = "6.0.3"
192
+ source = { registry = "https://pypi.org/simple" }
193
+ 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" }
194
+ wheels = [
195
+ { 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" },
196
+ { 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" },
197
+ { 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" },
198
+ { 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" },
199
+ { 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" },
200
+ { 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" },
201
+ { 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" },
202
+ { 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" },
203
+ { 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" },
204
+ { 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" },
205
+ { 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" },
206
+ { 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" },
207
+ { 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" },
208
+ { 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" },
209
+ { 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" },
210
+ { 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" },
211
+ { 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" },
212
+ { 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" },
213
+ { 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" },
214
+ { 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" },
215
+ { 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" },
216
+ { 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" },
217
+ { 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" },
218
+ { 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" },
219
+ { 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" },
220
+ { 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" },
221
+ { 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" },
222
+ { 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" },
223
+ ]
224
+
225
+ [[package]]
226
+ name = "ruff"
227
+ version = "0.15.7"
228
+ source = { registry = "https://pypi.org/simple" }
229
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
230
+ wheels = [
231
+ { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
232
+ { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
233
+ { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
234
+ { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
235
+ { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
236
+ { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
237
+ { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
238
+ { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
239
+ { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
240
+ { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
241
+ { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
242
+ { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
243
+ { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
244
+ { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
245
+ { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
246
+ { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
247
+ { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
248
+ ]
249
+
250
+ [[package]]
251
+ name = "tqdm"
252
+ version = "4.67.3"
253
+ source = { registry = "https://pypi.org/simple" }
254
+ dependencies = [
255
+ { name = "colorama", marker = "sys_platform == 'win32'" },
256
+ ]
257
+ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
258
+ wheels = [
259
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
260
+ ]
261
+
262
+ [[package]]
263
+ name = "ty"
264
+ version = "0.0.24"
265
+ source = { registry = "https://pypi.org/simple" }
266
+ sdist = { url = "https://files.pythonhosted.org/packages/7a/96/652a425030f95dc2c9548d9019e52502e17079e1daeefbc4036f1c0905b4/ty-0.0.24.tar.gz", hash = "sha256:9fe42f6b98207bdaef51f71487d6d087f2cb02555ee3939884d779b2b3cc8bfc", size = 5354286, upload-time = "2026-03-19T16:55:57.035Z" }
267
+ wheels = [
268
+ { url = "https://files.pythonhosted.org/packages/da/e5/34457ee11708e734ba81ad65723af83030e484f961e281d57d1eecf08951/ty-0.0.24-py3-none-linux_armv6l.whl", hash = "sha256:1ab4f1f61334d533a3fdf5d9772b51b1300ac5da4f3cdb0be9657a3ccb2ce3e7", size = 10394877, upload-time = "2026-03-19T16:55:54.246Z" },
269
+ { url = "https://files.pythonhosted.org/packages/44/81/bc9a1b1a87f43db15ab64ad781a4f999734ec3b470ad042624fa875b20e6/ty-0.0.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:facbf2c4aaa6985229e08f8f9bf152215eb078212f22b5c2411f35386688ab42", size = 10211109, upload-time = "2026-03-19T16:55:28.554Z" },
270
+ { url = "https://files.pythonhosted.org/packages/e4/63/cfc805adeaa61d63ba3ea71127efa7d97c40ba36d97ee7bd957341d05107/ty-0.0.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b6d2a3b6d4470c483552a31e9b368c86f154dcc964bccb5406159dc9cd362246", size = 9694769, upload-time = "2026-03-19T16:55:34.309Z" },
271
+ { url = "https://files.pythonhosted.org/packages/33/09/edc220726b6ec44a58900401f6b27140997ef15026b791e26b69a6e69eb5/ty-0.0.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c94c25d0500939fd5f8f16ce41cbed5b20528702c1d649bf80300253813f0a2", size = 10176287, upload-time = "2026-03-19T16:55:37.17Z" },
272
+ { url = "https://files.pythonhosted.org/packages/f8/bf/cbe2227be711e65017655d8ee4d050f4c92b113fb4dc4c3bd6a19d3a86d8/ty-0.0.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:89cbe7bc7df0fab02dbd8cda79b737df83f1ef7fb573b08c0ee043dc68cffb08", size = 10214832, upload-time = "2026-03-19T16:56:08.518Z" },
273
+ { url = "https://files.pythonhosted.org/packages/af/1d/d15803ee47e9143d10e10bd81ccc14761d08758082bda402950685f0ddfe/ty-0.0.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2c5d269bcc9b764850c99f457b5018a79b3ef40ecfbc03344e65effd6cf743", size = 10709892, upload-time = "2026-03-19T16:56:05.727Z" },
274
+ { url = "https://files.pythonhosted.org/packages/36/12/6db0d86c477147f67b9052de209421d76c3e855197b000c25fcbbe86b3a2/ty-0.0.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba44512db5b97c3bbd59d93e11296e8548d0c9a3bdd1280de36d7ff22d351896", size = 11280872, upload-time = "2026-03-19T16:56:02.899Z" },
275
+ { url = "https://files.pythonhosted.org/packages/1b/fc/155fe83a97c06d33ccc9e0f428258b32df2e08a428300c715d34757f0111/ty-0.0.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a52b7f589c3205512a9c50ba5b2b1e8c0698b72e51b8b9285c90420c06f1cae8", size = 11060520, upload-time = "2026-03-19T16:55:59.956Z" },
276
+ { url = "https://files.pythonhosted.org/packages/ac/f1/32c05a1c4c3c2a95c5b7361dee03a9bf1231d4ad096b161c838b45bce5a0/ty-0.0.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7981df5c709c054da4ac5d7c93f8feb8f45e69e829e4461df4d5f0988fe67d04", size = 10791455, upload-time = "2026-03-19T16:55:25.728Z" },
277
+ { url = "https://files.pythonhosted.org/packages/17/2c/53c1ea6bedfa4d4ab64d4de262d8f5e405ecbffefd364459c628c0310d33/ty-0.0.24-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2860151ad95a00d0f0280b8fef79900d08dcd63276b57e6e5774f2c055979c5", size = 10156708, upload-time = "2026-03-19T16:55:45.563Z" },
278
+ { url = "https://files.pythonhosted.org/packages/45/39/7d2919cf194707169474d80720a5f3d793e983416f25e7ffcf80504c9df2/ty-0.0.24-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5674a1146d927ab77ff198a88e0c4505134ced342a0e7d1beb4a076a728b7496", size = 10236263, upload-time = "2026-03-19T16:55:31.474Z" },
279
+ { url = "https://files.pythonhosted.org/packages/cf/7f/48eac722f2fd12a5b7aae0effdcb75c46053f94b783d989e3ef0d7380082/ty-0.0.24-py3-none-musllinux_1_2_i686.whl", hash = "sha256:438ecbf1608a9b16dd84502f3f1b23ef2ef32bbd0ab3e0ca5a82f0e0d1cd41ea", size = 10402559, upload-time = "2026-03-19T16:55:39.602Z" },
280
+ { url = "https://files.pythonhosted.org/packages/75/e0/8cf868b9749ce1e5166462759545964e95b02353243594062b927d8bff2a/ty-0.0.24-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ddeed3098dd92a83964e7aa7b41e509ba3530eb539fc4cd8322ff64a09daf1f5", size = 10893684, upload-time = "2026-03-19T16:55:51.439Z" },
281
+ { url = "https://files.pythonhosted.org/packages/17/9f/f54bf3be01d2c2ed731d10a5afa3324dc66f987a6ae0a4a6cbfa2323d080/ty-0.0.24-py3-none-win32.whl", hash = "sha256:83013fb3a4764a8f8bcc6ca11ff8bdfd8c5f719fc249241cb2b8916e80778eb1", size = 9781542, upload-time = "2026-03-19T16:56:11.588Z" },
282
+ { url = "https://files.pythonhosted.org/packages/fb/49/c004c5cc258b10b3a145666e9a9c28ae7678bc958c8926e8078d5d769081/ty-0.0.24-py3-none-win_amd64.whl", hash = "sha256:748a60eb6912d1cf27aaab105ffadb6f4d2e458a3fcadfbd3cf26db0d8062eeb", size = 10764801, upload-time = "2026-03-19T16:55:42.752Z" },
283
+ { url = "https://files.pythonhosted.org/packages/e2/59/006a074e185bfccf5e4c026015245ab4fcd2362b13a8d24cf37a277909a9/ty-0.0.24-py3-none-win_arm64.whl", hash = "sha256:280a3d31e86d0721947238f17030c33f0911cae851d108ea9f4e3ab12a5ed01f", size = 10194093, upload-time = "2026-03-19T16:55:48.303Z" },
284
+ ]
285
+
286
+ [[package]]
287
+ name = "virtualenv"
288
+ version = "21.2.0"
289
+ source = { registry = "https://pypi.org/simple" }
290
+ dependencies = [
291
+ { name = "distlib" },
292
+ { name = "filelock" },
293
+ { name = "platformdirs" },
294
+ { name = "python-discovery" },
295
+ ]
296
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" }
297
+ wheels = [
298
+ { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" },
299
+ ]