Skip to content
60 changes: 51 additions & 9 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -64,33 +79,56 @@ 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)
echo "Building a wheel release with version $RELEASE_VERSION"
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 '+' '-')
Comment on lines +122 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should use commit sha instead of a tag? I think it would be nice to have a single strategy between the different docker images naming?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but wouldn't that be more difficult if we had to quickly find an image on the Docker registry?

# 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:
Expand All @@ -110,7 +148,11 @@ workflows:
requires:
- lint
- tests
- publish:
filters:
branches:
only: << pipeline.parameters.publish-branch >>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried running the pipeline by changing the filter to allow it on this branch?
This would allow testing the resulting docker image :)

Copy link
Contributor Author

@bolinocroustibat bolinocroustibat Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it has been tested successfully using a test commit (later removed) on this branch:

https://app.circleci.com/pipelines/github/datagouv/api-tabular/690/workflows/b1b15700-d843-42d5-aa86-c2821e582513/jobs/2307

Images has been pushed successfully on our Docker registry.

context: org-global
- publish-pypi:
requires:
- build
filters:
Expand Down
27 changes: 27 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 -"]
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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:

Expand Down
32 changes: 32 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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