Skip to content

Commit 910ef37

Browse files
authored
Merge pull request #62 from mafzaal/copilot/fix-61
[FEATURE] Implement System Settings Management Feature with Database Storage and Admin UI
2 parents 64408f1 + ac8454a commit 910ef37

127 files changed

Lines changed: 5928 additions & 282 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,11 @@ This file provides guidelines for GitHub Copilot to interact with this repositor
4040
- Use `test_document_loader_pytest.py` for main tests
4141
- Use `test_document_loader_comprehensive_pytest.py` for comprehensive tests
4242
- Use `_pytest` suffix for new pytest files to avoid conflicts with existing unittest files
43+
44+
# Documentation and Comments
45+
- Use clear, concise comments in code to explain complex logic or important decisions
46+
- Use docstrings for functions and classes to describe their purpose, parameters, and return values
47+
- Maintain consistent formatting and style across all documentation files
48+
- Use Markdown for documentation files, ensuring proper headings, lists, and code blocks
49+
- Store documentation in the `docs/` directory, with subdirectories for specific topics (e.g., `docs/frontend/`, `docs/backend/`, etc.)
50+
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""FastAPI endpoints for settings management."""
2+
from fastapi import APIRouter, HTTPException, Depends
3+
from typing import List
4+
import logging
5+
6+
from lets_talk.api.models.settings import (
7+
SettingsResponse,
8+
SettingsUpdateSchema,
9+
SettingsUpdateResponse,
10+
RestoreDefaultsResponse
11+
)
12+
from lets_talk.core.services.settings import SettingsService
13+
from lets_talk.core.services.settings_init import settings_initializer
14+
from lets_talk.shared.config import LOGGER_NAME
15+
16+
logger = logging.getLogger(f"{LOGGER_NAME}.api.settings")
17+
18+
router = APIRouter(
19+
prefix="/settings",
20+
tags=["settings"],
21+
responses={404: {"description": "Not found"}},
22+
)
23+
24+
25+
def get_settings_service():
26+
"""Dependency to get settings service instance."""
27+
return SettingsService()
28+
29+
30+
@router.get("/", response_model=SettingsResponse)
31+
async def get_settings(settings_service: SettingsService = Depends(get_settings_service)):
32+
"""
33+
Get all system settings.
34+
35+
Returns all settings with secrets masked and organized by sections.
36+
"""
37+
try:
38+
# Ensure settings are initialized
39+
if not settings_initializer.ensure_settings_initialized():
40+
raise HTTPException(
41+
status_code=500,
42+
detail="Failed to initialize settings database"
43+
)
44+
45+
settings = settings_service.get_all_settings()
46+
sections = settings_service.get_sections()
47+
48+
return SettingsResponse(
49+
settings=settings,
50+
sections=sorted(sections)
51+
)
52+
53+
except Exception as e:
54+
logger.error(f"Error getting settings: {e}")
55+
raise HTTPException(
56+
status_code=500,
57+
detail=f"Internal server error: {str(e)}"
58+
)
59+
60+
61+
@router.put("/", response_model=SettingsUpdateResponse)
62+
async def update_settings(
63+
update_request: SettingsUpdateSchema,
64+
settings_service: SettingsService = Depends(get_settings_service)
65+
):
66+
"""
67+
Update multiple system settings.
68+
69+
Only read-write settings can be updated. Read-only settings will be rejected.
70+
"""
71+
try:
72+
# Ensure settings are initialized
73+
if not settings_initializer.ensure_settings_initialized():
74+
raise HTTPException(
75+
status_code=500,
76+
detail="Failed to initialize settings database"
77+
)
78+
79+
successful_updates, failed_updates = settings_service.update_settings(
80+
update_request.settings
81+
)
82+
83+
success = len(failed_updates) == 0
84+
message = "All settings updated successfully" if success else "Some settings failed to update"
85+
86+
return SettingsUpdateResponse(
87+
success=success,
88+
message=message,
89+
updated_settings=successful_updates,
90+
failed_settings=failed_updates
91+
)
92+
93+
except Exception as e:
94+
logger.error(f"Error updating settings: {e}")
95+
raise HTTPException(
96+
status_code=500,
97+
detail=f"Internal server error: {str(e)}"
98+
)
99+
100+
101+
@router.post("/restore-defaults", response_model=RestoreDefaultsResponse)
102+
async def restore_default_settings(
103+
settings_service: SettingsService = Depends(get_settings_service)
104+
):
105+
"""
106+
Restore all read-write settings to their default values.
107+
108+
This will reset all editable settings to their original default values.
109+
Read-only settings are not affected.
110+
"""
111+
try:
112+
# Ensure settings are initialized
113+
if not settings_initializer.ensure_settings_initialized():
114+
raise HTTPException(
115+
status_code=500,
116+
detail="Failed to initialize settings database"
117+
)
118+
119+
restored_count = settings_service.restore_defaults()
120+
121+
return RestoreDefaultsResponse(
122+
success=True,
123+
message=f"Successfully restored {restored_count} settings to defaults",
124+
restored_count=restored_count
125+
)
126+
127+
except Exception as e:
128+
logger.error(f"Error restoring defaults: {e}")
129+
raise HTTPException(
130+
status_code=500,
131+
detail=f"Internal server error: {str(e)}"
132+
)

backend/lets_talk/api/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99

1010
from lets_talk.api.dependencies import set_scheduler_instance
11-
from lets_talk.api.endpoints import scheduler, pipeline, health
11+
from lets_talk.api.endpoints import scheduler, pipeline, health, settings
1212
from lets_talk.core.scheduler.manager import PipelineScheduler
1313
from lets_talk.shared.config import LOGGER_NAME
1414

@@ -102,9 +102,9 @@ def create_app() -> FastAPI:
102102
"description": "Background task scheduler endpoints. Control the execution of scheduled tasks and monitor their status."
103103
},
104104
{
105-
"name": "examples",
106-
"description": "Example endpoints demonstrating API patterns, request/response models, and documentation best practices. These endpoints showcase common REST operations and can be used for testing and learning."
107-
}
105+
"name": "settings",
106+
"description": "System settings management endpoints. View, update, and restore system configurations through a user-friendly API."
107+
},
108108
],
109109
docs_url="/docs",
110110
redoc_url="/redoc",
@@ -126,6 +126,7 @@ def create_app() -> FastAPI:
126126
app.include_router(scheduler.router)
127127
app.include_router(pipeline.router)
128128
app.include_router(health.router)
129+
app.include_router(settings.router)
129130

130131

131132
def custom_openapi():
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Pydantic schemas for settings API."""
2+
from pydantic import BaseModel, Field
3+
from typing import Dict, List, Optional, Any, Union
4+
from datetime import datetime
5+
6+
7+
class SettingSchema(BaseModel):
8+
"""Schema for individual setting representation."""
9+
key: str = Field(..., description="Unique identifier for the setting")
10+
value: str = Field(..., description="Current value of the setting")
11+
default_value: str = Field(..., description="Default value of the setting")
12+
data_type: str = Field(..., description="Data type: string, integer, boolean, float")
13+
is_secret: bool = Field(..., description="Whether this is a secret setting")
14+
section: str = Field(..., description="Setting section/category")
15+
description: Optional[str] = Field(None, description="Human-readable description")
16+
is_read_only: bool = Field(..., description="Whether this setting is read-only")
17+
created_at: datetime = Field(..., description="When the setting was created")
18+
updated_at: datetime = Field(..., description="When the setting was last updated")
19+
20+
class Config:
21+
from_attributes = True
22+
23+
24+
class SettingDisplaySchema(BaseModel):
25+
"""Schema for displaying settings (masks secrets)."""
26+
key: str
27+
value: str # This will be masked if is_secret is True
28+
default_value: str
29+
data_type: str
30+
is_secret: bool
31+
section: str
32+
description: Optional[str]
33+
is_read_only: bool
34+
35+
class Config:
36+
from_attributes = True
37+
38+
39+
class SettingUpdateSchema(BaseModel):
40+
"""Schema for updating a single setting."""
41+
key: str = Field(..., description="Setting key to update")
42+
value: str = Field(..., description="New value for the setting")
43+
44+
45+
class SettingsUpdateSchema(BaseModel):
46+
"""Schema for updating multiple settings."""
47+
settings: List[SettingUpdateSchema] = Field(..., description="List of settings to update")
48+
49+
50+
class SettingsResponse(BaseModel):
51+
"""Response schema for settings list."""
52+
settings: List[SettingDisplaySchema] = Field(..., description="List of all settings")
53+
sections: List[str] = Field(..., description="Available setting sections")
54+
55+
56+
class SettingsUpdateResponse(BaseModel):
57+
"""Response schema for settings update."""
58+
success: bool = Field(..., description="Whether the update was successful")
59+
message: str = Field(..., description="Success or error message")
60+
updated_settings: List[str] = Field(..., description="List of updated setting keys")
61+
failed_settings: List[Dict[str, str]] = Field(..., description="List of failed updates with errors")
62+
63+
64+
class RestoreDefaultsResponse(BaseModel):
65+
"""Response schema for restore defaults."""
66+
success: bool = Field(..., description="Whether the restore was successful")
67+
message: str = Field(..., description="Success or error message")
68+
restored_count: int = Field(..., description="Number of settings restored to defaults")
69+
70+
71+
class SettingError(BaseModel):
72+
"""Schema for setting-related errors."""
73+
key: str = Field(..., description="Setting key that caused the error")
74+
error: str = Field(..., description="Error message")
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""SQLAlchemy models for system settings."""
2+
from __future__ import annotations
3+
4+
from sqlalchemy import Column, String, Text, Boolean, DateTime, create_engine
5+
from sqlalchemy.ext.declarative import declarative_base
6+
from sqlalchemy.orm import sessionmaker
7+
from datetime import datetime
8+
from typing import Optional, Dict, Any
9+
import os
10+
from pathlib import Path
11+
12+
from lets_talk.shared.config import OUTPUT_DIR
13+
14+
Base = declarative_base()
15+
16+
17+
class Setting(Base):
18+
"""Model for storing system settings."""
19+
__tablename__ = "settings"
20+
21+
key = Column(String(255), primary_key=True, nullable=False)
22+
value = Column(Text, nullable=False)
23+
default_value = Column(Text, nullable=False)
24+
data_type = Column(String(50), nullable=False) # "string", "integer", "boolean", "float"
25+
is_secret = Column(Boolean, default=False, nullable=False)
26+
section = Column(String(100), nullable=False) # e.g., "General", "API", "Database"
27+
description = Column(Text, nullable=True)
28+
is_read_only = Column(Boolean, default=False, nullable=False)
29+
created_at = Column(DateTime, default=datetime.utcnow)
30+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
31+
32+
def __repr__(self):
33+
return f"<Setting(key='{self.key}', section='{self.section}', is_read_only={self.is_read_only})>"
34+
35+
36+
def get_settings_database_url() -> str:
37+
"""Get the database URL for settings storage."""
38+
os.makedirs(OUTPUT_DIR, exist_ok=True)
39+
return f"sqlite:///{OUTPUT_DIR}/settings.db"
40+
41+
42+
def create_settings_engine():
43+
"""Create SQLAlchemy engine for settings."""
44+
database_url = get_settings_database_url()
45+
engine = create_engine(database_url, echo=False)
46+
return engine
47+
48+
49+
def create_settings_tables(engine):
50+
"""Create settings tables if they don't exist."""
51+
Base.metadata.create_all(engine)
52+
53+
54+
def get_settings_session():
55+
"""Get a session for interacting with the settings database."""
56+
engine = create_settings_engine()
57+
create_settings_tables(engine)
58+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
59+
return SessionLocal()
60+
61+
62+
def init_settings_db():
63+
"""Initialize the settings database and tables."""
64+
engine = create_settings_engine()
65+
create_settings_tables(engine)
66+
return engine
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Services package."""

0 commit comments

Comments
 (0)