Skip to content

refactor: utilize multistage builds to improve caching and compatibility #271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repos:
- id: mixed-line-ending
exclude: ^.*\.(lock)$
- id: detect-private-key
exclude: api/src/tests/integration/mock_authentication.py
exclude: api/tests/integration/mock_authentication.py
- id: no-commit-to-branch
args: [--branch, main, --branch, master]
stages: [commit-msg]
Expand Down Expand Up @@ -80,7 +80,7 @@ repos:
hooks:
- id: pytest
name: pytest-check
entry: sh -c "cd ./api/src/ && poetry run pytest ./tests/"
entry: sh -c "cd ./api/ && poetry run pytest ./tests/"
language: system
pass_filenames: false
always_run: true
Expand Down
14 changes: 11 additions & 3 deletions api/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
.venv
.pytest_cache
.mypy_cache
# Ignore all by default
*
!pyproject.toml
!poetry.lock

!src/**/*.py
!src/*.py
!src/init.sh

!tests/**/*.py
!tests/test_data/
76 changes: 63 additions & 13 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,76 @@
FROM --platform=linux/amd64 python:3.12-slim AS base
# syntax=docker/dockerfile:1
FROM python:3.13.2-alpine3.21 AS base
ENV XDG_CACHE_HOME=/var/lib/
ENV SITE_PACKAGES=/usr/local/lib/python3.13/site-packages

ENV USER="api-user"

RUN adduser \
--disabled-password \
--home /code \
--gecos "" \
--uid 1000 \
"$USER"

WORKDIR /code
CMD ["/code/src/init.sh", "api"]
EXPOSE 5000

ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/code
ENV PYTHONPATH=/code/src

FROM base AS dependencies
ENV POETRY_VERSION=1.8.5

ENV TO_BE_REMOVED="/usr/local/share/to-be-removed.txt"
# Enumerate files that should not be present in dependencies-slim
# that way, the relevant folders can be COPY-ed without incurring the size cost of already existing items
RUN <<EOF
#!/usr/bin/env sh
set -eu

RUN pip install --upgrade pip && \
pip install poetry && \
find /usr/local/bin -mindepth 1 > "$TO_BE_REMOVED"
find "$SITE_PACKAGES" -maxdepth 1 -mindepth 1 >> "$TO_BE_REMOVED"

EOF

ENV PATH="/root/.local/bin:$PATH"
RUN \
--mount=type=cache,target="$XDG_CACHE_HOME/pip" \
pip install --upgrade pip && \
pip install --user "poetry==$POETRY_VERSION" && \
poetry config virtualenvs.create false

COPY pyproject.toml pyproject.toml
COPY poetry.lock poetry.lock
RUN --mount=type=cache,target=/var/cache/apk \
apk add \
linux-headers \
musl-dev \
gcc

COPY --chown="$USER:$USER" \
pyproject.toml poetry.lock ./
RUN --mount=type=cache,target="$XDG_CACHE_HOME/pip" \
poetry install --no-dev

FROM dependencies AS dependencies-slim
RUN <<EOF
#!/usr/bin/env sh
set -eu

# Remove all files / directories that are (already) present in the final image
cat "$TO_BE_REMOVED" | while IFS= read -r path; do
rm -rf "$path"
done
EOF

FROM base AS development
RUN poetry install
WORKDIR /code/src
COPY src .
FROM dependencies AS development
RUN --mount=type=cache,target="$XDG_CACHE_HOME/pip" \
poetry install --with dev
COPY --chown="$USER:$USER" . .
USER 1000

FROM base AS prod
RUN poetry install --without dev
WORKDIR /code/src
COPY src .
COPY --from=dependencies-slim "$SITE_PACKAGES" "$SITE_PACKAGES"
COPY --from=dependencies-slim /usr/local/bin/ /usr/local/bin/
COPY --chown="$USER:$USER" src src
USER 1000
4 changes: 2 additions & 2 deletions api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pydantic-extra-types = "^2.10"
azure-monitor-opentelemetry = "^1.6.2"
opentelemetry-instrumentation-fastapi = "^0.48b0"
cryptography = "^44.0.0"
click = "^8.1.8"

[tool.poetry.group.dev.dependencies]
pre-commit = ">=3"
Expand Down Expand Up @@ -65,7 +66,7 @@ strict = true

[tool.ruff]

src = ["src"]
src = [".", "src"]
target-version = "py311"
line-length = 119

