Compare commits

...

13 Commits

Author SHA1 Message Date
Clemens Schwaighofer
6edf9398b7 v0.28.0: Enum base class added 2025-10-23 13:48:57 +09:00
Clemens Schwaighofer
30bf9c1bcb Add Enum base class
A helper class for handling enum classes with various lookup helpers
2025-10-23 13:47:13 +09:00
Clemens Schwaighofer
0b59f3cc7a v0.27.0: add json replace content method 2025-10-23 13:22:19 +09:00
Clemens Schwaighofer
2544fad9ce Add json helper function json_replace
Function can replace content for a json path string in a dictionary
2025-10-23 13:20:40 +09:00
Clemens Schwaighofer
e579ef5834 v0.26.0: Add Symmetric Encryption 2025-10-23 11:48:52 +09:00
Clemens Schwaighofer
543e9766a1 Add symmetric encryption and tests 2025-10-23 11:47:41 +09:00
Clemens Schwaighofer
4c3611aba7 v0.25.1: add missing jmespath exception check 2025-10-09 16:43:53 +09:00
Clemens Schwaighofer
dadc14563a jmespath search check update 2025-10-09 16:42:41 +09:00
Clemens Schwaighofer
c1eda7305b jmespath search, catch JMESPathTypeError error
This error can happend if we search for a key and try to make a value compare and the key does not exist.
Perhaps also when they key should return a list
2025-10-09 16:39:54 +09:00
Clemens Schwaighofer
2f4e236350 v0.25.0: add create datetime iso format 2025-10-08 16:09:29 +09:00
Clemens Schwaighofer
b858936c68 Add test file for datetime helpers 2025-10-08 16:08:23 +09:00
Clemens Schwaighofer
78ce30283e Version update in uv.lock (merge from master) 2025-10-08 15:58:58 +09:00
Clemens Schwaighofer
f85fbb86af Add iso datetime create with time zone support
The time zone check is for short mappings is limited, it is recommended
to use full TZ names like "Europe/Berlin", "Asia/Tokyo", "America/New_York"
2025-10-08 15:57:57 +09:00
20 changed files with 2666 additions and 103 deletions

View File

@@ -1,12 +1,14 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.24.4"
version = "0.28.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"cryptography>=46.0.3",
"jmespath>=1.0.1",
"jsonpath-ng>=1.7.0",
"psutil>=7.0.0",
"requests>=2.32.4",
]
@@ -27,6 +29,7 @@ build-backend = "hatchling.build"
[dependency-groups]
dev = [
"deepdiff>=8.6.1",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
]

View File

@@ -0,0 +1,152 @@
"""
simple symmetric encryption
Will be moved to CoreLibs
TODO: set key per encryption run
"""
import os
import json
import base64
import hashlib
from typing import TypedDict, cast
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
class PackageData(TypedDict):
"""encryption package"""
encrypted_data: str
salt: str
key_hash: str
class SymmetricEncryption:
"""
simple encryption
the encrypted package has "encrypted_data" and "salt" as fields, salt is needed to create the
key from the password to decrypt
"""
def __init__(self, password: str):
if not password:
raise ValueError("A password must be set")
self.password = password
self.password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
def __derive_key_from_password(self, password: str, salt: bytes) -> bytes:
_password = password.encode('utf-8')
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(_password))
return key
def __encrypt_with_metadata(self, data: str | bytes) -> PackageData:
"""Encrypt data and include salt if password-based"""
# convert to bytes (for encoding)
if isinstance(data, str):
data = data.encode('utf-8')
# generate salt and key from password
salt = os.urandom(16)
key = self.__derive_key_from_password(self.password, salt)
# init the cypher suit
cipher_suite = Fernet(key)
encrypted_data = cipher_suite.encrypt(data)
# If using password, include salt in the result
return {
'encrypted_data': base64.urlsafe_b64encode(encrypted_data).decode('utf-8'),
'salt': base64.urlsafe_b64encode(salt).decode('utf-8'),
'key_hash': hashlib.sha256(key).hexdigest()
}
def encrypt_with_metadata(self, data: str | bytes, return_as: str = 'str') -> str | bytes | PackageData:
"""encrypt with metadata, but returns data in string"""
match return_as:
case 'str':
return self.encrypt_with_metadata_return_str(data)
case 'json':
return self.encrypt_with_metadata_return_str(data)
case 'bytes':
return self.encrypt_with_metadata_return_bytes(data)
case 'dict':
return self.encrypt_with_metadata_return_dict(data)
case _:
# default is string json
return self.encrypt_with_metadata_return_str(data)
def encrypt_with_metadata_return_dict(self, data: str | bytes) -> PackageData:
"""encrypt with metadata, but returns data as PackageData dict"""
return self.__encrypt_with_metadata(data)
def encrypt_with_metadata_return_str(self, data: str | bytes) -> str:
"""encrypt with metadata, but returns data in string"""
return json.dumps(self.__encrypt_with_metadata(data))
def encrypt_with_metadata_return_bytes(self, data: str | bytes) -> bytes:
"""encrypt with metadata, but returns data in bytes"""
return json.dumps(self.__encrypt_with_metadata(data)).encode('utf-8')
def decrypt_with_metadata(self, encrypted_package: str | bytes | PackageData, password: str | None = None) -> str:
"""Decrypt data that may include metadata"""
try:
# Try to parse as JSON (password-based encryption)
if isinstance(encrypted_package, bytes):
package_data = cast(PackageData, json.loads(encrypted_package.decode('utf-8')))
elif isinstance(encrypted_package, str):
package_data = cast(PackageData, json.loads(str(encrypted_package)))
else:
package_data = encrypted_package
encrypted_data = base64.urlsafe_b64decode(package_data['encrypted_data'])
salt = base64.urlsafe_b64decode(package_data['salt'])
pwd = password or self.password
key = self.__derive_key_from_password(pwd, salt)
if package_data['key_hash'] != hashlib.sha256(key).hexdigest():
raise ValueError("Key hash is not matching, possible invalid password")
cipher_suite = Fernet(key)
decrypted_data = cipher_suite.decrypt(encrypted_data)
except (json.JSONDecodeError, KeyError, UnicodeDecodeError) as e:
raise ValueError(f"Invalid encrypted package format {e}") from e
return decrypted_data.decode('utf-8')
@staticmethod
def encrypt_data(data: str | bytes, password: str) -> str:
"""
Static method to encrypt some data
Arguments:
data {str | bytes} -- _description_
password {str} -- _description_
Returns:
str -- _description_
"""
encryptor = SymmetricEncryption(password)
return encryptor.encrypt_with_metadata_return_str(data)
@staticmethod
def decrypt_data(data: str | bytes | PackageData, password: str) -> str:
"""
Static method to decrypt some data
Arguments:
data {str | bytes | PackageData} -- _description_
password {str} -- _description_
Returns:
str -- _description_
"""
decryptor = SymmetricEncryption(password)
return decryptor.decrypt_with_metadata(data, password=password)
# __END__

View File

@@ -28,6 +28,8 @@ def jmespath_search(search_data: dict[Any, Any] | list[Any], search_params: str)
raise ValueError(f"Compile failed: {search_params}: {excp}") from excp
except jmespath.exceptions.ParseError as excp:
raise ValueError(f"Parse failed: {search_params}: {excp}") from excp
except jmespath.exceptions.JMESPathTypeError as excp:
raise ValueError(f"Search failed with JMESPathTypeError: {search_params}: {excp}") from excp
except TypeError as excp:
raise ValueError(f"Type error for search_params: {excp}") from excp
return search_result

View File

