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:
parent
9a06e9d059
commit
525c7bb511
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user