From 1eef4d7f49017c93b3cf73182a03f314904c0771 Mon Sep 17 00:00:00 2001 From: Vincent Vatelot Date: Wed, 15 Jul 2026 11:58:03 +0200 Subject: [PATCH 1/5] feat(api): allow choosing MySQL or PostgreSQL via DB_ENGINE Developers can now pick sqlite, mysql, or postgres at startup through a single DB_ENGINE setting, with Docker Compose profiles and local dev containers aligned on the same configuration. Co-authored-by: Cursor --- components/ecoindex/config/settings.py | 51 ++++++++++++- projects/ecoindex_api/.env.template | 9 ++- projects/ecoindex_api/README.md | 29 ++++++- projects/ecoindex_api/Taskfile.yml | 27 +++++-- .../ecoindex_api/docker-compose.yml.template | 54 +++++++++++-- .../ecoindex_api/docker/backend/dockerfile | 2 +- .../ecoindex_api/docker/worker/dockerfile | 2 +- projects/ecoindex_api/pyproject.toml | 2 + .../ecoindex_api/scripts/docker_compose_up.sh | 37 +++++++++ .../ecoindex_api/scripts/start_dev_infra.sh | 38 ++++++++++ projects/ecoindex_api/scripts/wait_db.sh | 29 +++++++ .../ecoindex/config/test_settings.py | 76 +++++++++++++++++++ uv.lock | 62 ++++++++++++++- 13 files changed, 393 insertions(+), 25 deletions(-) create mode 100755 projects/ecoindex_api/scripts/docker_compose_up.sh create mode 100755 projects/ecoindex_api/scripts/wait_db.sh create mode 100644 test/components/ecoindex/config/test_settings.py diff --git a/components/ecoindex/config/settings.py b/components/ecoindex/config/settings.py index 700c0f4..54b292b 100644 --- a/components/ecoindex/config/settings.py +++ b/components/ecoindex/config/settings.py @@ -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") @@ -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 | None = None + 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 @@ -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 self.DATABASE_URL is None: + 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 diff --git a/projects/ecoindex_api/.env.template b/projects/ecoindex_api/.env.template index 436e925..7d64928 100644 --- a/projects/ecoindex_api/.env.template +++ b/projects/ecoindex_api/.env.template @@ -1,11 +1,18 @@ # API_PORT=8001 # API_VERSION=latest # DAILY_LIMIT_PER_HOST=10 -# DB_HOST=db +# DB_ENGINE=sqlite +# DB_ENGINE=mysql +# DB_ENGINE=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"]' diff --git a/projects/ecoindex_api/README.md b/projects/ecoindex_api/README.md index f109dad..f0262be 100644 --- a/projects/ecoindex_api/README.md +++ b/projects/ecoindex_api/README.md @@ -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 @@ -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: @@ -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. | @@ -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 diff --git a/projects/ecoindex_api/Taskfile.yml b/projects/ecoindex_api/Taskfile.yml index 46e8c42..b7ef232 100644 --- a/projects/ecoindex_api/Taskfile.yml +++ b/projects/ecoindex_api/Taskfile.yml @@ -121,10 +121,24 @@ 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: @@ -132,7 +146,7 @@ tasks: 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: @@ -170,9 +184,6 @@ tasks: 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: @@ -226,7 +237,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 @@ -254,7 +265,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 diff --git a/projects/ecoindex_api/docker-compose.yml.template b/projects/ecoindex_api/docker-compose.yml.template index 4ec778f..b15e16d 100644 --- a/projects/ecoindex_api/docker-compose.yml.template +++ b/projects/ecoindex_api/docker-compose.yml.template @@ -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} @@ -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 @@ -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} @@ -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: @@ -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} @@ -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: @@ -120,6 +157,7 @@ services: restart: "no" volumes: - db: + db-mysql: + db-postgres: valkey: rustfs_data: diff --git a/projects/ecoindex_api/docker/backend/dockerfile b/projects/ecoindex_api/docker/backend/dockerfile index c06924c..9e9496b 100644 --- a/projects/ecoindex_api/docker/backend/dockerfile +++ b/projects/ecoindex_api/docker/backend/dockerfile @@ -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}/ diff --git a/projects/ecoindex_api/docker/worker/dockerfile b/projects/ecoindex_api/docker/worker/dockerfile index 8823400..0647c9d 100644 --- a/projects/ecoindex_api/docker/worker/dockerfile +++ b/projects/ecoindex_api/docker/worker/dockerfile @@ -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 diff --git a/projects/ecoindex_api/pyproject.toml b/projects/ecoindex_api/pyproject.toml index fcfe4d3..e766a1e 100644 --- a/projects/ecoindex_api/pyproject.toml +++ b/projects/ecoindex_api/pyproject.toml @@ -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", diff --git a/projects/ecoindex_api/scripts/docker_compose_up.sh b/projects/ecoindex_api/scripts/docker_compose_up.sh new file mode 100755 index 0000000..459c937 --- /dev/null +++ b/projects/ecoindex_api/scripts/docker_compose_up.sh @@ -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 diff --git a/projects/ecoindex_api/scripts/start_dev_infra.sh b/projects/ecoindex_api/scripts/start_dev_infra.sh index 2760be0..2b66cc8 100755 --- a/projects/ecoindex_api/scripts/start_dev_infra.sh +++ b/projects/ecoindex_api/scripts/start_dev_infra.sh @@ -11,6 +11,8 @@ if [ -f .env ]; then set +a fi +DB_ENGINE="${DB_ENGINE:-sqlite}" + ensure_container() { local lock_file="$1" local name="$2" @@ -63,3 +65,39 @@ ensure_container \ /data bash scripts/init_rustfs_bucket.sh + +case "$DB_ENGINE" in + mysql) + ensure_container \ + /tmp/ecoindex-dev-mysql.lock \ + ecoindex-dev-mysql \ + "${DB_PORT:-3306}" \ + -p "${DB_PORT:-3306}:3306" \ + -e MYSQL_DATABASE="${DB_NAME:-ecoindex}" \ + -e MYSQL_USER="${DB_USER:-ecoindex}" \ + -e MYSQL_PASSWORD="${DB_PASSWORD:-ecoindex}" \ + -e MYSQL_ROOT_PASSWORD="${DB_PASSWORD:-ecoindex}" \ + -v ecoindex-dev-mysql-data:/var/lib/mysql \ + mysql:8 + ECOINDEX_DEV_DB_CONTAINER=ecoindex-dev-mysql bash scripts/wait_db.sh + ;; + postgres) + ensure_container \ + /tmp/ecoindex-dev-postgres.lock \ + ecoindex-dev-postgres \ + "${DB_PORT:-5432}" \ + -p "${DB_PORT:-5432}:5432" \ + -e POSTGRES_DB="${DB_NAME:-ecoindex}" \ + -e POSTGRES_USER="${DB_USER:-ecoindex}" \ + -e POSTGRES_PASSWORD="${DB_PASSWORD:-ecoindex}" \ + -v ecoindex-dev-postgres-data:/var/lib/postgresql/data \ + postgres:16-alpine + ECOINDEX_DEV_DB_CONTAINER=ecoindex-dev-postgres bash scripts/wait_db.sh + ;; + sqlite) + ;; + *) + echo "Unsupported DB_ENGINE for local dev: $DB_ENGINE" >&2 + exit 1 + ;; +esac diff --git a/projects/ecoindex_api/scripts/wait_db.sh b/projects/ecoindex_api/scripts/wait_db.sh new file mode 100755 index 0000000..4bc1d98 --- /dev/null +++ b/projects/ecoindex_api/scripts/wait_db.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +DB_ENGINE="${DB_ENGINE:-sqlite}" +DB_USER="${DB_USER:-ecoindex}" +DB_NAME="${DB_NAME:-ecoindex}" +DB_PASSWORD="${DB_PASSWORD:-ecoindex}" + +case "$DB_ENGINE" in + sqlite) + exit 0 + ;; + mysql) + container="${ECOINDEX_DEV_DB_CONTAINER:-ecoindex-dev-mysql}" + until docker exec "$container" mysqladmin ping -h 127.0.0.1 -u "$DB_USER" --password="$DB_PASSWORD" --silent >/dev/null 2>&1; do + sleep 1 + done + ;; + postgres) + container="${ECOINDEX_DEV_DB_CONTAINER:-ecoindex-dev-postgres}" + until docker exec "$container" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; do + sleep 1 + done + ;; + *) + echo "Unsupported DB_ENGINE for local dev: $DB_ENGINE" >&2 + exit 1 + ;; +esac diff --git a/test/components/ecoindex/config/test_settings.py b/test/components/ecoindex/config/test_settings.py new file mode 100644 index 0000000..0f8d163 --- /dev/null +++ b/test/components/ecoindex/config/test_settings.py @@ -0,0 +1,76 @@ +import pytest + +from ecoindex.config.settings import Settings, build_database_url + + +@pytest.fixture(autouse=True) +def disable_env_file(monkeypatch): + monkeypatch.chdir("/tmp") + Settings.model_config["env_file"] = None + + +def test_build_database_url_sqlite(): + assert build_database_url() == "sqlite+aiosqlite:///db.sqlite3" + + +def test_build_database_url_mysql(): + assert ( + build_database_url( + engine="mysql", + host="db-mysql", + user="ecoindex", + password="secret", + name="ecoindex", + ) + == "mysql+aiomysql://ecoindex:secret@db-mysql:3306/ecoindex?charset=utf8mb4" + ) + + +def test_build_database_url_postgres(): + assert ( + build_database_url( + engine="postgres", + host="db-postgres", + port=5432, + user="ecoindex", + password="secret", + name="ecoindex", + ) + == "postgresql+asyncpg://ecoindex:secret@db-postgres:5432/ecoindex" + ) + + +def test_build_database_url_encodes_special_characters(): + assert ( + build_database_url( + engine="postgres", + password="p@ss#word", + ) + == "postgresql+asyncpg://ecoindex:p%40ss%23word@localhost:5432/ecoindex" + ) + + +def test_settings_uses_db_engine_when_database_url_is_not_set(monkeypatch): + monkeypatch.setenv("DB_ENGINE", "postgres") + monkeypatch.setenv("DB_HOST", "localhost") + monkeypatch.setenv("DB_PORT", "5432") + + settings = Settings() + + assert settings.DATABASE_URL == ( + "postgresql+asyncpg://ecoindex:ecoindex@localhost:5432/ecoindex" + ) + + +def test_settings_keeps_explicit_database_url(monkeypatch): + monkeypatch.setenv("DB_ENGINE", "postgres") + monkeypatch.setenv( + "DATABASE_URL", + "mysql+aiomysql://custom:custom@db:3306/custom?charset=utf8mb4", + ) + + settings = Settings() + + assert settings.DATABASE_URL == ( + "mysql+aiomysql://custom:custom@db:3306/custom?charset=utf8mb4" + ) diff --git a/uv.lock b/uv.lock index 825d7c5..20086d9 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, ] +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + [[package]] name = "aiosqlite" version = "0.22.1" @@ -133,6 +145,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/d9/507c80bdac2e95e5a525644af94b03fa7f9a44596a84bd48a6e80f854f92/asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61", size = 644865, upload-time = "2025-11-24T23:25:23.527Z" }, + { url = "https://files.pythonhosted.org/packages/ea/03/f93b5e543f65c5f504e91405e8d21bb9e600548be95032951a754781a41d/asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be", size = 639297, upload-time = "2025-11-24T23:25:25.192Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/de2177e57e03a06e697f6c1ddf2a9a7fcfdc236ce69966f54ffc830fd481/asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8", size = 2816679, upload-time = "2025-11-24T23:25:26.718Z" }, + { url = "https://files.pythonhosted.org/packages/d0/98/1a853f6870ac7ad48383a948c8ff3c85dc278066a4d69fc9af7d3d4b1106/asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1", size = 2867087, upload-time = "2025-11-24T23:25:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/7e76f2a51f2360a7c90d2cf6d0d9b210c8bb0ae342edebd16173611a55c2/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3", size = 2747631, upload-time = "2025-11-24T23:25:30.154Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3f/716e10cb57c4f388248db46555e9226901688fbfabd0afb85b5e1d65d5a7/asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8", size = 2855107, upload-time = "2025-11-24T23:25:31.888Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/3ebae9dfb23a1bd3f68acfd4f795983b65b413291c0e2b0d982d6ae6c920/asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095", size = 521990, upload-time = "2025-11-24T23:25:33.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/b4/9fbb4b0af4e36d96a61d026dd37acab3cf521a70290a09640b215da5ab7c/asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540", size = 581629, upload-time = "2025-11-24T23:25:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" }, + { url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" }, + { url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -654,7 +701,7 @@ wheels = [ [[package]] name = "ecoindex-api" -version = "3.14.1" +version = "3.15.0" source = { editable = "projects/ecoindex_api" } dependencies = [ { name = "aiofile", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -687,7 +734,9 @@ backend = [ { name = "uvicorn" }, ] dev = [ + { name = "aiomysql" }, { name = "aiosqlite" }, + { name = "asyncpg" }, { name = "rq-dashboard" }, { name = "typing-extensions" }, { name = "watchdog" }, @@ -724,7 +773,9 @@ provides-extras = ["webp"] [package.metadata.requires-dev] backend = [{ name = "uvicorn", specifier = ">=0.23.2" }] dev = [ + { name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiosqlite", specifier = ">=0.19.0" }, + { name = "asyncpg", specifier = ">=0.29.0" }, { name = "rq-dashboard" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "watchdog", specifier = ">=6.0.0" }, @@ -2044,6 +2095,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymysql" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" }, +] + [[package]] name = "pyopenssl" version = "26.3.0" From 433d257aa182b4c489bfbecb745cc3b6dc61a3e9 Mon Sep 17 00:00:00 2001 From: Vincent Vatelot Date: Wed, 15 Jul 2026 11:01:41 +0200 Subject: [PATCH 2/5] fix(api): support emoji and punycode domains in analysis tasks (#150) * fix(api): support emoji and punycode domains in analysis tasks WebPage validation stores URLs as Unicode, which breaks requests for emoji domains like xn--3s8h30f.ws. Use AnyHttpUrl for punycode conversion and pass the encoded URL to the worker queue. Fixes cnumr/EcoIndex#416 Co-authored-by: Cursor * fix(api): include request error details when URL pre-check fails Expose SSL, timeout, and DNS errors in the unreachable URL response instead of empty parentheses. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- bases/ecoindex/backend/routers/tasks.py | 51 +++++------------------ test/bases/ecoindex/backend/test_tasks.py | 16 +++++++ 2 files changed, 27 insertions(+), 40 deletions(-) create mode 100644 test/bases/ecoindex/backend/test_tasks.py diff --git a/bases/ecoindex/backend/routers/tasks.py b/bases/ecoindex/backend/routers/tasks.py index 48454fc..3497a1b 100644 --- a/bases/ecoindex/backend/routers/tasks.py +++ b/bases/ecoindex/backend/routers/tasks.py @@ -1,8 +1,8 @@ from typing import Annotated -from urllib.parse import urlparse, urlunparse -import idna import requests +from pydantic import TypeAdapter +from pydantic.networks import AnyHttpUrl from ecoindex.backend.dependencies.validation import validate_api_key_batch from ecoindex.backend.models.dependencies_parameters.id import IdParameter from ecoindex.backend.utils import check_quota @@ -42,42 +42,8 @@ def convert_url_to_punycode(url: str) -> str: """ Convert an URL with emoji domain (or any Unicode domain) to Punycode. This makes the URL compatible with requests library. - - Args: - url: The URL string that may contain Unicode characters in the domain - - Returns: - The URL with the domain converted to Punycode """ - parsed = urlparse(url) - - # Extract the hostname (netloc may contain port, so we need to handle that) - hostname = parsed.hostname - if not hostname: - return url - - try: - # Convert the hostname to Punycode - hostname_punycode = idna.encode(hostname).decode("ascii") - - # Reconstruct the netloc with the converted hostname - if parsed.port: - netloc = f"{hostname_punycode}:{parsed.port}" - else: - netloc = hostname_punycode - - # Reconstruct the URL with the converted hostname - return urlunparse(( - parsed.scheme, - netloc, - parsed.path, - parsed.params, - parsed.query, - parsed.fragment, - )) - except (idna.IDNAError, UnicodeError): - # If conversion fails, return the original URL - return url + return str(TypeAdapter(AnyHttpUrl).validate_python(url)) def _enqueue_settings(*, with_retry: bool = True) -> dict[str, object]: @@ -157,16 +123,21 @@ async def add_ecoindex_analysis_task( ) r.raise_for_status() except requests.exceptions.RequestException as e: + error_detail = ( + str(e.response.status_code) + if e.response is not None + else str(e) + ) raise HTTPException( status_code=e.response.status_code - if e.response + if e.response is not None else status.HTTP_400_BAD_REQUEST, - detail=f"The URL {web_page.url} is unreachable. Are you really sure of this url? 🤔 ({e.response.status_code if e.response else ''})", + detail=f"The URL {web_page.url} is unreachable. Are you really sure of this url? 🤔 ({error_detail})", ) job = ecoindex_queue.enqueue( ecoindex_task, - url=str(web_page.url), + url=url_for_request, width=web_page.width, height=web_page.height, custom_headers=headers, diff --git a/test/bases/ecoindex/backend/test_tasks.py b/test/bases/ecoindex/backend/test_tasks.py new file mode 100644 index 0000000..4d6f76d --- /dev/null +++ b/test/bases/ecoindex/backend/test_tasks.py @@ -0,0 +1,16 @@ +from ecoindex.backend.routers.tasks import convert_url_to_punycode +from ecoindex.models import WebPage + + +def test_convert_url_to_punycode_from_idna_url() -> None: + assert convert_url_to_punycode("https://xn--3s8h30f.ws/") == "https://xn--3s8h30f.ws/" + + +def test_convert_url_to_punycode_from_unicode_domain() -> None: + assert convert_url_to_punycode("https://🦊💻.ws/") == "https://xn--3s8h30f.ws/" + + +def test_convert_url_to_punycode_from_webpage_validation() -> None: + web_page = WebPage(url="https://xn--3s8h30f.ws/") + + assert convert_url_to_punycode(web_page.url) == "https://xn--3s8h30f.ws/" From ad2abd621e08df2ae58201eaf68c78e839e142ac Mon Sep 17 00:00:00 2001 From: Vincent Vatelot Date: Wed, 15 Jul 2026 12:01:16 +0200 Subject: [PATCH 3/5] fix(api): keep DATABASE_URL typed as str for ty compatibility Use an empty default and resolve it in the model validator so type checkers accept Settings().DATABASE_URL where a str is required. Co-authored-by: Cursor --- components/ecoindex/config/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ecoindex/config/settings.py b/components/ecoindex/config/settings.py index 54b292b..9f91560 100644 --- a/components/ecoindex/config/settings.py +++ b/components/ecoindex/config/settings.py @@ -42,7 +42,7 @@ class Settings(BaseSettings): CORS_ALLOWED_METHODS: list = ["*"] CORS_ALLOWED_ORIGINS: list = ["*"] DAILY_LIMIT_PER_HOST: int = 0 - DATABASE_URL: str | None = None + DATABASE_URL: str = "" DB_ENGINE: DbEngine = "sqlite" DB_HOST: str = "localhost" DB_PORT: int | None = None @@ -79,7 +79,7 @@ class Settings(BaseSettings): @model_validator(mode="after") def resolve_database_url(self) -> "Settings": - if self.DATABASE_URL is None: + if not self.DATABASE_URL: self.DATABASE_URL = build_database_url( engine=self.DB_ENGINE, host=self.DB_HOST, From 2ea25620b27378a04ddc1b0d5b251adc32d030c5 Mon Sep 17 00:00:00 2001 From: Vincent Vatelot Date: Wed, 15 Jul 2026 15:22:54 +0200 Subject: [PATCH 4/5] fix(api): run alembic migrations from the api project directory Alembic needs projects/ecoindex_api as the working directory so it can find alembic.ini and load the local .env during init-dev-project. Co-authored-by: Cursor --- projects/ecoindex_api/Taskfile.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/ecoindex_api/Taskfile.yml b/projects/ecoindex_api/Taskfile.yml index b7ef232..57218cd 100644 --- a/projects/ecoindex_api/Taskfile.yml +++ b/projects/ecoindex_api/Taskfile.yml @@ -170,14 +170,12 @@ 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 cmds: - uv run --package ecoindex_api alembic upgrade head - dir: ../.. silent: true start-dev-infra: From 2a95ada67421fec5c4397f119f05fe946fc10c23 Mon Sep 17 00:00:00 2001 From: Vincent Vatelot Date: Wed, 15 Jul 2026 15:43:26 +0200 Subject: [PATCH 5/5] fix(api): start dev DB before running alembic migrations Ensure mysql/postgres containers are up before migration-upgrade and document which DB_HOST to use for local dev versus Docker Compose. Co-authored-by: Cursor --- projects/ecoindex_api/.env.template | 2 ++ projects/ecoindex_api/Taskfile.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/projects/ecoindex_api/.env.template b/projects/ecoindex_api/.env.template index 7d64928..e8269ed 100644 --- a/projects/ecoindex_api/.env.template +++ b/projects/ecoindex_api/.env.template @@ -4,6 +4,8 @@ # 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 diff --git a/projects/ecoindex_api/Taskfile.yml b/projects/ecoindex_api/Taskfile.yml index 57218cd..f23ff1f 100644 --- a/projects/ecoindex_api/Taskfile.yml +++ b/projects/ecoindex_api/Taskfile.yml @@ -174,6 +174,7 @@ tasks: migration-upgrade: desc: Upgrade the database to the last migration + deps: [start-dev-infra] cmds: - uv run --package ecoindex_api alembic upgrade head silent: true