Skip to content

Instantly share code, notes, and snippets.

@goroba
Last active December 17, 2023 21:08
Show Gist options
  • Save goroba/1920d9cf8b6194467af870ed1845af53 to your computer and use it in GitHub Desktop.
Save goroba/1920d9cf8b6194467af870ed1845af53 to your computer and use it in GitHub Desktop.
Operators for data types that are leveraged in business rules
# уже реализован в app.models.enums
class Operator(Enum):
EQ = 'EQ'
...
# уже реализован в app.models.enums
class DataType(Enum):
INTEGER='INTEGER'
...
# app/services/business_rules/operators/__init__.py
import inspect
# apply operator for first and second value of given data type
def apply_operator(operator: Operator, first: str, second: str | None, data_type: DataType | DataTypeAwared) -> bool:
operators = get_operators(data_type)
operator_func = get_operator(operator, operators)
if not operator_func.is_binary:
return operator_func(operators.validate_and_cast(first))
return operator_func(operators.validate_and_cast(first), operators.validate_and_cast(second))
def validate_value(value: str, data_type: DataType | DataTypeAwared) -> bool:
operators = get_operators(data_type)
operators.validate_and_cast(value)
return True
def validate_operator(operator: Operator, data_type: DataType | DataTypeAwared) -> bool:
operators = get_operators(data_type)
get_operator(operator, operators)
return True
# Список доступных типов операторов, с перечнем доступных операторов для каждого типа
def available_operators() -> dict[str, list[dict[str, str | bool]]]:
operator_list = {}
for data_type in DataType:
operator_list[data_type.value] = []
operators = get_operators(data_type)
for name, method in inspect.getmembers(operators):
if getattr(method, 'is_operator', False) is True:
operator_list[data_type.value].append({
'operator': getattr(method, 'operator'),
'is_binary': getattr(method, 'is_binary', True),
'description': getattr(method, 'description', None),
})
return operator_list
operators_registry = {
DataType.INTEGER: IntegerOperators,
DataType.FLOAT: FloatOperators,
DataType.BOOLEAN: BooleanOperators,
DataType.STRING: StringOperators,
DataType.DATE: DateOperators,
DataType.DATETIME: DatetimeOperators,
DataType.PERCENT: PercentOperators,
DataType.ENUMERATE: EnumerateOperators,
}
def get_operators(data_type: DataType | DataTypeAwared) -> DataTypeOperators:
if isinstance(data_type, DataType):
return operators_registry[data_type]()
elif isinstance(data_type, DataTypeAwared):
if data_type.data_options:
return operators_registry[data_type.data_type](data_type.data_options)
else:
return operators_registry[data_type.data_type]()
raise ValueError()
def get_operator(operator: Operator, operators: DataTypeOperators) -> Callable[[CastedType, CastedType | None], bool]:
for name, method in inspect.getmembers(operators):
if getattr(method, 'operator', None) == operator.value:
return method
raise ValueError(f'{operator} not found')
# Данным декоратором отмечается реализация конкретного
# оператора для имплементируемого типа
def binary_operator(operator: Operator, description: str | None = None):
def wrapper(func):
func.is_operator = True
func.operator = operator.value
func.is_binary = True
func.description = description
@wraps(func)
def wrapped(self, first: str, second: str):
return func(self, self.validate_and_cast(first), self.validate_and_cast(second))
return wrapped
return wrapper
def unary_operator(operator: Operator, description: str | None = None):
def wrapper(func):
func.is_operator = True
func.operator = operator.value
func.is_binary = False
func.description = description
@wraps(func)
def wrapped(self, first: str):
return func(self, self.validate_and_cast(first))
return wrapped
return wrapper
# app/services/business_rules/operators/base.py
CastedType = TypeVar('CastedType')
class DataTypeOperators(Generic[CastedType]):
def validate_and_cast(self, value: str) -> CastedType:
raise NotImplemented()
def to_str(self, value: CastedType) -> str:
raise NotImplemented()
# app/services/business_rules/operators/integer.py
class IntegerOperators(DataTypeOperators[int]):
def validate_and_cast(self, value: str) -> int:
try:
return int(value)
except Exception:
raise ValueError('Invalid Integer value')
def to_str(self, value: int) -> str:
return str(value)
@binary_operator(Operator.EQ, description='Equal')
def eq(self, first: int, second: int) -> bool:
return first == second
@binary_operator(Operator.NE, description='Not Equal')
def ne(self, first: int, second: int) -> bool:
return first != second
# app/services/business_rules/operators/float.py
class FloatOperators(DataTypeOperators[float]):
def validate_and_cast(self, value: str) -> float:
try:
return float(value)
except Exception:
raise ValueError('Invalid Float value')
def to_str(self, value: float) -> str:
return str(value)
@binary_operator(Operator.EQ, description='Equal')
def eq(self, first: float, second: float) -> bool:
return math.isclose(first, second, abs_tol=0.01)
# app/services/business_rules/operators/boolean.py
class BooleanOperators(DataTypeOperators[bool]):
def validate_and_cast(self, value: str) -> bool:
if value.lower() in ['true', '1', 'yes', 'y']:
return True
elif value.lower() in ['false', '0', '-1', 'no', 'n']:
return False
raise ValueError()
# app/services/business_rules/operators/string.py
class StringOperators(DataTypeOperators[str]):
...
# app/services/business_rules/operators/percent.py
class PercentOperators(DataTypeOperators[float]):
def validate_and_cast(self, value: str) -> float:
try:
percent = float(value)
except e:
raise ValueError()
if percent < 0 or percent > 100:
raise ValueError()
return percent
# app/services/business_rules/operators/date.py
class DateOperators(DataTypeOperators[date]):
format = 'yyyy.mm.dd'
# app/services/business_rules/operators/datetime.py
class DatetimeOperators(DataTypeOperators[datetime]):
format = 'yyyy.mm.dd hh:ii:ss'
# app/services/business_rules/operators/enumerate.py
class EnumerateOperators(DataTypeOperators[str]):
def __init__(self, options: list[str] = []) -> None:
self.options = options
def validate_and_cast(self, value: str) -> str:
if value not in self.options:
raise ValueError('Incorrect enum value')
return value
@binary_operator(Operator.EQ, description='Equal')
def eq(self, first: str, second: str) -> bool:
return first == second
if __name__ == "__main__":
class ConcreteEnum(DataTypeAwared):
@property
def data_type(self) -> DataType:
return DataType.ENUMERATE
@property
def data_options(self) -> list[str] | None:
return ['yes', 'no']
assert validate_operator(Operator.EQ, DataType.INTEGER) is True
assert validate_value('2', DataType.INTEGER) is True
assert apply_operator(Operator.EQ, '2', '2', DataType.INTEGER) is True
assert apply_operator(Operator.EQ, '2', '3', DataType.INTEGER) is False
assert validate_value('yes', ConcreteEnum()) is True
assert apply_operator(Operator.EQ, 'yes', 'yes', ConcreteEnum()) is True
assert apply_operator(Operator.EQ, 'yes', 'no', ConcreteEnum()) is False
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment