Add basic inflator cython support

This commit is contained in:
2025-08-26 17:34:14 +03:00
parent 36e343d3bc
commit 0795a5f8bb
5 changed files with 346 additions and 16 deletions

View File

@@ -8,8 +8,10 @@ authors = [
license = "LGPL-3.0-or-later"
requires-python = ">=3.13"
dependencies = [
"cython>=3.1.3",
"hatchling>=1.27.0",
"jinja2>=3.1.6",
"setuptools>=80.9.0",
]
[build-system]

View File

@@ -5,18 +5,27 @@ from typing import Optional
from typing import TypedDict
import megasniff.exceptions
from megasniff.python_to_cython import python_obj_to_cython
from . import SchemaInflatorGenerator
@dataclass(frozen=True)
@dataclass
class ASchema:
a: int
a: int | None
b: float | str
bs: Optional[BSchema]
d: int
c: float = 1.1
def __init__(self, a: int | None, b: float | str, bs: Optional[BSchema], c: float = 1.1):
self.a = a
self.b = b
self.bs = bs
self.c = c
self.d = a or 0
class BSchema(TypedDict):
@dataclass
class BSchema:
a: int
b: str
c: float
@@ -27,10 +36,23 @@ class BSchema(TypedDict):
class CSchema:
l: set[int | ASchema]
@dataclass
class SomeData:
a: int
b: float
c: str
def main():
# ccode = python_obj_to_cython(ASchema)
# print(ccode)
# exit(0)
# infl = SchemaInflatorGenerator(strict_mode=True)
# fn = infl.schema_to_inflator(SomeData)
# print(fn({'a': 1, 'b': 1.1, 'c': 'asdf'}))
infl = SchemaInflatorGenerator(strict_mode=True)
fn = infl.schema_to_inflator(ASchema)
# exit(0)
# print(t)
# print(n)
# exec(t, n)
@@ -40,7 +62,8 @@ def main():
# d = {'a': 1, 'b': 1, 'c': 0, 'bs': {'a': 1, 'b': 2, 'c': 3, 'd': {'a': 1, 'b': 2.1, 'bs': None}}}
# d = {'a': 2, 'b': 2, 'bs': {'a': 2, 'b': 'a', 'c': 0, 'd': {'a': 2, 'b': 2}}}
# d = {'l': ['1', {'a': 42, 'b': 1}]}
d = {'a': 2, 'b': '2', 'bs': None}
# d = {'a': None, 'b': '2', 'bs': None}
d = {'a': None, 'b': '2', 'bs': {'a': 1, 'b': 'a', 'c': 1.1, 'd': {'a': 1, 'b': '', 'bs': None}}}
try:
o = fn(d)
print(o)

View File

@@ -2,11 +2,18 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from __future__ import annotations
import collections.abc
import hashlib
import importlib.resources
import importlib.util
import os
import subprocess
import sys
import tempfile
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass
from types import NoneType, UnionType
from pathlib import Path
from types import NoneType, UnionType, ModuleType
from typing import Optional, get_origin, get_args, Union, Annotated, Literal, Sequence, List, Set, TypeAliasType, \
OrderedDict
@@ -14,6 +21,7 @@ import jinja2
from . import utils
from .utils import *
import random, string
@dataclass
@@ -23,6 +31,7 @@ class TypeRenderData:
is_list: bool
is_union: bool
is_strict: bool
ctype: str
@dataclass
@@ -48,6 +57,7 @@ class FieldRenderData:
is_optional: bool
allow_none: bool
default_option: Optional[str]
ctype: str
def __init__(self,
argname: str,
@@ -55,7 +65,8 @@ class FieldRenderData:
typename: str,
is_optional: bool,
allow_none: bool,
default_option: Optional[str]):
default_option: Optional[str],
ctype: str):
self.argname = argname
self.constrs = constrs
self.typename = typename
@@ -63,6 +74,221 @@ class FieldRenderData:
self.allow_none = allow_none
self.default_option = default_option
self.argname_escaped = _escape_python_name(argname)
self.ctype = ctype
def randomword(length):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(length))
def exec_cython(txt: str, namespace: dict, name: str):
"""
Drop-in замена exec(txt, namespace), но через cython.
Возвращает callable объект из namespace['inflator'].
"""
# генерируем уникальное имя для модуля
h = hashlib.sha256(txt.encode() + str(sorted(namespace.keys())).encode()).hexdigest()[:16]
modname = f"_cyexec_{h}"
build_dir = tempfile.mkdtemp(prefix="cyexec_")
pyx_file = os.path.join(build_dir, f"{modname}.pyx")
# соберём код для .pyx
# сначала экспортируем namespace
export_lines = []
for k, v in namespace.items():
if k not in {'int', 'float', 'str'}:
export_lines.append(f"{k} = __ns__['{k}']")
pyx_code = f"""
# cython: language_level=3
# cython: boundscheck=False, wraparound=False, nonecheck=False
# AUTO-GENERATED
# Вставляем runtime namespace
import builtins
__ns__ = builtins.__dict__['_cyexec_ns']
cdef class NullableInt:
cdef long value
cdef bint has
cpdef set(self, long value):
self.value = value
self.has = 1
cpdef unset(self):
self.has = 0
cdef class NullableDouble:
cdef double value
cdef bint has
cpdef set(self, double value):
self.value = value
self.has = 1
cpdef unset(self):
self.has = 0
{os.linesep.join(export_lines)}
# пользовательский код
{txt}
"""
# пишем файл
with open(pyx_file, "w") as f:
f.write(pyx_code)
# нужно сохранить namespace в builtins, чтобы cython его видел
import builtins
builtins._cyexec_ns = namespace
# компилируем через cythonize
setup_code = f"""
from setuptools import setup
from Cython.Build import cythonize
setup(
name="{modname}",
ext_modules=cythonize("{pyx_file}", compiler_directives={{"language_level": "3"}}),
script_args=["build_ext", "--inplace"],
)
"""
setup_file = os.path.join(build_dir, "setup.py")
with open(setup_file, "w") as f:
f.write(setup_code)
subprocess.check_call([sys.executable, setup_file, "build_ext", "--inplace"], cwd=build_dir)
# находим .so файл
for fn in os.listdir(build_dir):
if fn.startswith(modname) and fn.endswith((".so", ".pyd")):
so_path = os.path.join(build_dir, fn)
break
else:
raise RuntimeError("Cython build failed, no .so produced")
# импортим как модуль
spec = importlib.util.spec_from_file_location(modname, so_path)
mod = importlib.util.module_from_spec(spec)
sys.modules[modname] = mod
spec.loader.exec_module(mod)
# чистим временный namespace в builtins
del builtins._cyexec_ns
return getattr(mod, name)
@dataclass
class BasicTypeVariationTest:
index: int
basic_type: str
@dataclass
class ObjectTypeVariationTest:
index: int
fields_contains: list[tuple[str, TypeVariationTest | None]]
@dataclass
class TypeVariationTest:
types: list[TypeConstructionSchema]
basic_tests: list[BasicTypeVariationTest]
object_tests: list[ObjectTypeVariationTest]
@dataclass
class TypeConstructionSchema:
tp: type
allow_none: bool
kwargs: Optional[dict[str, tuple[str, list[TypeConstructionSchema] | type]]]
@property
def typed_key_pairs(self) -> set[str]:
ret = set()
for k, (_, v) in self.kwargs.items():
if isinstance(v, type):
ret.add(f'{k}:{v}')
else:
if not isinstance(v, list):
v = [v]
for _v in v:
ret.add(f'{k}:{_v.tp}')
return ret
class Z1:
a: int
b: int
class Z2:
c: int
d: int
class Z3:
e: int
f: int
class A:
a: int
b: Z1 | Z3
class B:
a: int
b: Z2
class C:
z1: Z1
z2: Z2
_ = A | B | C
def find_types_variations(types: list[TypeConstructionSchema]) -> TypeVariationTest:
basic_tests = []
object_tests = []
for i, tp in enumerate(types):
if tp.kwargs is None:
basic_tests.append(BasicTypeVariationTest(i, _type_to_ctype(tp.tp, tp.allow_none)))
for i, tp in enumerate(types):
if tp.kwargs is not None:
tp_keys = []
uniq_keys = set()
if i < len(types):
keys = set()
for t in types[i + 1:]:
keys |= set((t.kwargs or {}).keys())
obj_keys = set(tp.kwargs.keys())
uniq_keys = (obj_keys ^ keys) & obj_keys
if len(uniq_keys) > 0:
k = list(uniq_keys)[0]
object_tests.append(ObjectTypeVariationTest(i, [(k, None)]))
else:
pass
return TypeVariationTest(types, basic_tests, object_tests)
def _type_to_ctype(t: type, allow_none: bool) -> str:
if t is int:
return 'NullableInt' if allow_none else 'long'
if t is float:
return 'NullableFloat' if allow_none else 'double'
return 'object'
class SchemaInflatorGenerator:
@@ -100,18 +326,23 @@ class SchemaInflatorGenerator:
strict_mode_override: Optional[bool] = None,
from_type_override: Optional[type | TypeAliasType] = None
) -> Callable[[dict[str, Any]], Any]:
name = 'inflate'
if isinstance(schema, type):
name = f'inflate_{schema.__name__}'
if from_type_override is not None and '__getitem__' not in dir(from_type_override):
raise RuntimeError('from_type_override must provide __getitem__')
txt, namespace = self._schema_to_inflator(schema,
_funcname='inflate',
_funcname=name,
strict_mode_override=strict_mode_override,
from_type_override=from_type_override,
)
imports = ('from typing import Any\n'
'from megasniff.exceptions import MissingFieldException, FieldValidationException\n')
txt = imports + '\n' + txt
exec(txt, namespace)
fn = namespace['inflate']
fn = exec_cython(txt, namespace, name)
# fn = exec_numba(txt, namespace, func_name=name)
# exec(txt, namespace)
# fn = namespace[name]
if self._store_sources:
setattr(fn, '__megasniff_sources__', txt)
return fn
@@ -132,10 +363,11 @@ class SchemaInflatorGenerator:
if is_union:
typerefs = list(map(lambda x: self._unwrap_typeref(x, strict_mode), argtypes))
return TypeRenderData(typerefs, allow_none, False, True, False)
return TypeRenderData(typerefs, allow_none, False, True, False, 'object')
elif type_origin in [list, set]:
rd = self._unwrap_typeref(argtypes[0], strict_mode)
return IterableTypeRenderData(rd, allow_none, True, False, False, type_origin.__name__)
return IterableTypeRenderData(rd, allow_none, True, False, False, type_origin.__name__,
'NullableList' if allow_none else 'list')
else:
t = argtypes[0]
@@ -148,7 +380,8 @@ class SchemaInflatorGenerator:
allow_none,
is_list,
False,
strict_mode if is_builtin else False)
strict_mode if is_builtin else False,
_type_to_ctype(t, allow_none))
def _schema_to_inflator(self,
schema: type | Sequence[TupleSchemaItem | tuple[str, type]] | OrderedDict[str, type],
@@ -243,6 +476,7 @@ class SchemaInflatorGenerator:
has_default,
allow_none,
default_option if not isinstance(default_option, str) else f"'{default_option}'",
typeref.ctype
)
)

