Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Database configuration
DATABASE_URL=postgresql://admin:root@postgresql:5432/pypistats

# Redis configuration
# Redis for Celery, the scheduler, and shared rate limits
REDIS_URL=redis://redis:6379/0

# Rate limits per client IP, endpoint, and worker (semicolon-separated)
# Rate limits per client IP and endpoint, shared across workers (semicolon-separated)
RATELIMIT_DEFAULT="5 per second;30 per minute"

# Google BigQuery service account JSON
Expand Down
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ENV=development
CELERY_BROKER_URL=redis://redis
REDIS_URL=redis://redis:6379/0
FLOWER_PORT=5555
FLASK_APP=pypistats/run.py
FLASK_DEBUG=1
Expand Down
10 changes: 7 additions & 3 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- `DATABASE_URL` - PostgreSQL connection URL (e.g., `postgresql://user:password@host:5432/dbname`)

#### Redis Configuration
- `REDIS_URL` - Redis connection URL (e.g., `redis://redis:6379/0`)
- `REDIS_URL` - Redis connection URL for Celery, RedBeat, and shared rate-limit counters (e.g., `redis://redis:6379/0`)

#### Google BigQuery Configuration
- `GOOGLE_SERVICE_ACCOUNT_JSON` - Complete Google service account JSON (as a string)
Expand Down Expand Up @@ -37,7 +37,11 @@ For example, to allow 10 requests per second and 60 requests per minute:
export RATELIMIT_DEFAULT="10 per second;60 per minute"
```

All configured limits apply per client IP and Flask endpoint, independently in each worker.
All configured limits apply per client IP and Flask endpoint, shared across workers and
app instances using the same Redis database. Increasing the worker count does not increase
the allowance. Rate-limit keys use the `pypistats` prefix.
Flask-Limiter uses `REDIS_URL` through the `RATELIMIT_STORAGE_URI` application setting;
Redis must be available for rate-limited web requests.
For Docker Compose, set the variable in `.env` or export it before starting the services.
Restart the web processes (or recreate the Compose web container) after changing the value.

Expand Down Expand Up @@ -71,7 +75,7 @@ Provides default values for local development:
## Services Required

1. **PostgreSQL 16+** - Primary database for storing aggregated statistics
2. **Redis 7+** - Message broker for Celery background tasks
2. **Redis 7+** - Celery message broker, RedBeat scheduler storage, and shared web rate-limit counters
3. **Google BigQuery Access** - For querying PyPI public download data
- Requires a service account with BigQuery Data Viewer permissions
- The service account JSON includes the project ID automatically
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ services:
command: webdev
depends_on:
- postgresql
- redis
<<: *envs
ports:
- "8000:5000"
Expand Down
1 change: 1 addition & 0 deletions pypistats/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = get_db_uri()
RATELIMIT_DEFAULT = os.environ.get("RATELIMIT_DEFAULT", "").strip() or "5 per second;30 per minute"
RATELIMIT_STORAGE_URI = broker_url

# Plotly chart definitions
PLOT_BASE = json.load(open(os.path.join(os.path.dirname(__file__), "plots", "plot_base.json")))
Expand Down
4 changes: 2 additions & 2 deletions pypistats/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

app = create_app(configs[env])

# Rate limiting per IP/worker
# Rate limiting per IP and endpoint, shared across workers through Redis
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2)
limiter = Limiter(get_remote_address, app=app, headers_enabled=True)
limiter = Limiter(get_remote_address, app=app, key_prefix="pypistats", headers_enabled=True)

app.logger.info(f"Environment: {env}")

Expand Down
Loading