feat: improve metadata

This commit is contained in:
2026-08-04 10:22:33 +08:00
parent 6f1c5312e0
commit 10a33f8921
5 changed files with 185 additions and 126 deletions
-1
View File
@@ -10,7 +10,6 @@ requires-python = ">=3.13"
dependencies = [
"jinja2==3.1.6",
"semver>=3.0.4",
"tomli>=2.4.1",
]
[project.scripts]
+32 -3
View File
@@ -1,8 +1,25 @@
import logging
from pathlib import Path
from semver import Version
from . import cli
from .archiver import Archiver
from .metadata import MetadataExtractor
from .renders.cmake import CMakeProperties, CMakeRender
from .renders.pkgconfig import PkgConfigProperties, PkgConfigRender
def build_cmake_render(extractor: MetadataExtractor) -> CMakeRender:
properties = CMakeProperties(
"wfassoc", "wfassoc", "wfassoc.dll", "wfassoc.lib", Version(1, 0, 0)
)
return CMakeRender(properties)
def build_pkgconfig_render(extractor: MetadataExtractor) -> PkgConfigRender:
properties = PkgConfigProperties(
"wfassoc", "wfassoc C/C++ FFI", "wfassoc", Version(1, 0, 0)
)
return PkgConfigRender(properties)
def main() -> None:
@@ -11,10 +28,22 @@ def main() -> None:
# setup logging
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
# build metadata
metadata = MetadataExtractor(opts.manifest)
# build metadata extractor
extractor = MetadataExtractor(opts.manifest)
# extract omrf metadata
metadata = extractor.get_metadata()
# create renders and their properties from metadata
cmake_render = extractor
# create distribution
with Archiver(opts.dist_dir, opts.dist_zip) as archiver:
pass
archiver.push_dir(Path("bin"))
archiver.push_dir(Path("include"))
archiver.push_dir(Path("lib"))
archiver.push_text(cmake_render.render_config(), Path("lib", "cmake", "wfassoc", "wfassocConfig.cmake"))
archiver.push_text(cmake_render.render_config_version(), Path("lib", "cmake", "wfassoc", "wfassocConfigVersion.cmake"))
archiver.push_text(pkgconfig_render.render(), Path("lib", "pkgconfig", "wfassoc.pc"))
+118 -83
View File
@@ -2,19 +2,12 @@ import json
import os
import subprocess
from dataclasses import dataclass
from functools import wraps
from pathlib import Path
from typing import Any
import tomli
from typing import Any, Callable
from semver import Version
# YYC MARK:
# We use `tomli` with at least 2.4.0 version by design.
# Considering that Cargo has approve the change allowing TOML 1.1 syntax in `Cargo.toml`,
# Python embedded `toml` package, which only support TOML 1.0 syntax until Python 3.15,
# is not suit for parsing `Cargo.toml` in future.
# So we use the upstream of official `toml` package, i.e. `tomli` as our solution.
# And according to the document if `tomli`, the version starting support TOML 1.1 syntax is 2.4.0.
from . import utils
from .utils import dict_chain_get, dict_typed_get, dict_typed_get_required
@dataclass(frozen=True)
@@ -24,19 +17,14 @@ class MetadataHeader:
def __post_init__(self) -> None:
if self.from_path == "":
raise ValueError("header asset 'from' must not be empty")
raise ValueError("header 'from' must not be empty")
@staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataHeader":
from_path = d.get("from")
if from_path is None:
raise ValueError("header asset is missing required key 'from'")
if not isinstance(from_path, str):
raise TypeError("header asset 'from' must be a string")
to_path = d.get("to", "")
if not isinstance(to_path, str):
raise TypeError("header asset 'to' must be a string")
return MetadataHeader(from_path=from_path, to_path=to_path)
return MetadataHeader(
from_path=dict_typed_get_required(d, "from", str),
to_path=dict_typed_get(d, "to", str, ""),
)
@dataclass(frozen=True)
@@ -46,13 +34,10 @@ class MetadataCMake:
@staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataCMake":
namespace_name = d.get("namespace_name")
if namespace_name is not None and not isinstance(namespace_name, str):
raise TypeError("cmake 'namespace_name' must be a string or None")
target_name = d.get("target_name")
if target_name is not None and not isinstance(target_name, str):
raise TypeError("cmake 'target_name' must be a string or None")
return MetadataCMake(namespace_name=namespace_name, target_name=target_name)
return MetadataCMake(
namespace_name=dict_typed_get(d, "namespace_name", str),
target_name=dict_typed_get(d, "target_name", str),
)
@dataclass(frozen=True)
@@ -62,13 +47,10 @@ class MetadataPkgConfig:
@staticmethod
def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig":
name = d.get("name")
if name is not None and not isinstance(name, str):
raise TypeError("pkgconfig 'name' must be a string or None")
description = d.get("description")
if description is not None and not isinstance(description, str):
raise TypeError("pkgconfig 'description' must be a string or None")
return MetadataPkgConfig(name=name, description=description)
return MetadataPkgConfig(
name=dict_typed_get(d, "name", str),
description=dict_typed_get(d, "description", str),
)
@dataclass(frozen=True)
@@ -80,24 +62,13 @@ class Metadata:
@staticmethod
def from_dict(d: dict[str, Any]) -> "Metadata":
raw_min_version = d.get("min_version")
raw_min_version = dict_typed_get(d, "min_version", str)
if raw_min_version is not None:
if not isinstance(raw_min_version, str):
raise TypeError("min_version must be a string")
try:
min_version = Version.parse(raw_min_version)
except ValueError as e:
raise ValueError(f"invalid min_version {raw_min_version!r}: {e}") from e
min_version = utils.parse_version(raw_min_version)
else:
min_version = None
raw_headers = d.get("headers")
if raw_headers is None:
raise ValueError(
"headers is required (use an empty list to distribute no headers)"
)
if not isinstance(raw_headers, list):
raise TypeError("headers must be a list")
raw_headers = dict_typed_get_required(d, "headers", list)
headers_list: list[MetadataHeader] = []
for i, item in enumerate(raw_headers):
if not isinstance(item, dict):
@@ -105,21 +76,15 @@ class Metadata:
headers_list.append(MetadataHeader.from_dict(item))
headers = tuple(headers_list)
raw_cmake = d.get("cmake")
if raw_cmake is not None:
if not isinstance(raw_cmake, dict):
raise TypeError("cmake must be a table")
cmake = MetadataCMake.from_dict(raw_cmake)
else:
cmake = None
raw_cmake = dict_typed_get(d, "cmake", dict)
cmake = MetadataCMake.from_dict(raw_cmake) if raw_cmake is not None else None
raw_pkgconfig = d.get("pkgconfig")
if raw_pkgconfig is not None:
if not isinstance(raw_pkgconfig, dict):
raise TypeError("pkgconfig must be a table")
pkgconfig = MetadataPkgConfig.from_dict(raw_pkgconfig)
else:
pkgconfig = None
raw_pkgconfig = dict_typed_get(d, "pkgconfig", dict)
pkgconfig = (
MetadataPkgConfig.from_dict(raw_pkgconfig)
if raw_pkgconfig is not None
else None
)
return Metadata(
min_version=min_version,
@@ -129,29 +94,36 @@ class Metadata:
)
def wrap_metadata_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
try:
return func(*args, **kwargs)
except Exception as e:
raise RuntimeError(f"error occurs when fetching metadata: {e}") from e
return wrapper
class MetadataExtractor:
__cargo_toml_path: Path
__cargo_metadata: dict[str, Any]
__cargo_toml: dict[str, Any]
__metadata: dict[str, Any]
"""The direct output of ``cargo metadata``"""
__metadata_package: dict[str, Any]
"""The package item in cargo metadata's packages list pointing to user request package"""
__metadata_target: dict[str, Any]
"""The target item in package item's targets list pointing to the main target"""
def __init__(self, cargo_toml: Path) -> None:
self.__cargo_toml_path = cargo_toml
self.__cargo_toml = MetadataExtractor.__extract_cargo_toml(cargo_toml)
self.__cargo_metadata = MetadataExtractor.__extract_cargo_metadata(cargo_toml)
def extract_omrf_metadata(self) -> Metadata:
omrf = self.__cargo_toml.get("package", {}).get("metadata", {}).get("omrf")
if not isinstance(omrf, dict):
raise ValueError("[package.metadata.omrf] is required")
return Metadata.from_dict(omrf)
def __init__(self, cargo_toml_path: Path) -> None:
self.__metadata = MetadataExtractor.__extract_metadata(cargo_toml_path)
self.__metadata_package = MetadataExtractor.__extract_metadata_package(
self.__metadata, cargo_toml_path
)
self.__metadata_target = MetadataExtractor.__extract_metadata_target(
self.__metadata_package, cargo_toml_path
)
@staticmethod
def __extract_cargo_toml(cargo_toml: Path) -> dict[str, Any]:
with open(cargo_toml, "rb") as f:
return tomli.load(f)
@staticmethod
def __extract_cargo_metadata(cargo_toml: Path) -> dict[str, Any]:
@wrap_metadata_errors
def __extract_metadata(cargo_toml_path: Path) -> dict[str, Any]:
cargo_bin = os.getenv("OMRF_PACKER_CARGO", "cargo")
cmd = [
cargo_bin,
@@ -160,7 +132,7 @@ class MetadataExtractor:
"--format-version",
"1",
"--manifest-path",
str(cargo_toml),
str(cargo_toml_path),
]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
@@ -176,3 +148,66 @@ class MetadataExtractor:
)
else:
return json.loads(stdout)
@staticmethod
@wrap_metadata_errors
def __extract_metadata_package(
cargo_metadata: dict[str, Any], cargo_toml_path: Path
) -> dict[str, Any]:
packages: list[Any] = dict_typed_get_required(cargo_metadata, "packages", list)
for i, package in enumerate(packages):
if not isinstance(package, dict):
raise TypeError(f"packages[{i}] must be a table")
raw_manifest_path = dict_typed_get_required(package, "manifest_path", str)
manifest_path = Path(raw_manifest_path)
if manifest_path == cargo_toml_path:
return package
raise RuntimeError("can not find user given package in metadata")
@staticmethod
@wrap_metadata_errors
def __extract_metadata_target(
cargo_package: dict[str, Any], cargo_toml_path: Path
) -> dict[str, Any]:
# build the path to lib.rs for comparing
librs = cargo_toml_path.parent / "src" / "lib.rs"
# start checking
targets: list[Any] = dict_typed_get_required(cargo_package, "targets", list)
for i, target in enumerate(targets):
if not isinstance(target, dict):
raise TypeError(f"targets[{i}] must be a table")
raw_src_path = dict_typed_get_required(target, "src_path", str)
src_path = Path(raw_src_path)
if src_path == librs:
return target
raise RuntimeError(
"can not find the main target of user given package in metadata"
)
@wrap_metadata_errors
def get_target_directory(self) -> str:
return dict_typed_get_required(self.__metadata, "target_directory", str)
@wrap_metadata_errors
def get_name(self) -> str:
return dict_typed_get_required(self.__metadata_package, "name", str)
@wrap_metadata_errors
def get_description(self) -> str | None:
return dict_typed_get(self.__metadata_package, "description", str)
@wrap_metadata_errors
def get_version(self) -> Version:
raw_version = dict_typed_get_required(self.__metadata_package, "version", str)
return utils.parse_version(raw_version)
@wrap_metadata_errors
def get_metadata(self) -> Metadata:
omrf = dict_chain_get(self.__metadata_package, "metadata", "omrf")
return Metadata.from_dict(omrf)
@wrap_metadata_errors
def get_target_name(self) -> str:
return dict_typed_get_required(self.__metadata_target, "name", str)
+35 -1
View File
@@ -1,16 +1,50 @@
from pathlib import Path
from typing import Any, cast, overload
from semver import Version
VERSION: Version = Version(1, 0, 0)
"""The current version of sarasacw-omrf-packer"""
def parse_version(vs: str) -> Version:
"""This package specific version parser which explicit only support x.x.x style version."""
v = Version.parse(vs)
if v.prerelease is not None or v.build is not None:
raise ValueError("prerelease and build component of version is not supported")
else:
return v
def get_root_dir() -> Path:
return Path(__file__).resolve().parent
def get_templates_dir() -> Path:
return get_root_dir() / 'templates'
return get_root_dir() / "templates"
@overload
def dict_typed_get[T](d: dict[str, Any], key: str, ty: type[T]) -> T | None: ...
@overload
def dict_typed_get[T, D](d: dict[str, Any], key: str, ty: type[T], default: D) -> T | D: ...
def dict_typed_get[T](d: dict[str, Any], key: str, ty: type[T], default: Any = None) -> Any:
tmp = d.get(key, None)
if tmp is None:
return default
if not isinstance(tmp, ty):
raise TypeError(f'the value of key "{key}" is not a {ty.__name__}')
return tmp
def dict_typed_get_required[T](d: dict[str, Any], key: str, ty: type[T]) -> T:
result = dict_typed_get(d, key, ty)
if result is None:
raise ValueError(f'can not find key "{key}" in given dictionary')
return result
def dict_chain_get(d: dict[str, Any], *args: str) -> dict[str, Any]:
for arg in args:
d = dict_typed_get_required(d, arg, dict)
return d
-38
View File
@@ -73,14 +73,12 @@ source = { editable = "." }
dependencies = [
{ name = "jinja2" },
{ name = "semver" },
{ name = "tomli" },
]
[package.metadata]
requires-dist = [
{ name = "jinja2", specifier = "==3.1.6" },
{ name = "semver", specifier = ">=3.0.4" },
{ name = "tomli", specifier = ">=2.4.1" },
]
[[package]]
@@ -91,39 +89,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]