-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement ConfigSetting Module to store and manage DB based configura…
…tion (#45)
- Loading branch information
Showing
12 changed files
with
412 additions
and
15 deletions.
There are no files selected for viewing
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
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
Empty file.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from datetime import datetime | ||
|
||
from sqlalchemy import Column, String, DateTime | ||
|
||
from src.database import Base | ||
|
||
|
||
class ConfigSetting(Base): | ||
"""Database model for storing configuration values""" | ||
|
||
__tablename__ = "config_settings" | ||
|
||
key = Column(String, primary_key=True, index=True) | ||
value = Column(String, nullable=True) | ||
value_type = Column(String) # Store the type of value for proper casting | ||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) | ||
|
||
def __repr__(self): | ||
return f"<ConfigSetting {self.key}={self.value} ({self.value_type})>" |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
from fastapi import APIRouter, Depends, HTTPException, Body | ||
from sqlalchemy.orm import Session | ||
from typing import List | ||
|
||
from .schemas import ( | ||
ConfigSettingCreate, | ||
ConfigSettingResponse, | ||
ConfigSettingsBulkUpdate, | ||
) | ||
from .service import get_all_setting, get_setting, set_setting, delete_setting | ||
from .. import Admin | ||
from ..database import get_db | ||
|
||
router = APIRouter() | ||
|
||
|
||
@router.get("/settings", response_model=List[ConfigSettingResponse]) | ||
async def list_configs( | ||
db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current) | ||
): | ||
"""List all configurations""" | ||
return get_all_setting(db) | ||
|
||
|
||
@router.get("/settings/{key}", response_model=ConfigSettingResponse) | ||
async def get_config( | ||
key: str, db: Session = Depends(get_db), admin: Admin = Depends(Admin.get_current) | ||
): | ||
"""Get specific configuration""" | ||
setting = get_setting(db, key) | ||
if not setting: | ||
raise HTTPException(status_code=404, detail="Configuration not found") | ||
return setting | ||
|
||
|
||
@router.post("/settings") | ||
async def update_config( | ||
config: ConfigSettingCreate, | ||
db: Session = Depends(get_db), | ||
admin: Admin = Depends(Admin.get_current), | ||
): | ||
"""Update or create configuration""" | ||
set_setting(db, config.key, config.value) | ||
return {"status": "success", "message": "Configuration Added"} | ||
|
||
|
||
@router.put("/settings/{key}") | ||
async def update_config( | ||
config: ConfigSettingCreate, | ||
db: Session = Depends(get_db), | ||
admin: Admin = Depends(Admin.get_current), | ||
): | ||
"""Update or create configuration""" | ||
set_setting(db, config.key, config.value) | ||
return {"status": "success", "message": "Configuration Added"} | ||
|
||
|
||
@router.post("/settings/bulk") | ||
async def update_bulk_config( | ||
configs: ConfigSettingsBulkUpdate, db: Session = Depends(get_db),admin: Admin = Depends(Admin.get_current) | ||
): | ||
"""Update or create configuration""" | ||
for key, value in configs.settings.items(): | ||
set_setting(db, key, value) | ||
return {"status": "success", "message": "Configurations updated"} | ||
|
||
|
||
@router.delete("/settings/{key}") | ||
async def delete_config(key: str, db: Session = Depends(get_db),admin: Admin = Depends(Admin.get_current)): | ||
"""Delete configuration""" | ||
if not delete_setting(db, key): | ||
raise HTTPException(status_code=404, detail="Configuration not found") | ||
return {"status": "success", "message": "Configuration deleted"} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Pydantic models for API | ||
from datetime import datetime | ||
from typing import Optional, Any, Dict | ||
|
||
from pydantic import BaseModel | ||
|
||
|
||
class ConfigSettingCreate(BaseModel): | ||
key: str | ||
value: Any | ||
description: Optional[str] = None | ||
|
||
|
||
class ConfigSettingsBulkUpdate(BaseModel): | ||
settings: Dict[str, Any] | ||
|
||
|
||
class ConfigSettingResponse(BaseModel): | ||
key: str | ||
value: Any | ||
value_type: str | ||
updated_at: datetime | ||
|
||
class Config: | ||
orm_mode = True |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import json | ||
from datetime import datetime | ||
from typing import Optional, Any | ||
|
||
from sqlalchemy.orm import Session | ||
|
||
from .models import ConfigSetting | ||
|
||
|
||
def get_all_setting(db: Session) -> Optional[Any]: | ||
"""List all configurations""" | ||
settings = db.query(ConfigSetting).all() | ||
return settings | ||
|
||
|
||
def get_setting(db: Session, key: str) -> Optional[Any]: | ||
"""Get a configuration value from database""" | ||
|
||
# Query database | ||
setting = db.query(ConfigSetting).filter(ConfigSetting.key == key).first() | ||
if setting: | ||
try: | ||
value = deserialize_value(setting.value, setting.value_type) | ||
return value | ||
except: | ||
return None | ||
return None | ||
|
||
|
||
def set_setting(db: Session, key: str, value: Any) -> None: | ||
"""Set a configuration value in database""" | ||
value_type = get_value_type(value) | ||
serialized_value = serialize_value(value) | ||
|
||
setting = db.query(ConfigSetting).filter(ConfigSetting.key == key).first() | ||
if setting: | ||
setting.value = serialized_value | ||
setting.value_type = value_type | ||
setting.updated_at = datetime.utcnow() | ||
else: | ||
setting = ConfigSetting(key=key, value=serialized_value, value_type=value_type) | ||
db.add(setting) | ||
|
||
db.commit() | ||
|
||
|
||
def delete_setting(db: Session, key: str) -> bool: | ||
"""Delete a configuration value from database""" | ||
setting = db.query(ConfigSetting).filter(ConfigSetting.key == key).first() | ||
if setting: | ||
db.delete(setting) | ||
db.commit() | ||
return True | ||
return False | ||
|
||
|
||
def get_value_type(value: Any) -> str: | ||
"""Determine the type of a value""" | ||
if value is None: | ||
return "none" | ||
elif isinstance(value, bool): | ||
return "bool" | ||
elif isinstance(value, int): | ||
return "int" | ||
elif isinstance(value, float): | ||
return "float" | ||
elif isinstance(value, (list, tuple)): | ||
return "list" | ||
elif isinstance(value, dict): | ||
return "dict" | ||
return "str" | ||
|
||
|
||
def serialize_value(value: Any) -> str: | ||
"""Serialize value for storage""" | ||
if value is None: | ||
return "None" | ||
if isinstance(value, (list, dict)): | ||
return json.dumps(value) | ||
return str(value) | ||
|
||
|
||
def deserialize_value(value: str, value_type: str) -> Any: | ||
"""Deserialize value from storage""" | ||
if value_type == "none" or value == "None": | ||
return None | ||
elif value_type == "bool": | ||
return value.lower() in ("true", "1", "yes", "on", "t") | ||
elif value_type == "int": | ||
return int(value) | ||
elif value_type == "float": | ||
return float(value) | ||
elif value_type in ("list", "dict"): | ||
return json.loads(value) | ||
return value |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import json | ||
from typing import Any, Optional, Callable, TypeVar, Union | ||
|
||
from decouple import config as decouple_config | ||
from exceptiongroup import catch | ||
|
||
from src.database import GetDB | ||
|
||
from src.config_setting import service as config_setting_service | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
def get_config( | ||
key: str, | ||
default: Any = None, | ||
cast: Optional[Callable[[Any], T]] = None, | ||
) -> Union[T, Any]: | ||
""" | ||
Get configuration value with better type casting and None handling. | ||
Args: | ||
key: The configuration key to look up | ||
default: Default value if key is not found or value is invalid | ||
cast: Optional function to cast the value to a specific type | ||
""" | ||
try: | ||
value = decouple_config(key, default=None) | ||
|
||
# Handle cases where value is None or 'None' string | ||
if value is None or (isinstance(value, str) and value.lower() == "none"): | ||
return default | ||
|
||
# Now value is not None, handle casting | ||
if cast is None: | ||
return value | ||
|
||
# Special handling for boolean values | ||
if cast is bool: | ||
if isinstance(value, str): | ||
return value.lower() in ("true", "1", "yes", "on", "t") | ||
return bool(value) | ||
|
||
# Try to cast the value | ||
try: | ||
return cast(value) | ||
except (ValueError, TypeError): | ||
return default | ||
|
||
except Exception: | ||
return default | ||
|
||
|
||
def get_setting(key: str, default: Any = None, cast: Optional[type] = None) -> Any: | ||
""" | ||
Get configuration value with database override capability. | ||
First checks database, falls back to environment variables if not found. | ||
Args: | ||
key: Configuration key to look up | ||
default: Default value if key is not found | ||
cast: Optional type casting function | ||
""" | ||
|
||
value = None | ||
|
||
# Try to get from database first | ||
try: | ||
with GetDB() as db: | ||
value = config_setting_service.get_setting(db, key) | ||
except Exception: | ||
pass | ||
|
||
# If not in database, fall back to environment | ||
if value is None: | ||
value = get_config(key, default, cast) | ||
|
||
return value |
Oops, something went wrong.