Coverage for src / lexigram / admin / lib / specifications.py: 0%

58 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-11 02:25 +0800

1"""Lightweight specification classes for admin filtering. 

2 

3Provides ``FieldSpecification`` and ``AndSpecification`` used by 

4:meth:`~lexigram.admin.controllers.base.AdminController.build_specification` 

5to compose query-parameter filters into an in-memory specification chain. 

6""" 

7 

8from __future__ import annotations 

9 

10from enum import StrEnum 

11from typing import Any 

12 

13from lexigram.contracts.domain.specification import SpecificationProtocol 

14 

15 

16class SpecificationBase(SpecificationProtocol[Any]): 

17 """Base class providing logical operators for specifications.""" 

18 

19 def is_satisfied_by(self, candidate: Any) -> bool: 

20 raise NotImplementedError 

21 

22 def __and__(self, other: SpecificationProtocol) -> AndSpecification: 

23 return AndSpecification(self, other) 

24 

25 

26class ComparisonOperator(StrEnum): 

27 """Operators supported by :class:`FieldSpecification`.""" 

28 

29 EQ = "eq" 

30 NE = "ne" 

31 GT = "gt" 

32 GTE = "gte" 

33 LT = "lt" 

34 LTE = "lte" 

35 IN = "in" 

36 CONTAINS = "contains" 

37 STARTSWITH = "startswith" 

38 ENDSWITH = "endswith" 

39 

40 

41class FieldSpecification(SpecificationBase): 

42 """SpecificationProtocol that matches objects by a named field and comparison operator. 

43 

44 Args: 

45 field: Attribute name to inspect on the candidate object. 

46 value: Target value to compare against. 

47 operator: Comparison operator (default ``ComparisonOperator.EQ``). 

48 """ 

49 

50 def __init__( 

51 self, 

52 field: str, 

53 value: Any, 

54 operator: ComparisonOperator | str = ComparisonOperator.EQ, 

55 ) -> None: 

56 self.field = field 

57 self.value = value 

58 self.operator = ComparisonOperator( 

59 operator.lower() if isinstance(operator, str) else operator.value 

60 ) 

61 

62 def is_satisfied_by(self, candidate: Any) -> bool: 

63 """Return whether *candidate* satisfies this specification.""" 

64 field_value = getattr(candidate, self.field, None) 

65 op = self.operator 

66 

67 if op == ComparisonOperator.EQ: 

68 return bool(field_value == self.value) 

69 if op == ComparisonOperator.NE: 

70 return bool(field_value != self.value) 

71 if op == ComparisonOperator.GT: 

72 return bool(field_value > self.value) 

73 if op == ComparisonOperator.GTE: 

74 return bool(field_value >= self.value) 

75 if op == ComparisonOperator.LT: 

76 return bool(field_value < self.value) 

77 if op == ComparisonOperator.LTE: 

78 return bool(field_value <= self.value) 

79 if op == ComparisonOperator.IN: 

80 return bool(field_value in self.value) 

81 if op == ComparisonOperator.CONTAINS: 

82 return bool(self.value in field_value if field_value else False) 

83 if op == ComparisonOperator.STARTSWITH: 

84 return bool( 

85 str(field_value).startswith(str(self.value)) if field_value else False 

86 ) 

87 # ENDSWITH 

88 return bool( 

89 str(field_value).endswith(str(self.value)) if field_value else False 

90 ) 

91 

92 

93class AndSpecification(SpecificationBase): 

94 """Combined specification using AND logic.""" 

95 

96 def __init__( 

97 self, left: SpecificationProtocol, right: SpecificationProtocol 

98 ) -> None: 

99 self._left = left 

100 self._right = right 

101 

102 def is_satisfied_by(self, candidate: Any) -> bool: 

103 return self._left.is_satisfied_by(candidate) and self._right.is_satisfied_by( 

104 candidate 

105 ) 

106 

107 def __invert__(self) -> SpecificationProtocol: 

108 raise NotImplementedError 

109 

110 def __or__(self, other: SpecificationProtocol) -> SpecificationProtocol: 

111 raise NotImplementedError 

112 

113 

114__all__ = [ 

115 "AndSpecification", 

116 "ComparisonOperator", 

117 "FieldSpecification", 

118 "SpecificationBase", 

119]