Initial project structure for rentcache
- Set up Python package structure with pyproject.toml - Add comprehensive configuration system with Pydantic - Support for multiple cache backends (SQLite, Redis, memory) - Intelligent cache invalidation strategy (soft delete, serve stale) - Rate limiting and cost management configuration - Ready for FastAPI proxy implementation
This commit is contained in:
commit
950a608992
72
.gitignore
vendored
Normal file
72
.gitignore
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
.tox/
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
data/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Cache
|
||||
cache/
|
||||
.cache/
|
||||
*.cache
|
||||
|
||||
# Temporary
|
||||
tmp/
|
||||
temp/
|
110
pyproject.toml
Normal file
110
pyproject.toml
Normal file
@ -0,0 +1,110 @@
|
||||
[project]
|
||||
name = "rentcache"
|
||||
version = "0.1.0"
|
||||
description = "Intelligent caching proxy for Rentcast API with cost management and rate limiting"
|
||||
authors = [
|
||||
{name = "Your Name", email = "your.email@example.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Internet :: Proxy Servers",
|
||||
"Topic :: Database :: Front-Ends",
|
||||
]
|
||||
keywords = ["rentcast", "api", "cache", "proxy", "real-estate", "property-data", "rate-limiting"]
|
||||
|
||||
dependencies = [
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn[standard]>=0.32.0",
|
||||
"httpx>=0.27.0",
|
||||
"pydantic>=2.10.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"aiosqlite>=0.20.0",
|
||||
"redis>=5.0.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
"structlog>=24.0.0",
|
||||
"tenacity>=9.0.0",
|
||||
"click>=8.1.0",
|
||||
"rich>=13.0.0",
|
||||
"python-multipart>=0.0.12",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0.0",
|
||||
"pytest-asyncio>=0.24.0",
|
||||
"pytest-cov>=5.0.0",
|
||||
"pytest-mock>=3.14.0",
|
||||
"ruff>=0.7.0",
|
||||
"mypy>=1.13.0",
|
||||
"black>=24.0.0",
|
||||
"pre-commit>=4.0.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/yourusername/rentcache"
|
||||
Repository = "https://github.com/yourusername/rentcache.git"
|
||||
Documentation = "https://github.com/yourusername/rentcache#readme"
|
||||
"Bug Tracker" = "https://github.com/yourusername/rentcache/issues"
|
||||
|
||||
[project.scripts]
|
||||
rentcache = "rentcache.cli:main"
|
||||
rentcache-server = "rentcache.server:run"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build]
|
||||
packages = ["src/rentcache"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
src = ["src", "tests"]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"B", # flake8-bugbear
|
||||
"C4", # flake8-comprehensions
|
||||
"UP", # pyupgrade
|
||||
"ARG", # flake8-unused-arguments
|
||||
"SIM", # flake8-simplify
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by formatter)
|
||||
"B008", # do not perform function calls in argument defaults
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py310', 'py311', 'py312', 'py313']
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.10"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
addopts = "-v --tb=short --cov=src/rentcache --cov-report=html --cov-report=term"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/rentcache"]
|
||||
omit = ["*/tests/*", "*/test_*.py"]
|
27
src/rentcache/__init__.py
Normal file
27
src/rentcache/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
Rentcache - Intelligent caching proxy for Rentcast API
|
||||
|
||||
A sophisticated caching layer that sits between your application and the Rentcast API,
|
||||
providing cost reduction through intelligent caching, rate limiting, and usage management.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Rentcache Contributors"
|
||||
|
||||
from .client import RentcacheClient
|
||||
from .config import settings
|
||||
from .exceptions import (
|
||||
RentcacheError,
|
||||
RateLimitError,
|
||||
CacheError,
|
||||
APIKeyError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"RentcacheClient",
|
||||
"settings",
|
||||
"RentcacheError",
|
||||
"RateLimitError",
|
||||
"CacheError",
|
||||
"APIKeyError",
|
||||
]
|
179
src/rentcache/config.py
Normal file
179
src/rentcache/config.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""Configuration management for Rentcache."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Literal
|
||||
from pydantic import Field, ConfigDict, field_validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Rentcache configuration settings."""
|
||||
|
||||
model_config = ConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore"
|
||||
)
|
||||
|
||||
# API Configuration
|
||||
rentcast_api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Rentcast API key"
|
||||
)
|
||||
rentcast_base_url: str = Field(
|
||||
default="https://api.rentcast.io/v1",
|
||||
description="Rentcast API base URL"
|
||||
)
|
||||
|
||||
# Proxy Server Configuration
|
||||
host: str = Field(default="0.0.0.0", description="Server host")
|
||||
port: int = Field(default=8100, description="Server port")
|
||||
reload: bool = Field(default=False, description="Auto-reload on code changes")
|
||||
log_level: str = Field(default="INFO", description="Logging level")
|
||||
|
||||
# Cache Configuration
|
||||
cache_backend: Literal["sqlite", "redis", "memory"] = Field(
|
||||
default="sqlite",
|
||||
description="Cache backend to use"
|
||||
)
|
||||
cache_ttl_hours: int = Field(
|
||||
default=24,
|
||||
description="Default cache TTL in hours"
|
||||
)
|
||||
cache_database_url: str = Field(
|
||||
default="sqlite:///./data/rentcache.db",
|
||||
description="Cache database URL"
|
||||
)
|
||||
redis_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Redis URL for cache backend"
|
||||
)
|
||||
cache_max_size_mb: int = Field(
|
||||
default=500,
|
||||
description="Maximum cache size in MB"
|
||||
)
|
||||
|
||||
# Cache Invalidation Strategy
|
||||
cache_soft_delete: bool = Field(
|
||||
default=True,
|
||||
description="Mark cache entries as invalid instead of deleting"
|
||||
)
|
||||
cache_serve_stale: bool = Field(
|
||||
default=True,
|
||||
description="Serve stale cache on API errors"
|
||||
)
|
||||
cache_stale_ttl_hours: int = Field(
|
||||
default=168, # 7 days
|
||||
description="How long to keep stale entries"
|
||||
)
|
||||
|
||||
# Rate Limiting
|
||||
rate_limit_enabled: bool = Field(default=True, description="Enable rate limiting")
|
||||
daily_request_limit: int = Field(default=1000, description="Daily request limit")
|
||||
monthly_request_limit: int = Field(default=10000, description="Monthly request limit")
|
||||
requests_per_minute: int = Field(default=10, description="Requests per minute limit")
|
||||
|
||||
# Cost Management
|
||||
cost_per_request: float = Field(
|
||||
default=0.10,
|
||||
description="Estimated cost per API request in USD"
|
||||
)
|
||||
require_confirmation: bool = Field(
|
||||
default=False,
|
||||
description="Require confirmation for cache misses"
|
||||
)
|
||||
confirmation_threshold: float = Field(
|
||||
default=10.00,
|
||||
description="Cost threshold requiring confirmation"
|
||||
)
|
||||
|
||||
# Security
|
||||
api_key_header: str = Field(
|
||||
default="X-API-Key",
|
||||
description="Header name for API key"
|
||||
)
|
||||
allowed_origins: list[str] = Field(
|
||||
default=["*"],
|
||||
description="Allowed CORS origins"
|
||||
)
|
||||
|
||||
# Paths
|
||||
data_dir: Path = Field(
|
||||
default=Path("./data"),
|
||||
description="Data directory"
|
||||
)
|
||||
log_dir: Path = Field(
|
||||
default=Path("./logs"),
|
||||
description="Log directory"
|
||||
)
|
||||
|
||||
# Mock Mode
|
||||
use_mock_api: bool = Field(
|
||||
default=False,
|
||||
description="Use mock API for testing"
|
||||
)
|
||||
mock_api_url: str = Field(
|
||||
default="http://localhost:8001",
|
||||
description="Mock API URL"
|
||||
)
|
||||
|
||||
# Advanced Features
|
||||
enable_analytics: bool = Field(
|
||||
default=True,
|
||||
description="Enable usage analytics"
|
||||
)
|
||||
enable_compression: bool = Field(
|
||||
default=True,
|
||||
description="Enable response compression"
|
||||
)
|
||||
enable_request_logging: bool = Field(
|
||||
default=True,
|
||||
description="Log all API requests"
|
||||
)
|
||||
|
||||
@field_validator("log_level")
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level."""
|
||||
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(f"Invalid log level: {v}")
|
||||
return v.upper()
|
||||
|
||||
@field_validator("cache_backend")
|
||||
def validate_cache_backend(cls, v: str, values) -> str:
|
||||
"""Validate cache backend configuration."""
|
||||
if v == "redis" and not values.data.get("redis_url"):
|
||||
raise ValueError("Redis URL required when using redis backend")
|
||||
return v
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Initialize settings and create directories."""
|
||||
super().__init__(**data)
|
||||
self._ensure_directories()
|
||||
|
||||
def _ensure_directories(self):
|
||||
"""Ensure required directories exist."""
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@property
|
||||
def is_development(self) -> bool:
|
||||
"""Check if running in development mode."""
|
||||
return self.reload or self.log_level == "DEBUG"
|
||||
|
||||
@property
|
||||
def has_api_key(self) -> bool:
|
||||
"""Check if API key is configured."""
|
||||
return bool(self.rentcast_api_key and self.rentcast_api_key.strip())
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Get the appropriate database URL based on backend."""
|
||||
if self.cache_backend == "redis":
|
||||
return self.redis_url or "redis://localhost:6379/0"
|
||||
return self.cache_database_url
|
||||
|
||||
|
||||
# Global settings instance
|
||||
settings = Settings()
|
Loading…
x
Reference in New Issue
Block a user