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.
This commit is contained in:
Candifloss 2025-03-14 11:32:03 +05:30
parent f398c9e35b
commit 3acbb85b08
11 changed files with 251 additions and 229 deletions

View File

@ -1,4 +1,4 @@
from definitions.attribute import textAttribute, intAttribute, dateAttribute, selectAttribute from definitions.attributes import *
app_secret_key = "test_phase_secret_key" app_secret_key = "test_phase_secret_key"

View File

@ -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

View File

@ -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

View File

@ -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",
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Enum, Integer, Float, String, Date, Column, Boolean from sqlalchemy import Enum, Integer, Float, String, Date, Column, Boolean
from config import item_attributes, sql_conf from config import item_attributes, sql_conf
from definitions.attribute import textAttribute, intAttribute, floatAttribute, dateAttribute, selectAttribute from definitions.attributes import *
# Initialize SQLAlchemy # Initialize SQLAlchemy
db = SQLAlchemy() db = SQLAlchemy()

View File

@ -1,5 +1,5 @@
from typing import List from typing import List
from definitions.attribute import Attribute from definitions.attributes import *
def validate_config(item_attributes: List[Attribute]) -> str: def validate_config(item_attributes: List[Attribute]) -> str:
""" """

View File

@ -3,13 +3,7 @@ import re
from marshmallow import Schema, fields, ValidationError from marshmallow import Schema, fields, ValidationError
from typing import Dict, Any, Optional, Type from typing import Dict, Any, Optional, Type
from config import item_attributes from config import item_attributes
from definitions.attribute import ( from definitions.attributes import *
textAttribute,
intAttribute,
floatAttribute,
dateAttribute,
selectAttribute,
)
def create_dynamic_schema() -> Type[Schema]: def create_dynamic_schema() -> Type[Schema]:
""" """