Files
orion/app/services/admin_settings_service.py
Samir Boulahtit 3520bcb069 refactor: move transaction management from services to API endpoints
- Services now use db.flush() instead of db.commit() for database operations
- API endpoints handle transaction commit after service calls
- Remove db.rollback() from services (let exception handlers manage this)
- Ensures consistent transaction boundaries at API layer

This pattern gives API endpoints full control over when to commit,
allowing for better error handling and potential multi-operation transactions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 18:34:41 +01:00

290 lines
9.8 KiB
Python

# app/services/admin_settings_service.py
"""
Admin settings service for platform-wide configuration.
This module provides functions for:
- Managing platform settings
- Getting/setting configuration values
- Encrypting sensitive settings
"""
import json
import logging
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import (
AdminOperationException,
ResourceNotFoundException,
ValidationException,
)
from models.database.admin import AdminSetting
from models.schema.admin import (
AdminSettingCreate,
AdminSettingResponse,
AdminSettingUpdate,
)
logger = logging.getLogger(__name__)
class AdminSettingsService:
"""Service for managing platform-wide settings."""
def get_setting_by_key(self, db: Session, key: str) -> AdminSetting | None:
"""Get setting by key."""
try:
return (
db.query(AdminSetting)
.filter(func.lower(AdminSetting.key) == key.lower())
.first()
)
except Exception as e:
logger.error(f"Failed to get setting {key}: {str(e)}")
return None
def get_setting_value(self, db: Session, key: str, default: Any = None) -> Any:
"""
Get setting value with type conversion.
Args:
key: Setting key
default: Default value if setting doesn't exist
Returns:
Typed setting value
"""
setting = self.get_setting_by_key(db, key)
if not setting:
return default
# Convert value based on type
try:
if setting.value_type == "integer":
return int(setting.value)
if setting.value_type == "float":
return float(setting.value)
if setting.value_type == "boolean":
return setting.value.lower() in ("true", "1", "yes")
if setting.value_type == "json":
return json.loads(setting.value)
return setting.value
except Exception as e:
logger.error(f"Failed to convert setting {key} value: {str(e)}")
return default
def get_all_settings(
self,
db: Session,
category: str | None = None,
is_public: bool | None = None,
) -> list[AdminSettingResponse]:
"""Get all settings with optional filtering."""
try:
query = db.query(AdminSetting)
if category:
query = query.filter(AdminSetting.category == category)
if is_public is not None:
query = query.filter(AdminSetting.is_public == is_public)
settings = query.order_by(AdminSetting.category, AdminSetting.key).all()
return [
AdminSettingResponse.model_validate(setting) for setting in settings
]
except Exception as e:
logger.error(f"Failed to get settings: {str(e)}")
raise AdminOperationException(
operation="get_all_settings", reason="Database query failed"
)
def get_settings_by_category(self, db: Session, category: str) -> dict[str, Any]:
"""
Get all settings in a category as a dictionary.
Returns:
Dictionary of key-value pairs
"""
settings = self.get_all_settings(db, category=category)
result = {}
for setting in settings:
# Convert value based on type
if setting.value_type == "integer":
result[setting.key] = int(setting.value)
elif setting.value_type == "float":
result[setting.key] = float(setting.value)
elif setting.value_type == "boolean":
result[setting.key] = setting.value.lower() in ("true", "1", "yes")
elif setting.value_type == "json":
result[setting.key] = json.loads(setting.value)
else:
result[setting.key] = setting.value
return result
def create_setting(
self, db: Session, setting_data: AdminSettingCreate, admin_user_id: int
) -> AdminSettingResponse:
"""Create new setting."""
try:
# Check if setting already exists
existing = self.get_setting_by_key(db, setting_data.key)
if existing:
raise ValidationException(
f"Setting with key '{setting_data.key}' already exists"
)
# Validate value based on type
self._validate_setting_value(setting_data.value, setting_data.value_type)
# TODO: Encrypt value if is_encrypted=True
value_to_store = setting_data.value
if setting_data.is_encrypted:
# value_to_store = self._encrypt_value(setting_data.value)
pass
setting = AdminSetting(
key=setting_data.key.lower(),
value=value_to_store,
value_type=setting_data.value_type,
category=setting_data.category,
description=setting_data.description,
is_encrypted=setting_data.is_encrypted,
is_public=setting_data.is_public,
last_modified_by_user_id=admin_user_id,
)
db.add(setting)
db.flush()
db.refresh(setting)
logger.info(f"Setting '{setting.key}' created by admin {admin_user_id}")
return AdminSettingResponse.model_validate(setting)
except ValidationException:
raise
except Exception as e:
logger.error(f"Failed to create setting: {str(e)}")
raise AdminOperationException(
operation="create_setting", reason="Database operation failed"
)
def update_setting(
self, db: Session, key: str, update_data: AdminSettingUpdate, admin_user_id: int
) -> AdminSettingResponse:
"""Update existing setting."""
setting = self.get_setting_by_key(db, key)
if not setting:
raise ResourceNotFoundException(resource_type="setting", identifier=key)
try:
# Validate new value
self._validate_setting_value(update_data.value, setting.value_type)
# TODO: Encrypt value if needed
value_to_store = update_data.value
if setting.is_encrypted:
# value_to_store = self._encrypt_value(update_data.value)
pass
setting.value = value_to_store
if update_data.description is not None:
setting.description = update_data.description
setting.last_modified_by_user_id = admin_user_id
setting.updated_at = datetime.now(UTC)
db.flush()
db.refresh(setting)
logger.info(f"Setting '{setting.key}' updated by admin {admin_user_id}")
return AdminSettingResponse.model_validate(setting)
except ValidationException:
raise
except Exception as e:
logger.error(f"Failed to update setting {key}: {str(e)}")
raise AdminOperationException(
operation="update_setting", reason="Database operation failed"
)
def upsert_setting(
self, db: Session, setting_data: AdminSettingCreate, admin_user_id: int
) -> AdminSettingResponse:
"""Create or update setting (upsert)."""
existing = self.get_setting_by_key(db, setting_data.key)
if existing:
update_data = AdminSettingUpdate(
value=setting_data.value, description=setting_data.description
)
return self.update_setting(db, setting_data.key, update_data, admin_user_id)
return self.create_setting(db, setting_data, admin_user_id)
def delete_setting(self, db: Session, key: str, admin_user_id: int) -> str:
"""Delete setting."""
setting = self.get_setting_by_key(db, key)
if not setting:
raise ResourceNotFoundException(resource_type="setting", identifier=key)
try:
db.delete(setting)
logger.warning(f"Setting '{key}' deleted by admin {admin_user_id}")
return f"Setting '{key}' successfully deleted"
except Exception as e:
logger.error(f"Failed to delete setting {key}: {str(e)}")
raise AdminOperationException(
operation="delete_setting", reason="Database operation failed"
)
# ============================================================================
# HELPER METHODS
# ============================================================================
def _validate_setting_value(self, value: str, value_type: str):
"""Validate setting value matches declared type."""
try:
if value_type == "integer":
int(value)
elif value_type == "float":
float(value)
elif value_type == "boolean":
if value.lower() not in ("true", "false", "1", "0", "yes", "no"):
raise ValueError("Invalid boolean value")
elif value_type == "json":
json.loads(value)
except Exception as e:
raise ValidationException(
f"Value '{value}' is not valid for type '{value_type}': {str(e)}"
)
def _encrypt_value(self, value: str) -> str:
"""Encrypt sensitive setting value."""
# TODO: Implement encryption using Fernet or similar
# from cryptography.fernet import Fernet
# return encrypted_value
return value
def _decrypt_value(self, encrypted_value: str) -> str:
"""Decrypt sensitive setting value."""
# TODO: Implement decryption
return encrypted_value
# Create service instance
admin_settings_service = AdminSettingsService()