Coverage for src/lexigram/admin/lib/specifications.py: 0%
58 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
1"""Lightweight specification classes for admin filtering.
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"""
8from __future__ import annotations
10from enum import StrEnum
11from typing import Any
13from lexigram.contracts.domain.specification import SpecificationProtocol
16class SpecificationBase(SpecificationProtocol[Any]):
17 """Base class providing logical operators for specifications."""
19 def is_satisfied_by(self, candidate: Any) -> bool:
20 raise NotImplementedError
22 def __and__(self, other: SpecificationProtocol) -> AndSpecification:
23 return AndSpecification(self, other)
26class ComparisonOperator(StrEnum):
27 """Operators supported by :class:`FieldSpecification`."""
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"
41class FieldSpecification(SpecificationBase):
42 """SpecificationProtocol that matches objects by a named field and comparison operator.
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 """
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 )
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
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 )
93class AndSpecification(SpecificationBase):
94 """Combined specification using AND logic."""
96 def __init__(
97 self, left: SpecificationProtocol, right: SpecificationProtocol
98 ) -> None:
99 self._left = left
100 self._right = right
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 )
107 def __invert__(self) -> SpecificationProtocol:
108 raise NotImplementedError
110 def __or__(self, other: SpecificationProtocol) -> SpecificationProtocol:
111 raise NotImplementedError
114__all__ = [
115 "AndSpecification",
116 "ComparisonOperator",
117 "FieldSpecification",
118 "SpecificationBase",
119]