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.
- batchedllm-0.1.0/.github/dependabot.yml +16 -0
- batchedllm-0.1.0/.github/workflows/lint_and_test.yml +49 -0
- batchedllm-0.1.0/.github/workflows/publish.yml +51 -0
- batchedllm-0.1.0/.gitignore +6 -0
- batchedllm-0.1.0/.pre-commit-config.yaml +17 -0
- batchedllm-0.1.0/.vscode/extensions.json +7 -0
- batchedllm-0.1.0/.vscode/settings.json +3 -0
- batchedllm-0.1.0/PKG-INFO +25 -0
- batchedllm-0.1.0/README.md +9 -0
- batchedllm-0.1.0/examples/openai_speed.py +99 -0
- batchedllm-0.1.0/flake.lock +27 -0
- batchedllm-0.1.0/flake.nix +50 -0
- batchedllm-0.1.0/pyproject.toml +45 -0
- batchedllm-0.1.0/src/batchedllm/__init__.py +8 -0
- batchedllm-0.1.0/src/batchedllm/cached_manager.py +85 -0
- batchedllm-0.1.0/src/batchedllm/manager.py +142 -0
- batchedllm-0.1.0/tests/test_manager.py +197 -0
- batchedllm-0.1.0/uv.lock +299 -0
|
@@ -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,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,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,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"))
|
batchedllm-0.1.0/uv.lock
ADDED
|
@@ -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
|
+
]
|