Compare commits

...

22 Commits

Author SHA1 Message Date
Clemens Schwaighofer
bf83c1c394 v0.29.0: Add SQLite IO class 2025-10-23 15:24:17 +09:00
Clemens Schwaighofer
84ce43ab93 Add SQLite IO class
This is a very basic class without many helper functions added yet
Add to the CoreLibs so when we develop it further it can be used by all projects
2025-10-23 15:22:12 +09:00
Clemens Schwaighofer
5e0765ee24 Rename the enum_test to enum_base for the test run file 2025-10-23 14:32:52 +09:00
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
Clemens Schwaighofer
ed22105ec8 v0.24.4: Fix Zone info data in TimestampStrings class 2025-09-25 15:54:54 +09:00
Clemens Schwaighofer
7c5af588c7 Update the TimestampStrings zone info handling
time_zone is the string version of the time zone data
time_zone_zi is the ZoneInfo object of above
2025-09-25 15:53:26 +09:00
Clemens Schwaighofer
2690a285d9 v0.24.3: Pytest fixes 2025-09-25 15:38:29 +09:00
Clemens Schwaighofer
bb60a570d0 Change the TimestampStrings check to check for str instead of not ZoneInfo.
This fixes the pytest problem which threw:
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

during Mocking
2025-09-25 15:36:47 +09:00
Clemens Schwaighofer
ca0ab2d7d1 v0.24.2: TimestampString allows ZoneInfo object as zone name 2025-09-25 15:16:19 +09:00
Clemens Schwaighofer
38bae7fb46 TimestampStrings allows ZoneInfo object as time_zone parameter
So we can use pre-parsed data

Some tests for parsing settings, timestamp output
2025-09-25 15:14:40 +09:00
32 changed files with 4238 additions and 117 deletions

View File

