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
51 changes: 50 additions & 1 deletion components/ecoindex/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
from typing import Literal
from urllib.parse import quote_plus

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

DbEngine = Literal["sqlite", "mysql", "postgres"]


def build_database_url(
*,
engine: DbEngine = "sqlite",
host: str = "localhost",
port: int | None = None,
user: str = "ecoindex",
password: str = "ecoindex",
name: str = "ecoindex",
) -> str:
if engine == "sqlite":
return "sqlite+aiosqlite:///db.sqlite3"

credentials = f"{quote_plus(user)}:{quote_plus(password)}"

if engine == "mysql":
db_port = port or 3306
return (
f"mysql+aiomysql://{credentials}@{host}:{db_port}/{name}?charset=utf8mb4"
)

db_port = port or 5432
return f"postgresql+asyncpg://{credentials}@{host}:{db_port}/{name}"


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
Expand All @@ -12,7 +42,13 @@ class Settings(BaseSettings):
CORS_ALLOWED_METHODS: list = ["*"]
CORS_ALLOWED_ORIGINS: list = ["*"]
DAILY_LIMIT_PER_HOST: int = 0
DATABASE_URL: str = "sqlite+aiosqlite:///db.sqlite3"
DATABASE_URL: str = ""
DB_ENGINE: DbEngine = "sqlite"
DB_HOST: str = "localhost"
DB_PORT: int | None = None
DB_USER: str = "ecoindex"
DB_PASSWORD: str = "ecoindex"
DB_NAME: str = "ecoindex"
DEBUG: bool = False
DOCKER_CONTAINER: bool = False
ENABLE_SCREENSHOT: bool = False
Expand Down Expand Up @@ -40,3 +76,16 @@ class Settings(BaseSettings):
TZ: str = "Europe/Paris"
WAIT_AFTER_SCROLL: int = 3
WAIT_BEFORE_SCROLL: int = 3

