Source code for LabGuruAPI._search_api

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)