diff --git a/.circleci/config.yml b/.circleci/config.yml index b35c907..0e62015 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,21 @@ parameters: publish-branch: type: string default: "main" + python-module: + type: string + default: "api_tabular" + api-port: + type: string + default: "8005" + docker-registry-url: + type: string + default: "registry.gitlab.com/etalab/data.gouv.fr/infra" + docker-image-name-tabular: + type: string + default: "tabular-api" + docker-image-name-metrics: + type: string + default: "metrics-api" jobs: lint: @@ -64,13 +79,21 @@ jobs: path: reports/python build: - docker: - - image: ghcr.io/astral-sh/uv:python<< pipeline.parameters.python-version >>-trixie + machine: + image: ubuntu-2404:current steps: - checkout - run: - name: Compute RELEASE_VERSION via setuptools_scm + name: Install uv and Python << pipeline.parameters.python-version >> + command: | + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + uv python install << pipeline.parameters.python-version >> + uv sync --frozen + - run: + name: Compute RELEASE_VERSION via setuptools_scm, and build wheel command: | + export PATH="$HOME/.local/bin:$PATH" uv pip install --system setuptools-scm # derive from setuptools_scm or base version + build number RELEASE_VERSION=$(python -m setuptools_scm) @@ -78,19 +101,34 @@ jobs: echo "Build number: $CIRCLE_BUILD_NUM" echo "Commit hash: ${CIRCLE_SHA1:0:7}" echo "Git tag: $CIRCLE_TAG" - - run: - name: Build a distributable package as a wheel release - command: | + echo "RELEASE_VERSION=$RELEASE_VERSION" >> "$BASH_ENV" uv build --wheel - # Build already executed above; artifacts are in dist/ - store_artifacts: path: dist - persist_to_workspace: root: . paths: - . + - run: + name: Log in to Docker registry + command: | + echo "${DOCKER_REGISTRY_PASSWORD}" | docker login -u "oauth2" --password-stdin registry.gitlab.com + - run: + name: Build and push Docker images (Tabular API and Metrics API) + command: | + source "$BASH_ENV" + export PATH="$HOME/.local/bin:$PATH" + REGISTRY="<< pipeline.parameters.docker-registry-url >>" + # Docker tags allow only [a-zA-Z0-9_.-]; setuptools_scm can output e.g. 0.4.0.dev5+gabc1234 + DOCKER_TAG=$(echo "$RELEASE_VERSION" | tr '+' '-') + # Tabular API: tabular endpoints only + docker build --build-arg APP_MODULE=api_tabular.tabular.app:app_factory -t "${REGISTRY}/<< pipeline.parameters.docker-image-name-tabular >>:${DOCKER_TAG}" . + docker push "${REGISTRY}/<< pipeline.parameters.docker-image-name-tabular >>:${DOCKER_TAG}" + # Metrics API: metrics endpoints only + docker build --build-arg APP_MODULE=api_tabular.metrics.app:app_factory -t "${REGISTRY}/<< pipeline.parameters.docker-image-name-metrics >>:${DOCKER_TAG}" . + docker push "${REGISTRY}/<< pipeline.parameters.docker-image-name-metrics >>:${DOCKER_TAG}" - publish: + publish-pypi: docker: - image: ghcr.io/astral-sh/uv:python<< pipeline.parameters.python-version >>-trixie-slim steps: @@ -110,7 +148,11 @@ workflows: requires: - lint - tests - - publish: + filters: + branches: + only: << pipeline.parameters.publish-branch >> + context: org-global + - publish-pypi: requires: - build filters: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1daaedb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM astral/uv:python3.11-trixie-slim + +# Which app to run (e.g. api_tabular.tabular.app:app_factory or api_tabular.metrics.app:app_factory) +ARG APP_MODULE=api_tabular.tabular.app:app_factory + +# install needed apt packages +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends git && \ + rm -rf /var/lib/apt/lists/* + +# create user & group +RUN groupadd --system datagouv && \ + useradd --system --gid datagouv --create-home datagouv + +# install +WORKDIR /home/datagouv +ADD . /home/datagouv/ +RUN uv sync --frozen +RUN chown -R datagouv:datagouv /home/datagouv + +# run (ENV from ARG so shell can expand APP_MODULE at runtime) +USER datagouv +ENV APP_MODULE=${APP_MODULE} +# Use `python -m gunicorn` instead of `gunicorn` due to uv issue #15246: https://github.com/astral-sh/uv/issues/15246 +# Shell so APP_MODULE is expanded; bind to 8005 (map to different host ports when running both containers) +ENTRYPOINT ["/bin/sh", "-c"] +CMD ["uv run python -m gunicorn $APP_MODULE --bind 0.0.0.0:8005 --worker-class aiohttp.GunicornWebWorker --workers 2 --access-logfile -"] diff --git a/README.md b/README.md index dac3375..57a6fc5 100644 --- a/README.md +++ b/README.md @@ -18,26 +18,39 @@ The production API is deployed on data.gouv.fr infrastructure at [`https://tabul - **[uv](https://docs.astral.sh/uv/)** for dependency management - **Docker & Docker Compose** +### Docker Compose profiles + +| Profile | Use when | Services started | +|----------|----------|------------------| +| **test** | Run locally with the test database (PostgreSQL + PostgREST + optional Tabular API + Metrics API in Docker) | `postgres-test`, `postgrest-test`, `tabular-api`, `metrics-api` | +| **hydra** | Use a real Hydra CSV database; PostgREST only in Docker, run the API with uv on the host | `postgrest` | + +**Run everything locally in Docker:** `docker compose --profile test up -d` โ†’ Tabular API at http://localhost:8005, Metrics API at http://localhost:8006, PostgREST at http://localhost:8080. + ### ๐Ÿงช Run with a test database -1. **Start the Infrastructure** +1. **Start the stack** + + With the **test** profile, you can either run the full stack in Docker (recommended for a quick local run) or only the infrastructure and run the API with uv (for development with hot reload). - Start the test CSV database and test PostgREST container: + **Option A โ€“ All in Docker (easiest for running locally):** ```shell docker compose --profile test up -d ``` - The `--profile test` flag tells Docker Compose to start the PostgREST and PostgreSQL services for the test CSV database. This starts PostgREST on port 8080, connecting to the test CSV database. You can access the raw PostgREST API on http://localhost:8080. + This starts the test PostgreSQL, PostgREST, Tabular API (port 8005), and Metrics API (port 8006). You can use the APIs at http://localhost:8005 and http://localhost:8006. -2. **Launch the main API proxy** - - Install dependencies and start the proxy services: + **Option B โ€“ Only infrastructure in Docker, API with uv (for development):** + ```shell + docker compose --profile test up -d postgres-test postgrest-test + ``` + PostgREST is available at http://localhost:8080. Then start the API proxy on the host: ```shell uv sync - uv run adev runserver -p8005 api_tabular/tabular/app.py # Api related to apified CSV files by udata-hydra (dev server) - uv run adev runserver -p8006 api_tabular/metrics/app.py # Api related to udata's metrics (dev server) + uv run adev runserver -p8005 api_tabular/tabular/app.py # Tabular API (dev server) + uv run adev runserver -p8006 api_tabular/metrics/app.py # Metrics API (dev server) ``` - **Note:** For production, use gunicorn with aiohttp worker: + **Note:** For production (on the host), use gunicorn with aiohttp worker: ```shell # Tabular API (port 8005) uv run gunicorn api_tabular.tabular.app:app_factory \ @@ -54,9 +67,9 @@ The production API is deployed on data.gouv.fr infrastructure at [`https://tabul --access-logfile - ``` - The main API provides a controlled layer over PostgREST - exposing PostgREST directly would be too permissive, so this adds a security and access control layer. + The API provides a controlled layer over PostgREST (security and access control); exposing PostgREST directly would be too permissive. -3. **Test the API** +2. **Test the API** Query the API using a `resource_id`. Several test resources are available in the fake database: diff --git a/docker-compose.yml b/docker-compose.yml index 57ffa5c..469a9b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,3 +47,35 @@ services: - POSTGRES_PASSWORD=csvapi profiles: - test + + # Tabular API (run locally via Docker, uses same Dockerfile as CI) + tabular-api: + build: + context: . + dockerfile: Dockerfile + args: + APP_MODULE: api_tabular.tabular.app:app_factory + ports: + - "8005:8005" + environment: + - PGREST_ENDPOINT=http://postgrest-test:8080 + depends_on: + - postgrest-test + profiles: + - test + + # Metrics API (run locally via Docker, uses same Dockerfile as CI) + metrics-api: + build: + context: . + dockerfile: Dockerfile + args: + APP_MODULE: api_tabular.metrics.app:app_factory + ports: + - "8006:8005" + environment: + - PGREST_ENDPOINT=http://postgrest-test:8080 + depends_on: + - postgrest-test + profiles: + - test