@@ -5,6 +5,8 @@ json encoder for datetime
from typing import Any
from json import JSONEncoder, dumps
from datetime import datetime, date
import copy
from jsonpath_ng import parse # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
# subclass JSONEncoder
@@ -41,4 +43,22 @@ def json_dumps(data: Any):
"""
return dumps(data, ensure_ascii=False, default=str)
def modify_with_jsonpath(data: dict[Any, Any], path: str, new_value: Any):
"""
Modify dictionary using JSONPath (more powerful than JMESPath for modifications)
"""
result = copy.deepcopy(data)
jsonpath_expr = parse(path) # pyright: ignore[reportUnknownVariableType]
# Find and update all matches
matches = jsonpath_expr.find(result) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
for match in matches: # pyright: ignore[reportUnknownVariableType]
match.full_path.update(result, new_value) # pyright: ignore[reportUnknownMemberType]
return result
# __END__
# __END__

View File

@@ -598,7 +598,7 @@ class Log(LogParent):
# log path, remove them stem (".log"), then add the datetime and add .log again
now = datetime.now()
# we add microseconds part to get milli seconds
new_stem=f"{log_path.stem}.{now.strftime('%Y-%m-%d_%H-%M-%S')}.{str(now.microsecond)[:3]}"
new_stem = f"{log_path.stem}.{now.strftime('%Y-%m-%d_%H-%M-%S')}.{str(now.microsecond)[:3]}"
file_handler = logging.FileHandler(
filename=log_path.with_name(f"{new_stem}{log_path.suffix}"),
encoding="utf-8",

View File

@@ -3,7 +3,9 @@ Various string based date/time helpers
"""
from math import floor
import time
import time as time_t
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
def convert_timestamp(timestamp: float | int, show_micro: bool = True) -> str:
@@ -58,6 +60,77 @@ def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -
Returns:
str -- _description_
"""
return time.strftime(timestamp_format, time.localtime(timestamp))
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
def get_system_timezone():
"""Get system timezone using datetime's automatic detection"""
# Get current time with system timezone
local_time = datetime.now().astimezone()
# Extract timezone info
system_tz = local_time.tzinfo
timezone_name = str(system_tz)
return system_tz, timezone_name
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
"""
parses a string to get the ZoneInfo
If not set or not valid gets local time,
if that is not possible get UTC
Keyword Arguments:
timezone_tz {str} -- _description_ (default: {''})
Returns:
ZoneInfo -- _description_
"""
try:
return ZoneInfo(timezone_tz)
except (ZoneInfoNotFoundError, ValueError, TypeError):
# use default
time_tz, time_tz_str = get_system_timezone()
if time_tz is None:
return ZoneInfo('UTC')
# TODO build proper TZ lookup
tz_mapping = {
'JST': 'Asia/Tokyo',
'KST': 'Asia/Seoul',
'IST': 'Asia/Kolkata',
'CST': 'Asia/Shanghai', # Default to China for CST
'AEST': 'Australia/Sydney',
'AWST': 'Australia/Perth',
'EST': 'America/New_York',
'EDT': 'America/New_York',
'CDT': 'America/Chicago',
'MST': 'America/Denver',
'MDT': 'America/Denver',
'PST': 'America/Los_Angeles',
'PDT': 'America/Los_Angeles',
'GMT': 'UTC',
'UTC': 'UTC',
'CET': 'Europe/Berlin',
'CEST': 'Europe/Berlin',
'BST': 'Europe/London',
}
try:
return ZoneInfo(tz_mapping[time_tz_str])
except (ZoneInfoNotFoundError, IndexError) as e:
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
"""
set a datetime in the iso8601 format with microseconds
Returns:
str -- _description_
"""
# parse if this is a string
if isinstance(timezone_tz, str):
timezone_tz = parse_timezone_data(timezone_tz)
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
# __END__

View File

@@ -0,0 +1,75 @@
"""
Enum base classes
"""
from enum import Enum
from typing import Any
class EnumBase(Enum):
"""
base for enum
lookup_any and from_any will return "EnumBase" and the sub class name
run the return again to "from_any" to get a clean value, or cast it
"""
@classmethod
def lookup_key(cls, enum_key: str):
"""Lookup from key side (must be string)"""
# if there is a ":", then this is legacy, replace with ___
if ":" in enum_key:
enum_key = enum_key.replace(':', '___')
try:
return cls[enum_key.upper()]
except KeyError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
except AttributeError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
@classmethod
def lookup_value(cls, enum_value: Any):
"""Lookup through value side"""
try:
return cls(enum_value)
except ValueError as e:
raise ValueError(f"Invalid value: {enum_value}") from e
@classmethod
def from_any(cls, enum_any: Any):
"""
This only works in the following order
-> class itself, as is
-> str, assume key lookup
-> if failed try other
Arguments:
enum_any {Any} -- _description_
Returns:
_type_ -- _description_
"""
if isinstance(enum_any, cls):
return enum_any
# try key first if it is string
# if failed try value
if isinstance(enum_any, str):
try:
return cls.lookup_key(enum_any)
except (ValueError, AttributeError):
try:
return cls.lookup_value(enum_any)
except ValueError as e:
raise ValueError(f"Could not find as key or value: {enum_any}") from e
return cls.lookup_value(enum_any)
def to_value(self) -> Any:
"""Convert to value"""
return self.value
def to_lower_case(self) -> str:
"""return lower case"""
return self.name.lower()
def __str__(self) -> str:
"""return [Enum].NAME like it was called with .name"""
return self.name

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""
Symmetric encryption test
"""
import json
from corelibs.debug_handling.dump_data import dump_data
from corelibs.encryption_handling.symmetric_encryption import SymmetricEncryption
def main() -> None:
"""
Comment
"""
password = "strongpassword"
se = SymmetricEncryption(password)
plaintext = "Hello, World!"
ciphertext = se.encrypt_with_metadata_return_str(plaintext)
decrypted = se.decrypt_with_metadata(ciphertext)
print(f"Encrypted: {dump_data(json.loads(ciphertext))}")
print(f"Input: {plaintext} -> {decrypted}")
static_ciphertext = SymmetricEncryption.encrypt_data(plaintext, password)
decrypted = SymmetricEncryption.decrypt_data(static_ciphertext, password)
print(f"Static Encrypted: {dump_data(json.loads(static_ciphertext))}")
print(f"Input: {plaintext} -> {decrypted}")
if __name__ == "__main__":
main()
# __END__

View File

@@ -39,7 +39,9 @@ def main() -> None:
'a',
'bar.a',
'foo.a',
'baz[].aa'
'baz[].aa',
"[?\"c\" && contains(\"c\", 'b')]",
"[?contains(\"c\", 'b')]",
]
for __jmespath in __get:
result = jmespath_search(__set, __jmespath)

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
JSON content replace tets
"""
from deepdiff import DeepDiff
from corelibs.debug_handling.dump_data import dump_data
from corelibs.json_handling.json_helper import modify_with_jsonpath
def main() -> None:
"""
Comment
"""
__data = {
'a': 'b',
'foobar': [1, 2, 'a'],
'bar': {
'a': 1,
'b': 'c'
},
'baz': [
{
'aa': 1,
'ab': 'cc'
},
{
'ba': 2,
'bb': 'dd'
},
],
'foo': {
'a': [1, 2, 3],
'b': ['a', 'b', 'c']
}
}
# Modify some values using JSONPath
__replace_data = modify_with_jsonpath(__data, 'bar.a', 42)
__replace_data = modify_with_jsonpath(__replace_data, 'foo.b[1]', 'modified')
__replace_data = modify_with_jsonpath(__replace_data, 'baz[0].ab', 'changed')
print(f"Original Data:\n{dump_data(__data)}\n")
print(f"Modified Data:\n{dump_data(__replace_data)}\n")
print(f"Differences:\n{dump_data(DeepDiff(__data, __replace_data, verbose_level=2))}\n")
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
date string helper test
"""
from corelibs.string_handling.datetime_helpers import get_datetime_iso8601
def main() -> None:
"""
Comment
"""
print(get_datetime_iso8601())
print(get_datetime_iso8601('Asia/Tokyo'))
print(get_datetime_iso8601('UTC'))
print(get_datetime_iso8601('Europe/Vienna'))
print(get_datetime_iso8601('America/New_York'))
print(get_datetime_iso8601('Australia/Sydney'))
print(get_datetime_iso8601('invalid'))
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
Enum handling
"""
from corelibs.var_handling.enum_base import EnumBase
class TestBlock(EnumBase):
"""Test block enum"""
BLOCK_A = "block_a"
HAS_NUM = 5
def main() -> None:
"""
Comment
"""
print(f"BLOCK A: {TestBlock.from_any('BLOCK_A')}")
print(f"HAS NUM: {TestBlock.from_any(5)}")
print(f"DIRECT BLOCK: {TestBlock.BLOCK_A.name} -> {TestBlock.BLOCK_A.value}")
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,3 @@
"""
Unit tests for encryption_handling module
"""

View File

