blob: 975f4146a0335ef8f8ceca43ee8acd3d9ac4b260 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import ctypes
import decimal
import inspect
import warnings
from functools import wraps
from typing import Any, Optional, Type, Tuple, Union
from pyignite.datatypes.base import IgniteDataType
from .constants import *
FALLBACK = False
try:
from pyignite import _cutils
except ImportError:
FALLBACK = True
LONG_MASK = 0xffffffff
DIGITS_PER_INT = 9
def is_pow2(value: int) -> bool:
""" Check if value is power of two. """
return value > 0 and ((value & (value - 1)) == 0)
def is_iterable(value: Any) -> bool:
""" Check if value is iterable. """
try:
iter(value)
return True
except TypeError:
return False
def is_binary(value):
"""
Check if a value is a pythonic representation of a Complex object.
"""
return all([
hasattr(value, 'type_name'),
hasattr(value, 'type_id'),
hasattr(value, 'schema'),
hasattr(value, 'schema_id'),
])
def is_hinted(value):
"""
Check if a value is a tuple of data item and its type hint.
"""
return isinstance(value, tuple) and len(value) == 2 and issubclass(value[1], IgniteDataType)
def is_wrapped(value: Any) -> bool:
"""
Check if a value is of WrappedDataObject type.
"""
return type(value) is tuple and len(value) == 2 and type(value[0]) is bytes and type(value[1]) is int
def int_overflow(value: int) -> int:
"""
Simulates 32bit integer overflow.
"""
return ((value ^ 0x80000000) & 0xffffffff) - 0x80000000
def hashcode(data: Union[str, bytes, bytearray, memoryview]) -> int:
"""
Calculate hash code used for identifying objects in Ignite binary API.
:param data: UTF-8-encoded string identifier of binary buffer or byte array
:return: hash code.
"""
if FALLBACK:
return __hashcode_fallback(data)
return _cutils.hashcode(data)
def __hashcode_fallback(data: Union[str, bytes, bytearray, memoryview]) -> int:
if data is None:
return 0
if isinstance(data, str):
"""
For strings we iterate over code point which are of the int type
and can take up to 4 bytes and can only be positive.
"""
result = 0
for char in data:
try:
char_val = ord(char)
result = int_overflow(31 * result + char_val)
except TypeError:
pass
else:
"""
For byte array we iterate over bytes which only take 1 byte. But
according to protocol, bytes during hashing should be treated as signed
integer numbers 8 bits long. On other hand elements in Python's `bytes`
are unsigned. For this reason we use ctypes.c_byte() to make them
signed.
"""
result = 1
for byte in data:
byte = ctypes.c_byte(byte).value
result = int_overflow(31 * result + byte)
return result
def cache_id(cache: Union[str, int]) -> int:
"""
Create a cache ID from cache name.
:param cache: cache name or ID,
:return: cache ID.
"""
return cache if type(cache) is int else hashcode(cache)
def entity_id(cache: Union[str, int]) -> Optional[int]:
"""
Create a type ID from type name or field ID from field name.
:param cache: entity name or ID,
:return: entity ID.
"""
if cache is None:
return None
return cache if type(cache) is int else hashcode(cache.lower())
def schema_id(schema: Union[int, dict]) -> int:
"""
Calculate Complex Object schema ID.
:param schema: a dict of field names: field types,
:return: schema ID.
"""
if FALLBACK:
return __schema_id_fallback(schema)
return _cutils.schema_id(schema)
def __schema_id_fallback(schema: Union[int, dict]) -> int:
if isinstance(schema, int):
return schema
if schema is None:
return 0
s_id = FNV1_OFFSET_BASIS if schema else 0
for field_name in schema.keys():
field_id = __hashcode_fallback(field_name.lower())
s_id ^= (field_id & 0xff)
s_id = int_overflow(s_id * FNV1_PRIME)
s_id ^= ((field_id >> 8) & 0xff)
s_id = int_overflow(s_id * FNV1_PRIME)
s_id ^= ((field_id >> 16) & 0xff)
s_id = int_overflow(s_id * FNV1_PRIME)
s_id ^= ((field_id >> 24) & 0xff)
s_id = int_overflow(s_id * FNV1_PRIME)
return s_id
def decimal_hashcode(value: decimal.Decimal) -> int:
"""
This is a translation of `java.math.BigDecimal` class `hashCode()` method
to Python.
:param value: pythonic decimal value,
:return: hashcode.
"""
sign, digits, scale = value.normalize().as_tuple()
sign = -1 if sign else 1
value = int(''.join([str(d) for d in digits]))
if value < MAX_LONG:
# this is the case when Java BigDecimal digits are stored
# compactly, in the internal 64-bit integer field
int_hash = (
(unsigned(value, ctypes.c_ulonglong) >> 32) * 31 + (value & LONG_MASK)
) & LONG_MASK
else:
# digits are not fit in the 64-bit long, so they get split internally
# to an array of values within 32-bit integer range each (it is really
# a part of `java.math.BigInteger` class internals)
magnitude = []
order = 0
while True:
elem = value >> order
if elem > 1:
magnitude.insert(0, ctypes.c_int(elem).value)
order += 32
else:
break
int_hash = 0
for v in magnitude:
int_hash = (31 * int_hash + (v & LONG_MASK)) & LONG_MASK
return ctypes.c_int(31 * int_hash * sign - scale).value
def datetime_hashcode(value: int) -> int:
"""
Calculates hashcode from UNIX epoch.
:param value: UNIX time,
:return: Java hashcode.
"""
return (value & LONG_MASK) ^ (unsigned(value, ctypes.c_ulonglong) >> 32)
def status_to_exception(exc: Type[Exception]):
"""
Converts erroneous status code with error message to an exception
of the given class. Supports coroutines.
:param exc: the class of exception to raise,
:return: decorated function.
"""
def process_result(result):
if result.status != 0:
raise exc(result.message)
return result.value
def ste_decorator(fn):
if inspect.iscoroutinefunction(fn):
@wraps(fn)
async def ste_wrapper_async(*args, **kwargs):
return process_result(await fn(*args, **kwargs))
return ste_wrapper_async
else:
@wraps(fn)
def ste_wrapper(*args, **kwargs):
return process_result(fn(*args, **kwargs))
return ste_wrapper
return ste_decorator
def get_field_by_id(obj: 'GenericObjectMeta', field_id: int) -> Tuple[Any, IgniteDataType]:
"""
Returns a complex object's field value, given the field's entity ID.
:param obj: complex object,
:param field_id: field ID,
:return: complex object field's value and type.
"""
for fname, ftype in obj._schema.items():
if entity_id(fname) == field_id:
return getattr(obj, fname, getattr(ftype, 'default')), ftype
def unsigned(value: int, c_type: ctypes._SimpleCData = ctypes.c_uint) -> int:
""" Convert signed integer value to unsigned. """
return c_type(value).value
def capitalize(string: str) -> str:
"""
Capitalizing the string, assuming the first character is a letter.
Does not touch any other character, unlike the `string.capitalize()`.
"""
return string[:1].upper() + string[1:]
def process_delimiter(name: str, delimiter: str) -> str:
"""
Splits the name by delimiter, capitalize each part, merge.
"""
return ''.join([capitalize(x) for x in name.split(delimiter)])
def deprecated(version, reason):
def decorator_deprecated(fn):
@wraps(fn)
def wrapper_deprecated(*args, **kwds):
warnings.warn(f'Deprecated since {version}. The reason: {reason}', category=DeprecationWarning)
return fn(*args, **kwds)
return wrapper_deprecated
return decorator_deprecated