llm_common 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,149 @@
1
+ .python-version
2
+ .aiignore
3
+
4
+ # Python-generated files
5
+ __pycache__/
6
+ __cache__/
7
+ *.py[oc]
8
+ *.egg-info
9
+
10
+ # PyCharm
11
+ .idea/
12
+
13
+ # Byte-compiled / optimized / DLL files
14
+ *.py[cod]
15
+ *$py.class
16
+
17
+ .ipynb_checkpoints
18
+ */.ipynb_checkpoints/*
19
+
20
+ # IPython
21
+ profile_default/
22
+ ipython_config.py
23
+
24
+ # Distribution / packaging
25
+ .Python
26
+ build/
27
+ develop-eggs/
28
+ dist/
29
+ downloads/
30
+ eggs/
31
+ .eggs/
32
+ lib/
33
+ lib64/
34
+ parts/
35
+ sdist/
36
+ var/
37
+ wheels/
38
+ share/python-wheels/
39
+ *.egg-info/
40
+ .installed.cfg
41
+ *.egg
42
+ MANIFEST
43
+
44
+ # PyInstaller
45
+ # Usually these files are written by a python script from a template
46
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
47
+ *.manifest
48
+ *.spec
49
+
50
+ # Installer logs
51
+ pip-log.txt
52
+ pip-delete-this-directory.txt
53
+
54
+ # Unit test / coverage reports
55
+ htmlcov/
56
+ .tox/
57
+ .nox/
58
+ .coverage
59
+ .coverage.*
60
+ .cache
61
+ nosetests.xml
62
+ coverage.xml
63
+ *.cover
64
+ *.py,cover
65
+ .hypothesis/
66
+ .pytest_cache/
67
+ cover/
68
+
69
+ # Translations
70
+ *.mo
71
+ *.pot
72
+
73
+ # Django stuff:
74
+ *.log
75
+ local_settings.py
76
+ db.sqlite3
77
+ db.sqlite3-journal
78
+ *.sqlite
79
+ sqlite.*
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
96
+ __pypackages__/
97
+
98
+ # Celery stuff
99
+ celerybeat-schedule
100
+ celerybeat.pid
101
+
102
+ # SageMath parsed files
103
+ *.sage.py
104
+
105
+ # Environments
106
+ .env
107
+ .venv
108
+ env/
109
+ venv/
110
+ ENV/
111
+ env.bak/
112
+ venv.bak/
113
+ .pyenv
114
+
115
+ # Spyder project settings
116
+ .spyderproject
117
+ .spyproject
118
+
119
+ # Rope project settings
120
+ .ropeproject
121
+
122
+ # mkdocs documentation
123
+ /site
124
+
125
+ # mypy
126
+ .mypy_cache/
127
+ .dmypy.json
128
+ dmypy.json
129
+
130
+ # Pyre type checker
131
+ .pyre/
132
+
133
+ # pytype static type analyzer
134
+ .pytype/
135
+
136
+ # Cython debug symbols
137
+ cython_debug/
138
+ !/.gitignore
139
+
140
+ # Additional entries
141
+ .bzr
142
+ .direnv
143
+ .pants.d
144
+ .ruff_cache
145
+ .svn
146
+ ,junie
147
+ buck-out
148
+ node_modules
149
+ check
@@ -0,0 +1,29 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: black-format
5
+ stages: [pre-commit]
6
+ name: black format
7
+ entry: black
8
+ language: python
9
+ args: [llm_common]
10
+ pass_filenames: false
11
+
12
+ - repo: local
13
+ hooks:
14
+ - id: ruff-check
15
+ stages: [pre-commit]
16
+ name: ruff linter
17
+ entry: ruff
18
+ language: python
19
+ args: [check, --fix, llm_common]
20
+ pass_filenames: false
21
+
22
+ - repo: local
23
+ hooks:
24
+ - id: vulture
25
+ stages: [pre-commit]
26
+ name: Vulture - find dead code
27
+ entry: vulture llm_common # Only report N% dead code.
28
+ language: system
29
+ pass_filenames: false
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm_common
3
+ Version: 0.1.0
4
+ Author-email: Pavel Maksimov <pavel.maximov@megafon.ru>
5
+ Requires-Python: >=3.12
File without changes
File without changes
@@ -0,0 +1,8 @@
1
+ import datetime as d
2
+ import logging
3
+ import typing as t
4
+
5
+ from project import datatypes as dt
6
+ from project.settings import Settings
7
+
8
+ logger = logging.getLogger(__name__)
@@ -0,0 +1,11 @@
1
+ from llm_common.prometheus import HttpxClientWithMonitoring
2
+
3
+
4
+ class AuthHttpClient(HttpxClientWithMonitoring):
5
+ name_for_monitoring = "auth_api"
6
+
7
+ def clear_resource_path(self, resource: str):
8
+ if resource.startswith("/api/check"):
9
+ return "api/check/{telegram_user_id}"
10
+
11
+ return super().clear_resource_path(resource)
@@ -0,0 +1,5 @@
1
+ from llm_common.prometheus import HttpxClientWithMonitoring
2
+
3
+
4
+ class LLMHttpClient(HttpxClientWithMonitoring):
5
+ name_for_monitoring = "llm"
@@ -0,0 +1,265 @@
1
+ import asyncio
2
+ import re
3
+ import time
4
+ import warnings
5
+ from contextlib import contextmanager
6
+ from functools import partial, wraps
7
+ from typing import Literal
8
+
9
+ from httpx import AsyncClient, Request, Response
10
+ from prometheus_client import Counter, Histogram
11
+ from prometheus_client import generate_latest, REGISTRY, CONTENT_TYPE_LATEST
12
+ from prometheus_client.metrics import MetricWrapperBase
13
+
14
+ COMMON_METRIC_TEMPLATE = "genapp_{}"
15
+
16
+ _metric: "PrometheusMetrics" = None
17
+
18
+
19
+ def build_prometheus_metrics(project_name: str, env: Literal["dev", "preprod", "prod"]) -> "PrometheusMetrics":
20
+ global _metric
21
+
22
+ if env not in ("dev", "preprod", "prod"):
23
+ raise ValueError(f"No valid env value for metrics: {env}")
24
+
25
+ _metric = PrometheusMetrics(project_name, env)
26
+
27
+ return _metric
28
+
29
+
30
+ class PrometheusMetrics:
31
+ def __init__(self, project_name: str, env: Literal["dev", "preprod", "prod"]):
32
+ self.env = env
33
+ self.project_name = project_name
34
+
35
+ # Common Metrics:
36
+ HTTP_REQUESTS = Counter(
37
+ COMMON_METRIC_TEMPLATE.format("http_requests_total"),
38
+ "Total number of HTTP requests",
39
+ ["method", "status", "resource", "app_type", "env", "app"],
40
+ )
41
+ HTTP_REQUEST_DURATION = Histogram(
42
+ COMMON_METRIC_TEMPLATE.format("http_request_duration_sec"),
43
+ "HTTP request latency in seconds",
44
+ ["method", "status", "resource", "app_type", "env", "app"],
45
+ buckets=[0.1, 0.5, 1, 3, 5, 10, 20, 30, 60, 120, 300, 600, 3600, float("inf")],
46
+ )
47
+ HTTP_REQUEST_DURATION_TOTAL = Counter(
48
+ COMMON_METRIC_TEMPLATE.format("http_request_duration_sec_total"),
49
+ "Total duration of HTTP request in seconds",
50
+ ["method", "status", "resource", "app_type", "env", "app"],
51
+ )
52
+ HTTP_REQUEST_SIZE = Histogram(
53
+ COMMON_METRIC_TEMPLATE.format("http_request_size_bytes"),
54
+ "HTTP request or response size in bytes",
55
+ ["resource", "status", "method", "direction", "app_type", "env", "app"],
56
+ buckets=[100, 1000, 5000, 10000, 50000, 100000, 500000, 1000000, float("inf")],
57
+ )
58
+ HTTP_REQUEST_SIZE_TOTAL = Counter(
59
+ COMMON_METRIC_TEMPLATE.format("http_request_size_bytes_total"),
60
+ "Total size of HTTP request or response in bytes",
61
+ ["resource", "status", "method", "direction", "app_type", "env", "app"],
62
+ )
63
+
64
+ ACTION_COUNT = Counter(
65
+ COMMON_METRIC_TEMPLATE.format("action_count_total"),
66
+ "Total action requests",
67
+ ["name", "status", "env", "app"],
68
+ )
69
+ ACTION_DURATION = Histogram(
70
+ COMMON_METRIC_TEMPLATE.format("action_duration_sec"),
71
+ "Action request latency",
72
+ ["name", "env", "app"],
73
+ buckets=[0.01, 0.1, 0.5, 1, 2, 5, 10, 20, 30, 60, 120, 600, 3600, 3600 * 12, 3600 * 24, float("inf")],
74
+ )
75
+ ACTION_SIZE = Counter(
76
+ COMMON_METRIC_TEMPLATE.format("action_size_total"),
77
+ "Total event size",
78
+ ["name", "env", "app"],
79
+ )
80
+
81
+ def __getattribute__(self, item):
82
+ """
83
+ Атрибут env и app есть у всех метрик, чтобы не загрязнять код установкой этого атрибута,
84
+ он подставляется здесь для всех.
85
+ Но метод .labels() все равно надо вызывать!
86
+ """
87
+ attr = super().__getattribute__(item)
88
+
89
+ if isinstance(attr, MetricWrapperBase):
90
+ # Проверяем, поддерживает ли метрика атрибут "app".
91
+ if hasattr(attr, "_labelnames") and "app" in attr._labelnames: # noqa: SLF001
92
+ attr.labels = partial(attr.labels, env=self.env, app=self.project_name)
93
+ else:
94
+ attr.labels = partial(attr.labels, env=self.env)
95
+
96
+ return attr
97
+
98
+
99
+ @contextmanager
100
+ def action_tracking(name: str):
101
+ """
102
+ Отслеживание кол-ва вызова и времени выполнения секций кода с отслеживаниием появления ошибок.
103
+ """
104
+ if _metric:
105
+ begin = time.perf_counter()
106
+ try:
107
+ # Наружу передается класс, через который можно зафиксировать дополнительные метрики.
108
+ class TrackSize:
109
+ @staticmethod
110
+ def size(size):
111
+ _metric.ACTION_SIZE.labels(name=name).inc(size)
112
+
113
+ yield TrackSize
114
+
115
+ except Exception:
116
+ _metric.ACTION_COUNT.labels(name=name, status="error").inc()
117
+ raise
118
+
119
+ else:
120
+ _metric.ACTION_COUNT.labels(name=name, status="ok").inc()
121
+
122
+ finally:
123
+ duration = round(time.perf_counter() - begin, 3)
124
+ _metric.ACTION_DURATION.labels(name=name).observe(duration)
125
+ else:
126
+
127
+ class TrackSize:
128
+ @staticmethod
129
+ def size(size):
130
+ pass
131
+
132
+ yield TrackSize
133
+ warnings.warn("Prometheus metrics not initialized", UserWarning)
134
+
135
+
136
+ def action_tracking_decorator(name: str):
137
+ """
138
+ Отслеживание кол-ва вызова и времени выполнения функций и корутин.
139
+ """
140
+
141
+ def decorator(func):
142
+ if asyncio.iscoroutinefunction(func):
143
+
144
+ @wraps(func)
145
+ async def async_wrapper(*args, **kwargs):
146
+ with action_tracking(name):
147
+ return await func(*args, **kwargs)
148
+
149
+ return async_wrapper
150
+
151
+ @wraps(func)
152
+ def wrapper(*args, **kwargs):
153
+ with action_tracking(name):
154
+ return func(*args, **kwargs)
155
+
156
+ return wrapper
157
+
158
+ return decorator
159
+
160
+
161
+ def http_tracking(
162
+ app_type: str, resource: str, method: str, response_size: int, status_code: int, duration: float, request_size: int
163
+ ):
164
+ if _metric:
165
+ resource = resource.strip("/")
166
+
167
+ _metric.HTTP_REQUESTS.labels(method=method, resource=resource, status=status_code, app_type=app_type).inc()
168
+ _metric.HTTP_REQUEST_DURATION.labels(
169
+ method=method, resource=resource, status=status_code, app_type=app_type
170
+ ).observe(duration)
171
+ _metric.HTTP_REQUEST_DURATION_TOTAL.labels(
172
+ method=method, resource=resource, status=status_code, app_type=app_type
173
+ ).inc(duration)
174
+ _metric.HTTP_REQUEST_SIZE.labels(
175
+ method=method, resource=resource, status=status_code, direction="in", app_type=app_type
176
+ ).observe(request_size)
177
+ _metric.HTTP_REQUEST_SIZE.labels(
178
+ method=method, resource=resource, status=status_code, direction="out", app_type=app_type
179
+ ).observe(response_size)
180
+ _metric.HTTP_REQUEST_SIZE_TOTAL.labels(
181
+ method=method, resource=resource, status=status_code, direction="in", app_type=app_type
182
+ ).inc(request_size)
183
+ _metric.HTTP_REQUEST_SIZE_TOTAL.labels(
184
+ method=method, resource=resource, status=status_code, direction="out", app_type=app_type
185
+ ).inc(response_size)
186
+
187
+ else:
188
+ warnings.warn("Prometheus metrics not initialized", UserWarning)
189
+
190
+
191
+ class HttpxClientWithMonitoring(AsyncClient):
192
+ """Custom async httpx client that collects metrics for HTTP requests."""
193
+
194
+ name_for_monitoring = None
195
+
196
+ def clear_resource_path(self, resource: str):
197
+ """
198
+ Надо очищать URL ресурсов от значений идентификаторов, которые мешают группировки запросов.
199
+ По умолчанию на всякий случай очищаются числа, если кто-то не позаботится об очистке самостоятельно.
200
+ """
201
+ resource = re.sub(r"/\d+", "/{int}", resource)
202
+ resource = re.sub(r"\d{2,}", "{int}", resource)
203
+
204
+ return resource
205
+
206
+ async def send(self, request: Request, **kwargs) -> Response:
207
+ if not self.name_for_monitoring:
208
+ raise NotImplementedError("Not set name http requests for monitoring")
209
+
210
+ response = await super().send(request, **kwargs)
211
+
212
+ if _metric:
213
+ request_size = int(request.headers.get("content-length", len(getattr(request, "content", ""))))
214
+ response_size = int(response.headers.get("content-length", len(getattr(response, "content", ""))))
215
+ resource = self.clear_resource_path(request.url.path)
216
+
217
+ http_tracking(
218
+ app_type=self.name_for_monitoring,
219
+ resource=resource,
220
+ method=request.method,
221
+ response_size=response_size,
222
+ status_code=response.status_code,
223
+ duration=response.elapsed.total_seconds(),
224
+ request_size=request_size,
225
+ )
226
+
227
+ else:
228
+ warnings.warn("Prometheus metrics not initialized", UserWarning)
229
+
230
+ return response
231
+
232
+
233
+ async def fastapi_tracking_middleware(request: Request, call_next):
234
+ begin = time.perf_counter()
235
+ response = await call_next(request)
236
+
237
+ if _metric:
238
+ process_time = round(time.perf_counter() - begin, 3)
239
+
240
+ resource = re.sub(r"/eapi/servisy-raboty-llm-bigdata[^/]+", "", request.url.path)
241
+
242
+ if resource not in ("/docs", "/openapi.json", "/health", "/prometheus", "/favicon.ico"):
243
+ resource = re.sub(r"/\d+", "/{int}", resource)
244
+ resource = re.sub(r"\d{2,}", "{int}", resource)
245
+
246
+ http_tracking(
247
+ app_type="app_api",
248
+ resource=resource,
249
+ method=request.method,
250
+ status_code=response.status_code,
251
+ duration=process_time,
252
+ response_size=int(response.headers.get("content-length", 0)),
253
+ request_size=int(request.headers.get("content-length", 0)),
254
+ )
255
+
256
+ else:
257
+ warnings.warn("Prometheus metrics not initialized", UserWarning)
258
+
259
+ return response
260
+
261
+
262
+ async def fastapi_endpoint_for_prometheus():
263
+ from starlette.responses import Response
264
+
265
+ return Response(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)
File without changes
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "llm_common"
3
+ version = "0.1.0"
4
+ description = ""
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Pavel Maksimov", email = "pavel.maximov@megafon.ru" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = []
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "ruff>=0.12.5",
15
+ "black>=25.1.0",
16
+ "pre-commit>=4.2.0",
17
+ "vulture>=2.14",
18
+ ]
19
+
20
+ [tool.ruff]
21
+ line-length = 120
22
+ indent-width = 4
23
+ target-version = "py312"
24
+ include = ["project/**/*.py", "tests/**/*.py"]
25
+
26
+ [tool.ruff.lint]
27
+ ignore = [
28
+ "E731", "PYI001", "SIM117", "ARG001", "E731", "E741", "UP035", "UP028", "RET504", "S101", "COM812", "RUF002",
29
+ "C901", "RUF001", "RUF003", "BLE001", "UP038", "EM101", "ICN003", "FBT001", "FBT002", "TRY301",
30
+ ]
31
+ select = [
32
+ "E", "E4", "E7", "E9", "F", "B", "ERA", "FAST", "YTT", "UP", "FURB", "RUF", "PLC", "PLE",
33
+ "ASYNC", "S", "BLE", "FBT", "A", "COM", "C", "EM", "FA", "INT", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20",
34
+ "PYI", "PT", "Q", "RSE", "RET", "SLF", "SIM", "SLOT", "TID", "TC", "ARG", "PTH", "FLY", "C90", "NPY", "N", "PERF",
35
+ "D201", "D204", "D206", "D207", "D208", "D209", "D210", "D211", "D213", "D300", "D402", "D403", "D418", "D419",
36
+ "PLW0245", "PLW0128", "PLW0127", "PLW0406", "PLW0602", "PLW0603", "PLW0642", "PLW0711",
37
+ "PLW1501", "PLW1508", "PLW1509", "PLW1510", "PLW2101", "PLW2901", "PLW3301",
38
+ "TRY004", "TRY201", "TRY203", "TRY300", "TRY301", "TRY401",
39
+ "TD004", "TD005", "TD006", "TD007",
40
+ ]
41
+
42
+ [tool.black]
43
+ line-length = 120
44
+ include = '(llm_common/.*\.py|tests/.*\.py)$'
45
+
46
+ [tool.ruff.lint.flake8-builtins]
47
+ builtins-ignorelist = ["id", "type", "filter"]
48
+
49
+ [tool.vulture]
50
+ exclude = []
51
+ ignore_decorators = []
52
+ ignore_names = []
53
+ make_whitelist = false
54
+ min_confidence = 100
55
+ paths = []
56
+ sort_by_size = true
57
+ verbose = false
58
+
59
+ [build-system]
60
+ requires = ["hatchling"]
61
+ build-backend = "hatchling.build"
@@ -0,0 +1,243 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "black"
7
+ version = "25.1.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "click" },
11
+ { name = "mypy-extensions" },
12
+ { name = "packaging" },
13
+ { name = "pathspec" },
14
+ { name = "platformdirs" },
15
+ ]
16
+ sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
17
+ wheels = [
18
+ { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
19
+ { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
20
+ { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
21
+ { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
22
+ { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
23
+ { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
24
+ { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
25
+ { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
26
+ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
27
+ ]
28
+
29
+ [[package]]
30
+ name = "cfgv"
31
+ version = "3.4.0"
32
+ source = { registry = "https://pypi.org/simple" }
33
+ sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
34
+ wheels = [
35
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
36
+ ]
37
+
38
+ [[package]]
39
+ name = "click"
40
+ version = "8.2.1"
41
+ source = { registry = "https://pypi.org/simple" }
42
+ dependencies = [
43
+ { name = "colorama", marker = "sys_platform == 'win32'" },
44
+ ]
45
+ sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 }
46
+ wheels = [
47
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
48
+ ]
49
+
50
+ [[package]]
51
+ name = "colorama"
52
+ version = "0.4.6"
53
+ source = { registry = "https://pypi.org/simple" }
54
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
55
+ wheels = [
56
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "distlib"
61
+ version = "0.4.0"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "filelock"
70
+ version = "3.19.1"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687 }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988 },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "identify"
79
+ version = "2.6.14"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283 }
82
+ wheels = [
83
+ { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172 },
84
+ ]
85
+
86
+ [[package]]
87
+ name = "llm-common"
88
+ version = "0.1.0"
89
+ source = { editable = "." }
90
+
91
+ [package.dev-dependencies]
92
+ dev = [
93
+ { name = "black" },
94
+ { name = "pre-commit" },
95
+ { name = "ruff" },
96
+ { name = "vulture" },
97
+ ]
98
+
99
+ [package.metadata]
100
+
101
+ [package.metadata.requires-dev]
102
+ dev = [
103
+ { name = "black", specifier = ">=25.1.0" },
104
+ { name = "pre-commit", specifier = ">=4.2.0" },
105
+ { name = "ruff", specifier = ">=0.12.5" },
106
+ { name = "vulture", specifier = ">=2.14" },
107
+ ]
108
+
109
+ [[package]]
110
+ name = "mypy-extensions"
111
+ version = "1.1.0"
112
+ source = { registry = "https://pypi.org/simple" }
113
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 }
114
+ wheels = [
115
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
116
+ ]
117
+
118
+ [[package]]
119
+ name = "nodeenv"
120
+ version = "1.9.1"
121
+ source = { registry = "https://pypi.org/simple" }
122
+ sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
123
+ wheels = [
124
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
125
+ ]
126
+
127
+ [[package]]
128
+ name = "packaging"
129
+ version = "25.0"
130
+ source = { registry = "https://pypi.org/simple" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
132
+ wheels = [
133
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
134
+ ]
135
+
136
+ [[package]]
137
+ name = "pathspec"
138
+ version = "0.12.1"
139
+ source = { registry = "https://pypi.org/simple" }
140
+ sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
141
+ wheels = [
142
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
143
+ ]
144
+
145
+ [[package]]
146
+ name = "platformdirs"
147
+ version = "4.4.0"
148
+ source = { registry = "https://pypi.org/simple" }
149
+ sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634 }
150
+ wheels = [
151
+ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654 },
152
+ ]
153
+
154
+ [[package]]
155
+ name = "pre-commit"
156
+ version = "4.3.0"
157
+ source = { registry = "https://pypi.org/simple" }
158
+ dependencies = [
159
+ { name = "cfgv" },
160
+ { name = "identify" },
161
+ { name = "nodeenv" },
162
+ { name = "pyyaml" },
163
+ { name = "virtualenv" },
164
+ ]
165
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792 }
166
+ wheels = [
167
+ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965 },
168
+ ]
169
+
170
+ [[package]]
171
+ name = "pyyaml"
172
+ version = "6.0.2"
173
+ source = { registry = "https://pypi.org/simple" }
174
+ sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
175
+ wheels = [
176
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
177
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
178
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
179
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
180
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
181
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
182
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
183
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
184
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
185
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
186
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
187
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
188
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
189
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
190
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
191
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
192
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
193
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
194
+ ]
195
+
196
+ [[package]]
197
+ name = "ruff"
198
+ version = "0.12.12"
199
+ source = { registry = "https://pypi.org/simple" }
200
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915 }
201
+ wheels = [
202
+ { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602 },
203
+ { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393 },
204
+ { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967 },
205
+ { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038 },
206
+ { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110 },
207
+ { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352 },
208
+ { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365 },
209
+ { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812 },
210
+ { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208 },
211
+ { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444 },
212
+ { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474 },
213
+ { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204 },
214
+ { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347 },
215
+ { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844 },
216
+ { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687 },
217
+ { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870 },
218
+ { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016 },
219
+ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762 },
220
+ ]
221
+
222
+ [[package]]
223
+ name = "virtualenv"
224
+ version = "20.34.0"
225
+ source = { registry = "https://pypi.org/simple" }
226
+ dependencies = [
227
+ { name = "distlib" },
228
+ { name = "filelock" },
229
+ { name = "platformdirs" },
230
+ ]
231
+ sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808 }
232
+ wheels = [
233
+ { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279 },
234
+ ]
235
+
236
+ [[package]]
237
+ name = "vulture"
238
+ version = "2.14"
239
+ source = { registry = "https://pypi.org/simple" }
240
+ sdist = { url = "https://files.pythonhosted.org/packages/8e/25/925f35db758a0f9199113aaf61d703de891676b082bd7cf73ea01d6000f7/vulture-2.14.tar.gz", hash = "sha256:cb8277902a1138deeab796ec5bef7076a6e0248ca3607a3f3dee0b6d9e9b8415", size = 58823 }
241
+ wheels = [
242
+ { url = "https://files.pythonhosted.org/packages/a0/56/0cc15b8ff2613c1d5c3dc1f3f576ede1c43868c1bc2e5ccaa2d4bcd7974d/vulture-2.14-py2.py3-none-any.whl", hash = "sha256:d9a90dba89607489548a49d557f8bac8112bd25d3cbc8aeef23e860811bd5ed9", size = 28915 },
243
+ ]