@@ -0,0 +1,665 @@
"""
PyTest: encryption_handling/symmetric_encryption
"""
# pylint: disable=redefined-outer-name
# ^ Disabled because pytest fixtures intentionally redefine names
import os
import json
import base64
import hashlib
import pytest
from corelibs.encryption_handling.symmetric_encryption import (
SymmetricEncryption
)
class TestSymmetricEncryptionInitialization:
"""Tests for SymmetricEncryption initialization"""
def test_valid_password_initialization(self):
"""Test initialization with a valid password"""
encryptor = SymmetricEncryption("test_password")
assert encryptor.password == "test_password"
assert encryptor.password_hash == hashlib.sha256("test_password".encode('utf-8')).hexdigest()
def test_empty_password_raises_error(self):
"""Test that empty password raises ValueError"""
with pytest.raises(ValueError, match="A password must be set"):
SymmetricEncryption("")
def test_password_hash_is_consistent(self):
"""Test that password hash is consistently generated"""
encryptor1 = SymmetricEncryption("test_password")
encryptor2 = SymmetricEncryption("test_password")
assert encryptor1.password_hash == encryptor2.password_hash
def test_different_passwords_different_hashes(self):
"""Test that different passwords produce different hashes"""
encryptor1 = SymmetricEncryption("password1")
encryptor2 = SymmetricEncryption("password2")
assert encryptor1.password_hash != encryptor2.password_hash
class TestEncryptWithMetadataReturnDict:
"""Tests for encrypt_with_metadata_return_dict method"""
def test_encrypt_string_returns_package_data(self):
"""Test encrypting a string returns PackageData dict"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
assert isinstance(result, dict)
assert 'encrypted_data' in result
assert 'salt' in result
assert 'key_hash' in result
def test_encrypt_bytes_returns_package_data(self):
"""Test encrypting bytes returns PackageData dict"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict(b"test data")
assert isinstance(result, dict)
assert 'encrypted_data' in result
assert 'salt' in result
assert 'key_hash' in result
def test_encrypted_data_is_base64_encoded(self):
"""Test that encrypted_data is base64 encoded"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should not raise exception when decoding
base64.urlsafe_b64decode(result['encrypted_data'])
def test_salt_is_base64_encoded(self):
"""Test that salt is base64 encoded"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should not raise exception when decoding
salt = base64.urlsafe_b64decode(result['salt'])
# Salt should be 16 bytes
assert len(salt) == 16
def test_key_hash_is_valid_hex(self):
"""Test that key_hash is a valid hex string"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should be 64 characters (SHA256 hex)
assert len(result['key_hash']) == 64
# Should only contain hex characters
int(result['key_hash'], 16)
def test_different_salts_for_each_encryption(self):
"""Test that each encryption uses a different salt"""
encryptor = SymmetricEncryption("test_password")
result1 = encryptor.encrypt_with_metadata_return_dict("test data")
result2 = encryptor.encrypt_with_metadata_return_dict("test data")
assert result1['salt'] != result2['salt']
assert result1['encrypted_data'] != result2['encrypted_data']
class TestEncryptWithMetadataReturnStr:
"""Tests for encrypt_with_metadata_return_str method"""
def test_returns_json_string(self):
"""Test that method returns a valid JSON string"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_str("test data")
assert isinstance(result, str)
# Should be valid JSON
parsed = json.loads(result)
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
def test_json_string_parseable(self):
"""Test that returned JSON string can be parsed back"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_str("test data")
parsed = json.loads(result)
assert isinstance(parsed, dict)
class TestEncryptWithMetadataReturnBytes:
"""Tests for encrypt_with_metadata_return_bytes method"""
def test_returns_bytes(self):
"""Test that method returns bytes"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_bytes("test data")
assert isinstance(result, bytes)
def test_bytes_contains_valid_json(self):
"""Test that returned bytes contain valid JSON"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_bytes("test data")
# Should be valid JSON when decoded
parsed = json.loads(result.decode('utf-8'))
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
class TestEncryptWithMetadata:
"""Tests for encrypt_with_metadata method with different return types"""
def test_return_as_str(self):
"""Test encrypt_with_metadata with return_as='str'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='str')
assert isinstance(result, str)
json.loads(result) # Should be valid JSON
def test_return_as_json(self):
"""Test encrypt_with_metadata with return_as='json'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='json')
assert isinstance(result, str)
json.loads(result) # Should be valid JSON
def test_return_as_bytes(self):
"""Test encrypt_with_metadata with return_as='bytes'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='bytes')
assert isinstance(result, bytes)
def test_return_as_dict(self):
"""Test encrypt_with_metadata with return_as='dict'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='dict')
assert isinstance(result, dict)
assert 'encrypted_data' in result
def test_default_return_type(self):
"""Test encrypt_with_metadata default return type"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data")
# Default should be 'str'
assert isinstance(result, str)
def test_invalid_return_type_defaults_to_str(self):
"""Test that invalid return_as defaults to str"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='invalid')
assert isinstance(result, str)
class TestDecryptWithMetadata:
"""Tests for decrypt_with_metadata method"""
def test_decrypt_string_package(self):
"""Test decrypting a string JSON package"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_bytes_package(self):
"""Test decrypting a bytes JSON package"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_bytes("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_dict_package(self):
"""Test decrypting a dict PackageData"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_dict("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_with_different_password_fails(self):
"""Test that decrypting with wrong password fails"""
encryptor = SymmetricEncryption("password1")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decryptor = SymmetricEncryption("password2")
with pytest.raises(ValueError, match="Key hash is not matching"):
decryptor.decrypt_with_metadata(encrypted)
def test_decrypt_with_explicit_password(self):
"""Test decrypting with explicitly provided password"""
encryptor = SymmetricEncryption("password1")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Decrypt with different password parameter
decryptor = SymmetricEncryption("password1")
decrypted = decryptor.decrypt_with_metadata(encrypted, password="password1")
assert decrypted == "test data"
def test_decrypt_invalid_json_raises_error(self):
"""Test that invalid JSON raises ValueError"""
encryptor = SymmetricEncryption("test_password")
with pytest.raises(ValueError, match="Invalid encrypted package format"):
encryptor.decrypt_with_metadata("not valid json")
def test_decrypt_missing_fields_raises_error(self):
"""Test that missing required fields raises ValueError"""
encryptor = SymmetricEncryption("test_password")
invalid_package = json.dumps({"encrypted_data": "test"})
with pytest.raises(ValueError, match="Invalid encrypted package format"):
encryptor.decrypt_with_metadata(invalid_package)
def test_decrypt_unicode_data(self):
"""Test encrypting and decrypting unicode data"""
encryptor = SymmetricEncryption("test_password")
unicode_data = "Hello 世界 🌍"
encrypted = encryptor.encrypt_with_metadata_return_str(unicode_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == unicode_data
def test_decrypt_empty_string(self):
"""Test encrypting and decrypting empty string"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == ""
def test_decrypt_long_data(self):
"""Test encrypting and decrypting long data"""
encryptor = SymmetricEncryption("test_password")
long_data = "A" * 10000
encrypted = encryptor.encrypt_with_metadata_return_str(long_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == long_data
class TestStaticMethods:
"""Tests for static methods encrypt_data and decrypt_data"""
def test_encrypt_data_static_method(self):
"""Test static encrypt_data method"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
assert isinstance(encrypted, str)
# Should be valid JSON
parsed = json.loads(encrypted)
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
def test_decrypt_data_static_method(self):
"""Test static decrypt_data method"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == "test data"
def test_static_methods_roundtrip(self):
"""Test complete roundtrip using static methods"""
original = "test data with special chars: !@#$%^&*()"
encrypted = SymmetricEncryption.encrypt_data(original, "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == original
def test_static_decrypt_with_bytes(self):
"""Test static decrypt_data with bytes input"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
encrypted_bytes = encrypted.encode('utf-8')
decrypted = SymmetricEncryption.decrypt_data(encrypted_bytes, "test_password")
assert decrypted == "test data"
def test_static_decrypt_with_dict(self):
"""Test static decrypt_data with PackageData dict"""
encryptor = SymmetricEncryption("test_password")
encrypted_dict = encryptor.encrypt_with_metadata_return_dict("test data")
decrypted = SymmetricEncryption.decrypt_data(encrypted_dict, "test_password")
assert decrypted == "test data"
def test_static_encrypt_bytes_data(self):
"""Test static encrypt_data with bytes input"""
encrypted = SymmetricEncryption.encrypt_data(b"test data", "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == "test data"
class TestEncryptionSecurity:
"""Security-related tests for encryption"""
def test_same_data_different_encryption(self):
"""Test that same data produces different encrypted outputs due to salt"""
encryptor = SymmetricEncryption("test_password")
encrypted1 = encryptor.encrypt_with_metadata_return_str("test data")
encrypted2 = encryptor.encrypt_with_metadata_return_str("test data")
assert encrypted1 != encrypted2
def test_password_not_recoverable_from_hash(self):
"""Test that password hash is one-way"""
encryptor = SymmetricEncryption("secret_password")
# The password_hash should be SHA256 hex (64 chars)
assert len(encryptor.password_hash) == 64
# Password should not be easily derivable from hash
assert "secret_password" not in encryptor.password_hash
def test_encrypted_data_not_plaintext(self):
"""Test that encrypted data doesn't contain plaintext"""
encryptor = SymmetricEncryption("test_password")
plaintext = "very_secret_data_12345"
encrypted = encryptor.encrypt_with_metadata_return_str(plaintext)
# Plaintext should not appear in encrypted output
assert plaintext not in encrypted
def test_modified_encrypted_data_fails_decryption(self):
"""Test that modified encrypted data fails to decrypt"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Modify the encrypted data
encrypted_dict = json.loads(encrypted)
encrypted_dict['encrypted_data'] = encrypted_dict['encrypted_data'][:-5] + "AAAAA"
modified_encrypted = json.dumps(encrypted_dict)
# Should fail to decrypt
with pytest.raises(Exception): # Fernet will raise an exception
encryptor.decrypt_with_metadata(modified_encrypted)
def test_modified_salt_fails_decryption(self):
"""Test that modified salt fails to decrypt"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Modify the salt
encrypted_dict = json.loads(encrypted)
original_salt = base64.urlsafe_b64decode(encrypted_dict['salt'])
modified_salt = bytes([b ^ 1 for b in original_salt])
encrypted_dict['salt'] = base64.urlsafe_b64encode(modified_salt).decode('utf-8')
modified_encrypted = json.dumps(encrypted_dict)
# Should fail to decrypt due to key hash mismatch
with pytest.raises(ValueError, match="Key hash is not matching"):
encryptor.decrypt_with_metadata(modified_encrypted)
class TestEdgeCases:
"""Edge case tests"""
def test_very_long_password(self):
"""Test with very long password"""
long_password = "a" * 1000
encryptor = SymmetricEncryption(long_password)
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_special_characters_in_data(self):
"""Test encryption of data with special characters"""
special_data = "!@#$%^&*()_+-=[]{}|;':\",./<>?\n\t\r"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(special_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == special_data
def test_binary_data_utf8_bytes(self):
"""Test encryption of UTF-8 encoded bytes"""
# Test with UTF-8 encoded bytes
utf8_bytes = "Hello 世界 🌍".encode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(utf8_bytes)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "Hello 世界 🌍"
def test_binary_data_with_base64_encoding(self):
"""Test encryption of arbitrary binary data using base64 encoding"""
# For arbitrary binary data, encode to base64 first
binary_data = bytes(range(256))
base64_encoded = base64.b64encode(binary_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Decode back to binary
decoded_binary = base64.b64decode(decrypted)
assert decoded_binary == binary_data
def test_binary_data_image_simulation(self):
"""Test encryption of simulated binary image data"""
# Simulate image binary data (random bytes)
image_data = os.urandom(1024) # 1KB of random binary data
base64_encoded = base64.b64encode(image_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Verify round-trip
decoded_data = base64.b64decode(decrypted)
assert decoded_data == image_data
def test_binary_data_with_null_bytes(self):
"""Test encryption of data containing null bytes"""
# Create data with null bytes
data_with_nulls = "text\x00with\x00nulls\x00bytes"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(data_with_nulls)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == data_with_nulls
def test_binary_data_bytes_input(self):
"""Test encryption with bytes input directly"""
# UTF-8 compatible bytes
byte_data = b"Binary data test"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(byte_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "Binary data test"
def test_binary_data_large_file_simulation(self):
"""Test encryption of large binary data (simulated file)"""
# Simulate a larger binary file (10KB)
large_data = os.urandom(10240)
base64_encoded = base64.b64encode(large_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Verify integrity
decoded_data = base64.b64decode(decrypted)
assert len(decoded_data) == 10240
assert decoded_data == large_data
def test_binary_data_json_with_base64(self):
"""Test encryption of JSON containing base64 encoded binary data"""
binary_data = os.urandom(256)
json_data = json.dumps({
"filename": "test.bin",
"data": base64.b64encode(binary_data).decode('utf-8'),
"size": len(binary_data)
})
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(json_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Parse and verify
parsed = json.loads(decrypted)
assert parsed["filename"] == "test.bin"
assert parsed["size"] == 256
decoded_binary = base64.b64decode(parsed["data"])
assert decoded_binary == binary_data
def test_numeric_password(self):
"""Test with numeric string password"""
encryptor = SymmetricEncryption("12345")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_unicode_password(self):
"""Test with unicode password"""
encryptor = SymmetricEncryption("パスワード123")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
class TestIntegration:
"""Integration tests"""
def test_multiple_encrypt_decrypt_cycles(self):
"""Test multiple encryption/decryption cycles"""
encryptor = SymmetricEncryption("test_password")
original = "test data"
# Encrypt and decrypt multiple times
for _ in range(5):
encrypted = encryptor.encrypt_with_metadata_return_str(original)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == original
def test_different_return_types_interoperability(self):
"""Test that different return types can be decrypted"""
encryptor = SymmetricEncryption("test_password")
original = "test data"
# Encrypt with different return types
encrypted_str = encryptor.encrypt_with_metadata_return_str(original)
encrypted_bytes = encryptor.encrypt_with_metadata_return_bytes(original)
encrypted_dict = encryptor.encrypt_with_metadata_return_dict(original)
# All should decrypt to the same value
assert encryptor.decrypt_with_metadata(encrypted_str) == original
assert encryptor.decrypt_with_metadata(encrypted_bytes) == original
assert encryptor.decrypt_with_metadata(encrypted_dict) == original
def test_cross_instance_encryption_decryption(self):
"""Test that different instances with same password can decrypt"""
encryptor1 = SymmetricEncryption("test_password")
encryptor2 = SymmetricEncryption("test_password")
encrypted = encryptor1.encrypt_with_metadata_return_str("test data")
decrypted = encryptor2.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_static_and_instance_methods_compatible(self):
"""Test that static and instance methods are compatible"""
# Encrypt with static method
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
# Decrypt with instance method
decryptor = SymmetricEncryption("test_password")
decrypted = decryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
# And vice versa
encryptor = SymmetricEncryption("test_password")
encrypted2 = encryptor.encrypt_with_metadata_return_str("test data 2")
decrypted2 = SymmetricEncryption.decrypt_data(encrypted2, "test_password")
assert decrypted2 == "test data 2"
# Parametrized tests
@pytest.mark.parametrize("data", [
"simple text",
"text with spaces and punctuation!",
"123456789",
"unicode: こんにちは",
"emoji: 🔐🔑",
"",
"a" * 1000, # Long string
])
def test_encrypt_decrypt_various_data(data: str):
"""Parametrized test for various data types"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == data
@pytest.mark.parametrize("password", [
"simple",
"with spaces",
"special!@#$%",
"unicode世界",
"123456",
"a" * 100, # Long password
])
def test_various_passwords(password: str):
"""Parametrized test for various passwords"""
encryptor = SymmetricEncryption(password)
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
@pytest.mark.parametrize("return_type,expected_type", [
("str", str),
("json", str),
("bytes", bytes),
("dict", dict),
])
def test_return_types_parametrized(return_type: str, expected_type: type):
"""Parametrized test for different return types"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as=return_type)
assert isinstance(result, expected_type)
# Fixtures
@pytest.fixture
def encryptor() -> SymmetricEncryption:
"""Fixture providing a basic encryptor instance"""
return SymmetricEncryption("test_password")
@pytest.fixture
def sample_encrypted_data(encryptor: SymmetricEncryption) -> str:
"""Fixture providing sample encrypted data"""
return encryptor.encrypt_with_metadata_return_str("sample data")
def test_with_encryptor_fixture(encryptor: SymmetricEncryption) -> None:
"""Test using encryptor fixture"""
encrypted: str = encryptor.encrypt_with_metadata_return_str("test")
decrypted: str = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test"
def test_with_encrypted_data_fixture(encryptor: SymmetricEncryption, sample_encrypted_data: str) -> None:
"""Test using encrypted data fixture"""
decrypted: str = encryptor.decrypt_with_metadata(sample_encrypted_data)
assert decrypted == "sample data"
# __END__

View File

@@ -0,0 +1,3 @@
"""
tests for json_handling module
"""

View File

@@ -0,0 +1,698 @@
"""
tests for corelibs.json_handling.json_helper
"""
import json
from datetime import datetime, date
from typing import Any
from corelibs.json_handling.json_helper import (
DateTimeEncoder,
default,
json_dumps,
modify_with_jsonpath
)
# MARK: DateTimeEncoder tests
class TestDateTimeEncoder:
"""Test cases for DateTimeEncoder class"""
def test_datetime_encoding(self):
"""Test encoding datetime objects"""
dt = datetime(2025, 10, 23, 15, 30, 45, 123456)
data = {"timestamp": dt}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45.123456"
def test_date_encoding(self):
"""Test encoding date objects"""
d = date(2025, 10, 23)
data = {"date": d}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["date"] == "2025-10-23"
def test_mixed_datetime_date_encoding(self):
"""Test encoding mixed datetime and date objects"""
dt = datetime(2025, 10, 23, 15, 30, 45)
d = date(2025, 10, 23)
data = {
"timestamp": dt,
"date": d,
"name": "test"
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45"
assert decoded["date"] == "2025-10-23"
assert decoded["name"] == "test"
def test_nested_datetime_encoding(self):
"""Test encoding nested structures with datetime objects"""
data = {
"event": {
"name": "Meeting",
"start": datetime(2025, 10, 23, 10, 0, 0),
"end": datetime(2025, 10, 23, 11, 0, 0),
"participants": [
{"name": "Alice", "joined": datetime(2025, 10, 23, 10, 5, 0)},
{"name": "Bob", "joined": datetime(2025, 10, 23, 10, 10, 0)}
]
}
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["event"]["start"] == "2025-10-23T10:00:00"
assert decoded["event"]["end"] == "2025-10-23T11:00:00"
assert decoded["event"]["participants"][0]["joined"] == "2025-10-23T10:05:00"
assert decoded["event"]["participants"][1]["joined"] == "2025-10-23T10:10:00"
def test_list_of_datetimes(self):
"""Test encoding list of datetime objects"""
data = {
"timestamps": [
datetime(2025, 10, 23, 10, 0, 0),
datetime(2025, 10, 23, 11, 0, 0),
datetime(2025, 10, 23, 12, 0, 0)
]
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamps"][0] == "2025-10-23T10:00:00"
assert decoded["timestamps"][1] == "2025-10-23T11:00:00"
assert decoded["timestamps"][2] == "2025-10-23T12:00:00"
def test_encoder_with_normal_types(self):
"""Test that encoder works with standard JSON types"""
data = {
"string": "test",
"number": 42,
"float": 3.14,
"boolean": True,
"null": None,
"list": [1, 2, 3],
"dict": {"key": "value"}
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded == data
def test_encoder_returns_none_for_unsupported_types(self):
"""Test that encoder default method returns None for unsupported types"""
encoder = DateTimeEncoder()
# The default method should return None for non-date/datetime objects
result = encoder.default("string")
assert result is None
result = encoder.default(42)
assert result is None
result = encoder.default([1, 2, 3])
assert result is None
# MARK: default function tests
class TestDefaultFunction:
"""Test cases for the default function"""
def test_default_datetime(self):
"""Test default function with datetime"""
dt = datetime(2025, 10, 23, 15, 30, 45)
result = default(dt)
assert result == "2025-10-23T15:30:45"
def test_default_date(self):
"""Test default function with date"""
d = date(2025, 10, 23)
result = default(d)
assert result == "2025-10-23"
def test_default_with_microseconds(self):
"""Test default function with datetime including microseconds"""
dt = datetime(2025, 10, 23, 15, 30, 45, 123456)
result = default(dt)
assert result == "2025-10-23T15:30:45.123456"
def test_default_returns_none_for_other_types(self):
"""Test that default returns None for non-date/datetime objects"""
assert default("string") is None
assert default(42) is None
assert default(3.14) is None
assert default(True) is None
assert default(None) is None
assert default([1, 2, 3]) is None
assert default({"key": "value"}) is None
def test_default_as_json_default_parameter(self):
"""Test using default function as default parameter in json.dumps"""
data = {
"timestamp": datetime(2025, 10, 23, 15, 30, 45),
"date": date(2025, 10, 23),
"name": "test"
}
result = json.dumps(data, default=default)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45"
assert decoded["date"] == "2025-10-23"
assert decoded["name"] == "test"
# MARK: json_dumps tests
class TestJsonDumps:
"""Test cases for json_dumps function"""
def test_basic_dict(self):
"""Test json_dumps with basic dictionary"""
data = {"name": "test", "value": 42}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_unicode_characters(self):
"""Test json_dumps preserves unicode characters (ensure_ascii=False)"""
data = {"name": "テスト", "emoji": "🎉", "chinese": "测试"}
result = json_dumps(data)
# ensure_ascii=False means unicode characters should be preserved
assert "テスト" in result
assert "🎉" in result
assert "测试" in result
decoded = json.loads(result)
assert decoded == data
def test_datetime_objects_as_string(self):
"""Test json_dumps converts datetime to string (default=str)"""
dt = datetime(2025, 10, 23, 15, 30, 45)
data = {"timestamp": dt}
result = json_dumps(data)
decoded = json.loads(result)
# default=str will convert datetime to its string representation
assert isinstance(decoded["timestamp"], str)
assert "2025-10-23" in decoded["timestamp"]
def test_date_objects_as_string(self):
"""Test json_dumps converts date to string"""
d = date(2025, 10, 23)
data = {"date": d}
result = json_dumps(data)
decoded = json.loads(result)
assert isinstance(decoded["date"], str)
assert "2025-10-23" in decoded["date"]
def test_complex_nested_structure(self):
"""Test json_dumps with complex nested structures"""
data = {
"user": {
"name": "John",
"age": 30,
"active": True,
"balance": 100.50,
"tags": ["admin", "user"],
"metadata": {
"created": datetime(2025, 1, 1, 0, 0, 0),
"updated": date(2025, 10, 23)
}
},
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
]
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["user"]["name"] == "John"
assert decoded["user"]["age"] == 30
assert decoded["user"]["active"] is True
assert decoded["user"]["balance"] == 100.50
assert decoded["user"]["tags"] == ["admin", "user"]
assert decoded["items"][0]["id"] == 1
def test_empty_dict(self):
"""Test json_dumps with empty dictionary"""
data: dict[str, Any] = {}
result = json_dumps(data)
assert result == "{}"
def test_empty_list(self):
"""Test json_dumps with empty list"""
data: list[Any] = []
result = json_dumps(data)
assert result == "[]"
def test_list_data(self):
"""Test json_dumps with list as root element"""
data = [1, 2, 3, "test", True, None]
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_none_value(self):
"""Test json_dumps with None"""
data = None
result = json_dumps(data)
assert result == "null"
def test_boolean_values(self):
"""Test json_dumps with boolean values"""
data = {"true_val": True, "false_val": False}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["true_val"] is True
assert decoded["false_val"] is False
def test_numeric_values(self):
"""Test json_dumps with various numeric values"""
data = {
"int": 42,
"float": 3.14,
"negative": -10,
"zero": 0,
"scientific": 1e10
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_custom_object_conversion(self):
"""Test json_dumps with custom objects (converted via str)"""
class CustomObject:
"""test class"""
def __str__(self):
return "custom_value"
data = {"custom": CustomObject()}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["custom"] == "custom_value"
def test_special_float_values(self):
"""Test json_dumps handles special float values"""
data = {
"infinity": float('inf'),
"neg_infinity": float('-inf'),
"nan": float('nan')
}
result = json_dumps(data)
# These should be converted to strings via default=str
assert "Infinity" in result or "inf" in result.lower()
# MARK: modify_with_jsonpath tests
class TestModifyWithJsonpath:
"""Test cases for modify_with_jsonpath function"""
def test_simple_path_modification(self):
"""Test modifying a simple path"""
data = {"name": "old_name", "age": 30}
result = modify_with_jsonpath(data, "$.name", "new_name")
assert result["name"] == "new_name"
assert result["age"] == 30
# Original data should not be modified
assert data["name"] == "old_name"
def test_nested_path_modification(self):
"""Test modifying nested path"""
data = {
"user": {
"profile": {
"name": "John",
"age": 30
}
}
}
result = modify_with_jsonpath(data, "$.user.profile.name", "Jane")
assert result["user"]["profile"]["name"] == "Jane"
assert result["user"]["profile"]["age"] == 30
# Original should be unchanged
assert data["user"]["profile"]["name"] == "John"
def test_array_index_modification(self):
"""Test modifying array element by index"""
data = {
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
]
}
result = modify_with_jsonpath(data, "$.items[1].name", "Updated Item 2")
assert result["items"][1]["name"] == "Updated Item 2"
assert result["items"][0]["name"] == "Item 1"
assert result["items"][2]["name"] == "Item 3"
# Original unchanged
assert data["items"][1]["name"] == "Item 2"
def test_wildcard_modification(self):
"""Test modifying multiple elements with wildcard"""
data = {
"users": [
{"name": "Alice", "active": True},
{"name": "Bob", "active": True},
{"name": "Charlie", "active": True}
]
}
result = modify_with_jsonpath(data, "$.users[*].active", False)
# All active fields should be updated
for user in result["users"]:
assert user["active"] is False
# Original unchanged
for user in data["users"]:
assert user["active"] is True
def test_deep_copy_behavior(self):
"""Test that modifications don't affect the original data"""
original = {
"level1": {
"level2": {
"level3": {
"value": "original"
}
}
}
}
result = modify_with_jsonpath(original, "$.level1.level2.level3.value", "modified")
assert result["level1"]["level2"]["level3"]["value"] == "modified"
assert original["level1"]["level2"]["level3"]["value"] == "original"
# Verify deep copy by modifying nested dict in result
result["level1"]["level2"]["new_key"] = "new_value"
assert "new_key" not in original["level1"]["level2"]
def test_modify_to_different_type(self):
"""Test changing value to different type"""
data = {"count": "10"}
result = modify_with_jsonpath(data, "$.count", 10)
assert result["count"] == 10
assert isinstance(result["count"], int)
assert data["count"] == "10"
def test_modify_to_complex_object(self):
"""Test replacing value with complex object"""
data = {"simple": "value"}
new_value = {"complex": {"nested": "structure"}}
result = modify_with_jsonpath(data, "$.simple", new_value)
assert result["simple"] == new_value
assert result["simple"]["complex"]["nested"] == "structure"
def test_modify_to_list(self):
"""Test replacing value with list"""
data = {"items": None}
result = modify_with_jsonpath(data, "$.items", [1, 2, 3])
assert result["items"] == [1, 2, 3]
assert data["items"] is None
def test_modify_to_none(self):
"""Test setting value to None"""
data = {"value": "something"}
result = modify_with_jsonpath(data, "$.value", None)
assert result["value"] is None
assert data["value"] == "something"
def test_recursive_descent(self):
"""Test using recursive descent operator"""
data: dict[str, Any] = {
"store": {
"book": [
{"title": "Book 1", "price": 10},
{"title": "Book 2", "price": 20}
],
"bicycle": {
"price": 100
}
}
}
# Update all prices
result = modify_with_jsonpath(data, "$..price", 0)
assert result["store"]["book"][0]["price"] == 0
assert result["store"]["book"][1]["price"] == 0
assert result["store"]["bicycle"]["price"] == 0
# Original unchanged
assert data["store"]["book"][0]["price"] == 10
def test_specific_array_elements(self):
"""Test updating specific array elements by index"""
data = {
"products": [
{"name": "Product 1", "price": 100, "stock": 5},
{"name": "Product 2", "price": 200, "stock": 0},
{"name": "Product 3", "price": 150, "stock": 10}
]
}
# Update first product's price
result = modify_with_jsonpath(data, "$.products[0].price", 0)
assert result["products"][0]["price"] == 0
assert result["products"][1]["price"] == 200 # not modified
assert result["products"][2]["price"] == 150 # not modified
def test_empty_dict(self):
"""Test modifying empty dictionary"""
data: dict[str, Any] = {}
result = modify_with_jsonpath(data, "$.nonexistent", "value")
# Should return the original empty dict since path doesn't exist
assert result == {}
def test_complex_real_world_scenario(self):
"""Test complex real-world modification scenario"""
data: dict[str, Any] = {
"api_version": "1.0",
"config": {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"username": "admin",
"password": "secret"
}
},
"services": [
{"name": "auth", "enabled": True, "port": 8001},
{"name": "api", "enabled": True, "port": 8002},
{"name": "cache", "enabled": False, "port": 8003}
]
}
}
# Update database port
result = modify_with_jsonpath(data, "$.config.database.port", 5433)
assert result["config"]["database"]["port"] == 5433
# Update all service ports
result2 = modify_with_jsonpath(result, "$.config.services[*].enabled", True)
assert all(service["enabled"] for service in result2["config"]["services"])
# Original unchanged
assert data["config"]["database"]["port"] == 5432
assert data["config"]["services"][2]["enabled"] is False
def test_list_slice_modification(self):
"""Test modifying list slice"""
data = {"numbers": [1, 2, 3, 4, 5]}
# Modify first three elements
result = modify_with_jsonpath(data, "$.numbers[0:3]", 0)
assert result["numbers"][0] == 0
assert result["numbers"][1] == 0
assert result["numbers"][2] == 0
assert result["numbers"][3] == 4
assert result["numbers"][4] == 5
def test_modify_with_datetime_value(self):
"""Test modifying with datetime value"""
data = {"timestamp": "2025-01-01T00:00:00"}
new_datetime = datetime(2025, 10, 23, 15, 30, 45)
result = modify_with_jsonpath(data, "$.timestamp", new_datetime)
assert result["timestamp"] == new_datetime
assert isinstance(result["timestamp"], datetime)
# MARK: Integration tests
class TestIntegration:
"""Integration tests combining multiple functions"""
def test_encoder_and_json_dumps_comparison(self):
"""Test that DateTimeEncoder and json_dumps handle datetimes differently"""
dt = datetime(2025, 10, 23, 15, 30, 45)
data = {"timestamp": dt}
# Using DateTimeEncoder produces ISO format
with_encoder = json.dumps(data, cls=DateTimeEncoder)
decoded_encoder = json.loads(with_encoder)
assert decoded_encoder["timestamp"] == "2025-10-23T15:30:45"
# Using json_dumps (default=str) produces string representation
with_dumps = json_dumps(data)
decoded_dumps = json.loads(with_dumps)
assert isinstance(decoded_dumps["timestamp"], str)
assert "2025-10-23" in decoded_dumps["timestamp"]
def test_modify_and_serialize(self):
"""Test modifying data and then serializing it"""
data = {
"event": {
"name": "Meeting",
"date": date(2025, 10, 23),
"attendees": [
{"name": "Alice", "confirmed": False},
{"name": "Bob", "confirmed": False}
]
}
}
# Modify confirmation status
modified = modify_with_jsonpath(data, "$.event.attendees[*].confirmed", True)
# Serialize with datetime handling
serialized = json.dumps(modified, cls=DateTimeEncoder)
decoded = json.loads(serialized)
assert decoded["event"]["date"] == "2025-10-23"
assert decoded["event"]["attendees"][0]["confirmed"] is True
assert decoded["event"]["attendees"][1]["confirmed"] is True
def test_round_trip_with_modification(self):
"""Test full round trip: serialize -> modify -> serialize"""
original = {
"config": {
"updated": datetime(2025, 10, 23, 15, 30, 45),
"version": "1.0"
}
}
# Serialize
json_str = json.dumps(original, cls=DateTimeEncoder)
# Deserialize
deserialized = json.loads(json_str)
# Modify
modified = modify_with_jsonpath(deserialized, "$.config.version", "2.0")
# Serialize again
final_json = json_dumps(modified)
final_data = json.loads(final_json)
assert final_data["config"]["version"] == "2.0"
assert final_data["config"]["updated"] == "2025-10-23T15:30:45"
# MARK: Edge cases
class TestEdgeCases:
"""Test edge cases and error scenarios"""
def test_circular_reference_in_modify(self):
"""Test that modify_with_jsonpath handles data without circular references"""
# Note: JSON doesn't support circular references, so we test normal nested data
data = {
"a": {
"b": {
"c": "value"
}
}
}
result = modify_with_jsonpath(data, "$.a.b.c", "new_value")
assert result["a"]["b"]["c"] == "new_value"
def test_unicode_in_keys_and_values(self):
"""Test handling unicode in both keys and values"""
data = {
"日本語": "テスト",
"emoji_🎉": "🚀",
"normal": "value"
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["日本語"] == "テスト"
assert decoded["emoji_🎉"] == "🚀"
assert decoded["normal"] == "value"
def test_very_nested_structure(self):
"""Test deeply nested structure"""
# Create a 10-level deep nested structure
data: dict[str, Any] = {"level0": {}}
current = data["level0"]
for i in range(1, 10):
current[f"level{i}"] = {}
current = current[f"level{i}"]
current["value"] = "deep_value"
result = modify_with_jsonpath(data, "$..value", "modified_deep_value")
# Navigate to the deep value
current = result["level0"]
for i in range(1, 10):
current = current[f"level{i}"]
assert current["value"] == "modified_deep_value"
def test_large_list_modification(self):
"""Test modifying large list"""
data = {"items": [{"id": i, "value": i * 10} for i in range(100)]}
result = modify_with_jsonpath(data, "$.items[*].value", 0)
assert all(item["value"] == 0 for item in result["items"])
assert len(result["items"]) == 100
def test_mixed_date_types_encoding(self):
"""Test encoding with both date and datetime in same structure"""
data = {
"created_date": date(2025, 10, 23),
"created_datetime": datetime(2025, 10, 23, 15, 30, 45),
"updated_date": date(2025, 10, 24),
"updated_datetime": datetime(2025, 10, 24, 16, 45, 30)
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["created_date"] == "2025-10-23"
assert decoded["created_datetime"] == "2025-10-23T15:30:45"
assert decoded["updated_date"] == "2025-10-24"
assert decoded["updated_datetime"] == "2025-10-24T16:45:30"

View File

@@ -0,0 +1,3 @@
"""
var_handling tests
"""

View File

@@ -0,0 +1,546 @@
"""
var_handling.enum_base tests
"""
from typing import Any
import pytest
from corelibs.var_handling.enum_base import EnumBase
class SampleBlock(EnumBase):
"""Sample block enum for testing purposes"""
BLOCK_A = "block_a"
BLOCK_B = "block_b"
HAS_NUM = 5
HAS_FLOAT = 3.14
LEGACY_KEY = "legacy_value"
class SimpleEnum(EnumBase):
"""Simple enum with string values"""
OPTION_ONE = "one"
OPTION_TWO = "two"
OPTION_THREE = "three"
class NumericEnum(EnumBase):
"""Enum with only numeric values"""
FIRST = 1
SECOND = 2
THIRD = 3
class TestEnumBaseLookupKey:
"""Test cases for lookup_key class method"""
def test_lookup_key_valid_uppercase(self):
"""Test lookup_key with valid uppercase key"""
result = SampleBlock.lookup_key("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_key_valid_lowercase(self):
"""Test lookup_key with valid lowercase key (should convert to uppercase)"""
result = SampleBlock.lookup_key("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_valid_mixed_case(self):
"""Test lookup_key with mixed case key"""
result = SampleBlock.lookup_key("BlOcK_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_with_numeric_enum(self):
"""Test lookup_key with numeric enum member"""
result = SampleBlock.lookup_key("HAS_NUM")
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_lookup_key_legacy_colon_replacement(self):
"""Test lookup_key with legacy colon format (converts : to ___)"""
# This assumes the enum has a key that might be accessed with legacy format
# Should convert : to ___ and look up LEGACY___KEY
# Since we don't have this key, we test the behavior with a valid conversion
# Let's test with a known key that would work
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("BLOCK:A") # Should fail as BLOCK___A doesn't exist
def test_lookup_key_invalid_key(self):
"""Test lookup_key with invalid key"""
with pytest.raises(ValueError, match="Invalid key: NONEXISTENT"):
SampleBlock.lookup_key("NONEXISTENT")
def test_lookup_key_empty_string(self):
"""Test lookup_key with empty string"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("")
def test_lookup_key_with_special_characters(self):
"""Test lookup_key with special characters that might cause AttributeError"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("@#$%")
def test_lookup_key_numeric_string(self):
"""Test lookup_key with numeric string that isn't a key"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("123")
class TestEnumBaseLookupValue:
"""Test cases for lookup_value class method"""
def test_lookup_value_valid_string(self):
"""Test lookup_value with valid string value"""
result = SampleBlock.lookup_value("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_value_valid_integer(self):
"""Test lookup_value with valid integer value"""
result = SampleBlock.lookup_value(5)
assert result == SampleBlock.HAS_NUM
assert result.name == "HAS_NUM"
assert result.value == 5
def test_lookup_value_valid_float(self):
"""Test lookup_value with valid float value"""
result = SampleBlock.lookup_value(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.name == "HAS_FLOAT"
assert result.value == 3.14
def test_lookup_value_invalid_string(self):
"""Test lookup_value with invalid string value"""
with pytest.raises(ValueError, match="Invalid value: nonexistent"):
SampleBlock.lookup_value("nonexistent")
def test_lookup_value_invalid_integer(self):
"""Test lookup_value with invalid integer value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.lookup_value(999)
def test_lookup_value_case_sensitive(self):
"""Test that lookup_value is case-sensitive for string values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value("BLOCK_A") # Value is "block_a", not "BLOCK_A"
class TestEnumBaseFromAny:
"""Test cases for from_any class method"""
def test_from_any_with_enum_instance(self):
"""Test from_any with an enum instance (should return as-is)"""
enum_instance = SampleBlock.BLOCK_A
result = SampleBlock.from_any(enum_instance)
assert result is enum_instance
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_key(self):
"""Test from_any with string that matches a key"""
result = SampleBlock.from_any("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_from_any_with_string_as_key_lowercase(self):
"""Test from_any with lowercase string key"""
result = SampleBlock.from_any("block_a")
# Should first try as key (convert to uppercase and find BLOCK_A)
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_value(self):
"""Test from_any with string that only matches a value"""
# Use a value that isn't also a valid key
result = SampleBlock.from_any("block_b")
# Should try key first (fail), then value (succeed)
assert result == SampleBlock.BLOCK_B
assert result.value == "block_b"
def test_from_any_with_integer(self):
"""Test from_any with integer value"""
result = SampleBlock.from_any(5)
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_from_any_with_float(self):
"""Test from_any with float value"""
result = SampleBlock.from_any(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.value == 3.14
def test_from_any_with_invalid_string(self):
"""Test from_any with string that doesn't match key or value"""
with pytest.raises(ValueError, match="Could not find as key or value: invalid_string"):
SampleBlock.from_any("invalid_string")
def test_from_any_with_invalid_integer(self):
"""Test from_any with integer that doesn't match any value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.from_any(999)
def test_from_any_string_key_priority(self):
"""Test that from_any tries key lookup before value for strings"""
# Create an enum where a value matches another key
class AmbiguousEnum(EnumBase):
KEY_A = "key_b" # Value is the name of another key
KEY_B = "value_b"
# When we look up "KEY_B", it should find it as a key, not as value "key_b"
result = AmbiguousEnum.from_any("KEY_B")
assert result == AmbiguousEnum.KEY_B
assert result.value == "value_b"
class TestEnumBaseToValue:
"""Test cases for to_value instance method"""
def test_to_value_string_value(self):
"""Test to_value with string enum value"""
result = SampleBlock.BLOCK_A.to_value()
assert result == "block_a"
assert isinstance(result, str)
def test_to_value_integer_value(self):
"""Test to_value with integer enum value"""
result = SampleBlock.HAS_NUM.to_value()
assert result == 5
assert isinstance(result, int)
def test_to_value_float_value(self):
"""Test to_value with float enum value"""
result = SampleBlock.HAS_FLOAT.to_value()
assert result == 3.14
assert isinstance(result, float)
def test_to_value_equals_value_attribute(self):
"""Test that to_value returns the same as .value"""
enum_instance = SampleBlock.BLOCK_A
assert enum_instance.to_value() == enum_instance.value
class TestEnumBaseToLowerCase:
"""Test cases for to_lower_case instance method"""
def test_to_lower_case_uppercase_name(self):
"""Test to_lower_case with uppercase enum name"""
result = SampleBlock.BLOCK_A.to_lower_case()
assert result == "block_a"
assert isinstance(result, str)
def test_to_lower_case_mixed_name(self):
"""Test to_lower_case with name containing underscores"""
result = SampleBlock.HAS_NUM.to_lower_case()
assert result == "has_num"
def test_to_lower_case_consistency(self):
"""Test that to_lower_case always returns lowercase"""
for member in SampleBlock:
result = member.to_lower_case()
assert result == result.lower()
assert result == member.name.lower()
class TestEnumBaseStrMethod:
"""Test cases for __str__ magic method"""
def test_str_returns_name(self):
"""Test that str() returns the enum name"""
result = str(SampleBlock.BLOCK_A)
assert result == "BLOCK_A"
assert result == SampleBlock.BLOCK_A.name
def test_str_all_members(self):
"""Test str() for all enum members"""
for member in SampleBlock:
result = str(member)
assert result == member.name
assert isinstance(result, str)
def test_str_in_formatting(self):
"""Test that str works in string formatting"""
formatted = f"Enum: {SampleBlock.BLOCK_A}"
assert formatted == "Enum: BLOCK_A"
def test_str_vs_repr(self):
"""Test difference between str and repr"""
enum_instance = SampleBlock.BLOCK_A
str_result = str(enum_instance)
repr_result = repr(enum_instance)
assert str_result == "BLOCK_A"
# repr should include class name
assert "SampleBlock" in repr_result
# Parametrized tests for comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("key,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("BLOCK_B", SampleBlock.BLOCK_B),
("HAS_NUM", SampleBlock.HAS_NUM),
("has_num", SampleBlock.HAS_NUM),
("HAS_FLOAT", SampleBlock.HAS_FLOAT),
])
def test_lookup_key_parametrized(self, key: str, expected_member: EnumBase):
"""Test lookup_key with various valid keys"""
result = SampleBlock.lookup_key(key)
assert result == expected_member
@pytest.mark.parametrize("value,expected_member", [
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
("legacy_value", SampleBlock.LEGACY_KEY),
])
def test_lookup_value_parametrized(self, value: Any, expected_member: EnumBase):
"""Test lookup_value with various valid values"""
result = SampleBlock.lookup_value(value)
assert result == expected_member
@pytest.mark.parametrize("input_any,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
(SampleBlock.BLOCK_A, SampleBlock.BLOCK_A), # Pass enum instance
])
def test_from_any_parametrized(self, input_any: Any, expected_member: EnumBase):
"""Test from_any with various valid inputs"""
result = SampleBlock.from_any(input_any)
assert result == expected_member
@pytest.mark.parametrize("invalid_key", [
"NONEXISTENT",
"invalid",
"123",
"",
"BLOCK_C",
])
def test_lookup_key_invalid_parametrized(self, invalid_key: str):
"""Test lookup_key with various invalid keys"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key(invalid_key)
@pytest.mark.parametrize("invalid_value", [
"nonexistent",
999,
-1,
0.0,
"BLOCK_A", # This is a key name, not a value
])
def test_lookup_value_invalid_parametrized(self, invalid_value: Any):
"""Test lookup_value with various invalid values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value(invalid_value)
# Edge cases and special scenarios
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_enum_with_single_member(self):
"""Test EnumBase with only one member"""
class SingleEnum(EnumBase):
ONLY_ONE = "single"
result = SingleEnum.from_any("ONLY_ONE")
assert result == SingleEnum.ONLY_ONE
assert result.to_value() == "single"
def test_enum_iteration(self):
"""Test iterating over enum members"""
members = list(SampleBlock)
assert len(members) == 5
assert SampleBlock.BLOCK_A in members
assert SampleBlock.BLOCK_B in members
assert SampleBlock.HAS_NUM in members
def test_enum_membership(self):
"""Test checking membership in enum"""
assert SampleBlock.BLOCK_A in SampleBlock
assert SampleBlock.HAS_NUM in SampleBlock
def test_enum_comparison(self):
"""Test comparing enum members"""
assert SampleBlock.BLOCK_A == SampleBlock.BLOCK_A
assert SampleBlock.BLOCK_A != SampleBlock.BLOCK_B
assert SampleBlock.from_any("BLOCK_A") == SampleBlock.BLOCK_A
def test_enum_identity(self):
"""Test enum member identity"""
member1 = SampleBlock.BLOCK_A
member2 = SampleBlock.lookup_key("BLOCK_A")
member3 = SampleBlock.from_any("BLOCK_A")
assert member1 is member2
assert member1 is member3
assert member2 is member3
def test_different_enum_classes(self):
"""Test that different enum classes are distinct"""
# Even if they have same keys/values, they're different
class OtherEnum(EnumBase):
BLOCK_A = "block_a"
result1 = SampleBlock.from_any("BLOCK_A")
result2 = OtherEnum.from_any("BLOCK_A")
assert result1 != result2
assert not isinstance(result1, type(result2))
def test_numeric_enum_operations(self):
"""Test operations specific to numeric enums"""
assert NumericEnum.FIRST.to_value() == 1
assert NumericEnum.SECOND.to_value() == 2
assert NumericEnum.THIRD.to_value() == 3
# Test from_any with integers
assert NumericEnum.from_any(1) == NumericEnum.FIRST
assert NumericEnum.from_any(2) == NumericEnum.SECOND
def test_mixed_value_types_in_same_enum(self):
"""Test enum with mixed value types"""
# SampleBlock already has mixed types (strings, int, float)
assert isinstance(SampleBlock.BLOCK_A.to_value(), str)
assert isinstance(SampleBlock.HAS_NUM.to_value(), int)
assert isinstance(SampleBlock.HAS_FLOAT.to_value(), float)
def test_from_any_chained_calls(self):
"""Test that from_any can be chained (idempotent)"""
result1 = SampleBlock.from_any("BLOCK_A")
result2 = SampleBlock.from_any(result1)
result3 = SampleBlock.from_any(result2)
assert result1 == result2 == result3
assert result1 is result2 is result3
# Integration tests
class TestIntegration:
"""Integration tests combining multiple methods"""
def test_round_trip_key_lookup(self):
"""Test round-trip from key to enum and back"""
original_key = "BLOCK_A"
enum_member = SampleBlock.lookup_key(original_key)
result_name = str(enum_member)
assert result_name == original_key
def test_round_trip_value_lookup(self):
"""Test round-trip from value to enum and back"""
original_value = "block_a"
enum_member = SampleBlock.lookup_value(original_value)
result_value = enum_member.to_value()
assert result_value == original_value
def test_from_any_workflow(self):
"""Test realistic workflow using from_any"""
# Simulate receiving various types of input
inputs = [
"BLOCK_A", # Key as string
"block_b", # Value as string
5, # Numeric value
SampleBlock.HAS_FLOAT, # Already an enum
]
expected = [
SampleBlock.BLOCK_A,
SampleBlock.BLOCK_B,
SampleBlock.HAS_NUM,
SampleBlock.HAS_FLOAT,
]
for input_val, expected_val in zip(inputs, expected):
result = SampleBlock.from_any(input_val)
assert result == expected_val
def test_enum_in_dictionary(self):
"""Test using enum as dictionary key"""
enum_dict = {
SampleBlock.BLOCK_A: "Value A",
SampleBlock.BLOCK_B: "Value B",
SampleBlock.HAS_NUM: "Value Num",
}
assert enum_dict[SampleBlock.BLOCK_A] == "Value A"
block_b = SampleBlock.from_any("BLOCK_B")
assert isinstance(block_b, SampleBlock)
assert enum_dict[block_b] == "Value B"
def test_enum_in_set(self):
"""Test using enum in a set"""
enum_set = {SampleBlock.BLOCK_A, SampleBlock.BLOCK_B, SampleBlock.BLOCK_A}
assert len(enum_set) == 2 # BLOCK_A should be deduplicated
assert SampleBlock.BLOCK_A in enum_set
assert SampleBlock.from_any("BLOCK_B") in enum_set
# Real-world usage scenarios
class TestRealWorldScenarios:
"""Test real-world usage scenarios from enum_test.py"""
def test_original_enum_test_scenario(self):
"""Test the scenario from the original enum_test.py"""
# BLOCK A: {SampleBlock.from_any('BLOCK_A')}
result_a = SampleBlock.from_any('BLOCK_A')
assert result_a == SampleBlock.BLOCK_A
assert str(result_a) == "BLOCK_A"
# HAS NUM: {SampleBlock.from_any(5)}
result_num = SampleBlock.from_any(5)
assert result_num == SampleBlock.HAS_NUM
assert result_num.to_value() == 5
# DIRECT BLOCK: {SampleBlock.BLOCK_A.name} -> {SampleBlock.BLOCK_A.value}
assert SampleBlock.BLOCK_A.name == "BLOCK_A"
assert SampleBlock.BLOCK_A.value == "block_a"
def test_config_value_parsing(self):
"""Test parsing values from configuration (common use case)"""
# Simulate config values that might come as strings
config_values = ["OPTION_ONE", "option_two", "OPTION_THREE"]
results = [SimpleEnum.from_any(val) for val in config_values]
assert results[0] == SimpleEnum.OPTION_ONE
assert results[1] == SimpleEnum.OPTION_TWO
assert results[2] == SimpleEnum.OPTION_THREE
def test_api_response_mapping(self):
"""Test mapping API response values to enum"""
# Simulate API returning numeric codes
api_codes = [1, 2, 3]
results = [NumericEnum.from_any(code) for code in api_codes]
assert results[0] == NumericEnum.FIRST
assert results[1] == NumericEnum.SECOND
assert results[2] == NumericEnum.THIRD
def test_validation_with_error_handling(self):
"""Test validation with proper error handling"""
valid_input = "BLOCK_A"
invalid_input = "INVALID"
# Valid input should work
result = SampleBlock.from_any(valid_input)
assert result == SampleBlock.BLOCK_A
# Invalid input should raise ValueError
try:
SampleBlock.from_any(invalid_input)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Could not find as key or value" in str(e)
assert "INVALID" in str(e)

373
uv.lock generated
View File

@@ -4,42 +4,97 @@ requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2025.8.3"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.3"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
@@ -53,102 +108,184 @@ wheels = [
[[package]]
name = "corelibs"
version = "0.24.3"
version = "0.28.0"
source = { editable = "." }
dependencies = [
{ name = "cryptography" },
{ name = "jmespath" },
{ name = "jsonpath-ng" },
{ name = "psutil" },
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "deepdiff" },
{ name = "pytest" },
{ name = "pytest-cov" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "jmespath", specifier = ">=1.0.1" },
{ name = "jsonpath-ng", specifier = ">=1.7.0" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "requests", specifier = ">=2.32.4" },
]
[package.metadata.requires-dev]
dev = [
{ name = "deepdiff", specifier = ">=8.6.1" },
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
]
[[package]]
name = "coverage"
version = "7.10.5"
version = "7.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" },
{ url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" },
{ url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" },
{ url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" },
{ url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" },
{ url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" },
{ url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" },
{ url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" },
{ url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" },
{ url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" },
{ url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" },
{ url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" },
{ url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" },
{ url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" },
{ url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" },
{ url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" },
{ url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" },
{ url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" },
{ url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" },
{ url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" },
{ url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" },
{ url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" },
{ url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" },
{ url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" },
{ url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" },
{ url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" },
{ url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" },
{ url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" },
{ url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" },
{ url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" },
{ url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" },
{ url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" },
{ url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" },
{ url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" },
{ url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" },
{ url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" },
{ url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" },
{ url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" },
{ url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" },
{ url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" },
{ url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" },
{ url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" },
{ url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" },
{ url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" },
{ url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" },
{ url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" },
{ url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" },
{ url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" },
{ url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" },
{ url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" },
{ url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" },
{ url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" },
{ url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" },
{ url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" },
{ url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" },
{ url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" },
{ url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" },
{ url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" },
{ url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" },
{ url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" },
{ url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" },
{ url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" },
{ url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" },
{ url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" },
{ url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" },
{ url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" },
{ url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" },
{ url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" },
{ url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" },
{ url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" },
{ url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" },
{ url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" },
{ url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" },
{ url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" },
{ url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" },
{ url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[package]]
name = "deepdiff"
version = "8.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "orderly-set" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" },
]
[[package]]
name = "idna"
version = "3.10"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
@@ -160,6 +297,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
[[package]]
name = "jsonpath-ng"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ply" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" },
]
[[package]]
name = "orderly-set"
version = "5.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" },
]
[[package]]
name = "packaging"
version = "25.0"
@@ -179,18 +337,37 @@ wheels = [
]
[[package]]
name = "psutil"
version = "7.0.0"
name = "ply"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
{ url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" },
]
[[package]]
name = "psutil"
version = "7.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/89/fc/889242351a932d6183eec5df1fc6539b6f36b6a88444f1e63f18668253aa/psutil-7.1.1.tar.gz", hash = "sha256:092b6350145007389c1cfe5716050f02030a05219d90057ea867d18fe8d372fc", size = 487067, upload-time = "2025-10-19T15:43:59.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/30/f97f8fb1f9ecfbeae4b5ca738dcae66ab28323b5cfbc96cb5565f3754056/psutil-7.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8fa59d7b1f01f0337f12cd10dbd76e4312a4d3c730a4fedcbdd4e5447a8b8460", size = 244221, upload-time = "2025-10-19T15:44:03.145Z" },
{ url = "https://files.pythonhosted.org/packages/7b/98/b8d1f61ebf35f4dbdbaabadf9208282d8adc820562f0257e5e6e79e67bf2/psutil-7.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a95104eae85d088891716db676f780c1404fc15d47fde48a46a5d61e8f5ad2c", size = 245660, upload-time = "2025-10-19T15:44:05.657Z" },
{ url = "https://files.pythonhosted.org/packages/f0/4a/b8015d7357fefdfe34bc4a3db48a107bae4bad0b94fb6eb0613f09a08ada/psutil-7.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98629cd8567acefcc45afe2f4ba1e9290f579eacf490a917967decce4b74ee9b", size = 286963, upload-time = "2025-10-19T15:44:08.877Z" },
{ url = "https://files.pythonhosted.org/packages/3d/3c/b56076bb35303d0733fc47b110a1c9cce081a05ae2e886575a3587c1ee76/psutil-7.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ebc58030fb054fa0f26c3206ef01c31c29d67aee1367e3483c16665c25c8d2", size = 290118, upload-time = "2025-10-19T15:44:11.897Z" },
{ url = "https://files.pythonhosted.org/packages/dc/af/c13d360c0adc6f6218bf9e2873480393d0f729c8dd0507d171f53061c0d3/psutil-7.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146a704f224fb2ded2be3da5ac67fc32b9ea90c45b51676f9114a6ac45616967", size = 292587, upload-time = "2025-10-19T15:44:14.67Z" },
{ url = "https://files.pythonhosted.org/packages/90/2d/c933e7071ba60c7862813f2c7108ec4cf8304f1c79660efeefd0de982258/psutil-7.1.1-cp37-abi3-win32.whl", hash = "sha256:295c4025b5cd880f7445e4379e6826f7307e3d488947bf9834e865e7847dc5f7", size = 243772, upload-time = "2025-10-19T15:44:16.938Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/11fd213fff15427bc2853552138760c720fd65032d99edfb161910d04127/psutil-7.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:9b4f17c5f65e44f69bd3a3406071a47b79df45cf2236d1f717970afcb526bcd3", size = 246936, upload-time = "2025-10-19T15:44:18.663Z" },
{ url = "https://files.pythonhosted.org/packages/0a/8d/8a9a45c8b655851f216c1d44f68e3533dc8d2c752ccd0f61f1aa73be4893/psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a", size = 243944, upload-time = "2025-10-19T15:44:20.666Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
@@ -204,7 +381,7 @@ wheels = [
[[package]]
name = "pytest"
version = "8.4.1"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -213,23 +390,23 @@ dependencies = [
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-cov"
version = "6.2.1"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]