Last active
December 17, 2023 21:08
-
-
Save goroba/1920d9cf8b6194467af870ed1845af53 to your computer and use it in GitHub Desktop.
Operators for data types that are leveraged in business rules
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# уже реализован в 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