View File

@@ -2,16 +2,45 @@
{% import "unwrap_type_data.jinja2" as unwrap_type_data %}
def {{funcname}}(from_data: {% if from_type is none %}dict[str, Any]{% else %}{{from_type}}{% endif %}) {% if tgt_type is not none %} -> {{tgt_type}} {% endif %}:
{% macro render_setter(argname, argval) -%}
{%- set out -%}
{% if argname.startswith('Nullable') %}
{% if argval == 'None' %}
{{argname}}.unset()
{% else %}
{{argname}}.set({{argval}})
{%endif%}
{% else %}
{{argname}} = {{argval}}
{% endif %}
{%- endset %}
{{out}}
{%- endmacro %}
{% macro check_null(argname) -%}
{%- set out -%}
{% if argname.startswith('Nullable') %}
if {{argname}}.has:
{% else %}
if {{argname}} is None:
{% endif %}
{%- endset %}
{{out}}
{%- endmacro %}
cpdef object {{funcname}}(dict from_data):
"""
{{tgt_type}}
"""
from_data_keys = from_data.keys()
cdef object conv_data
{% for conv in conversions %}
cdef {{conv.argctype}} {{conv.argname_escaped}}
if '{{conv.argname}}' not in from_data_keys:
{% if conv.is_optional %}
{{conv.argname_escaped}} = {{conv.default_option}}
{{ render_setter(conv.argname_escaped, conv.default_option) | indent(4*2) }}
{% else %}
raise MissingFieldException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}")
{% endif %}
@@ -21,7 +50,7 @@ def {{funcname}}(from_data: {% if from_type is none %}dict[str, Any]{% else %}{{
{% if not conv.allow_none %}
raise FieldValidationException('{{conv.argname}}', "{{conv.typename | replace('"', "'")}}", conv_data)
{% else %}
{{conv.argname_escaped}} = None
{{ render_setter(conv.argname_escaped, 'None') | indent(4*3) }}
{% endif %}
else:

44
uv.lock generated
View File

@@ -42,6 +42,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" },
]
[[package]]
name = "cython"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/ab/915337fb39ab4f4539a313df38fc69938df3bf14141b90d61dfd5c2919de/cython-3.1.3.tar.gz", hash = "sha256:10ee785e42328924b78f75a74f66a813cb956b4a9bc91c44816d089d5934c089", size = 3186689, upload-time = "2025-08-13T06:19:13.619Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/93/0e5dfcc6215a6c2cae509d7e40f8fb197237ba5998c936e9c19692f8eedf/cython-3.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9458d540ef0853ea4fc65b8a946587bd483ef7244b470b3d93424eb7b04edeb1", size = 2998232, upload-time = "2025-08-13T06:20:35.817Z" },
{ url = "https://files.pythonhosted.org/packages/6b/6c/01b22de45e3a9b86fbe4a18cd470146514209448cb4d3d3ba9c72390d45b/cython-3.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:32d1b22c3b231326e9f16480a7f508c6841bbf7d0615c2d6f489ebc72dd05205", size = 2830052, upload-time = "2025-08-13T06:20:37.71Z" },
{ url = "https://files.pythonhosted.org/packages/52/08/a7d4b91b144b4bd015e932303861061cd43221f737ecdc6e380a438f245f/cython-3.1.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4c7e0b8584b02a349952de7d7d47f89c97cbf3fee74962e89e3caa78139ec84", size = 3359478, upload-time = "2025-08-13T06:20:39.811Z" },
{ url = "https://files.pythonhosted.org/packages/f5/7d/b44ee735439ee73a88c6532536cfbc5b2f146c5f315effa124e85aadb447/cython-3.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9178f0c06f4bc92372dc44e3867e9285bebd556953e47857c26b389aabe2828", size = 3155157, upload-time = "2025-08-13T06:20:42.305Z" },
{ url = "https://files.pythonhosted.org/packages/a8/e0/ef1a44ba765057b04e99cf34dcc1910706a666ea66fcd2b92175ab645416/cython-3.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4da2e624d381e9790152672bfc599a5fb4b823b99d82700a10f5db3311851f9", size = 3305331, upload-time = "2025-08-13T06:20:44.423Z" },
{ url = "https://files.pythonhosted.org/packages/62/f1/8bf3ea5babdef82df3023e72522c71bfc5cc5091e9710828a0dda81bda88/cython-3.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:680c9168882c5e8031dd31df199b9a5ee897e95136d15f8c6454b62162ede25e", size = 3171968, upload-time = "2025-08-13T06:20:48.962Z" },
{ url = "https://files.pythonhosted.org/packages/b5/c3/c1383f987d3add9cb8655943f6a0f164bfd06951f28e51b7887d12c8716a/cython-3.1.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:833cd0fdba9210d2f1f29e097579565a296d7ff567fd63e8cf5fde4c14339f4f", size = 3372840, upload-time = "2025-08-13T06:20:51.495Z" },
{ url = "https://files.pythonhosted.org/packages/71/d5/02fb7454756cb31b0c044050ee563ac172314aa8e74e5a4dd73bf77041d3/cython-3.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c04367fa0e6c35b199eb51d64b5e185584b810f6c2b96726ce450300faf99686", size = 3317912, upload-time = "2025-08-13T06:20:53.461Z" },
{ url = "https://files.pythonhosted.org/packages/91/62/b96227adf45236952f7cf07f869ff4157b82fe25ff7bb5ba9a3037c98993/cython-3.1.3-cp313-cp313-win32.whl", hash = "sha256:f02ef2bf72a576bf541534c704971b8901616db431bc46d368eed1d6b20aaa1e", size = 2479889, upload-time = "2025-08-13T06:20:55.437Z" },
{ url = "https://files.pythonhosted.org/packages/74/09/100c0727d0fc8e4d7134c44c12b8c623e40f309401af56b7f6faf795c4bb/cython-3.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:00264cafcc451dcefc01eaf29ed5ec150fb73af21d4d21105d97e9d829a53e99", size = 2701550, upload-time = "2025-08-13T06:20:57.503Z" },
{ url = "https://files.pythonhosted.org/packages/23/0e/6e535f2eedf0ddc3c84b087e5d0f04a7b88d8229ec8c27be41a142bcbbfa/cython-3.1.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62b0a9514b68391aae9784405b65738bbe19cdead3dd7b90dd9e963281db1ee3", size = 2995613, upload-time = "2025-08-13T06:20:59.408Z" },
{ url = "https://files.pythonhosted.org/packages/77/10/3c9e2abf315f608bc22f49b6f9ee66859c23e07edbf484522d5f27b61ab7/cython-3.1.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:976db373c315f342dcb24cd65b5e4c08d2c7b42f9f6ac1b3f677eb2abc9bfb0f", size = 2841282, upload-time = "2025-08-13T06:21:01.274Z" },
{ url = "https://files.pythonhosted.org/packages/cd/77/04e39af308d5716640bc638e7d90d8be34277ebc642ea5bda5ac09628215/cython-3.1.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e765c12a02dea0bd968cf1e85af77be1dc6d21909c3fbf5bd81815a7cdd4a65e", size = 3361624, upload-time = "2025-08-13T06:21:03.418Z" },
{ url = "https://files.pythonhosted.org/packages/75/f4/bdbc989ad88401e03ffe17e0bc3a03e3fe5dccbeb9c90e8762d7da4c7a45/cython-3.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097374fa1370e9967e48442a41a0acbebb94fe9d63976cad31eacd38424847bf", size = 3194014, upload-time = "2025-08-13T06:21:05.719Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c8/9f282e5d31280f3912199b638c71557062443608eb3909a562283eda376d/cython-3.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d8fda4d62b693e62992c665a688e3a220be70958c48eb4c2634093c9998156", size = 3309703, upload-time = "2025-08-13T06:21:08.026Z" },
{ url = "https://files.pythonhosted.org/packages/0a/09/83416a454a575e3ea7e84ec138f0b6dbfb34de28de4968359d7fdb428028/cython-3.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:da23fa5082940ae1eed487ee9b7c1da7015b53f9feffeee661f4ee57f696dcd5", size = 3210317, upload-time = "2025-08-13T06:21:10.92Z" },
{ url = "https://files.pythonhosted.org/packages/8f/dc/901ed74302d52105588c59a41a239ef6bd01ff708391a15938aba9670b9e/cython-3.1.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8880daa7a0ddf971593f24da161c976bc1bea895393fdfebb8e54269321d9d2b", size = 3378211, upload-time = "2025-08-13T06:21:13.067Z" },
{ url = "https://files.pythonhosted.org/packages/b7/6d/1e077b99a678b69a39bfe96e1888bcf6c868830220e635f862a44c7761b4/cython-3.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20d6b5a9fc210d3bc2880413011f606e1208e12ee6efc74717445a63f9795af1", size = 3321051, upload-time = "2025-08-13T06:21:17.314Z" },
{ url = "https://files.pythonhosted.org/packages/00/cd/2c442e9e41eafa851d89af1f62720007e03a12e1c01d9a71ed75f550a6c5/cython-3.1.3-cp314-cp314-win32.whl", hash = "sha256:3b2243fed3eeb129dedf2cebbe3be0d9b02fbf3bc75b387aafd54aac3950baa6", size = 2502067, upload-time = "2025-08-13T06:21:19.404Z" },
{ url = "https://files.pythonhosted.org/packages/ae/63/7a1f2f06331f7dcf3fd31721fdaa8b60762748b82395631c0324672a4f2b/cython-3.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:d32792c80b1fa8be9de207ec8844d49c4d1d0d60e5136d20f344729270db6490", size = 2733427, upload-time = "2025-08-13T06:21:21.525Z" },
{ url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" },
]
[[package]]
name = "hatchling"
version = "1.27.0"
@@ -108,11 +137,13 @@ wheels = [
[[package]]
name = "megasniff"
version = "0.1.2"
version = "0.2.3.post2"
source = { editable = "." }
dependencies = [
{ name = "cython" },
{ name = "hatchling" },
{ name = "jinja2" },
{ name = "setuptools" },
]
[package.dev-dependencies]
@@ -123,8 +154,10 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "cython", specifier = ">=3.1.3" },
{ name = "hatchling", specifier = ">=1.27.0" },
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "setuptools", specifier = ">=80.9.0" },
]
[package.metadata.requires-dev]
@@ -199,6 +232,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "trove-classifiers"
version = "2025.5.9.12"