Compare commits
8 Commits
v0.46.0
...
d098eb58f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d098eb58f3 | ||
|
|
5319a059ad | ||
|
|
163b8c4018 | ||
|
|
6322b95068 | ||
|
|
715ed1f9c2 | ||
|
|
82a759dd21 | ||
|
|
fe913608c4 | ||
|
|
79f9c5d1c6 |
@@ -1,7 +1,7 @@
|
||||
# MARK: Project info
|
||||
[project]
|
||||
name = "corelibs"
|
||||
version = "0.46.0"
|
||||
version = "0.48.0"
|
||||
description = "Collection of utils for Python scripts"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
@@ -4,11 +4,38 @@ Various dictionary, object and list hashers
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from typing import Any
|
||||
from typing import Any, cast, Sequence
|
||||
|
||||
|
||||
def hash_object(obj: Any) -> str:
|
||||
"""
|
||||
RECOMMENDED for new use
|
||||
Create a hash for any dict or list with mixed key types
|
||||
|
||||
Arguments:
|
||||
obj {Any} -- _description_
|
||||
|
||||
Returns:
|
||||
str -- _description_
|
||||
"""
|
||||
def normalize(o: Any) -> Any:
|
||||
if isinstance(o, dict):
|
||||
# Sort by repr of keys to handle mixed types (str, int, etc.)
|
||||
o = cast(dict[Any, Any], o)
|
||||
return tuple(sorted((repr(k), normalize(v)) for k, v in o.items()))
|
||||
if isinstance(o, (list, tuple)):
|
||||
o = cast(Sequence[Any], o)
|
||||
return tuple(normalize(item) for item in o)
|
||||
return repr(o)
|
||||
|
||||
normalized = normalize(obj)
|
||||
return hashlib.sha256(str(normalized).encode()).hexdigest()
|
||||
|
||||
|
||||
def dict_hash_frozen(data: dict[Any, Any]) -> int:
|
||||
"""
|
||||
NOT RECOMMENDED, use dict_hash_crc or hash_object instead
|
||||
If used, DO NOT CHANGE
|
||||
hash a dict via freeze
|
||||
|
||||
Args:
|
||||
@@ -22,18 +49,25 @@ def dict_hash_frozen(data: dict[Any, Any]) -> int:
|
||||
|
||||
def dict_hash_crc(data: dict[Any, Any] | list[Any]) -> str:
|
||||
"""
|
||||
Create a sha256 hash over dict
|
||||
LEGACY METHOD, must be kept for fallback, if used by other code, DO NOT CHANGE
|
||||
Create a sha256 hash over dict or list
|
||||
alternative for
|
||||
dict_hash_frozen
|
||||
|
||||
Args:
|
||||
data (dict | list): _description_
|
||||
data (dict[Any, Any] | list[Any]): _description_
|
||||
|
||||
Returns:
|
||||
str: _description_
|
||||
str: sha256 hash, prefiex with HO_ if fallback used
|
||||
"""
|
||||
return hashlib.sha256(
|
||||
json.dumps(data, sort_keys=True, ensure_ascii=True).encode('utf-8')
|
||||
).hexdigest()
|
||||
try:
|
||||
return hashlib.sha256(
|
||||
# IT IS IMPORTANT THAT THE BELOW CALL STAYS THE SAME AND DOES NOT CHANGE OR WE WILL GET DIFFERENT HASHES
|
||||
# separators=(',', ':') to get rid of spaces, but if this is used the hash will be different, DO NOT ADD
|
||||
json.dumps(data, sort_keys=True, ensure_ascii=True, default=str).encode('utf-8')
|
||||
).hexdigest()
|
||||
except TypeError:
|
||||
# Fallback tod different hasher, will return DIFFERENT hash than above, so only usable in int/str key mixes
|
||||
return "HO_" + hash_object(data)
|
||||
|
||||
# __END__
|
||||
|
||||
@@ -58,7 +58,12 @@ def make_unique_list_of_dicts(dict_list: list[Any]) -> list[Any]:
|
||||
"""
|
||||
try:
|
||||
# try json dumps, can fail with int and str index types
|
||||
return list({json.dumps(d, sort_keys=True, ensure_ascii=True): d for d in dict_list}.values())
|
||||
return list(
|
||||
{
|
||||
json.dumps(d, sort_keys=True, ensure_ascii=True, separators=(',', ':')): d
|
||||
for d in dict_list
|
||||
}.values()
|
||||
)
|
||||
except TypeError:
|
||||
# Fallback for non-serializable entries, slow but works
|
||||
unique: list[Any] = []
|
||||
|
||||
@@ -3,32 +3,61 @@ requests lib interface
|
||||
V2 call type
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
import warnings
|
||||
from typing import Any, TypedDict, cast
|
||||
import requests
|
||||
# to hide the verfiy warnings because of the bad SSL settings from Netskope, Akamai, etc
|
||||
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
|
||||
from requests import exceptions
|
||||
|
||||
|
||||
class ErrorResponse:
|
||||
"""
|
||||
Error response structure. This is returned if a request could not be completed
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
message: str,
|
||||
action: str,
|
||||
url: str,
|
||||
exception: exceptions.InvalidSchema | exceptions.ReadTimeout | exceptions.ConnectionError | None = None
|
||||
) -> None:
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.action = action
|
||||
self.url = url
|
||||
self.exception_name = type(exception).__name__ if exception is not None else None
|
||||
self.exception_trace = exception if exception is not None else None
|
||||
|
||||
|
||||
class ProxyConfig(TypedDict):
|
||||
"""
|
||||
Socks proxy settings
|
||||
"""
|
||||
type: str
|
||||
host: str
|
||||
port: str
|
||||
|
||||
|
||||
class Caller:
|
||||
"""_summary_"""
|
||||
"""
|
||||
requests lib interface
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
header: dict[str, str],
|
||||
verify: bool = True,
|
||||
timeout: int = 20,
|
||||
proxy: dict[str, str] | None = None,
|
||||
proxy: ProxyConfig | None = None,
|
||||
verify: bool = True,
|
||||
ca_file: str | None = None
|
||||
):
|
||||
self.headers = header
|
||||
self.timeout: int = timeout
|
||||
self.cafile = ca_file
|
||||
self.ca_file = ca_file
|
||||
self.verify = verify
|
||||
self.proxy = proxy
|
||||
self.proxy = cast(dict[str, str], proxy) if proxy is not None else None
|
||||
|
||||
def __timeout(self, timeout: int | None) -> int:
|
||||
if timeout is not None:
|
||||
if timeout is not None and timeout >= 0:
|
||||
return timeout
|
||||
return self.timeout
|
||||
|
||||
@@ -39,7 +68,7 @@ class Caller:
|
||||
data: dict[str, Any] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | None:
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""
|
||||
call wrapper, on error returns None
|
||||
|
||||
@@ -56,67 +85,96 @@ class Caller:
|
||||
if data is None:
|
||||
data = {}
|
||||
try:
|
||||
response = None
|
||||
if action == "get":
|
||||
response = requests.get(
|
||||
return requests.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=self.headers,
|
||||
timeout=self.__timeout(timeout),
|
||||
verify=self.verify,
|
||||
proxies=self.proxy
|
||||
proxies=self.proxy,
|
||||
cert=self.ca_file
|
||||
)
|
||||
elif action == "post":
|
||||
response = requests.post(
|
||||
if action == "post":
|
||||
return requests.post(
|
||||
url,
|
||||
params=params,
|
||||
json=data,
|
||||
headers=self.headers,
|
||||
timeout=self.__timeout(timeout),
|
||||
verify=self.verify,
|
||||
proxies=self.proxy
|
||||
proxies=self.proxy,
|
||||
cert=self.ca_file
|
||||
)
|
||||
elif action == "put":
|
||||
response = requests.put(
|
||||
if action == "put":
|
||||
return requests.put(
|
||||
url,
|
||||
params=params,
|
||||
json=data,
|
||||
headers=self.headers,
|
||||
timeout=self.__timeout(timeout),
|
||||
verify=self.verify,
|
||||
proxies=self.proxy
|
||||
proxies=self.proxy,
|
||||
cert=self.ca_file
|
||||
)
|
||||
elif action == "patch":
|
||||
response = requests.patch(
|
||||
if action == "patch":
|
||||
return requests.patch(
|
||||
url,
|
||||
params=params,
|
||||
json=data,
|
||||
headers=self.headers,
|
||||
timeout=self.__timeout(timeout),
|
||||
verify=self.verify,
|
||||
proxies=self.proxy
|
||||
proxies=self.proxy,
|
||||
cert=self.ca_file
|
||||
)
|
||||
elif action == "delete":
|
||||
response = requests.delete(
|
||||
if action == "delete":
|
||||
return requests.delete(
|
||||
url,
|
||||
params=params,
|
||||
headers=self.headers,
|
||||
timeout=self.__timeout(timeout),
|
||||
verify=self.verify,
|
||||
proxies=self.proxy
|
||||
proxies=self.proxy,
|
||||
cert=self.ca_file
|
||||
)
|
||||
return response
|
||||
except requests.exceptions.InvalidSchema as e:
|
||||
print(f"Invalid URL during '{action}' for {url}:\n\t{e}")
|
||||
return None
|
||||
except requests.exceptions.ReadTimeout as e:
|
||||
print(f"Timeout ({self.timeout}s) during '{action}' for {url}:\n\t{e}")
|
||||
return None
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
print(f"Connection error during '{action}' for {url}:\n\t{e}")
|
||||
return None
|
||||
return ErrorResponse(
|
||||
100,
|
||||
f"Unsupported action '{action}'",
|
||||
action,
|
||||
url
|
||||
)
|
||||
except exceptions.InvalidSchema as e:
|
||||
return ErrorResponse(
|
||||
200,
|
||||
f"Invalid URL during '{action}' for {url}",
|
||||
action,
|
||||
url,
|
||||
e
|
||||
)
|
||||
except exceptions.ReadTimeout as e:
|
||||
return ErrorResponse(
|
||||
300,
|
||||
f"Timeout ({self.timeout}s) during '{action}' for {url}",
|
||||
action,
|
||||
url,
|
||||
e
|
||||
)
|
||||
except exceptions.ConnectionError as e:
|
||||
return ErrorResponse(
|
||||
400,
|
||||
f"Connection error during '{action}' for {url}",
|
||||
action,
|
||||
url,
|
||||
e
|
||||
)
|
||||
|
||||
def get(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
|
||||
def get(
|
||||
self,
|
||||
url: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""
|
||||
get data
|
||||
|
||||
@@ -127,11 +185,15 @@ class Caller:
|
||||
Returns:
|
||||
requests.Response: _description_
|
||||
"""
|
||||
return self.__call('get', url, params=params)
|
||||
return self.__call('get', url, params=params, timeout=timeout)
|
||||
|
||||
def post(
|
||||
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
|
||||
) -> requests.Response | None:
|
||||
self,
|
||||
url: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""
|
||||
post data
|
||||
|
||||
@@ -143,11 +205,15 @@ class Caller:
|
||||
Returns:
|
||||
requests.Response | None: _description_
|
||||
"""
|
||||
return self.__call('post', url, data, params)
|
||||
return self.__call('post', url, data, params, timeout=timeout)
|
||||
|
||||
def put(
|
||||
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
|
||||
) -> requests.Response | None:
|
||||
self,
|
||||
url: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
@@ -158,11 +224,15 @@ class Caller:
|
||||
Returns:
|
||||
requests.Response | None: _description_
|
||||
"""
|
||||
return self.__call('put', url, data, params)
|
||||
return self.__call('put', url, data, params, timeout=timeout)
|
||||
|
||||
def patch(
|
||||
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
|
||||
) -> requests.Response | None:
|
||||
self,
|
||||
url: str,
|
||||
data: dict[str, Any] | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
@@ -173,9 +243,14 @@ class Caller:
|
||||
Returns:
|
||||
requests.Response | None: _description_
|
||||
"""
|
||||
return self.__call('patch', url, data, params)
|
||||
return self.__call('patch', url, data, params, timeout=timeout)
|
||||
|
||||
def delete(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
|
||||
def delete(
|
||||
self,
|
||||
url: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: int | None = None
|
||||
) -> requests.Response | ErrorResponse:
|
||||
"""
|
||||
delete
|
||||
|
||||
@@ -186,6 +261,6 @@ class Caller:
|
||||
Returns:
|
||||
requests.Response | None: _description_
|
||||
"""
|
||||
return self.__call('delete', url, params=params)
|
||||
return self.__call('delete', url, params=params, timeout=timeout)
|
||||
|
||||
# __END__
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
test list helpers
|
||||
"""
|
||||
|
||||
# from typing import Any
|
||||
from typing import Any
|
||||
from corelibs.debug_handling.dump_data import dump_data
|
||||
from corelibs.iterator_handling.list_helpers import is_list_in_list, convert_to_list, make_unique_list_of_dicts
|
||||
from corelibs.iterator_handling.fingerprint import dict_hash_crc
|
||||
|
||||
|
||||
def __test_is_list_in_list_a():
|
||||
@@ -29,7 +30,8 @@ def __make_unique_list_of_dicts():
|
||||
{"a": 3, "b": 4, "nested": {"x": 30, "y": 40}}
|
||||
]
|
||||
unique_dicts = make_unique_list_of_dicts(dict_list)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)}")
|
||||
dhf = dict_hash_crc(unique_dicts)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
|
||||
|
||||
dict_list = [
|
||||
{"a": 1, 1: "one"},
|
||||
@@ -37,7 +39,8 @@ def __make_unique_list_of_dicts():
|
||||
{"a": 2, 1: "one"}
|
||||
]
|
||||
unique_dicts = make_unique_list_of_dicts(dict_list)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)}")
|
||||
dhf = dict_hash_crc(unique_dicts)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
|
||||
|
||||
dict_list = [
|
||||
{"a": 1, "b": [1, 2, 3]},
|
||||
@@ -46,7 +49,31 @@ def __make_unique_list_of_dicts():
|
||||
1, 2, "String", 1, "Foobar"
|
||||
]
|
||||
unique_dicts = make_unique_list_of_dicts(dict_list)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)}")
|
||||
dhf = dict_hash_crc(unique_dicts)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
|
||||
|
||||
dict_list: list[Any] = [
|
||||
[],
|
||||
{},
|
||||
[],
|
||||
{},
|
||||
{"a": []},
|
||||
{"a": []},
|
||||
{"a": {}},
|
||||
{"a": {}},
|
||||
]
|
||||
unique_dicts = make_unique_list_of_dicts(dict_list)
|
||||
dhf = dict_hash_crc(unique_dicts)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
|
||||
|
||||
dict_list: list[Any] = [
|
||||
(1, 2),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
]
|
||||
unique_dicts = make_unique_list_of_dicts(dict_list)
|
||||
dhf = dict_hash_crc(unique_dicts)
|
||||
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -4,7 +4,101 @@ tests for corelibs.iterator_handling.fingerprint
|
||||
|
||||
from typing import Any
|
||||
import pytest
|
||||
from corelibs.iterator_handling.fingerprint import dict_hash_frozen, dict_hash_crc
|
||||
from corelibs.iterator_handling.fingerprint import dict_hash_frozen, dict_hash_crc, hash_object
|
||||
|
||||
|
||||
class TestHashObject:
|
||||
"""Tests for hash_object function"""
|
||||
|
||||
def test_hash_object_simple_dict(self):
|
||||
"""Test hashing a simple dictionary with hash_object"""
|
||||
data = {"key1": "value1", "key2": "value2"}
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64 # SHA256 produces 64 hex characters
|
||||
|
||||
def test_hash_object_mixed_keys(self):
|
||||
"""Test hash_object with mixed int and string keys"""
|
||||
data = {"key1": "value1", 1: "value2", 2: "value3"}
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
def test_hash_object_consistency(self):
|
||||
"""Test that hash_object produces consistent results"""
|
||||
data = {"str_key": "value", 123: "number_key"}
|
||||
hash1 = hash_object(data)
|
||||
hash2 = hash_object(data)
|
||||
|
||||
assert hash1 == hash2
|
||||
|
||||
def test_hash_object_order_independence(self):
|
||||
"""Test that hash_object is order-independent"""
|
||||
data1 = {"a": 1, 1: "one", "b": 2, 2: "two"}
|
||||
data2 = {2: "two", "b": 2, 1: "one", "a": 1}
|
||||
hash1 = hash_object(data1)
|
||||
hash2 = hash_object(data2)
|
||||
|
||||
assert hash1 == hash2
|
||||
|
||||
def test_hash_object_list_of_dicts_mixed_keys(self):
|
||||
"""Test hash_object with list of dicts containing mixed keys"""
|
||||
data = [
|
||||
{"name": "item1", 1: "value1"},
|
||||
{"name": "item2", 2: "value2"}
|
||||
]
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
def test_hash_object_nested_mixed_keys(self):
|
||||
"""Test hash_object with nested structures containing mixed keys"""
|
||||
data = {
|
||||
"outer": {
|
||||
"inner": "value",
|
||||
1: "mixed_key"
|
||||
},
|
||||
2: "another_mixed"
|
||||
}
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
def test_hash_object_different_data(self):
|
||||
"""Test that different data produces different hashes"""
|
||||
data1 = {"key": "value", 1: "one"}
|
||||
data2 = {"key": "value", 2: "two"}
|
||||
hash1 = hash_object(data1)
|
||||
hash2 = hash_object(data2)
|
||||
|
||||
assert hash1 != hash2
|
||||
|
||||
def test_hash_object_complex_nested(self):
|
||||
"""Test hash_object with complex nested structures"""
|
||||
data = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
1: "value",
|
||||
"key": [1, 2, {"nested": "deep", 3: "int_key"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
def test_hash_object_list_with_tuples(self):
|
||||
"""Test hash_object with lists containing tuples"""
|
||||
data = [("a", 1), ("b", 2), {1: "mixed", "key": "value"}]
|
||||
result = hash_object(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
|
||||
class TestDictHashFrozen:
|
||||
@@ -279,6 +373,116 @@ class TestDictHashCrc:
|
||||
assert isinstance(result, str)
|
||||
assert len(result) == 64
|
||||
|
||||
def test_dict_hash_crc_fallback_mixed_keys(self):
|
||||
"""Test dict_hash_crc fallback with mixed int and string keys"""
|
||||
data = {"key1": "value1", 1: "value2", 2: "value3"}
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
# Fallback prefixes with "HO_"
|
||||
assert result.startswith("HO_")
|
||||
# Hash should be 64 chars + 3 char prefix = 67 total
|
||||
assert len(result) == 67
|
||||
|
||||
def test_dict_hash_crc_fallback_consistency(self):
|
||||
"""Test that fallback produces consistent hashes"""
|
||||
data = {"str_key": "value", 123: "number_key", 456: "another"}
|
||||
hash1 = dict_hash_crc(data)
|
||||
hash2 = dict_hash_crc(data)
|
||||
|
||||
assert hash1 == hash2
|
||||
assert hash1.startswith("HO_")
|
||||
|
||||
def test_dict_hash_crc_fallback_order_independence(self):
|
||||
"""Test that fallback is order-independent for mixed-key dicts"""
|
||||
data1 = {"a": 1, 1: "one", "b": 2, 2: "two"}
|
||||
data2 = {2: "two", "b": 2, 1: "one", "a": 1}
|
||||
hash1 = dict_hash_crc(data1)
|
||||
hash2 = dict_hash_crc(data2)
|
||||
|
||||
assert hash1 == hash2
|
||||
assert hash1.startswith("HO_")
|
||||
|
||||
def test_dict_hash_crc_fallback_list_of_dicts_mixed_keys(self):
|
||||
"""Test fallback with list of dicts containing mixed keys"""
|
||||
data = [
|
||||
{"name": "item1", 1: "value1"},
|
||||
{"name": "item2", 2: "value2"},
|
||||
{3: "value3", "type": "mixed"}
|
||||
]
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert result.startswith("HO_")
|
||||
assert len(result) == 67
|
||||
|
||||
def test_dict_hash_crc_fallback_nested_mixed_keys(self):
|
||||
"""Test fallback with nested dicts containing mixed keys"""
|
||||
data = {
|
||||
"outer": {
|
||||
"inner": "value",
|
||||
1: "mixed_key"
|
||||
},
|
||||
2: "another_mixed"
|
||||
}
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert result.startswith("HO_")
|
||||
assert len(result) == 67
|
||||
|
||||
def test_dict_hash_crc_fallback_different_data(self):
|
||||
"""Test that different mixed-key data produces different hashes"""
|
||||
data1 = {"key": "value", 1: "one"}
|
||||
data2 = {"key": "value", 2: "two"}
|
||||
hash1 = dict_hash_crc(data1)
|
||||
hash2 = dict_hash_crc(data2)
|
||||
|
||||
assert hash1 != hash2
|
||||
assert hash1.startswith("HO_")
|
||||
assert hash2.startswith("HO_")
|
||||
|
||||
def test_dict_hash_crc_fallback_complex_structure(self):
|
||||
"""Test fallback with complex nested structure with mixed keys"""
|
||||
data = [
|
||||
{
|
||||
"id": 1,
|
||||
1: "first",
|
||||
"data": {
|
||||
"nested": "value",
|
||||
100: "nested_int_key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
2: "second",
|
||||
"items": [1, 2, 3]
|
||||
}
|
||||
]
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert result.startswith("HO_")
|
||||
assert len(result) == 67
|
||||
|
||||
def test_dict_hash_crc_no_fallback_string_keys_only(self):
|
||||
"""Test that string-only keys don't trigger fallback"""
|
||||
data = {"key1": "value1", "key2": "value2", "key3": "value3"}
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert not result.startswith("HO_")
|
||||
assert len(result) == 64
|
||||
|
||||
def test_dict_hash_crc_no_fallback_int_keys_only(self):
|
||||
"""Test that int-only keys don't trigger fallback"""
|
||||
data = {1: "one", 2: "two", 3: "three"}
|
||||
result = dict_hash_crc(data)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert not result.startswith("HO_")
|
||||
assert len(result) == 64
|
||||
|
||||
|
||||
class TestComparisonBetweenHashFunctions:
|
||||
"""Tests comparing dict_hash_frozen and dict_hash_crc"""
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
PyTest: requests_handling/caller
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
import pytest
|
||||
import requests
|
||||
from corelibs.requests_handling.caller import Caller
|
||||
from corelibs.requests_handling.caller import Caller, ErrorResponse, ProxyConfig
|
||||
|
||||
|
||||
class TestCallerInit:
|
||||
@@ -21,13 +20,17 @@ class TestCallerInit:
|
||||
assert caller.timeout == 20
|
||||
assert caller.verify is True
|
||||
assert caller.proxy is None
|
||||
assert caller.cafile is None
|
||||
assert caller.ca_file is None
|
||||
|
||||
def test_init_with_all_params(self):
|
||||
"""Test Caller initialization with all parameters"""
|
||||
header = {"Authorization": "Bearer token", "Content-Type": "application/json"}
|
||||
proxy = {"http": "http://proxy.example.com:8080", "https": "https://proxy.example.com:8080"}
|
||||
caller = Caller(header=header, verify=False, timeout=30, proxy=proxy)
|
||||
proxy: ProxyConfig = {
|
||||
"type": "socks5",
|
||||
"host": "proxy.example.com:8080",
|
||||
"port": "8080"
|
||||
}
|
||||
caller = Caller(header=header, timeout=30, proxy=proxy, verify=False)
|
||||
|
||||
assert caller.headers == header
|
||||
assert caller.timeout == 30
|
||||
@@ -58,7 +61,7 @@ class TestCallerInit:
|
||||
ca_file_path = "/path/to/ca/cert.pem"
|
||||
caller = Caller(header={}, ca_file=ca_file_path)
|
||||
|
||||
assert caller.cafile == ca_file_path
|
||||
assert caller.ca_file == ca_file_path
|
||||
|
||||
|
||||
class TestCallerGet:
|
||||
@@ -81,7 +84,8 @@ class TestCallerGet:
|
||||
headers={"Authorization": "Bearer token"},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
@@ -101,7 +105,8 @@ class TestCallerGet:
|
||||
headers={},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
@@ -134,7 +139,11 @@ class TestCallerGet:
|
||||
mock_response = Mock(spec=requests.Response)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
proxy = {"http": "http://proxy.example.com:8080"}
|
||||
proxy: ProxyConfig = {
|
||||
"type": "socks5",
|
||||
"host": "proxy.example.com:8080",
|
||||
"port": "8080"
|
||||
}
|
||||
caller = Caller(header={}, proxy=proxy)
|
||||
caller.get("https://api.example.com/data")
|
||||
|
||||
@@ -142,40 +151,46 @@ class TestCallerGet:
|
||||
assert mock_get.call_args[1]["proxies"] == proxy
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
def test_get_invalid_schema_returns_none(self, mock_get: Mock, capsys: Any):
|
||||
"""Test GET request with invalid URL schema returns None"""
|
||||
def test_get_invalid_schema_returns_none(self, mock_get: Mock):
|
||||
"""Test GET request with invalid URL schema returns ErrorResponse"""
|
||||
mock_get.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.get("invalid://example.com")
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Invalid URL during 'get'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 200
|
||||
assert "Invalid URL during 'get'" in response.message
|
||||
assert response.action == "get"
|
||||
assert response.url == "invalid://example.com"
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
def test_get_timeout_returns_none(self, mock_get: Mock, capsys: Any):
|
||||
"""Test GET request timeout returns None"""
|
||||
def test_get_timeout_returns_none(self, mock_get: Mock):
|
||||
"""Test GET request timeout returns ErrorResponse"""
|
||||
mock_get.side_effect = requests.exceptions.ReadTimeout("Timeout")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.get("https://api.example.com/data")
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Timeout (20s) during 'get'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 300
|
||||
assert "Timeout (20s) during 'get'" in response.message
|
||||
assert response.action == "get"
|
||||
assert response.url == "https://api.example.com/data"
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
def test_get_connection_error_returns_none(self, mock_get: Mock, capsys: Any):
|
||||
"""Test GET request connection error returns None"""
|
||||
def test_get_connection_error_returns_none(self, mock_get: Mock):
|
||||
"""Test GET request connection error returns ErrorResponse"""
|
||||
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.get("https://api.example.com/data")
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Connection error during 'get'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 400
|
||||
assert "Connection error during 'get'" in response.message
|
||||
assert response.action == "get"
|
||||
assert response.url == "https://api.example.com/data"
|
||||
|
||||
|
||||
class TestCallerPost:
|
||||
@@ -200,7 +215,8 @@ class TestCallerPost:
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.post')
|
||||
@@ -234,40 +250,46 @@ class TestCallerPost:
|
||||
assert mock_post.call_args[1]["json"] == data
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.post')
|
||||
def test_post_invalid_schema_returns_none(self, mock_post: Mock, capsys: Any):
|
||||
"""Test POST request with invalid URL schema returns None"""
|
||||
def test_post_invalid_schema_returns_none(self, mock_post: Mock):
|
||||
"""Test POST request with invalid URL schema returns ErrorResponse"""
|
||||
mock_post.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.post("invalid://example.com", data={"test": "data"})
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Invalid URL during 'post'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 200
|
||||
assert "Invalid URL during 'post'" in response.message
|
||||
assert response.action == "post"
|
||||
assert response.url == "invalid://example.com"
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.post')
|
||||
def test_post_timeout_returns_none(self, mock_post: Mock, capsys: Any):
|
||||
"""Test POST request timeout returns None"""
|
||||
def test_post_timeout_returns_none(self, mock_post: Mock):
|
||||
"""Test POST request timeout returns ErrorResponse"""
|
||||
mock_post.side_effect = requests.exceptions.ReadTimeout("Timeout")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.post("https://api.example.com/data", data={"test": "data"})
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Timeout (20s) during 'post'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 300
|
||||
assert "Timeout (20s) during 'post'" in response.message
|
||||
assert response.action == "post"
|
||||
assert response.url == "https://api.example.com/data"
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.post')
|
||||
def test_post_connection_error_returns_none(self, mock_post: Mock, capsys: Any):
|
||||
"""Test POST request connection error returns None"""
|
||||
def test_post_connection_error_returns_none(self, mock_post: Mock):
|
||||
"""Test POST request connection error returns ErrorResponse"""
|
||||
mock_post.side_effect = requests.exceptions.ConnectionError("Connection failed")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.post("https://api.example.com/data", data={"test": "data"})
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Connection error during 'post'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 400
|
||||
assert "Connection error during 'post'" in response.message
|
||||
assert response.action == "post"
|
||||
assert response.url == "https://api.example.com/data"
|
||||
|
||||
|
||||
class TestCallerPut:
|
||||
@@ -292,7 +314,8 @@ class TestCallerPut:
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.put')
|
||||
@@ -311,16 +334,18 @@ class TestCallerPut:
|
||||
assert mock_put.call_args[1]["params"] == params
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.put')
|
||||
def test_put_timeout_returns_none(self, mock_put: Mock, capsys: Any):
|
||||
"""Test PUT request timeout returns None"""
|
||||
def test_put_timeout_returns_none(self, mock_put: Mock):
|
||||
"""Test PUT request timeout returns ErrorResponse"""
|
||||
mock_put.side_effect = requests.exceptions.ReadTimeout("Timeout")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.put("https://api.example.com/data/1", data={"test": "data"})
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Timeout (20s) during 'put'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 300
|
||||
assert "Timeout (20s) during 'put'" in response.message
|
||||
assert response.action == "put"
|
||||
assert response.url == "https://api.example.com/data/1"
|
||||
|
||||
|
||||
class TestCallerPatch:
|
||||
@@ -345,7 +370,8 @@ class TestCallerPatch:
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.patch')
|
||||
@@ -364,16 +390,18 @@ class TestCallerPatch:
|
||||
assert mock_patch.call_args[1]["params"] == params
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.patch')
|
||||
def test_patch_connection_error_returns_none(self, mock_patch: Mock, capsys: Any):
|
||||
"""Test PATCH request connection error returns None"""
|
||||
def test_patch_connection_error_returns_none(self, mock_patch: Mock):
|
||||
"""Test PATCH request connection error returns ErrorResponse"""
|
||||
mock_patch.side_effect = requests.exceptions.ConnectionError("Connection failed")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.patch("https://api.example.com/data/1", data={"test": "data"})
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Connection error during 'patch'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 400
|
||||
assert "Connection error during 'patch'" in response.message
|
||||
assert response.action == "patch"
|
||||
assert response.url == "https://api.example.com/data/1"
|
||||
|
||||
|
||||
class TestCallerDelete:
|
||||
@@ -396,7 +424,8 @@ class TestCallerDelete:
|
||||
headers={"Authorization": "Bearer token"},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.delete')
|
||||
@@ -414,16 +443,18 @@ class TestCallerDelete:
|
||||
assert mock_delete.call_args[1]["params"] == params
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.delete')
|
||||
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock, capsys: Any):
|
||||
"""Test DELETE request with invalid URL schema returns None"""
|
||||
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock):
|
||||
"""Test DELETE request with invalid URL schema returns ErrorResponse"""
|
||||
mock_delete.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
|
||||
|
||||
caller = Caller(header={})
|
||||
response = caller.delete("invalid://example.com/data/1")
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert "Invalid URL during 'delete'" in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert response.code == 200
|
||||
assert "Invalid URL during 'delete'" in response.message
|
||||
assert response.action == "delete"
|
||||
assert response.url == "invalid://example.com/data/1"
|
||||
|
||||
|
||||
class TestCallerParametrized:
|
||||
@@ -492,7 +523,7 @@ class TestCallerParametrized:
|
||||
])
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
def test_exception_handling(
|
||||
self, mock_get: Mock, exception_class: type, expected_message: str, capsys: Any
|
||||
self, mock_get: Mock, exception_class: type, expected_message: str
|
||||
):
|
||||
"""Test exception handling for all exception types"""
|
||||
mock_get.side_effect = exception_class("Test error")
|
||||
@@ -500,9 +531,8 @@ class TestCallerParametrized:
|
||||
caller = Caller(header={})
|
||||
response = caller.get("https://api.example.com/data")
|
||||
|
||||
assert response is None
|
||||
captured = capsys.readouterr()
|
||||
assert expected_message in captured.out
|
||||
assert isinstance(response, ErrorResponse)
|
||||
assert expected_message in response.message
|
||||
|
||||
|
||||
class TestCallerIntegration:
|
||||
@@ -599,7 +629,8 @@ class TestCallerEdgeCases:
|
||||
headers={},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.post')
|
||||
@@ -659,7 +690,8 @@ class TestCallerEdgeCases:
|
||||
headers={},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
@patch('corelibs.requests_handling.caller.requests.get')
|
||||
@@ -679,7 +711,8 @@ class TestCallerEdgeCases:
|
||||
headers={},
|
||||
timeout=20,
|
||||
verify=True,
|
||||
proxies=None
|
||||
proxies=None,
|
||||
cert=None
|
||||
)
|
||||
|
||||
def test_timeout_zero(self):
|
||||
@@ -730,9 +763,10 @@ class TestCallerProxyHandling:
|
||||
mock_response = Mock(spec=requests.Response)
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
proxy = {
|
||||
"http": "http://proxy.example.com:8080",
|
||||
"https": "https://proxy.example.com:8080"
|
||||
proxy: ProxyConfig = {
|
||||
"type": "socks5",
|
||||
"host": "proxy.example.com:8080",
|
||||
"port": "8080"
|
||||
}
|
||||
caller = Caller(header={}, proxy=proxy)
|
||||
caller.get("https://api.example.com/data")
|
||||
@@ -746,9 +780,10 @@ class TestCallerProxyHandling:
|
||||
mock_response = Mock(spec=requests.Response)
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
proxy = {
|
||||
"http": "http://user:pass@proxy.example.com:8080",
|
||||
"https": "https://user:pass@proxy.example.com:8080"
|
||||
proxy: ProxyConfig = {
|
||||
"type": "socks5",
|
||||
"host": "proxy.example.com:8080",
|
||||
"port": "8080"
|
||||
}
|
||||
caller = Caller(header={}, proxy=proxy)
|
||||
caller.post("https://api.example.com/data", data={"test": "data"})
|
||||
@@ -789,7 +824,7 @@ class TestCallerResponseHandling:
|
||||
caller = Caller(header={})
|
||||
response = caller.get("https://api.example.com/data")
|
||||
|
||||
assert response is not None
|
||||
assert not isinstance(response, ErrorResponse)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Success"
|
||||
assert response.json() == {"status": "ok"}
|
||||
@@ -805,7 +840,7 @@ class TestCallerResponseHandling:
|
||||
caller = Caller(header={})
|
||||
response = caller.get("https://api.example.com/data")
|
||||
|
||||
assert response is not None
|
||||
assert not isinstance(response, ErrorResponse)
|
||||
assert response.status_code == status_code
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user