From 3acbb85b0892041f3c393d2869193b8d7a6379de Mon Sep 17 00:00:00 2001 From: candifloss Date: Fri, 14 Mar 2025 11:32:03 +0530 Subject: [PATCH] refactor: Modularize attribute class definitions - Split `definitions/attribute.py` into separate files under `definitions/attributes/`. - Added `__init__.py` for simplified imports. - Improved code organization and maintainability. --- config.py | 2 +- definitions/attribute.py | 219 ---------------------- definitions/attributes/Attribute.py | 29 +++ definitions/attributes/__init__.py | 16 ++ definitions/attributes/dateAttribute.py | 51 +++++ definitions/attributes/numAttribute.py | 83 ++++++++ definitions/attributes/selectAttribute.py | 24 +++ definitions/attributes/textAttribute.py | 44 +++++ definitions/models.py | 2 +- functions/validate_config.py | 2 +- functions/validate_values.py | 8 +- 11 files changed, 251 insertions(+), 229 deletions(-) delete mode 100644 definitions/attribute.py create mode 100644 definitions/attributes/Attribute.py create mode 100644 definitions/attributes/__init__.py create mode 100644 definitions/attributes/dateAttribute.py create mode 100644 definitions/attributes/numAttribute.py create mode 100644 definitions/attributes/selectAttribute.py create mode 100644 definitions/attributes/textAttribute.py diff --git a/config.py b/config.py index 77ca50a..e40f03a 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute +from definitions.attributes import * app_secret_key = "test_phase_secret_key" diff --git a/definitions/attribute.py b/definitions/attribute.py deleted file mode 100644 index c60993c..0000000 --- a/definitions/attribute.py +++ /dev/null @@ -1,219 +0,0 @@ -from datetime import datetime -import re -from typing import List, Optional, Tuple, Set -from dataclasses import dataclass, field - -ALLOWED_INPUT_TYPES = {"text", "number", "date", "select"} - -@dataclass -class Attribute: - """Base class for all attribute types.""" - attrib_name: str - display_name: str - html_input_type: Optional[str] = "text" - required: bool = False - unique: bool = False - primary: bool = False - default_val: Optional[str] = None - compareto: Optional[List[Tuple[str, str]]] = None - valid_comparisons: Optional[Set[str]] = None - - def validate(self) -> Optional[str]: - """Validate common attributes. Returns an error message if invalid, otherwise None.""" - if not self.attrib_name: - return "Missing attribute name." - if not self.display_name: - return f"Missing display name for attribute '{self.attrib_name}'." - if not self.html_input_type: - return f"Missing input type for attribute '{self.attrib_name}'." - if self.html_input_type not in ALLOWED_INPUT_TYPES: - return f"Invalid input type '{self.html_input_type}' for attribute '{self.attrib_name}'." - return None - -@dataclass -class textAttribute(Attribute): - """Class for text attributes.""" - regex: Optional[str] = None - min_length: Optional[int] = None - max_length: Optional[int] = 50 - allowed_chars: Optional[str] = None - - def __post_init__(self): - """Post-initialization to set the HTML input type.""" - self.html_input_type = "text" - self.valid_comparisons = {"lt", "st", "eq", "ne"}, # longer than, shorter than, eq, or not eq, to the ref attrib - - def validate(self) -> Optional[str]: - """Validate text-specific attributes.""" - error = super().validate() - if error: - return error - if self.min_length is not None and self.max_length is not None: - if not isinstance(self.min_length, int) or not isinstance(self.max_length, int): - return f"Min and max lengths must be integers for '{self.attrib_name}'." - if int(self.min_length) > int(self.max_length): - return f"Max length must be greater than min length for '{self.attrib_name}'." - if self.default_val is not None: - if self.regex is not None: - compiled_regex = re.compile(self.regex) - if not compiled_regex.match(str(self.default_val)): - return f"Default value for '{self.attrib_name}' must match the pattern: {self.regex}" - if self.allowed_chars is not None: - for char in self.default_val: - if char not in self.allowed_chars: - return f"Invalid character '{char}' in default value for '{self.attrib_name}'. Allowed characters are: {self.allowed_chars}" - if self.min_length is not None: - if len(self.default_val) < int(self.min_length): - return f"Invalid default value for '{self.attrib_name}'. The minimum length is: {self.min_length}" - if self.max_length is not None: - if len(self.default_val) > int(self.max_length): - return f"Invalid default value for '{self.attrib_name}'. The maximum length is: {self.max_length}" - return None - -@dataclass -class numAttribute(Attribute): - """Base class for numeric attributes (int and float).""" - min_val: Optional[float] = None - max_val: Optional[float] = None - step: float = 1.0 - - def __post_init__(self): - """Post-initialization to set the HTML input type.""" - self.html_input_type = "number" - self.valid_comparisons = {"lt", "gt", "le", "ge", "eq", "ne"} # <, >, <=, >=, ==, != - - def validate(self) -> Optional[str]: - """Validate numeric-specific attributes.""" - error = super().validate() - if error: - return error - - # Validate min_val and max_val - if self.min_val is not None and not isinstance(self.min_val, (int, float)): - return f"min_val must be a number for attribute '{self.attrib_name}'." - if self.max_val is not None and not isinstance(self.max_val, (int, float)): - return f"max_val must be a number for attribute '{self.attrib_name}'." - if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val: - return f"min_val cannot be greater than max_val for attribute '{self.attrib_name}'." - - # Validate step - if self.step is not None: - if not isinstance(self.step, (int, float)): - return f"Step must be a number for attribute '{self.attrib_name}'." - if self.step <= 0: - return f"Step must be a positive number for attribute '{self.attrib_name}'." - if self.min_val is not None and self.max_val is not None: - range_val = self.max_val - self.min_val - if self.step > range_val: - return f"Step value is too large for attribute '{self.attrib_name}'." - - # Validate default_val - if self.default_val is not None: - if not isinstance(self.default_val, (int, float)): - return f"default_val must be a number for attribute '{self.attrib_name}'." - if self.min_val is not None and self.default_val < self.min_val: - return f"default_val cannot be less than min_val for attribute '{self.attrib_name}'." - if self.max_val is not None and self.default_val > self.max_val: - return f"default_val cannot be greater than max_val for attribute '{self.attrib_name}'." - - return None - -@dataclass -class intAttribute(numAttribute): - """Class for integer attributes.""" - - def validate(self) -> Optional[str]: - """Validate integer-specific attributes.""" - error = super().validate() - if error: - return error - - # Ensure default_val is an integer - if self.default_val is not None and not isinstance(self.default_val, int): - return f"default_val must be an integer for attribute '{self.attrib_name}'." - - return None - -@dataclass -class floatAttribute(numAttribute): - """Class for float attributes.""" - - def validate(self) -> Optional[str]: - """Validate float-specific attributes.""" - error = super().validate() - if error: - return error - - # Ensure default_val is a float - if self.default_val is not None and not isinstance(self.default_val, float): - return f"default_val must be a float for attribute '{self.attrib_name}'." - - return None - -@dataclass -class dateAttribute(Attribute): - """Class for date attributes.""" - min_val: Optional[str] = None - max_val: Optional[str] = None - - def __post_init__(self): - """Post-initialization to set the HTML input type.""" - self.html_input_type = "date" - self.valid_comparisons = {"lt", "gt", "le", "ge", "eq", "ne"}, # <, >, <=, >=, ==, != - - def _is_date(self, value: str) -> bool: - """Check if a value is a valid date in YYYY-MM-DD format.""" - try: - datetime.strptime(value, "%Y-%m-%d") - return True - except ValueError: - return False - - def validate(self) -> Optional[str]: - """Validate date-specific attributes.""" - error = super().validate() - if error: - return error - if self.min_val is not None and not self._is_date(self.min_val): - return f"min_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." - if self.max_val is not None and not self._is_date(self.max_val): - return f"max_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." - if self.min_val is not None and self.max_val is not None: - min_date = datetime.strptime(self.min_val, "%Y-%m-%d") - max_date = datetime.strptime(self.max_val, "%Y-%m-%d") - if max_date < min_date: - return f"max_val cannot be earlier than min_val for attribute '{self.attrib_name}'." - if self.default_val is not None and not self._is_date(self.default_val): - return f"default_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." - if self.default_val is not None: - default_date = datetime.strptime(self.default_val, "%Y-%m-%d") - if self.min_val is not None: - min_date = datetime.strptime(self.min_val, "%Y-%m-%d") - if default_date < min_date: - return f"default_val cannot be earlier than min_val for attribute '{self.attrib_name}'." - if self.max_val is not None: - max_date = datetime.strptime(self.max_val, "%Y-%m-%d") - if default_date > max_date: - return f"default_val cannot be later than max_val for attribute '{self.attrib_name}'." - return None - -@dataclass -class selectAttribute(Attribute): - """Class for select attributes.""" - options: List[str] = None - - def __post_init__(self): - """Post-initialization to set the HTML input type.""" - self.html_input_type = "select" - self.valid_comparisons = {"eq", "ne"} # eq, or not eq, to the ref attrib - - def validate(self) -> Optional[str]: - """Validate select-specific attributes.""" - error = super().validate() - if error: - return error - if not self.options: - return f"options cannot be empty for attribute '{self.attrib_name}'." - if self.default_val is not None and self.default_val not in self.options: - return f"default_val must be one of the options for attribute '{self.attrib_name}'." - return None \ No newline at end of file diff --git a/definitions/attributes/Attribute.py b/definitions/attributes/Attribute.py new file mode 100644 index 0000000..aeca201 --- /dev/null +++ b/definitions/attributes/Attribute.py @@ -0,0 +1,29 @@ +from typing import List, Optional, Tuple, Set +from dataclasses import dataclass, field + +ALLOWED_INPUT_TYPES = {"text", "number", "date", "select"} + +@dataclass +class Attribute: + """Base class for all attribute types.""" + attrib_name: str + display_name: str + html_input_type: Optional[str] = "text" + required: bool = False + unique: bool = False + primary: bool = False + default_val: Optional[str] = None + compareto: Optional[List[Tuple[str, str]]] = None + valid_comparisons: Optional[Set[str]] = None + + def validate(self) -> Optional[str]: + """Validate common attributes. Returns an error message if invalid, otherwise None.""" + if not self.attrib_name: + return "Missing attribute name." + if not self.display_name: + return f"Missing display name for attribute '{self.attrib_name}'." + if not self.html_input_type: + return f"Missing input type for attribute '{self.attrib_name}'." + if self.html_input_type not in ALLOWED_INPUT_TYPES: + return f"Invalid input type '{self.html_input_type}' for attribute '{self.attrib_name}'." + return None \ No newline at end of file diff --git a/definitions/attributes/__init__.py b/definitions/attributes/__init__.py new file mode 100644 index 0000000..b57ecee --- /dev/null +++ b/definitions/attributes/__init__.py @@ -0,0 +1,16 @@ +from .Attribute import Attribute +from .textAttribute import textAttribute +from .numAttribute import numAttribute, intAttribute, floatAttribute +from .dateAttribute import dateAttribute +from .selectAttribute import selectAttribute + +# Export all classes in a list for easier introspection +__all__ = [ + "Attribute", + "textAttribute", + "numAttribute", + "intAttribute", + "floatAttribute", + "dateAttribute", + "selectAttribute", +] \ No newline at end of file diff --git a/definitions/attributes/dateAttribute.py b/definitions/attributes/dateAttribute.py new file mode 100644 index 0000000..b47d267 --- /dev/null +++ b/definitions/attributes/dateAttribute.py @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Optional +from dataclasses import dataclass, field +from .Attribute import Attribute + +@dataclass +class dateAttribute(Attribute): + """Class for date attributes.""" + min_val: Optional[str] = None + max_val: Optional[str] = None + + def __post_init__(self): + """Post-initialization to set the HTML input type.""" + self.html_input_type = "date" + self.valid_comparisons = {"lt", "gt", "le", "ge", "eq", "ne"}, # <, >, <=, >=, ==, != + + def _is_date(self, value: str) -> bool: + """Check if a value is a valid date in YYYY-MM-DD format.""" + try: + datetime.strptime(value, "%Y-%m-%d") + return True + except ValueError: + return False + + def validate(self) -> Optional[str]: + """Validate date-specific attributes.""" + error = super().validate() + if error: + return error + if self.min_val is not None and not self._is_date(self.min_val): + return f"min_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." + if self.max_val is not None and not self._is_date(self.max_val): + return f"max_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." + if self.min_val is not None and self.max_val is not None: + min_date = datetime.strptime(self.min_val, "%Y-%m-%d") + max_date = datetime.strptime(self.max_val, "%Y-%m-%d") + if max_date < min_date: + return f"max_val cannot be earlier than min_val for attribute '{self.attrib_name}'." + if self.default_val is not None and not self._is_date(self.default_val): + return f"default_val must be a valid date (YYYY-MM-DD) for attribute '{self.attrib_name}'." + if self.default_val is not None: + default_date = datetime.strptime(self.default_val, "%Y-%m-%d") + if self.min_val is not None: + min_date = datetime.strptime(self.min_val, "%Y-%m-%d") + if default_date < min_date: + return f"default_val cannot be earlier than min_val for attribute '{self.attrib_name}'." + if self.max_val is not None: + max_date = datetime.strptime(self.max_val, "%Y-%m-%d") + if default_date > max_date: + return f"default_val cannot be later than max_val for attribute '{self.attrib_name}'." + return None \ No newline at end of file diff --git a/definitions/attributes/numAttribute.py b/definitions/attributes/numAttribute.py new file mode 100644 index 0000000..43deaa1 --- /dev/null +++ b/definitions/attributes/numAttribute.py @@ -0,0 +1,83 @@ +from typing import Optional +from dataclasses import dataclass, field +from .Attribute import Attribute + +@dataclass +class numAttribute(Attribute): + """Base class for numeric attributes (int and float).""" + min_val: Optional[float] = None + max_val: Optional[float] = None + step: float = 1.0 + + def __post_init__(self): + """Post-initialization to set the HTML input type.""" + self.html_input_type = "number" + self.valid_comparisons = {"lt", "gt", "le", "ge", "eq", "ne"} # <, >, <=, >=, ==, != + + def validate(self) -> Optional[str]: + """Validate numeric-specific attributes.""" + error = super().validate() + if error: + return error + + # Validate min_val and max_val + if self.min_val is not None and not isinstance(self.min_val, (int, float)): + return f"min_val must be a number for attribute '{self.attrib_name}'." + if self.max_val is not None and not isinstance(self.max_val, (int, float)): + return f"max_val must be a number for attribute '{self.attrib_name}'." + if self.min_val is not None and self.max_val is not None and self.min_val > self.max_val: + return f"min_val cannot be greater than max_val for attribute '{self.attrib_name}'." + + # Validate step + if self.step is not None: + if not isinstance(self.step, (int, float)): + return f"Step must be a number for attribute '{self.attrib_name}'." + if self.step <= 0: + return f"Step must be a positive number for attribute '{self.attrib_name}'." + if self.min_val is not None and self.max_val is not None: + range_val = self.max_val - self.min_val + if self.step > range_val: + return f"Step value is too large for attribute '{self.attrib_name}'." + + # Validate default_val + if self.default_val is not None: + if not isinstance(self.default_val, (int, float)): + return f"default_val must be a number for attribute '{self.attrib_name}'." + if self.min_val is not None and self.default_val < self.min_val: + return f"default_val cannot be less than min_val for attribute '{self.attrib_name}'." + if self.max_val is not None and self.default_val > self.max_val: + return f"default_val cannot be greater than max_val for attribute '{self.attrib_name}'." + + return None + +@dataclass +class intAttribute(numAttribute): + """Class for integer attributes.""" + + def validate(self) -> Optional[str]: + """Validate integer-specific attributes.""" + error = super().validate() + if error: + return error + + # Ensure default_val is an integer + if self.default_val is not None and not isinstance(self.default_val, int): + return f"default_val must be an integer for attribute '{self.attrib_name}'." + + return None + +@dataclass +class floatAttribute(numAttribute): + """Class for float attributes.""" + + def validate(self) -> Optional[str]: + """Validate float-specific attributes.""" + error = super().validate() + if error: + return error + + # Ensure default_val is a float + if self.default_val is not None and not isinstance(self.default_val, float): + return f"default_val must be a float for attribute '{self.attrib_name}'." + + return None \ No newline at end of file diff --git a/definitions/attributes/selectAttribute.py b/definitions/attributes/selectAttribute.py new file mode 100644 index 0000000..03fd0bb --- /dev/null +++ b/definitions/attributes/selectAttribute.py @@ -0,0 +1,24 @@ +from typing import List, Optional +from dataclasses import dataclass, field +from .Attribute import Attribute + +@dataclass +class selectAttribute(Attribute): + """Class for select attributes.""" + options: List[str] = None + + def __post_init__(self): + """Post-initialization to set the HTML input type.""" + self.html_input_type = "select" + self.valid_comparisons = {"eq", "ne"} # eq, or not eq, to the ref attrib + + def validate(self) -> Optional[str]: + """Validate select-specific attributes.""" + error = super().validate() + if error: + return error + if not self.options: + return f"options cannot be empty for attribute '{self.attrib_name}'." + if self.default_val is not None and self.default_val not in self.options: + return f"default_val must be one of the options for attribute '{self.attrib_name}'." + return None \ No newline at end of file diff --git a/definitions/attributes/textAttribute.py b/definitions/attributes/textAttribute.py new file mode 100644 index 0000000..790f600 --- /dev/null +++ b/definitions/attributes/textAttribute.py @@ -0,0 +1,44 @@ +import re +from typing import Optional +from dataclasses import dataclass, field +from .Attribute import Attribute + +@dataclass +class textAttribute(Attribute): + """Class for text attributes.""" + regex: Optional[str] = None + min_length: Optional[int] = None + max_length: Optional[int] = 50 + allowed_chars: Optional[str] = None + + def __post_init__(self): + """Post-initialization to set the HTML input type.""" + self.html_input_type = "text" + self.valid_comparisons = {"lt", "st", "eq", "ne"}, # longer than, shorter than, eq, or not eq, to the ref attrib + + def validate(self) -> Optional[str]: + """Validate text-specific attributes.""" + error = super().validate() + if error: + return error + if self.min_length is not None and self.max_length is not None: + if not isinstance(self.min_length, int) or not isinstance(self.max_length, int): + return f"Min and max lengths must be integers for '{self.attrib_name}'." + if int(self.min_length) > int(self.max_length): + return f"Max length must be greater than min length for '{self.attrib_name}'." + if self.default_val is not None: + if self.regex is not None: + compiled_regex = re.compile(self.regex) + if not compiled_regex.match(str(self.default_val)): + return f"Default value for '{self.attrib_name}' must match the pattern: {self.regex}" + if self.allowed_chars is not None: + for char in self.default_val: + if char not in self.allowed_chars: + return f"Invalid character '{char}' in default value for '{self.attrib_name}'. Allowed characters are: {self.allowed_chars}" + if self.min_length is not None: + if len(self.default_val) < int(self.min_length): + return f"Invalid default value for '{self.attrib_name}'. The minimum length is: {self.min_length}" + if self.max_length is not None: + if len(self.default_val) > int(self.max_length): + return f"Invalid default value for '{self.attrib_name}'. The maximum length is: {self.max_length}" + return None \ No newline at end of file diff --git a/definitions/models.py b/definitions/models.py index 721a7c9..01e370c 100644 --- a/definitions/models.py +++ b/definitions/models.py @@ -1,7 +1,7 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Enum, Integer, Float, String, Date, Column, Boolean from config import item_attributes, sql_conf -from definitions.attribute import textAttribute, intAttribute, floatAttribute, dateAttribute, selectAttribute +from definitions.attributes import * # Initialize SQLAlchemy db = SQLAlchemy() diff --git a/functions/validate_config.py b/functions/validate_config.py index d6d1191..1e46eec 100644 --- a/functions/validate_config.py +++ b/functions/validate_config.py @@ -1,5 +1,5 @@ from typing import List -from definitions.attribute import Attribute +from definitions.attributes import * def validate_config(item_attributes: List[Attribute]) -> str: """ diff --git a/functions/validate_values.py b/functions/validate_values.py index 0dd38ff..2159269 100644 --- a/functions/validate_values.py +++ b/functions/validate_values.py @@ -3,13 +3,7 @@ import re from marshmallow import Schema, fields, ValidationError from typing import Dict, Any, Optional, Type from config import item_attributes -from definitions.attribute import ( - textAttribute, - intAttribute, - floatAttribute, - dateAttribute, - selectAttribute, -) +from definitions.attributes import * def create_dynamic_schema() -> Type[Schema]: """