Skip to content

Commit 5c45b81

Browse files
committed
feat: add Redis client and database session utilities
1 parent 8a584ca commit 5c45b81

3 files changed

Lines changed: 197 additions & 3 deletions

File tree

backend/app/core/config.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ def parse_cors(v: Any) -> list[str] | str:
2525

2626
class Settings(BaseSettings):
2727
model_config = SettingsConfigDict(
28-
# Use top level .env file (one level above ./backend/)
29-
env_file="../.env",
28+
# Use top level .env file (one level above ./backend/), fallback to .env.dev
29+
env_file=["../.env", "../.env.dev"],
3030
env_ignore_empty=True,
3131
extra="ignore",
3232
)
@@ -49,7 +49,13 @@ def all_cors_origins(self) -> list[str]:
4949
]
5050

5151
PROJECT_NAME: str
52-
SENTRY_DSN: HttpUrl | None = None
52+
# SENTRY_DSN: HttpUrl | None = None
53+
54+
# Redis Configuration
55+
REDIS_URL: str = "redis://localhost:6379"
56+
REDIS_PASSWORD: str | None = None
57+
REDIS_DB: int = 0
58+
5359
POSTGRES_SERVER: str
5460
POSTGRES_PORT: int = 5432
5561
POSTGRES_USER: str
@@ -94,6 +100,32 @@ def emails_enabled(self) -> bool:
94100
FIRST_SUPERUSER: EmailStr
95101
FIRST_SUPERUSER_PASSWORD: str
96102

103+
# AWS S3 Configuration
104+
AWS_ACCESS_KEY_ID: str = "changethis"
105+
AWS_SECRET_ACCESS_KEY: str = "changethis"
106+
AWS_REGION: str = "us-east-1"
107+
AWS_S3_BUCKET: str = "changethis"
108+
AWS_S3_BUCKET_URL: HttpUrl | None = None
109+
110+
# CloudFront Configuration
111+
AWS_CLOUDFRONT_DOMAIN: str | None = None
112+
AWS_CLOUDFRONT_KEY_PAIR_ID: str | None = None
113+
AWS_CLOUDFRONT_PRIVATE_KEY_PATH: str | None = None
114+
115+
# File Upload Configuration
116+
MAX_FILE_SIZE: int = 50 * 1024 * 1024 # 50MB
117+
ALLOWED_IMAGE_TYPES: list[str] = ["image/jpeg", "image/png", "image/webp", "image/gif"]
118+
ALLOWED_IMAGE_EXTENSIONS: list[str] = ["jpg", "jpeg", "png", "webp", "gif"]
119+
120+
# Image Processing Configuration
121+
IMAGE_VARIANT_LARGE_SIZE: int = 1200
122+
IMAGE_VARIANT_MEDIUM_SIZE: int = 800
123+
IMAGE_VARIANT_THUMB_SIZE: int = 300
124+
IMAGE_QUALITY_LARGE: int = 85
125+
IMAGE_QUALITY_MEDIUM: int = 85
126+
IMAGE_QUALITY_THUMB: int = 75
127+
IMAGE_MAX_DIMENSIONS: tuple[int, int] = (10000, 10000) # Prevent DOS attacks
128+
97129
def _check_default_secret(self, var_name: str, value: str | None) -> None:
98130
if value == "changethis":
99131
message = (
@@ -113,6 +145,12 @@ def _enforce_non_default_secrets(self) -> Self:
113145
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
114146
)
115147

148+
# Only check AWS secrets in production
149+
if self.ENVIRONMENT != "local":
150+
self._check_default_secret("AWS_ACCESS_KEY_ID", self.AWS_ACCESS_KEY_ID)
151+
self._check_default_secret("AWS_SECRET_ACCESS_KEY", self.AWS_SECRET_ACCESS_KEY)
152+
self._check_default_secret("AWS_S3_BUCKET", self.AWS_S3_BUCKET)
153+
116154
return self
117155

118156

backend/app/core/db.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from sqlmodel import Session, create_engine, select
2+
from contextlib import contextmanager
23

34
from app import crud
45
from app.core.config import settings
@@ -7,6 +8,39 @@
78
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
89

910

