Major refactor: security enhancements, modularization, config integration, reduced Parquet reliance

- Migrated codebase to class-based architecture for better modularity and maintainability
- Introduced system_config.json for centralized configuration (required for runtime)
- Added structured working directories for improved file organization
- Significantly reduced reliance on Parquet; replaced with alternative data handling
- Implemented security improvements across modules
- Several TODOs remain in the main script for future enhancements
- Linter formatting affected readability in some files (e.g., utils); cleanup is on the agenda
This commit is contained in:
2025-10-05 22:20:42 -04:00
parent b1e6af01c6
commit 89db386ffe
26 changed files with 3530 additions and 2389 deletions
+121
View File
@@ -0,0 +1,121 @@
# Copyright (C) 2025 James Brotosky, Brandon Wickline
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from typing import Any, List, Optional, Union
logger = logging.getLogger(__name__)
class Selector:
@staticmethod
def select_objects(
objects: List[Any],
allow_multiple: bool = False,
prompt_each: bool = False
) -> Union[Optional[Any], List[Any]]:
if not objects:
logger.warning("No objects available for selection.")
return None
# Sort objects alphabetically by their 'name' attribute
sorted_objects = sorted(objects, key=lambda obj: getattr(obj, "name", str(obj)).lower())
# Display in 4 columns with extra spacing
num_columns = 4
rows = (len(sorted_objects) + num_columns - 1) // num_columns
print("\nAvailable Choices:")
for row in range(rows):
line = ""
for col in range(num_columns):
idx = row + col * rows
if idx < len(sorted_objects):
obj = sorted_objects[idx]
name = getattr(obj, "name", str(obj))
line += f"{idx + 1}: {name:<30}"
print(line)
selected = []
if allow_multiple:
while True:
choice = input("Select an object by number (or Q to finish): ").strip().lower()
if choice == "q":
break
try:
index = int(choice)
if 1 <= index <= len(sorted_objects):
obj = sorted_objects[index - 1]
if obj not in selected:
selected.append(obj)
if prompt_each:
logger.info(f"Selected: {getattr(obj, 'name', str(obj))}")
else:
logger.warning("Object already selected.")
else:
logger.warning("Selection out of range. Try again.")
except ValueError:
logger.warning("Invalid input. Enter a number or 'Q' to quit.")
return selected if selected else None
else:
try:
choice = int(input("Select one object by number: "))
if 1 <= choice <= len(sorted_objects):
selected_obj = sorted_objects[choice - 1]
logger.info(f"Selected: {getattr(selected_obj, 'name', str(selected_obj))}")
return selected_obj
else:
logger.warning("Selection out of range.")
except ValueError:
logger.warning("Invalid input.")
return None
@staticmethod
def select_value(
prompt: str,
value_type: type = int,
valid_range: Optional[tuple] = None,
allow_quit: bool = False
) -> Optional[Any]:
while True:
user_input = input(prompt).strip().lower()
if allow_quit and user_input == "q":
logger.info("User opted to quit value selection.")
return None
try:
value = value_type(user_input)
if valid_range:
min_val, max_val = valid_range
if not (min_val <= value <= max_val):
logger.warning(f"Value out of range ({min_val}{max_val}).")
continue
logger.info(f"User selected value: {value}")
return value
except ValueError:
logger.warning(f"Invalid input. Expected a {value_type.__name__}.")
@staticmethod
def confirm(prompt: str = "Are you sure? (Y/N): ") -> bool:
while True:
response = input(prompt).strip().lower()
if response in ["y", "yes"]:
logger.info("User confirmed action.")
return True
elif response in ["n", "no"]:
logger.info("User declined action.")
return False
else:
logger.warning("Invalid confirmation input. Expected 'Y' or 'N'.")