@@ -1,12 +1,14 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.24.1"
version = "0.29.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",
]
@@ -60,3 +63,7 @@ ignore = [
[tool.pylint.MASTER]
# this is for the tests/etc folders
init-hook='import sys; sys.path.append("src/")'
[tool.pytest.ini_options]
testpaths = [
"tests",
]

View File

View File

@@ -0,0 +1,214 @@
"""
SQLite DB::IO
Will be moved to the CoreLibs
also method names are subject to change
"""
# import gc
from pathlib import Path
from typing import Any, Literal, TYPE_CHECKING
import sqlite3
from corelibs.debug_handling.debug_helpers import call_stack
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger
class SQLiteIO():
"""Mini SQLite interface"""
def __init__(
self,
log: 'Logger',
db_name: str | Path,
autocommit: bool = False,
enable_fkey: bool = True,
row_factory: str | None = None
):
self.log = log
self.db_name = db_name
self.autocommit = autocommit
self.enable_fkey = enable_fkey
self.row_factory = row_factory
self.conn: sqlite3.Connection | None = self.db_connect()
# def __del__(self):
# self.db_close()
def db_connect(self) -> sqlite3.Connection | None:
"""
Connect to SQLite database, create if it doesn't exist
"""
try:
# Connect to database (creates if doesn't exist)
self.conn = sqlite3.connect(self.db_name, autocommit=self.autocommit)
self.conn.setconfig(sqlite3.SQLITE_DBCONFIG_ENABLE_FKEY, True)
# self.conn.execute("PRAGMA journal_mode=WAL")
# self.log.debug(f"Connected to database: {self.db_name}")
def dict_factory(cursor: sqlite3.Cursor, row: list[Any]):
fields = [column[0] for column in cursor.description]
return dict(zip(fields, row))
match self.row_factory:
case 'Row':
self.conn.row_factory = sqlite3.Row
case 'Dict':
self.conn.row_factory = dict_factory
case _:
self.conn.row_factory = None
return self.conn
except (sqlite3.Error, sqlite3.OperationalError) as e:
self.log.error(f"Error connecting to database [{type(e).__name__}] [{self.db_name}]: {e} [{call_stack()}]")
self.log.error(f"Error code: {e.sqlite_errorcode if hasattr(e, 'sqlite_errorcode') else 'N/A'}")
self.log.error(f"Error name: {e.sqlite_errorname if hasattr(e, 'sqlite_errorname') else 'N/A'}")
return None
def db_close(self):
"""close connection"""
if self.conn is not None:
self.conn.close()
self.conn = None
def db_connected(self) -> bool:
"""
Return True if db connection is not none
Returns:
bool -- _description_
"""
return True if self.conn else False
def __content_exists(self, content_name: str, sql_type: str) -> bool:
"""
Check if some content name for a certain type exists
Arguments:
content_name {str} -- _description_
sql_type {str} -- _description_
Returns:
bool -- _description_
"""
if self.conn is None:
return False
try:
cursor = self.conn.cursor()
cursor.execute("""
SELECT name
FROM sqlite_master
WHERE type = ? AND name = ?
""", (sql_type, content_name,))
return cursor.fetchone() is not None
except sqlite3.Error as e:
self.log.error(f"Error checking table [{content_name}/{sql_type}] existence: {e} [{call_stack()}]")
return False
def table_exists(self, table_name: str) -> bool:
"""
Check if a table exists in the database
"""
return self.__content_exists(table_name, 'table')
def trigger_exists(self, trigger_name: str) -> bool:
"""
Check if a triggere exits
"""
return self.__content_exists(trigger_name, 'trigger')
def index_exists(self, index_name: str) -> bool:
"""
Check if a triggere exits
"""
return self.__content_exists(index_name, 'index')
def meta_data_detail(self, table_name: str) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
"""table detail"""
query_show_table = """
SELECT
ti.cid, ti.name, ti.type, ti.'notnull', ti.dflt_value, ti.pk,
il_ii.idx_name, il_ii.idx_unique, il_ii.idx_origin, il_ii.idx_partial
FROM
sqlite_schema AS m,
pragma_table_info(m.name) AS ti
LEFT JOIN (
SELECT
il.name AS idx_name, il.'unique' AS idx_unique, il.origin AS idx_origin, il.partial AS idx_partial,
ii.cid AS tbl_cid
FROM
sqlite_schema AS m,
pragma_index_list(m.name) AS il,
pragma_index_info(il.name) AS ii
WHERE m.name = ?1
) AS il_ii ON (ti.cid = il_ii.tbl_cid)
WHERE
m.name = ?1
"""
return self.execute_query(query_show_table, (table_name,))
def execute_cursor(
self, query: str, params: tuple[Any, ...] | None = None
) -> sqlite3.Cursor | Literal[False]:
"""execute a cursor, used in execute query or return one and for fetch_row"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
cursor = self.conn.cursor()
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
return cursor
except sqlite3.Error as e:
self.log.error(f"Error during executing cursor [{query}:{params}]: {e} [{call_stack()}]")
return False
def execute_query(
self, query: str, params: tuple[Any, ...] | None = None
) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
"""query execute with or without params, returns result"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
if (cursor := self.execute_cursor(query, params)) is False:
return False
# fetch before commit because we need to get the RETURN before
result = cursor.fetchall()
# this is for INSERT/UPDATE/CREATE only
self.conn.commit()
return result
except sqlite3.Error as e:
self.log.error(f"Error during executing query [{query}:{params}]: {e} [{call_stack()}]")
return False
def return_one(
self, query: str, params: tuple[Any, ...] | None = None
) -> tuple[Any, ...] | dict[str, Any] | Literal[False] | None:
"""return one row, only for SELECT"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
if (cursor := self.execute_cursor(query, params)) is False:
return False
return cursor.fetchone()
except sqlite3.Error as e:
self.log.error(f"Error during return one: {e} [{call_stack()}]")
return False
def fetch_row(
self, cursor: sqlite3.Cursor | Literal[False]
) -> tuple[Any, ...] | dict[str, Any] | Literal[False] | None:
"""read from cursor"""
if self.conn is None or cursor is False:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
return cursor.fetchone()
except sqlite3.Error as e:
self.log.error(f"Error during fetch row: {e} [{call_stack()}]")
return False
# __END__

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

@@ -23,13 +23,17 @@ class TimestampStrings:
TIME_ZONE: str = 'Asia/Tokyo'
def __init__(self, time_zone: str | None = None):
def __init__(self, time_zone: str | ZoneInfo | None = None):
self.timestamp_now = datetime.now()
self.time_zone = time_zone if time_zone is not None else self.TIME_ZONE
# set time zone as string
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
# set ZoneInfo type
try:
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
self.time_zone_zi = ZoneInfo(self.time_zone)
except ZoneInfoNotFoundError as e:
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
self.today = self.timestamp_now.strftime('%Y-%m-%d')
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")

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

@@ -24,6 +24,7 @@ match_source_list=foo,bar
element_a=Static energy
element_b=123.5
element_c=True
elemend_d=AB:CD;EF
email=foo@bar.com,other+bar-fee@domain-com.cp,
email_not_mandatory=
email_bad=gii@bar.com

View File

@@ -12,6 +12,7 @@ from corelibs.config_handling.settings_loader_handling.settings_loader_check imp
SCRIPT_PATH: Path = Path(__file__).resolve().parent
ROOT_PATH: Path = SCRIPT_PATH
CONFIG_DIR: Path = Path("config")
LOG_DIR: Path = Path("log")
CONFIG_FILE: str = "settings.ini"
@@ -26,9 +27,8 @@ def main():
print(f"regex {regex_c} check against {value} -> {result}")
# for log testing
script_path: Path = Path(__file__).resolve().parent
log = Log(
log_path=script_path.joinpath('log', 'settings_loader.log'),
log_path=ROOT_PATH.joinpath(LOG_DIR, 'settings_loader.log'),
log_name="Settings Loader",
log_settings={
"log_level_console": 'DEBUG',

View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
test-run/db_handling/log/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Main comment
"""
from pathlib import Path
from uuid import uuid4
import json
import sqlite3
from corelibs.debug_handling.dump_data import dump_data
from corelibs.logging_handling.log import Log, Logger
from corelibs.db_handling.sqlite_io import SQLiteIO
SCRIPT_PATH: Path = Path(__file__).resolve().parent
ROOT_PATH: Path = SCRIPT_PATH
DATABASE_DIR: Path = Path("database")
LOG_DIR: Path = Path("log")
def main() -> None:
"""
Comment
"""
log = Log(
log_path=ROOT_PATH.joinpath(LOG_DIR, 'sqlite_io.log'),
log_name="SQLite IO",
log_settings={
"log_level_console": 'DEBUG',
"log_level_file": 'DEBUG',
}
)
db = SQLiteIO(
log=Logger(log.get_logger_settings()),
db_name=ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_io.db'),
row_factory='Dict'
)
if db.db_connected():
log.info(f"Connected to DB: {db.db_name}")
if db.trigger_exists('trg_test_a_set_date_updated_on_update'):
log.info("Trigger trg_test_a_set_date_updated_on_update exists")
if db.table_exists('test_a'):
log.info("Table test_a exists, dropping for clean test")
db.execute_query("DROP TABLE test_a;")
# create a dummy table
table_sql = """
CREATE TABLE IF NOT EXISTS test_a (
test_a_id INTEGER PRIMARY KEY,
date_created TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
date_updated TEXT,
uid TEXT NOT NULL UNIQUE,
set_current_timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
text_a TEXT,
content,
int_a INTEGER,
float_a REAL
);
"""
result = db.execute_query(table_sql)
log.debug(f"Create table result: {result}")
trigger_sql = """
CREATE TRIGGER trg_test_a_set_date_updated_on_update
AFTER UPDATE ON test_a
FOR EACH ROW
WHEN OLD.date_updated IS NULL OR NEW.date_updated = OLD.date_updated
BEGIN
UPDATE test_a
SET date_updated = (strftime('%Y-%m-%d %H:%M:%f', 'now'))
WHERE test_a_id = NEW.test_a_id;
END;
"""
result = db.execute_query(trigger_sql)
log.debug(f"Create trigger result: {result}")
result = db.meta_data_detail('test_a')
log.debug(f"Table meta data detail: {dump_data(result)}")
# INSERT DATA
sql = """
INSERT INTO test_a (uid, text_a, content, int_a, float_a)
VALUES (?, ?, ?, ?, ?)
RETURNING test_a_id, uid;
"""
result = db.execute_query(
sql,
(
str(uuid4()),
'Some text A',
json.dumps({'foo': 'bar', 'number': 42}),
123,
123.456,
)
)
log.debug(f"[1] Insert data result: {dump_data(result)}")
__uid: str = ''
if result is not False:
# first one only of interest
result = dict(result[0])
__uid = str(result.get('uid', ''))
# second insert
result = db.execute_query(
sql,
(
str(uuid4()),
'Some text A',
json.dumps({'foo': 'bar', 'number': 42}),
123,
123.456,
)
)
log.debug(f"[2] Insert data result: {dump_data(result)}")
result = db.execute_query("SELECT * FROM test_a;")
log.debug(f"Select data result: {dump_data(result)}")
result = db.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
log.debug(f"Fetch row result: {dump_data(result)}")
sql = """
UPDATE test_a
SET text_a = ?
WHERE uid = ?;
"""
result = db.execute_query(
sql,
(
'Some updated text A',
__uid,
)
)
log.debug(f"Update data result: {dump_data(result)}")
result = db.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
log.debug(f"Fetch row after update result: {dump_data(result)}")
db.db_close()
db = SQLiteIO(
log=Logger(log.get_logger_settings()),
db_name=ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_io.db'),
row_factory='Row'
)
result = db.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
if result is not None and result is not False:
log.debug(f"Fetch row result: {dump_data(result)} -> {dict(result)} -> {result.keys()}")
log.debug(f"Access via index: {result[5]} -> {result['text_a']}")
if isinstance(result, sqlite3.Row):
log.debug('Result is sqlite3.Row as expected')
if __name__ == "__main__":
main()
# __END__

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

@@ -4,10 +4,12 @@
Test for double byte format
"""
from zoneinfo import ZoneInfo
from corelibs.string_handling.timestamp_strings import TimestampStrings
def main():
"""test"""
ts = TimestampStrings()
print(f"TS: {ts.timestamp_now}")
@@ -16,6 +18,14 @@ def main():
except ValueError as e:
print(f"Value error: {e}")
ts = TimestampStrings("Europe/Vienna")
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
ts = TimestampStrings(ZoneInfo("Europe/Vienna"))
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
if __name__ == "__main__":
main()

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 @@
"""
db_handling tests
"""

File diff suppressed because it is too large Load Diff

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

@@ -3,7 +3,7 @@ PyTest: string_handling/timestamp_strings
"""
from datetime import datetime
from unittest.mock import patch, MagicMock
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
@@ -119,18 +119,55 @@ class TestTimestampStrings:
assert ts2.time_zone == 'Europe/London'
assert ts1.time_zone != ts2.time_zone
@patch('corelibs.string_handling.timestamp_strings.ZoneInfo')
def test_zoneinfo_called_correctly(self, mock_zoneinfo: MagicMock):
"""Test that ZoneInfo is called with correct timezone"""
def test_zoneinfo_called_correctly_with_string(self):
"""Test that ZoneInfo is called with correct timezone when passing string"""
with patch('corelibs.string_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
mock_zoneinfo.assert_called_with(custom_tz)
def test_zoneinfo_object_parameter(self):
"""Test that ZoneInfo objects can be passed directly as timezone parameter"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
# Create a ZoneInfo object
custom_tz_obj = ZoneInfo('Europe/Paris')
ts = TimestampStrings(time_zone=custom_tz_obj)
mock_zoneinfo.assert_called_with(custom_tz)
# The time_zone should be the ZoneInfo object itself
assert ts.time_zone_zi is custom_tz_obj
assert isinstance(ts.time_zone_zi, ZoneInfo)
def test_zoneinfo_object_vs_string_equivalence(self):
"""Test that ZoneInfo object and string produce equivalent results"""
with patch('corelibs.string_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz, mock_now, mock_now_tz]
# Test with string
ts_string = TimestampStrings(time_zone='Europe/Paris')
# Test with ZoneInfo object
ts_zoneinfo = TimestampStrings(time_zone=ZoneInfo('Europe/Paris'))
# Both should produce the same timestamp formats (though time_zone attributes will differ)
assert ts_string.today == ts_zoneinfo.today
assert ts_string.timestamp == ts_zoneinfo.timestamp
assert ts_string.timestamp_file == ts_zoneinfo.timestamp_file
# The time_zone attributes will be different types but represent the same timezone
assert str(ts_string.time_zone) == 'Europe/Paris'
assert isinstance(ts_zoneinfo.time_zone_zi, ZoneInfo)
def test_edge_case_midnight(self):
"""Test timestamp formatting at midnight"""

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.23.0"
version = "0.29.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]]