import datetime
import inspect
from collections import defaultdict
from functools import wraps, update_wrapper
from itertools import groupby
from typing import Union, Dict, TypeVar, List, Callable, Any, Optional, Type
import operator
LGSearchable = Union[str, int, float, datetime.date, datetime.datetime]
JSONVal = Union[None, bool, str, float, int, List['JSONVal'], Dict[str, 'JSONVal']]
_R = TypeVar('_R')
LGI = TypeVar('LGI', bound='LabGuruItem')
class LGSearchOperator:
def __init__(self, attr_name: str, lg_field: str, py_operator: Callable,
lg_operator: str, value: LGSearchable) -> None:
self.attr_name = attr_name
self.lg_field = lg_field
self.lg_operator = lg_operator
self.py_operator = py_operator
self.value = value
super().__init__()
def kendo_filter(self) -> Dict[str, JSONVal]:
if isinstance(self.value, datetime.date):
value = self.value.isoformat()
elif isinstance(self.value, bool):
value = 'Yes' if self.value else 'No'
elif 'LabGuruItem' in [c.__name__ for c in self.value.__class__.__mro__]:
value = getattr(self.value, 'name', self.value)
self.lg_operator = self.lg_operator.replace('_', '')
else:
value = self.value
odict = dict(value=value, operator=self.lg_operator, field=self.lg_field)
if 'null' in self.lg_operator:
del odict['value']
return odict
def __add__(self, other) -> "LGSearchAPI":
if isinstance(other, LGSearchOperator):
return LGSearchAPI(self, other)
elif other == 0:
return LGSearchAPI(self)
else:
raise NotImplemented(f"Cannot add {type(self)} with {type(other)}")
def __radd__(self, other) -> "LGSearchAPI":
return self.__add__(other)
def __str__(self):
return str(self.kendo_filter())
class LGSortOperator(LGSearchOperator):
def __init__(self, attr_name: str, lg_field: str, descending=False) -> None:
super().__init__(attr_name, lg_field, lambda a, b: True, 'sorting', 'desc' if descending else 'asc')
def kendo_filter(self) -> Dict[str, JSONVal]:
return dict(field=self.lg_field, dir=self.value)
class LGSearchAPI:
def __init__(self, *filters: Union[LGSearchOperator, LGSortOperator]):
self.filters: List[LGSearchOperator] = []
self.sorters: List[LGSortOperator] = []
for f in filters:
if type(f) == LGSortOperator:
self.sorters.append(f)
elif type(f) == LGSearchOperator:
self.filters.append(f)
def make_filter(self, page_size: int = 50) -> Dict[str, JSONVal]:
if len(self.filters) == 0:
return {}
parameters = {
"kendo" : True,
"filter" : {
"logic" : "and",
"filters": {str(i): f.kendo_filter() for i, f in enumerate(self.filters[:2])}
},
"page_size": page_size
}
if self.sorters:
parameters['sort'] = {str(i): f.kendo_filter() for i, f in enumerate(self.sorters)}
return parameters
def continue_filtering(self, results: List[_R]) -> List[_R]:
if len(self.filters) < 3:
return results
out_list = []
for cur_lgi in results:
try:
for cur_filter in self.filters[2:]:
assert cur_filter.py_operator(getattr(cur_lgi, cur_filter.attr_name), cur_filter.value)
out_list.append(cur_lgi)
except (AssertionError, AttributeError):
pass
return out_list
def __add__(self, other) -> "LGSearchAPI":
if other == 0:
return self
elif isinstance(other, LGSortOperator):
self.sorters.append(other)
elif isinstance(other, LGSearchOperator):
self.filters.append(other)
elif isinstance(other, LGSearchAPI):
self.filters.extend(other.filters)
self.sorters.extend(other.sorters)
else:
raise NotImplemented(f"Cannot add {type(self)} with {type(other)}")
return self
def __radd__(self, other) -> "LGSearchAPI":
return self.__add__(other)
[docs]
class SearchInterface:
"""
Represents an abstract interface for defining search operations and generating search operators.
This class provides methods and operator overloads for creating search operations (like comparison,
string containment, null checks, etc.) and constructing corresponding logical expressions using
searchable attributes. It is intended to be used as a base interface for entities requiring advanced
search capabilities.
Attributes:
labguru_name (str or None): The external name used in the context of Labguru search operations,
or None if not specified. Only used while initializing the searchable object.
"""
private_name = '_' #: :meta private:
labguru_name = None
def _get_property_names(self):
self_name = getattr(self, 'private_name', '_')[1:]
self_lg_name = getattr(self, 'labguru_name', None) or self_name
return self_lg_name, self_name
def _make_search_operator(self, operator_str: str, py_operator: Callable, other: LGSearchable):
self_lg_name, self_name = self._get_property_names()
return LGSearchOperator(self_name, self_lg_name, py_operator, operator_str, other)
[docs]
def asc(self):
"""
Represents an ascending sort operation for the given property name.
Returns:
LGSortOperator: An instance of LGSortOperator representing a sort operation in ascending order.
"""
self_lg_name, self_name = self._get_property_names()
return LGSortOperator(self_name, self_lg_name, False)
[docs]
def desc(self):
"""
Represents a descending sort operation for the given property name.
Returns:
LGSortOperator: An instance of LGSortOperator representing a sort operation in descending order.
"""
self_lg_name, self_name = self._get_property_names()
return LGSortOperator(self_name, self_lg_name, True)
[docs]
def __eq__(self, other: LGSearchable) -> LGSearchOperator:
"""
Checks equality between the current object and another object, returning a
search operator.
Args:
other (LGSearchable | str | bool): The object to compare with the current
`LGSearchable` instance for equality. It can be an instance of
`LGSearchable`, `str`, or `bool`.
Returns:
LGSearchOperator: An operator indicating the equality condition between
the current object and the provided `other` object.
:meta public:
"""
if isinstance(other, (str, bool)):
return self._make_search_operator('eq', operator.eq, other)
else:
return self._make_search_operator('_eq', operator.eq, other)
[docs]
def __ne__(self, other: LGSearchable) -> LGSearchOperator:
"""
Compares the current instance with another value for inequality.
Args:
other (LGSearchable): The value to compare with the current instance.
Returns:
LGSearchOperator: A search operator representing the inequality comparison.
:meta public:
"""
if isinstance(other, str):
return self._make_search_operator('neq', operator.ne, other)
else:
return self._make_search_operator('_neq', operator.ne, other)
[docs]
def __le__(self, other: LGSearchable) -> LGSearchOperator:
"""
Compares the current object with another object to check if it is less than or
equal to the other object. Generates a logical search operator for this comparison.
Args:
other (LGSearchable): The object to compare with the current object.
Returns:
LGSearchOperator: A logical search operator representing the less-than-or-equal-to
comparison.
:meta public:
"""
return self._make_search_operator('_lte', operator.le, other)
[docs]
def __lt__(self, other: LGSearchable) -> LGSearchOperator:
"""
Compares the current object with another object to determine if the current
object is less than the other object. This comparison generates a search
operator encapsulating the comparison logic.
Args:
other (LGSearchable): The object to compare with the current object.
Returns:
LGSearchOperator: A search operator representing the less-than comparison.
:meta public:
"""
return self._make_search_operator('_lt', operator.lt, other)
[docs]
def __gt__(self, other: LGSearchable) -> LGSearchOperator:
"""
Compares the current object with another LGSearchable object to determine if the
current object is greater than the other.
Args:
other: The LGSearchable object to compare against the current object.
Returns:
LGSearchOperator: Represents the comparison operation indicating whether
the current object is greater than the provided object.
:meta public:
"""
return self._make_search_operator('_gt', operator.gt, other)
[docs]
def __ge__(self, other: LGSearchable) -> LGSearchOperator:
"""
Compares the current instance with another LGSearchable object to determine if the
current instance is greater than or equal to the other.
Args:
other (LGSearchable): The LGSearchable instance to compare against.
Returns:
LGSearchOperator: An operator encapsulating the logical comparison of
greater than or equal.
:meta public:
"""
return self._make_search_operator('_gte', operator.ge, other)
[docs]
def contains(self, other):
"""
Generates and applies a search operation for containment check.
This method creates a search operator based on the specified containment
logic. It uses the 'contains' operator to evaluate whether the current
object contains the provided value.
Args:
other: The value to be checked for containment within the current
object.
Returns:
The result of the search operation as an outcome of containment
verification.
"""
return self._make_search_operator('contains', operator.contains, other)
[docs]
def not_contains(self, other):
"""
Determines whether a value is not contained within another value.
Args:
other: The value to determine if it is not contained in the target.
Returns:
A new search operator object configured with the 'doesnotcontain'
logic evaluating whether `other` is not contained in the subject.
"""
def _nc(a, b):
return not operator.contains(a, b)
return self._make_search_operator('doesnotcontain', _nc, other)
[docs]
def starts_with(self, other: str) -> LGSearchOperator:
"""
Creates a search operator that checks whether the current value starts with
the specified string.
Args:
other (str): The string to check if the current value starts with.
Returns:
LGSearchOperator: A search operator for evaluating the condition.
"""
return self._make_search_operator('startswith', lambda a, b: a.startswith(b), other)
[docs]
def ends_with(self, other: str) -> LGSearchOperator:
"""
Creates and returns a search operator to determine if a string ends with a
specified substring.
Args:
other (str): The substring to check if the string ends with.
Returns:
LGSearchOperator: An object representing the search operation for checking
if the string ends with the specified substring.
"""
return self._make_search_operator('endswith', lambda a, b: a.endswith(b), other)
[docs]
def is_null(self) -> LGSearchOperator:
"""
Checks if the given attribute is null or empty.
Returns:
LGSearchOperator: A search operator object for evaluating a "null" or
"empty" condition.
Raises:
AttributeError: If the attribute `base_type` is not accessible.
"""
if issubclass(getattr(self, 'base_type', int), str):
return self._make_search_operator('isnullorempty', lambda a, b: a is None or a == '', '')
else:
return self._make_search_operator('isnull', lambda a, b: a is None or a == 0, '')
[docs]
def is_not_null(self) -> LGSearchOperator:
"""
Determines whether the value is not null or, in the case of string types, neither null nor empty, and
creates a corresponding search operator.
Returns:
LGSearchOperator: A search operator instance that represents the "is not null" check, tailored for
the specific type of the class.
"""
if issubclass(getattr(self, 'base_type', int), str):
return self._make_search_operator('isnotnullorempty', lambda a, b: a is not None and a != '', '')
else:
return self._make_search_operator('isnotnull', lambda a, b: a is not None and a != 0, '')
# @class_wrapper
class _SearchableProperty(property, SearchInterface):
def __init__(self, fget: Optional[Callable[[Any], Any]] = ..., fset: Optional[Callable[[Any, Any], None]] = ...,
fdel: Optional[Callable[[Any], None]] = ..., doc: Optional[str]= ...):
super().__init__(fget, fset, fdel, doc)
d = dict(inspect.getmembers(fget))
self.private_name = "_" + d['__name__']
def make_lg_searchable(prop: property, lg_name: str):
prop = _SearchableProperty(fget=prop.fget, fset=prop.fset, fdel=prop.fdel)
prop.labguru_name = lg_name
return prop
if __name__ == '__main__':
from LabGuruAPI._base import SESSION, LGInt
from LabGuruAPI._collections import Plasmid
#
# x = LGSearchOperator('name', 'name', operator.contains, 'contains', 'pGRO')
x = Plasmid.name.contains('pGRO')
# y = LGSearchOperator('clone_no', 'custom3', operator.contains, 'contains', '-04')
y = Plasmid.clone_no.contains('-04')
# z = LGSearchOperator('clone_no', 'custom3', operator.contains, 'contains', 'GBFP')
z = Plasmid.clone_no.contains('GBFP')
print(x, y, z)
result = Plasmid.find_one(x, y, z)
print(result)