@model_validator(mode="after")
def resolve_database_url(self) -> "Settings":
if not self.DATABASE_URL:
self.DATABASE_URL = build_database_url(
engine=self.DB_ENGINE,
host=self.DB_HOST,
port=self.DB_PORT,
user=self.DB_USER,
password=self.DB_PASSWORD,
name=self.DB_NAME,
)
return self
11 changes: 10 additions & 1 deletion projects/ecoindex_api/.env.template
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# API_PORT=8001
# API_VERSION=latest
# DAILY_LIMIT_PER_HOST=10
# DB_HOST=db
# DB_ENGINE=sqlite
# DB_ENGINE=mysql
# DB_ENGINE=postgres
# Local dev (task api:start-dev): DB_HOST=localhost
# Docker Compose: DB_HOST=db-mysql or DB_HOST=db-postgres
# DB_HOST=localhost
# DB_HOST=db-mysql
# DB_HOST=db-postgres
# DB_NAME=ecoindex
# DB_PASSWORD=ecoindex
# DB_PORT=3306
# DB_PORT=5432
# DB_USER=ecoindex
# DATABASE_URL=
# DEBUG=1
# ENABLE_SCREENSHOT=1
# EXCLUDED_HOSTS='["localhost","127.0.0.1"]'
Expand Down
29 changes: 25 additions & 4 deletions projects/ecoindex_api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The API specification can be found in the [documentation](projects/ecoindex_api/

With this docker setup you get 5 services running that are enough to make it all work:

- `db`: A MySQL instance
- `db-mysql` or `db-postgres`: database instance (selected with `DB_ENGINE`)
- `api`: The API instance running FastAPI application
- `worker`: The RQ task worker that runs ecoindex analysis
- `valkey`: The [Valkey](https://valkey.io/) instance (Redis-compatible) used by the RQ worker and API cache
Expand All @@ -40,7 +40,16 @@ With this docker setup you get 5 services running that are enough to make it all

```bash
cp docker-compose.yml.template docker-compose.yml && \
docker compose up -d
cp .env.template .env && \
task api:docker-up-mysql -- -d
```

For PostgreSQL instead:

```bash
cp docker-compose.yml.template docker-compose.yml && \
cp .env.template .env && \
task api:docker-up-postgres -- -d
```

Every services should start normaly, then you can go to:
Expand All @@ -62,7 +71,13 @@ Here are the environment variables you can configure in your `.env` file:
| API | `CORS_ALLOWED_ORIGINS` | `*` | See [MDN web doc](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) |
| API | `EXCLUDED_HOSTS` | `["localhost", "127.0.0.1"]` | You can configure a list of hosts that will be excluded from the analysis. |
| API, Worker | `DAILY_LIMIT_PER_HOST` | 0 | When this variable is set, it won't be possible for a same host to make more request than defined in the same day to avoid overload. If the variable is set, you will get a header `x-remaining-daily-requests: 6` in your response. It is used for the POST methods. If you reach your authorized request quota for the day, the next requests will give you a 429 response. If the variable is set to 0, no limit is set |
| API, Worker | `DATABASE_URL` | `sqlite+aiosqlite:///./sql_app.db` | If you run your mysql instance on a dedicated server, you can configure it with your credentials. By default, it uses an sqlite database when running in local | |
| API, Worker | `DB_ENGINE` | `sqlite` | Database backend: `sqlite`, `mysql` or `postgres`. Used to build `DATABASE_URL` when it is not set explicitly. |
| API, Worker | `DB_HOST` | `localhost` | Database host. Use `db-mysql` or `db-postgres` in Docker Compose. |
| API, Worker | `DB_PORT` | `3306` / `5432` | Database port. Defaults to the standard port of the selected engine when omitted. |
| API, Worker | `DB_USER` | `ecoindex` | Database user. |
| API, Worker | `DB_PASSWORD` | `ecoindex` | Database password. |
| API, Worker | `DB_NAME` | `ecoindex` | Database name. |
| API, Worker | `DATABASE_URL` | built from `DB_ENGINE` | Optional explicit SQLAlchemy URL. When set, it overrides `DB_ENGINE` and related variables. Examples: `sqlite+aiosqlite:///db.sqlite3`, `mysql+aiomysql://user:pass@host/db?charset=utf8mb4`, `postgresql+asyncpg://user:pass@host:5432/db` |
| API, Worker | `SENTRY_DSN` | `` | If you want to use [Sentry](https://sentry.io/) to monitor your application, set this variable with your project DSN. |
| API, Worker | `SENTRY_ENVIRONMENT` | `` | Optional Sentry environment name (e.g. `production`, `staging`). If not set, defaults to `development` when `DEBUG=True`, otherwise `production`. |
| API, Worker | `SENTRY_TRACES_SAMPLE_RATE`| `0.0` | Fraction of transactions to send to Sentry for performance monitoring (0.0 to 1.0). Set to `0.1` in production to sample 10% of requests. |
Expand Down Expand Up @@ -132,7 +147,13 @@ task api:init-dev-project # Initialize API dev environment (Playwright, .env, mi

### Run the API locally

Valkey and RustFS are started automatically via Docker. Then run:
Valkey and RustFS are started automatically via Docker. Set `DB_ENGINE` in `.env` to choose the database:

- `sqlite` (default): no database container, file stored locally
- `mysql`: starts a local MySQL container on port 3306
- `postgres`: starts a local PostgreSQL container on port 5432

Then run:

```bash
task api:start-dev
Expand Down
30 changes: 20 additions & 10 deletions projects/ecoindex_api/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,32 @@ tasks:
silent: true

docker-up:
desc: Start the docker-compose API
desc: Start the docker-compose API (uses DB_ENGINE from .env, default mysql)
deps: [init-env, init-docker-compose]
cmds:
- docker compose up {{.CLI_ARGS}}
- bash scripts/docker_compose_up.sh {{.CLI_ARGS}}
silent: true

docker-up-mysql:
desc: Start the docker-compose API with MySQL
deps: [init-env, init-docker-compose]
cmds:
- DB_ENGINE=mysql DB_HOST=db-mysql DB_PORT=3306 bash scripts/docker_compose_up.sh {{.CLI_ARGS}}
silent: true

docker-up-postgres:
desc: Start the docker-compose API with PostgreSQL
deps: [init-env, init-docker-compose]
cmds:
- DB_ENGINE=postgres DB_HOST=db-postgres DB_PORT=5432 bash scripts/docker_compose_up.sh {{.CLI_ARGS}}
silent: true

docker-down:
desc: Stop the docker-compose API
preconditions:
- test -f docker-compose.yml
cmds:
- docker compose down {{.CLI_ARGS}}
- docker compose --profile mysql --profile postgres down {{.CLI_ARGS}}
silent: true

docker-exec:
Expand All @@ -156,23 +170,19 @@ tasks:
desc: Create a new alembic migration
cmds:
- uv run --package ecoindex_api alembic revision --autogenerate -m "{{.CLI_ARGS}}"
dir: ../..
silent: true

migration-upgrade:
desc: Upgrade the database to the last migration
deps: [start-dev-infra]
cmds:
- uv run --package ecoindex_api alembic upgrade head
dir: ../..
silent: true

start-dev-infra:
internal: true
cmds:
- bash scripts/start_dev_infra.sh
status:
- docker inspect ecoindex-dev-valkey --format '{{.State.Running}}' 2>/dev/null | grep -q true
- docker inspect ecoindex-dev-rustfs --format '{{.State.Running}}' 2>/dev/null | grep -q true
silent: true

start-worker:
Expand Down Expand Up @@ -226,7 +236,7 @@ tasks:

start-dev:
deps: [start-backend, start-worker, start-rq-dashboard]
desc: Start the backend, the worker and the RQ dashboard
desc: Start the backend, the worker and the RQ dashboard (set DB_ENGINE in .env)
cmds:
- echo "Starting the backend, worker and RQ dashboard (http://localhost:{{.RQ_DASHBOARD_PORT}})"
silent: true
Expand Down Expand Up @@ -254,7 +264,7 @@ tasks:
pkill -f "uvicorn ecoindex.backend.main:app" 2>/dev/null || true

echo "Stopping Docker containers..."
docker rm -f ecoindex-dev-valkey ecoindex-dev-rustfs 2>/dev/null || true
docker rm -f ecoindex-dev-valkey ecoindex-dev-rustfs ecoindex-dev-mysql ecoindex-dev-postgres 2>/dev/null || true
echo "Local development environment stopped."
silent: true

Expand Down
54 changes: 46 additions & 8 deletions projects/ecoindex_api/docker-compose.yml.template
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
services:
db:
image: mysql
db-mysql:
profiles: ["mysql"]
image: mysql:8
restart: always
volumes:
- db:/var/lib/mysql
- db-mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: ${DB_NAME:-ecoindex}
MYSQL_USER: ${DB_USER:-ecoindex}
Expand All @@ -17,6 +18,24 @@ services:
retries: 10
interval: 2s

db-postgres:
profiles: ["postgres"]
image: postgres:16-alpine
restart: always
volumes:
- db-postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME:-ecoindex}
POSTGRES_USER: ${DB_USER:-ecoindex}
POSTGRES_PASSWORD: ${DB_PASSWORD:-ecoindex}
ports:
- "${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
timeout: 5s
retries: 10
interval: 2s

backend:
image: vvatelot/ecoindex-api-backend:${API_VERSION:-latest}
restart: always
Expand All @@ -25,7 +44,12 @@ services:
ports:
- "${API_PORT:-8001}:8000"
environment:
DATABASE_URL: mysql+aiomysql://${DB_USER:-ecoindex}:${DB_PASSWORD:-ecoindex}@${DB_HOST:-db}/${DB_NAME:-ecoindex}?charset=utf8mb4
DB_ENGINE: ${DB_ENGINE:-mysql}
DB_HOST: ${DB_HOST:-db-mysql}
DB_PORT: ${DB_PORT:-}
DB_USER: ${DB_USER:-ecoindex}
DB_PASSWORD: ${DB_PASSWORD:-ecoindex}
DB_NAME: ${DB_NAME:-ecoindex}
DEBUG: ${DEBUG:-0}
REDIS_CACHE_HOST: ${REDIS_CACHE_HOST:-valkey}
SCREENSHOT_FILESYSTEM_PATH: ${SCREENSHOT_FILESYSTEM_PATH:-/code/screenshots}
Expand All @@ -39,8 +63,12 @@ services:
SCREENSHOT_S3_SECRET_ACCESS_KEY: ${SCREENSHOT_S3_SECRET_ACCESS_KEY:-ecoindex-secret-key-change-me}
TZ: ${TZ:-Europe/Paris}
depends_on:
db:
db-mysql:
condition: service_healthy
required: false
db-postgres:
condition: service_healthy
required: false
rustfs-init:
condition: service_completed_successfully
valkey:
Expand All @@ -54,7 +82,12 @@ services:
env_file:
- .env
environment:
DATABASE_URL: mysql+aiomysql://${DB_USER:-ecoindex}:${DB_PASSWORD:-ecoindex}@${DB_HOST:-db}/${DB_NAME:-ecoindex}?charset=utf8mb4
DB_ENGINE: ${DB_ENGINE:-mysql}
DB_HOST: ${DB_HOST:-db-mysql}
DB_PORT: ${DB_PORT:-}
DB_USER: ${DB_USER:-ecoindex}
DB_PASSWORD: ${DB_PASSWORD:-ecoindex}
DB_NAME: ${DB_NAME:-ecoindex}
DEBUG: ${DEBUG:-0}
REDIS_CACHE_HOST: ${REDIS_CACHE_HOST:-valkey}
RQ_WORKERS: ${RQ_WORKERS:-3}
Expand All @@ -70,8 +103,12 @@ services:
TZ: ${TZ:-Europe/Paris}
ENABLE_SCREENSHOT: ${ENABLE_SCREENSHOT:-0}
depends_on:
db:
db-mysql:
condition: service_healthy
required: false
db-postgres:
condition: service_healthy
required: false
rustfs-init:
condition: service_completed_successfully
valkey:
Expand Down Expand Up @@ -120,6 +157,7 @@ services:
restart: "no"

volumes:
db:
db-mysql:
db-postgres:
valkey:
rustfs_data:
2 changes: 1 addition & 1 deletion projects/ecoindex_api/docker/backend/dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY projects/ecoindex_api/dist/$wheel $wheel
RUN pip install --no-cache-dir $wheel
RUN pip install --no-cache-dir aiomysql gunicorn
RUN pip install --no-cache-dir aiomysql asyncpg gunicorn

RUN rm -rf $wheel requirements.txt /tmp/dist /var/lib/{apt,dpkg,cache,log}/

Expand Down
2 changes: 1 addition & 1 deletion projects/ecoindex_api/docker/worker/dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY projects/ecoindex_api/dist/$wheel $wheel
RUN pip install --no-cache-dir $wheel
RUN pip install --no-cache-dir aiomysql
RUN pip install --no-cache-dir aiomysql asyncpg

RUN playwright install chromium --with-deps

Expand Down
2 changes: 2 additions & 0 deletions projects/ecoindex_api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ worker = [
"playwright-stealth>=1.0.6",
]
dev = [
"aiomysql>=0.2.0",
"aiosqlite>=0.19.0",
"asyncpg>=0.29.0",
"rq-dashboard",
"typing-extensions>=4.8.0",
"watchdog>=6.0.0",
Expand Down
37 changes: 37 additions & 0 deletions projects/ecoindex_api/scripts/docker_compose_up.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail

API_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$API_DIR"

if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi

DB_ENGINE="${DB_ENGINE:-mysql}"

case "$DB_ENGINE" in
mysql)
export DB_ENGINE
export DB_HOST="${DB_HOST:-db-mysql}"
export DB_PORT="${DB_PORT:-3306}"
docker compose --profile mysql up "$@"
;;
postgres)
export DB_ENGINE
export DB_HOST="${DB_HOST:-db-postgres}"
export DB_PORT="${DB_PORT:-5432}"
docker compose --profile postgres up "$@"
;;
sqlite)
echo "DB_ENGINE=sqlite is not supported in Docker Compose. Use mysql or postgres." >&2
exit 1
;;
*)
echo "Unknown DB_ENGINE: $DB_ENGINE" >&2
exit 1
;;
esac
Loading
Loading