Expand All @@ -87,7 +88,7 @@ ignore = [

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"] # Ignore `E402` (import violations) in all `__init__.py` files
"src/tests/*" = ["S101"] # Allow the use of ´assert´ in tests
"tests/*" = ["S101"] # Allow the use of ´assert´ in tests

[tool.codespell]
skip = "*.lock,*.cjs"
Expand All @@ -100,6 +101,9 @@ markers = [
"integration: mark a test as integration test."
]
testpaths = [
"src/tests/unit",
"src/tests/integration"
"tests/unit",
"tests/integration"
]
pythonpath = [
"src"
]
7 changes: 5 additions & 2 deletions api/src/init.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/bin/sh
#!/usr/bin/env sh
set -eu

ROOT_DIR="$(cd "$(dirname -- "$0")" && pwd -P)"
readonly ROOT_DIR

if [ "$1" = 'api' ]; then
python3 ./app.py run
python3 "$ROOT_DIR/app.py" run
else
exec "$@"
fi
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import pytest
from pydantic import ValidationError
from features.todo.repository.todo_repository_interface import TodoRepositoryInterface
from features.todo.use_cases.add_todo import AddTodoRequest, add_todo_use_case
def test_add_with_valid_title_should_return_todo(todo_repository: TodoRepositoryInterface):
data = AddTodoRequest(title="new todo")
result = add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository)
assert result.title == data.title
def test_add_with_empty_title_should_throw_validation_error(todo_repository: TodoRepositoryInterface):
with pytest.raises(ValidationError):
data = AddTodoRequest(title="")
add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository)
import pytest
from pydantic import ValidationError

from features.todo.repository.todo_repository_interface import TodoRepositoryInterface
from features.todo.use_cases.add_todo import AddTodoRequest, add_todo_use_case


def test_add_with_valid_title_should_return_todo(todo_repository: TodoRepositoryInterface):
data = AddTodoRequest(title="new todo")
result = add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository)
assert result.title == data.title


def test_add_with_empty_title_should_throw_validation_error(todo_repository: TodoRepositoryInterface):
with pytest.raises(ValidationError):
data = AddTodoRequest(title="")
add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository)
1 change: 1 addition & 0 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
image: template-api-dev
volumes:
- ./api/src/:/code/src
- ./api/tests/:/code/tests
env_file:
- .env
environment:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The application has two types of API tests: unit tests and integration tests.

### Unit tests

You will find unit tests under `src/tests/unit`.
You will find unit tests under `tests/unit`.

<Tabs groupId="api-testing">
<TabItem value="using-docker" label="Using docker">
Expand All @@ -36,7 +36,7 @@ As a general rule, unit tests should not have any external dependencies - especi

### Integration tests

The integrations tests can be found under `src/tests/integration`.
The integrations tests can be found under `tests/integration`.

To run integration tests add `--integration` as argument for pytest.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Entities should not be affected by any change external to them.
## Testing entities

```mdx-code-block
import Test from '!!raw-loader!@site/../api/src/tests/unit/features/todo/entities/test_todo_item.py';
import Test from '!!raw-loader!@site/../api/tests/unit/features/todo/entities/test_todo_item.py';

<CodeBlock language="jsx">{Test}</CodeBlock>
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import MongoClient from '!!raw-loader!@site/../api/src/data_providers/clients/mo
The `test_client` fixture are using the mongomock instead of real database.

```mdx-code-block
import Test from '!!raw-loader!@site/../api/src/tests/unit/data_providers/clients/mongodb/test_mongo_database_client.py';
import Test from '!!raw-loader!@site/../api/tests/unit/data_providers/clients/mongodb/test_mongo_database_client.py';

<CodeBlock language="jsx">{Test}</CodeBlock>
```
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Use the `test_client` fixture as input to TodoRepository. The `test_client` fixt
real database.

```mdx-code-block
import Test from '!!raw-loader!@site/../api/src/tests/unit/features/todo/repository/test_todo_repository.py';
import Test from '!!raw-loader!@site/../api/tests/unit/features/todo/repository/test_todo_repository.py';

<CodeBlock language="jsx">{Test}</CodeBlock>
```
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ FastAPI is built around the [OpenAPI Specification](https://github.com/OAI/OpenA
Use the `test_client` fixture to populate the database with test data and `test_app` fixture to perform REST API calls.

```mdx-code-block
import Test from '!!raw-loader!@site/../api/src/tests/integration/features/todo/test_todo_feature.py';
import Test from '!!raw-loader!@site/../api/tests/integration/features/todo/test_todo_feature.py';

<CodeBlock language="jsx">{Test}</CodeBlock>
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The use-case should only know of the repository interface (abstract class) befor
Use the `todo_repository` fixture as input to use_cases.

```mdx-code-block
import Test from '!!raw-loader!@site/../api/src/tests/unit/features/todo/use_cases/test_add_todo.py';
import Test from '!!raw-loader!@site/../api/tests/unit/features/todo/use_cases/test_add_todo.py';

<CodeBlock language="jsx">{Test}</CodeBlock>
```
25 changes: 23 additions & 2 deletions web/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
node_modules
src/build
# ignore all by default
*

# Nginx
!nginx/*.conf
!nginx/*/*.conf

# Single Page Application
# Dependencies
!package.json
!yarn.lock

# Application
!public/
!index.html
!src/**/*.tsx
!src/**/*.ts
!src/*.tsx
!src/*.ts

# Configuration / build files
!vite.config.mts
!tsconfig.json
Loading
Loading