diff --git a/packer/pyproject.toml b/packer/pyproject.toml index 0840278..3facf5e 100644 --- a/packer/pyproject.toml +++ b/packer/pyproject.toml @@ -10,7 +10,6 @@ requires-python = ">=3.13" dependencies = [ "jinja2==3.1.6", "semver>=3.0.4", - "tomli>=2.4.1", ] [project.scripts] diff --git a/packer/src/sarasacw_omrf_packer/__init__.py b/packer/src/sarasacw_omrf_packer/__init__.py index f1ce012..5acf234 100644 --- a/packer/src/sarasacw_omrf_packer/__init__.py +++ b/packer/src/sarasacw_omrf_packer/__init__.py @@ -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")) diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index fdd1f32..ef854f5 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -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) diff --git a/packer/src/sarasacw_omrf_packer/utils.py b/packer/src/sarasacw_omrf_packer/utils.py index c092ab2..99a294a 100644 --- a/packer/src/sarasacw_omrf_packer/utils.py +++ b/packer/src/sarasacw_omrf_packer/utils.py @@ -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 diff --git a/packer/uv.lock b/packer/uv.lock index 296a4dd..d780305 100644 --- a/packer/uv.lock +++ b/packer/uv.lock @@ -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" }, -]