Add role-based filtering and imporve code

This commit is contained in:
Urtzi Alfaro
2025-10-15 16:12:49 +02:00
parent 96ad5c6692
commit 8f9e9a7edc
158 changed files with 11033 additions and 1544 deletions

View File

@@ -15,7 +15,7 @@ from app.schemas.traffic import TrafficDataResponse
from app.registry.city_registry import CityRegistry
from app.registry.geolocation_mapper import GeolocationMapper
from app.repositories.city_data_repository import CityDataRepository
from app.cache.redis_cache import ExternalDataCache
from app.cache.redis_wrapper import ExternalDataCache
from app.services.weather_service import WeatherService
from app.services.traffic_service import TrafficService
from shared.routing.route_builder import RouteBuilder

View File

@@ -12,6 +12,8 @@ import structlog
from app.schemas.traffic import TrafficDataResponse
from app.services.traffic_service import TrafficService
from shared.routing.route_builder import RouteBuilder
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import analytics_tier_required
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -29,6 +31,7 @@ def get_traffic_service():
route_builder.build_base_route("traffic-data"),
response_model=List[TrafficDataResponse]
)
@analytics_tier_required
async def list_traffic_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[date] = Query(None),
@@ -36,10 +39,11 @@ async def list_traffic_data(
latitude: Optional[float] = Query(None),
longitude: Optional[float] = Query(None),
limit: int = Query(100, ge=1, le=1000),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db),
traffic_service: TrafficService = Depends(get_traffic_service)
):
"""List stored traffic data records"""
"""List stored traffic data records (Professional+ tier required)"""
try:
logger.info("Listing traffic data", tenant_id=tenant_id)
@@ -64,9 +68,11 @@ async def list_traffic_data(
route_builder.build_resource_detail_route("traffic-data", "traffic_id"),
response_model=TrafficDataResponse
)
@analytics_tier_required
async def get_traffic_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
traffic_id: UUID = Path(..., description="Traffic data ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db),
traffic_service: TrafficService = Depends(get_traffic_service)
):

View File

@@ -12,6 +12,8 @@ import structlog
from app.schemas.weather import WeatherDataResponse
from app.services.weather_service import WeatherService
from shared.routing.route_builder import RouteBuilder
from shared.auth.decorators import get_current_user_dep
from shared.auth.access_control import analytics_tier_required
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
@@ -29,6 +31,7 @@ def get_weather_service():
route_builder.build_base_route("weather-data"),
response_model=List[WeatherDataResponse]
)
@analytics_tier_required
async def list_weather_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
start_date: Optional[date] = Query(None),
@@ -36,10 +39,11 @@ async def list_weather_data(
latitude: Optional[float] = Query(None),
longitude: Optional[float] = Query(None),
limit: int = Query(100, ge=1, le=1000),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db),
weather_service: WeatherService = Depends(get_weather_service)
):
"""List stored weather data records"""
"""List stored weather data records (Professional+ tier required)"""
try:
logger.info("Listing weather data", tenant_id=tenant_id)
@@ -64,9 +68,11 @@ async def list_weather_data(
route_builder.build_resource_detail_route("weather-data", "weather_id"),
response_model=WeatherDataResponse
)
@analytics_tier_required
async def get_weather_data(
tenant_id: UUID = Path(..., description="Tenant ID"),
weather_id: UUID = Path(..., description="Weather data ID"),
current_user: dict = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db),
weather_service: WeatherService = Depends(get_weather_service)
):

View File

@@ -1,15 +1,13 @@
# services/external/app/cache/redis_cache.py
# services/external/app/cache/redis_wrapper.py
"""
Redis cache layer for fast training data access
Redis cache layer for fast training data access using shared Redis implementation
"""
from typing import List, Dict, Any, Optional
import json
from datetime import datetime, timedelta
import structlog
import redis.asyncio as redis
from app.core.config import settings
from shared.redis_utils import get_redis_client
logger = structlog.get_logger()
@@ -18,12 +16,11 @@ class ExternalDataCache:
"""Redis cache for external data service"""
def __init__(self):
self.redis_client = redis.from_url(
settings.REDIS_URL,
encoding="utf-8",
decode_responses=True
)
self.ttl = 86400 * 7
self.ttl = 86400 * 7 # 7 days
async def _get_client(self):
"""Get the shared Redis client"""
return await get_redis_client()
def _weather_cache_key(
self,
@@ -43,7 +40,8 @@ class ExternalDataCache:
"""Get cached weather data"""
try:
key = self._weather_cache_key(city_id, start_date, end_date)
cached = await self.redis_client.get(key)
client = await self._get_client()
cached = await client.get(key)
if cached:
logger.debug("Weather cache hit", city_id=city_id, key=key)
@@ -84,7 +82,8 @@ class ExternalDataCache:
serializable_data.append(record_dict)
await self.redis_client.setex(
client = await self._get_client()
await client.setex(
key,
self.ttl,
json.dumps(serializable_data)
@@ -113,7 +112,8 @@ class ExternalDataCache:
"""Get cached traffic data"""
try:
key = self._traffic_cache_key(city_id, start_date, end_date)
cached = await self.redis_client.get(key)
client = await self._get_client()
cached = await client.get(key)
if cached:
logger.debug("Traffic cache hit", city_id=city_id, key=key)
@@ -154,7 +154,8 @@ class ExternalDataCache:
serializable_data.append(record_dict)
await self.redis_client.setex(
client = await self._get_client()
await client.setex(
key,
self.ttl,
json.dumps(serializable_data)
@@ -168,11 +169,18 @@ class ExternalDataCache:
async def invalidate_city_cache(self, city_id: str):
"""Invalidate all cache entries for a city"""
try:
client = await self._get_client()
pattern = f"*:{city_id}:*"
async for key in self.redis_client.scan_iter(match=pattern):
await self.redis_client.delete(key)
logger.info("City cache invalidated", city_id=city_id)
# Use scan_iter for safer key pattern matching
keys_to_delete = []
async for key in client.scan_iter(match=pattern):
keys_to_delete.append(key)
if keys_to_delete:
await client.delete(*keys_to_delete)
logger.info("City cache invalidated", city_id=city_id, keys_deleted=len(keys_to_delete))
except Exception as e:
logger.error("Error invalidating cache", error=str(e))

View File

@@ -4,6 +4,13 @@ External Service Models Package
Import all models to ensure they are registered with SQLAlchemy Base.
"""
# Import AuditLog model for this service
from shared.security import create_audit_log_model
from shared.database.base import Base
# Create audit log model for this service
AuditLog = create_audit_log_model(Base)
# Import all models to register them with the Base metadata
from .traffic import (
TrafficData,
@@ -31,4 +38,5 @@ __all__ = [
# City-based models (new)
"CityWeatherData",
"CityTrafficData",
"AuditLog",
]