From 7e369a9dcb963b317c021b042bb6560a3355f30e Mon Sep 17 00:00:00 2001 From: BarbossHack Date: Sat, 6 Jun 2026 01:20:33 +0200 Subject: [PATCH] Switch to semantic resources.arsc comparison in apkdiff --- reproducible-builds/apkdiff/apkdiff.py | 152 ++++------------ reproducible-builds/apkdiff/pyproject.toml | 6 +- reproducible-builds/apkdiff/util.py | 197 --------------------- reproducible-builds/apkdiff/uv.lock | 68 ++----- 4 files changed, 57 insertions(+), 366 deletions(-) delete mode 100644 reproducible-builds/apkdiff/util.py diff --git a/reproducible-builds/apkdiff/apkdiff.py b/reproducible-builds/apkdiff/apkdiff.py index 93862c45095..f3f9ed8045c 100755 --- a/reproducible-builds/apkdiff/apkdiff.py +++ b/reproducible-builds/apkdiff/apkdiff.py @@ -1,5 +1,7 @@ #! /usr/bin/env python3 +import difflib +import subprocess import sys import re import logging @@ -8,14 +10,10 @@ import xml.etree.ElementTree as ET from dataclasses import dataclass from typing import Optional -from collections import defaultdict from androguard.core import axml from loguru import logger -from util import deep_compare, format_differences -from tqdm import tqdm - logging.getLogger("deepdiff").setLevel(logging.ERROR) logger.disable("androguard") @@ -58,8 +56,9 @@ def compare(apk1, apk2) -> bool: entry_names = compare_entry_names(zip1, zip2) entry_contents = compare_entry_contents(zip1, zip2) + resources = compare_resources_arsc(apk1, apk2) - return entry_names and entry_contents + return entry_names and entry_contents and resources def compare_entry_names(zip1: ZipFile, zip2: ZipFile) -> bool: @@ -142,8 +141,8 @@ def handle_special_cases(filename: str, bytes1: bytes, bytes2: bytes): print("Comparing AndroidManifest.xml...") return compare_android_xml(bytes1, bytes2) elif filename == "resources.arsc": - print("Comparing resources.arsc (may take a while)...") - return compare_resources_arsc(bytes1, bytes2) + # we will compare resources.arsc separately with aapt2, so we can ignore any differences here + return True elif re.match("res/xml/splits[0-9]+\\.xml", filename): print(f"Comparing {filename}...") return compare_split_xml(bytes1, bytes2) @@ -187,118 +186,45 @@ def compare_split_xml(bytes1: bytes, bytes2: bytes) -> bool: return True -def compare_resources_arsc(first_entry_bytes: bytes, second_entry_bytes: bytes) -> bool: +def compare_resources_arsc(apk1: str, apk2: str) -> bool: """ Compares two resources.arsc files. - Largely taken from https://github.com/TheTechZone/reproducible-tests/blob/d8c73772b87fbe337eb852e338238c95703d59d6/comparators/arsc_compare.py """ - first_arsc = axml.ARSCParser(first_entry_bytes) - second_arsc = axml.ARSCParser(second_entry_bytes) - - all_package_names = sorted(set(first_arsc.packages.keys()) | set(second_arsc.packages.keys())) - total_diffs = defaultdict(list) - - success = True - - for package_name in all_package_names: - # Check if package exists in both files - if package_name not in first_arsc.packages: - print(f"Package only in source file: {package_name}") - success = False - continue - - if package_name not in second_arsc.packages: - print(f"Package only in target file: {package_name}") - success = False - continue - - packages1 = first_arsc.packages[package_name] - packages2 = second_arsc.packages[package_name] - - # Check package length - if len(packages1) != len(packages2): - print(f"Package length mismatch: {len(packages1)} vs {len(packages2)}") - success = False - continue - - # Compare each package element - for i in tqdm(range(len(packages1))): - pkg1 = packages1[i] - pkg2 = packages2[i] - - if type(pkg1) is not type(pkg2): - print(f"Element type mismatch at index {i}: {type(pkg1).__name__} vs {type(pkg2).__name__}") - success = False - continue - - # Different comparison strategies based on type - if isinstance(pkg1, axml.ARSCResTablePackage): - diffs = deep_compare(pkg1, pkg2) - if diffs: - print(f"Differences in ARSCResTablePackage at index {i}:") - total_diffs["ARSCResTablePackage"].append((i, diffs)) - success = False - - elif isinstance(pkg1, axml.StringBlock): - diffs = deep_compare(pkg1, pkg2) - if diffs: - print(f"Differences in StringBlock at index {i}:") - total_diffs["StringBlock"].append((i, diffs)) - success = False - - elif isinstance(pkg1, axml.ARSCHeader): - diffs = deep_compare(pkg1, pkg2) - if diffs: - print(f"Differences in ARSCHeader at index {i}:") - total_diffs["ARSCHeader"].append((i, diffs)) - success = False - - elif isinstance(pkg1, axml.ARSCResTypeSpec): - diffs = deep_compare(pkg1, pkg2) + print("Comparing resources.arsc...") - if diffs and not all(path in ALLOWED_ARSC_DIFF_PATHS for path in diffs.keys()): - print(f"Disallowed differences in ARSCResTypeSpec at index {i}:") - print(format_differences(diffs)) - total_diffs["ARSCResTypeSpec"].append((i, diffs)) - success = False - - elif isinstance(pkg1, axml.ARSCResTableEntry): - # Use string representation for comparison - if pkg1.__repr__() != pkg2.__repr__(): - print(f"Differences in ARSCResTableEntry at index {i}") - print(f"Target: {pkg1.__repr__()}", 3) - print(f"Source: {pkg2.__repr__()}", 3) - total_diffs["ARSCResTableEntry"].append((i, {"representation": f"{pkg1.__repr__()} vs {pkg2.__repr__()}"})) - success = False - - elif isinstance(pkg1, list): - if pkg1 != pkg2: - print(f"List difference at index {i}") - total_diffs["list"].append((i, {"diff": "Lists differ"})) - success = False - - elif isinstance(pkg1, axml.ARSCResType): - diffs = deep_compare(pkg1, pkg2) - if diffs: - print(f"Differences in ARSCResType at index {i}:") - total_diffs["ARSCResType"].append((i, diffs)) - success = False - else: - # Other types - print(f"Unhandled type: {type(pkg1).__name__} at index {i}") - diffs = deep_compare(pkg1, pkg2) - if diffs: - total_diffs[type(pkg1).__name__].append((i, diffs)) - success = False - - for type_name, diffs in total_diffs.items(): - if diffs: - print(f" {type_name}: {len(diffs)}", 1) + resources1 = dump_resources(apk1) + resources2 = dump_resources(apk2) - if not success: - print("Files have differences beyond the allowed .res1 differences.") - return True + if resources1 == resources2: + return True + else: + print("resources.arsc files differ!") + diff = difflib.unified_diff( + resources1, + resources2, + fromfile=apk1, + tofile=apk2, + lineterm='' + ) + for line in diff: + print(line) + return False +def dump_resources(apk): + try: + with subprocess.Popen( + ['aapt2', 'dump', 'resources', apk], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) as process: + stdout, stderr = process.communicate() + if process.returncode != 0: + raise RuntimeError(f"aapt2 failed with error: {stderr.strip()}") + except FileNotFoundError: + raise RuntimeError("aapt2 is not installed or not in the PATH.") + + return stdout.strip().splitlines() def compare_xml(bytes1: bytes, bytes2: bytes) -> list[XmlDifference]: printer = axml.AXMLPrinter(bytes1) diff --git a/reproducible-builds/apkdiff/pyproject.toml b/reproducible-builds/apkdiff/pyproject.toml index 903acf54fee..d749e695139 100644 --- a/reproducible-builds/apkdiff/pyproject.toml +++ b/reproducible-builds/apkdiff/pyproject.toml @@ -5,9 +5,5 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "androguard", - "tqdm>=4.67.1", + "androguard>=4.1.4", ] - -[tool.uv.sources] -androguard = { git = "https://github.com/androguard/androguard", rev = "943932d35c08b8ee5102ace398882858d3bd7567" } diff --git a/reproducible-builds/apkdiff/util.py b/reproducible-builds/apkdiff/util.py deleted file mode 100644 index 8fb454267ec..00000000000 --- a/reproducible-builds/apkdiff/util.py +++ /dev/null @@ -1,197 +0,0 @@ -# Utility functions taken from https://github.com/TheTechZone/reproducible-tests/blob/d8c73772b87fbe337eb852e338238c95703d59d6/comparators/arsc_compare.py - - -def format_differences(diffs, indent=0): - """Format differences in a human-readable form""" - output = [] - indent_str = " " * indent - - for path, diff in sorted(diffs.items()): - if isinstance(diff, dict): - output.append(f"{indent_str}{path}:") - output.append(format_differences(diff, indent + 2)) - elif isinstance(diff, list): - output.append(f"{indent_str}{path}: [{', '.join(map(str, diff))}]") - else: - output.append(f"{indent_str}{path}: {diff}") - - return "\n".join(output) - - -def deep_compare( - obj1, - obj2, - path="", - max_depth=10, - current_depth=0, - exclude_attrs=None, - include_callable=False, -): - """ - Generic deep comparison of two Python objects. - - Args: - obj1: First object to compare - obj2: Second object to compare - path: Current attribute path (for nested comparisons) - max_depth: Maximum recursion depth - current_depth: Current recursion depth - exclude_attrs: List of attribute names to exclude from comparison - include_callable: Whether to include callable attributes in comparison - - Returns: - A dictionary mapping paths to differences, empty if objects are identical - """ - - if exclude_attrs is None: - exclude_attrs = set() - else: - exclude_attrs = set(exclude_attrs) - - # Add common attributes to exclude - exclude_attrs.update(["__dict__", "__weakref__", "__module__", "__doc__"]) - - differences = {} - - # Check the recursion limit - if current_depth > max_depth: - return {f"{path} [max depth reached]": "Recursion limit reached"} - - # Basic identity/equality check - if obj1 is obj2: # Same object (identity) - return {} - - if obj1 == obj2: # Equal values - return {} - - # Check for different types - if type(obj1) != type(obj2): - return {path: f"Type mismatch: {type(obj1).__name__} vs {type(obj2).__name__}"} - - # Handle None - if obj1 is None or obj2 is None: - return {path: f"{obj1} vs {obj2}"} - - # Handle primitive types - if isinstance(obj1, (int, float, str, bool, bytes, complex)): - return {path: f"{obj1} vs {obj2}"} - - # Handle sequences (list, tuple) - if isinstance(obj1, (list, tuple)): - if len(obj1) != len(obj2): - differences[f"{path}.length"] = f"{len(obj1)} vs {len(obj2)}" - - # Compare elements - for i in range(min(len(obj1), len(obj2))): - item_path = f"{path}[{i}]" - item_diffs = deep_compare( - obj1[i], - obj2[i], - item_path, - max_depth, - current_depth + 1, - exclude_attrs, - include_callable, - ) - differences.update(item_diffs) - - # Report extra elements - if len(obj1) > len(obj2): - for i in range(len(obj2), len(obj1)): - differences[f"{path}[{i}]"] = f"{obj1[i]} vs [missing]" - elif len(obj2) > len(obj1): - for i in range(len(obj1), len(obj2)): - differences[f"{path}[{i}]"] = f"[missing] vs {obj2[i]}" - - return differences - - # Handle dictionaries - if isinstance(obj1, dict): - keys1 = set(obj1.keys()) - keys2 = set(obj2.keys()) - - # Check for different keys - if keys1 != keys2: - only_in_1 = keys1 - keys2 - only_in_2 = keys2 - keys1 - if only_in_1: - differences[f"{path}.keys_only_in_first"] = sorted(only_in_1) - if only_in_2: - differences[f"{path}.keys_only_in_second"] = sorted(only_in_2) - - # Compare common keys - for key in keys1 & keys2: - key_path = f"{path}[{repr(key)}]" - key_diffs = deep_compare( - obj1[key], - obj2[key], - key_path, - max_depth, - current_depth + 1, - exclude_attrs, - include_callable, - ) - differences.update(key_diffs) - - return differences - - # Handle sets - if isinstance(obj1, set): - only_in_1 = obj1 - obj2 - only_in_2 = obj2 - obj1 - - if only_in_1: - differences[f"{path}.items_only_in_first"] = sorted(only_in_1) - if only_in_2: - differences[f"{path}.items_only_in_second"] = sorted(only_in_2) - - return differences - - # Handle custom objects and classes - try: - # Try to get all attributes - attrs1 = dir(obj1) - - # Filter attributes - filtered_attrs = [attr for attr in attrs1 if not attr.startswith("__") and attr not in exclude_attrs and (include_callable or not callable(getattr(obj1, attr, None)))] - - # Compare each attribute - for attr in filtered_attrs: - try: - # Skip unintended attributes - if attr in exclude_attrs: - continue - - # Get attribute values - val1 = getattr(obj1, attr) - - # Skip callables unless explicitly included - if callable(val1) and not include_callable: - continue - - # Check if attr exists in obj2 - if not hasattr(obj2, attr): - differences[f"{path}.{attr}"] = f"{val1} vs [attribute missing]" - continue - - val2 = getattr(obj2, attr) - - # Compare values - attr_path = f"{path}.{attr}" - attr_diffs = deep_compare( - val1, - val2, - attr_path, - max_depth, - current_depth + 1, - exclude_attrs, - include_callable, - ) - differences.update(attr_diffs) - except Exception as e: - differences[f"{path}.{attr}"] = f"Error comparing: {str(e)}" - - except Exception as e: - differences[path] = f"Error accessing attributes: {str(e)}" - - return differences diff --git a/reproducible-builds/apkdiff/uv.lock b/reproducible-builds/apkdiff/uv.lock index 62440fdc611..0f296f67805 100644 --- a/reproducible-builds/apkdiff/uv.lock +++ b/reproducible-builds/apkdiff/uv.lock @@ -18,8 +18,8 @@ wheels = [ [[package]] name = "androguard" -version = "4.1.3" -source = { git = "https://github.com/androguard/androguard?rev=943932d35c08b8ee5102ace398882858d3bd7567#943932d35c08b8ee5102ace398882858d3bd7567" } +version = "4.1.4" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apkinspector" }, { name = "asn1crypto" }, @@ -27,7 +27,6 @@ dependencies = [ { name = "colorama" }, { name = "cryptography" }, { name = "dataset" }, - { name = "frida" }, { name = "ipython" }, { name = "loguru" }, { name = "lxml" }, @@ -37,6 +36,10 @@ dependencies = [ { name = "pygments" }, { name = "pyyaml" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/fb/a4/c6a1bcc4f4b40098259202f7155214f2ec315eb3ac5923f093646cf352c6/androguard-4.1.4.tar.gz", hash = "sha256:1e117ee4574366a2d7376b8c858433ad724b0a29e4036d9f1a9fda4372180267", size = 956315, upload-time = "2026-06-01T08:58:44.095Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/fc/dc0df02bcfb4c5182e2c15a60b2cd823f11470ddce8abcf3f99a43394434/androguard-4.1.4-py3-none-any.whl", hash = "sha256:6265a6d4007401cf5a62a98fa99a1c2994644d4311da031a5398efe3bcaa63b0", size = 1026935, upload-time = "2026-06-01T08:58:42.638Z" }, +] [[package]] name = "apkdiff" @@ -44,14 +47,10 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "androguard" }, - { name = "tqdm" }, ] [package.metadata] -requires-dist = [ - { name = "androguard", git = "https://github.com/androguard/androguard?rev=943932d35c08b8ee5102ace398882858d3bd7567" }, - { name = "tqdm", specifier = ">=4.67.1" }, -] +requires-dist = [{ name = "androguard", specifier = ">=4.1.4" }] [[package]] name = "apkinspector" @@ -242,27 +241,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] -[[package]] -name = "frida" -version = "17.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/de/c134db0cfdca8978f4c8c4188edbe3f14207c85b11eae981c3cec65baffe/frida-17.9.11.tar.gz", hash = "sha256:e2e91e26c386361680babd36a579a05359ee07b45a7731c36d03a9c2f807dbcf", size = 927654, upload-time = "2026-05-23T13:21:23.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c0/5b4d03d385ddfe5d539a484e51832b1a7126d994d8a8c01088eb227d77f4/frida-17.9.11-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:a29f1033a7f92ca0a8a3329ac138bc8586513b9549d64cd68d293adaeac624b4", size = 22880273, upload-time = "2026-05-23T13:20:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/5d/d4/17d19b312a8fba5e2403b3d915517ae8220ac1ef34101fbb624f26708818/frida-17.9.11-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:f4998f704e02c731e234b83a8cca52b7f082d1e1a4d15c8aaf5bb3513c9046a2", size = 32259395, upload-time = "2026-05-23T13:20:49.394Z" }, - { url = "https://files.pythonhosted.org/packages/94/77/b2c45a5467dc9e6d646763a8fd6998995eed13ee010d71fc7c14753ba899/frida-17.9.11-cp37-abi3-manylinux1_i686.whl", hash = "sha256:2198d141d39f99b7e1abb175d742ea03272aa38e554cf97ade9a33f070842e73", size = 20689467, upload-time = "2026-05-23T13:20:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4c/68b9581775ae410f09096ed0db2e81b8bbc1cd8018e667fec376ab1b5807/frida-17.9.11-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:a2f2138b4cf13ea24408286bc7c700788c3b9bb3631876be725d4da2660bef5f", size = 32918592, upload-time = "2026-05-23T13:20:54.933Z" }, - { url = "https://files.pythonhosted.org/packages/70/7a/a336718b9164271a49909b275e5c5533233c853276d426e829acdf0eb9bb/frida-17.9.11-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:903481582f86da6de967a61684c8e4bed5df47a1cc56ae73c6cc59526069aeb3", size = 21500132, upload-time = "2026-05-23T13:20:57.967Z" }, - { url = "https://files.pythonhosted.org/packages/14/65/312b853c90f8580099427854dfd859a9d2719ad58b0b19dbce7335ead663/frida-17.9.11-cp37-abi3-manylinux2014_armv7l.whl", hash = "sha256:2cbbf93302c19764175eba0b22433cb054589b4851c5de8be6af284bff2f2208", size = 19336126, upload-time = "2026-05-23T13:21:00.436Z" }, - { url = "https://files.pythonhosted.org/packages/80/56/f08b07168d3d332d4efbacd854c89e49d30744ce58657c9e10ff24498bfa/frida-17.9.11-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:4597c9a3a10e9b6959f58bc057317127133d443aaeaaf2c4d597faca46af276a", size = 21500135, upload-time = "2026-05-23T13:21:03.053Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f2/07fe15da8948a00990ea12a37bf0ade347efb49c5d11e98ac698d0f82676/frida-17.9.11-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:9df8c78e314470c916a8d4636ad1483895598d9829c929d245702374b907bec2", size = 19336127, upload-time = "2026-05-23T13:21:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/7b/73/3d2b055281522cc9013f0aedeca1adc45fbc7bb92aa8ddab929cd8f32208/frida-17.9.11-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:00815389768817210b887821a80c92709bea3cd839e0bb253b120bbfaaf9952d", size = 20689472, upload-time = "2026-05-23T13:21:08.199Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5d/8eeba8913fd82751e007f5a84e58c03a4b0dae9fd5e5d2205dba634d9241/frida-17.9.11-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:d6045069816fa4f5f725542982874cd13d7975601dbf9bdc00913bb283f5c8da", size = 32918596, upload-time = "2026-05-23T13:21:11.031Z" }, - { url = "https://files.pythonhosted.org/packages/54/7a/1fd9c354a9c5c9d3ab40fb8341d19a4962b16d1f72de4250496212f99662/frida-17.9.11-cp37-abi3-win32.whl", hash = "sha256:c4e08358e5fcdfbcf2ed2d7c01b2e2a48b3cf84b9f760a882d82750ebe19d4b3", size = 39319555, upload-time = "2026-05-23T13:21:14.131Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/c91f1820463bd62667ae152b621091d8c93506092961485c57c7d4f7e9a5/frida-17.9.11-cp37-abi3-win_amd64.whl", hash = "sha256:f60bdf022157527894af5c29de1cc9ebbbb2666a7903e518d6ddda3c3af5bc5d", size = 41967310, upload-time = "2026-05-23T13:21:17.432Z" }, - { url = "https://files.pythonhosted.org/packages/fc/29/256bce6e73b2214c2afbf8be6ab600d8bfd84f0ad26dcad3bd4745ae898d/frida-17.9.11-cp37-abi3-win_arm64.whl", hash = "sha256:6515847e2e0c94a68abe09bc4461468934c527d1e91623e54d5c4ea274beb930", size = 51855908, upload-time = "2026-05-23T13:21:20.542Z" }, -] - [[package]] name = "greenlet" version = "3.5.1" @@ -320,7 +298,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.13.0" +version = "9.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -330,14 +308,14 @@ dependencies = [ { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, - { name = "psutil" }, + { name = "psutil", marker = "sys_platform != 'emscripten'" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/58818a63eaf8982b67632b1bc20585c811611b15a8da19d6012323dc76a5/ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c", size = 627770, upload-time = "2026-06-05T08:12:33.045Z" }, ] [[package]] @@ -778,25 +756,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - [[package]] name = "traitlets" -version = "5.15.0" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] @@ -810,11 +776,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/44/c833e6b746ffb654e9abacf7ad6c2480a9c8c42e9637c1ae849964fb4dde/wcwidth-0.8.0.tar.gz", hash = "sha256:68a882ff6d14e3d14e0cae590b96a0551be64ce4905408112a8254434a1bdf69", size = 1305357, upload-time = "2026-06-05T21:19:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/c68b6cbcfeadbf420b3c3edaf8fda51335bc9c38732adb2d3ba8984dc607/wcwidth-0.8.0-py3-none-any.whl", hash = "sha256:8c75e6099cefd197c4bcc67a486f70b5dbc68f997c05f34a811d853910450d64", size = 324935, upload-time = "2026-06-05T21:19:33.999Z" }, ] [[package]]