first commit - MVE Incrementables Parser microservice with FastAPI, JWT, Celery, Redis

This commit is contained in:
Ernesto Herrera
2026-03-02 21:31:50 -07:00
commit 068d859f42
27 changed files with 2337 additions and 0 deletions

0
app/core/__init__.py Normal file
View File

25
app/core/celery_app.py Normal file
View File

@@ -0,0 +1,25 @@
"""Celery application configuration."""
from celery import Celery
from .config import get_settings
settings = get_settings()
celery_app = Celery(
"mve_incrementables_parser",
broker=settings.celery_broker_url,
backend=settings.celery_result_backend,
include=["app.tasks.parse_tasks"]
)
# Celery configuration
celery_app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="UTC",
enable_utc=True,
task_track_started=True,
task_time_limit=300, # 5 minutes max
task_soft_time_limit=240, # 4 minutes soft limit
result_expires=3600, # Results expire after 1 hour
)

39
app/core/config.py Normal file
View File

@@ -0,0 +1,39 @@
"""Configuration management using Pydantic Settings."""
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# Service Info
service_name: str = "mve-incrementables-parser"
service_version: str = "1.0.0"
# Authentication
auth_username: str
auth_password_hash: str
jwt_secret: str
jwt_expires_minutes: int = 60
jwt_algorithm: str = "HS256"
# File Upload
max_file_mb: int = 10
# Logging
log_level: str = "INFO"
# Redis/Celery
redis_url: str = "redis://localhost:6379/0"
celery_broker_url: str = "redis://localhost:6379/0"
celery_result_backend: str = "redis://localhost:6379/0"
class Config:
env_file = ".env"
case_sensitive = False
@lru_cache()
def get_settings() -> Settings:
"""Get cached settings instance."""
return Settings()

99
app/core/security.py Normal file
View File

@@ -0,0 +1,99 @@
"""Security utilities for authentication and JWT token management."""
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
import bcrypt
from fastapi import HTTPException, status
from .config import get_settings
import time
from collections import defaultdict
# Simple in-memory rate limiter for login attempts
login_attempts = defaultdict(list)
MAX_LOGIN_ATTEMPTS = 5
LOGIN_WINDOW_SECONDS = 300 # 5 minutes
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash."""
try:
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
except Exception as e:
return False
def get_password_hash(password: str) -> str:
"""Generate password hash."""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token."""
settings = get_settings()
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.jwt_expires_minutes)
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
encoded_jwt = jwt.encode(
to_encode,
settings.jwt_secret,
algorithm=settings.jwt_algorithm
)
return encoded_jwt
def decode_access_token(token: str) -> dict:
"""Decode and verify JWT token."""
settings = get_settings()
try:
payload = jwt.decode(
token,
settings.jwt_secret,
algorithms=[settings.jwt_algorithm]
)
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid credentials",
headers={"WWW-Authenticate": "Bearer"},
)
def check_rate_limit(identifier: str) -> bool:
"""
Simple rate limiter for login attempts.
Returns True if rate limit exceeded.
"""
current_time = time.time()
# Clean old attempts
login_attempts[identifier] = [
attempt_time for attempt_time in login_attempts[identifier]
if current_time - attempt_time < LOGIN_WINDOW_SECONDS
]
# Check if limit exceeded
if len(login_attempts[identifier]) >= MAX_LOGIN_ATTEMPTS:
return True
# Record this attempt
login_attempts[identifier].append(current_time)
return False
def authenticate_user(username: str, password: str) -> bool:
"""Authenticate user with username and password."""
settings = get_settings()
if username != settings.auth_username:
return False
if not verify_password(password, settings.auth_password_hash):
return False
return True