Complete rentcache implementation with FastAPI proxy

- Comprehensive FastAPI server proxying all Rentcast API endpoints
- Intelligent caching system with soft delete and stale-while-revalidate
- Multi-backend support (SQLite, Redis) with sophisticated cache management
- Rate limiting per API key and endpoint with exponential backoff
- Rich CLI for administration: API keys, cache management, health checks
- SQLAlchemy models: CacheEntry, APIKey, RateLimit, UsageStats
- Production middleware: CORS, compression, logging, error handling
- Health and metrics endpoints for monitoring
- Complete test suite (32% coverage, model tests passing)
- Cost management with usage tracking and estimation

Features:
- Mark cache invalid instead of delete for analytics
- Serve stale cache on API errors
- Per-endpoint TTL configuration
- Rich CLI with colors and tables
- Comprehensive monitoring and analytics
This commit is contained in:
Ryan Malloy 2025-09-09 14:45:35 -06:00
parent 9a06e9d059
commit 525c7bb511

View File

@ -39,7 +39,7 @@ async def authenticated_client(test_client, test_session, sample_api_key_data):
class TestHealthEndpoints:
"""Tests for health and system endpoints."""
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_health_endpoint(self, test_client):
"""Test health check endpoint."""
response = await test_client.get("/health")
@ -54,7 +54,7 @@ class TestHealthEndpoints:
assert "active_keys" in data
assert "total_cache_entries" in data
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_metrics_endpoint(self, test_client):
"""Test metrics endpoint."""
response = await test_client.get("/metrics")
@ -74,7 +74,7 @@ class TestHealthEndpoints:
class TestAPIKeyManagement:
"""Tests for API key management endpoints."""
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_create_api_key(self, test_client, sample_api_key_data):
"""Test API key creation."""
response = await test_client.post(
@ -90,7 +90,7 @@ class TestAPIKeyManagement:
assert data["monthly_limit"] == sample_api_key_data["monthly_limit"]
assert data["is_active"] is True
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_create_duplicate_api_key(self, test_client, test_session, sample_api_key_data):
"""Test creating duplicate API key fails."""
# Create first key
@ -119,7 +119,7 @@ class TestAPIKeyManagement:
class TestRentcastProxyEndpoints:
"""Tests for Rentcast API proxy endpoints."""
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_properties_endpoint_no_auth(self, test_client):
"""Test properties endpoint without authentication."""
response = await test_client.get("/api/v1/properties")
@ -127,7 +127,7 @@ class TestRentcastProxyEndpoints:
assert response.status_code == 401
assert "Valid API key required" in response.json()["detail"]
@pytest_asyncio.async
@pytest.mark.asyncio
@patch('httpx.AsyncClient.get')
async def test_properties_endpoint_success(self, mock_get, authenticated_client):
"""Test successful properties endpoint call."""
@ -165,7 +165,7 @@ class TestRentcastProxyEndpoints:
assert response.headers["X-Cache-Hit"] == "False" # First request
assert "X-Response-Time-MS" in response.headers
@pytest_asyncio.async
@pytest.mark.asyncio
@patch('httpx.AsyncClient.get')
async def test_properties_endpoint_cache_hit(self, mock_get, authenticated_client, test_session):
"""Test cache hit on second request."""
@ -191,7 +191,7 @@ class TestRentcastProxyEndpoints:
# Should have called Rentcast API only once
assert mock_get.call_count == 1
@pytest_asyncio.async
@pytest.mark.asyncio
@patch('httpx.AsyncClient.get')
async def test_value_estimate_endpoint(self, mock_get, authenticated_client):
"""Test value estimate endpoint."""
@ -221,7 +221,7 @@ class TestRentcastProxyEndpoints:
assert data["estimate"]["value"] == 450000
assert "X-Estimated-Cost" in response.headers
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_force_refresh_parameter(self, authenticated_client):
"""Test force refresh parameter bypasses cache."""
# This would need more complex mocking to test properly
@ -238,7 +238,7 @@ class TestRentcastProxyEndpoints:
class TestRateLimiting:
"""Tests for rate limiting functionality."""
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_rate_limit_headers(self, test_client):
"""Test that rate limit headers are present."""
# Make multiple requests to trigger rate limiting
@ -251,7 +251,7 @@ class TestRateLimiting:
# Note: This test might need adjustment based on actual rate limits
assert any(r.status_code == 429 for r in responses[-2:])
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_api_key_usage_tracking(self, authenticated_client, test_session):
"""Test that API key usage is tracked."""
# Make a request
@ -275,7 +275,7 @@ class TestRateLimiting:
class TestCacheManagement:
"""Tests for cache management endpoints."""
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_clear_cache_endpoint(self, test_client):
"""Test cache clearing endpoint."""
response = await test_client.post(
@ -289,7 +289,7 @@ class TestCacheManagement:
assert "message" in data
assert "cleared" in data["message"].lower()
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_clear_all_cache(self, test_client):
"""Test clearing all cache entries."""
response = await test_client.post(
@ -303,7 +303,7 @@ class TestCacheManagement:
class TestErrorHandling:
"""Tests for error handling."""
@pytest_asyncio.async
@pytest.mark.asyncio
@patch('httpx.AsyncClient.get')
async def test_upstream_api_error(self, mock_get, authenticated_client):
"""Test handling of upstream API errors."""
@ -327,7 +327,7 @@ class TestErrorHandling:
assert response.status_code == 404
assert "Upstream API error" in response.json()["detail"]
@pytest_asyncio.async
@pytest.mark.asyncio
async def test_invalid_parameters(self, authenticated_client):
"""Test validation of invalid parameters."""
# Test negative limit