11+
def get_db_session():
12+
"""
13+
Get a database session.
14+
15+
Returns:
16+
Session: Database session object
17+
"""
18+
return Session(engine)
19+
20+
21+
@contextmanager
22+
def get_db_context():
23+
"""
24+
Context manager for database sessions.
25+
26+
Usage:
27+
with get_db_context() as db:
28+
# Database operations here
29+
30+
Yields:
31+
Session: Database session object
32+
"""
33+
session = Session(engine)
34+
try:
35+
yield session
36+
session.commit()
37+
except Exception:
38+
session.rollback()
39+
raise
40+
finally:
41+
session.close()
42+
43+
1044
# make sure all SQLModel models are imported (app.models) before initializing DB
1145
# otherwise, SQLModel might fail to initialize relationships properly
1246
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28

backend/app/core/redis.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import redis
2+
from typing import Any, Optional
3+
import json
4+
import pickle
5+
from functools import wraps
6+
7+
from app.core.config import settings
8+
9+
10+
class RedisClient:
11+
def __init__(self):
12+
self._client: Optional[redis.Redis] = None
13+
14+
@property
15+
def client(self) -> redis.Redis:
16+
if self._client is None:
17+
self._client = redis.from_url(
18+
settings.REDIS_URL,
19+
password=settings.REDIS_PASSWORD,
20+
db=settings.REDIS_DB,
21+
decode_responses=False, # Keep binary data for pickle
22+
socket_connect_timeout=5,
23+
socket_timeout=5,
24+
retry_on_timeout=True,
25+
)
26+
return self._client
27+
28+
def ping(self) -> bool:
29+
"""Check Redis connection"""
30+
try:
31+
return bool(self.client.ping())
32+
except redis.ConnectionError:
33+
return False
34+
35+
def get(self, key: str, default: Any = None) -> Any:
36+
"""Get value from Redis"""
37+
try:
38+
value = self.client.get(key)
39+
if value is None:
40+
return default
41+
# Try to deserialize
42+
try:
43+
return pickle.loads(value)
44+
except (pickle.PickleError, TypeError):
45+
# Fallback to JSON or string
46+
try:
47+
return json.loads(value.decode('utf-8'))
48+
except (json.JSONDecodeError, UnicodeDecodeError):
49+
return value.decode('utf-8')
50+
except redis.ConnectionError:
51+
return default
52+
53+
def set(
54+
self,
55+
key: str,
56+
value: Any,
57+
expire: Optional[int] = None,
58+
serialize: bool = True
59+
) -> bool:
60+
"""Set value in Redis"""
61+
try:
62+
if serialize:
63+
serialized = pickle.dumps(value)
64+
else:
65+
serialized = str(value).encode('utf-8')
66+
67+
return bool(self.client.set(key, serialized, ex=expire))
68+
except redis.ConnectionError:
69+
return False
70+
71+
def delete(self, key: str) -> bool:
72+
"""Delete key from Redis"""
73+
try:
74+
return bool(self.client.delete(key))
75+
except redis.ConnectionError:
76+
return False
77+
78+
def exists(self, key: str) -> bool:
79+
"""Check if key exists"""
80+
try:
81+
return bool(self.client.exists(key))
82+
except redis.ConnectionError:
83+
return False
84+
85+
def flushdb(self) -> bool:
86+
"""Flush current database"""
87+
try:
88+
return bool(self.client.flushdb())
89+
except redis.ConnectionError:
90+
return False
91+
92+
def close(self):
93+
"""Close Redis connection"""
94+
if self._client:
95+
self._client.close()
96+
self._client = None
97+
98+
99+
# Global Redis client instance
100+
redis_client = RedisClient()
101+
102+
103+
def cache_result(expire: int = 3600, key_prefix: str = ""):
104+
"""Decorator to cache function results"""
105+
def decorator(func):
106+
@wraps(func)
107+
def wrapper(*args, **kwargs):
108+
# Generate cache key
109+
cache_key = f"{key_prefix}:{func.__name__}:{hash(str(args) + str(sorted(kwargs.items())))}"
110+
111+
# Try to get from cache
112+
cached_result = redis_client.get(cache_key)
113+
if cached_result is not None:
114+
return cached_result
115+
116+
# Execute function and cache result
117+
result = func(*args, **kwargs)
118+
redis_client.set(cache_key, result, expire=expire)
119+
return result
120+
121+
return wrapper
122+
return decorator

0 commit comments

Comments
 (0)