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
57 changes: 34 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
# This stage builds the application and its dependencies.
# ------------------------------------------------------------------------------
FROM python:3.13.3-slim-bookworm AS builder

WORKDIR /app

# Install system build tools for packages with native extensions
RUN apt-get update && \

Check warning on line 10 in Dockerfile

View check run for this annotation

Codeac.io / Codeac Code Quality

DL3008

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
apt-get install -y --no-install-recommends build-essential gcc libffi-dev libssl-dev && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb

# Pre-build all dependencies into wheels for reproducibility and speed
# Build all dependencies into wheels for reproducibility and speed
COPY --chown=root:root --chmod=644 requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir=/app/wheelhouse -r requirements.txt

Expand All @@ -19,49 +20,59 @@
# This stage creates the final, minimal image to run the application.
# ------------------------------------------------------------------------------
FROM python:3.13.3-slim-bookworm AS runtime

WORKDIR /app

# Metadata labels
# Install curl for health check
RUN apt-get update && apt-get install -y --no-install-recommends curl && \

Check warning on line 27 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L27

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`

Check warning on line 27 in Dockerfile

View check run for this annotation

Codeac.io / Codeac Code Quality

DL3008

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
rm -rf /var/lib/apt/lists/*

# Add metadata labels
LABEL org.opencontainers.image.title="🧪 RESTful API with Python 3 and FastAPI"
LABEL org.opencontainers.image.description="Proof of Concept for a RESTful API made with Python 3 and FastAPI"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.source="https://github.com/nanotaboada/python-samples-fastapi-restful"

# Copy prebuilt wheels and install dependencies
COPY --chown=root:root --chmod=644 requirements.txt .
COPY --from=builder --chown=root:root --chmod=755 /app/wheelhouse /app/wheelhouse
# Copy metadata docs for container registries (e.g.: GitHub Container Registry)
COPY README.md ./
COPY assets ./assets

# Copy pre-built wheels from builder
COPY --from=builder /app/wheelhouse /app/wheelhouse

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links /app/wheelhouse -r requirements.txt && \
rm -rf /app/wheelhouse

# Copy application code (read-only)
COPY --chown=root:root --chmod=644 main.py ./
COPY --chown=root:root --chmod=755 database ./database
COPY --chown=root:root --chmod=755 models ./models
COPY --chown=root:root --chmod=755 routes ./routes
COPY --chown=root:root --chmod=755 schemas ./schemas
COPY --chown=root:root --chmod=755 services ./services
# Copy application source code
COPY main.py ./
COPY database ./database
COPY models ./models
COPY routes ./routes
COPY schemas ./schemas
COPY services ./services

# Copy metadata for GHCR (read-only)
COPY --chown=root:root --chmod=644 README.md ./
COPY --chown=root:root --chmod=755 assets ./assets
# Copy entrypoint script and image-bundled, pre-seeded SQLite database
COPY --chmod=755 scripts/entrypoint.sh ./entrypoint.sh
COPY --chmod=755 scripts/healthcheck.sh ./healthcheck.sh
COPY --chmod=755 storage ./docker-compose

# Copy entrypoint sctipt and SQLite database
COPY --chown=root:root --chmod=755 scripts/entrypoint.sh ./entrypoint.sh
COPY --chown=root:root --chmod=755 storage ./docker-compose

# Create non-root user and make volume mount point writable
# Add non-root user and make volume mount point writable
RUN groupadd --system fastapi && \
adduser --system --ingroup fastapi --disabled-password --gecos '' fastapi && \
mkdir -p /storage && \
chown fastapi:fastapi /storage

ENV PYTHONUNBUFFERED=1

# Drop privileges
USER fastapi

# Logging output immediately
ENV PYTHONUNBUFFERED=1

EXPOSE 9000

HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD ["./healthcheck.sh"]

ENTRYPOINT ["./entrypoint.sh"]
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
3 changes: 2 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from contextlib import asynccontextmanager
import logging
from typing import AsyncIterator
from fastapi import FastAPI

Check failure on line 8 in main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-error

Unable to import 'fastapi'
from routes import player_route
from routes import player_route, health_route

# https://github.com/encode/uvicorn/issues/562
UVICORN_LOGGER = "uvicorn.error"
Expand All @@ -26,3 +26,4 @@
version="1.0.0",)

app.include_router(player_route.api_router)
app.include_router(health_route.api_router)
15 changes: 15 additions & 0 deletions routes/health_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ------------------------------------------------------------------------------
# Route
# ------------------------------------------------------------------------------

from fastapi import APIRouter

Check failure on line 5 in routes/health_route.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-error

Unable to import 'fastapi'


api_router = APIRouter()

@api_router.get("/health", tags=["Health"])
async def health_check():
"""
Simple health check endpoint. Returns a JSON response with a single key "status" and value "ok".
"""
return {"status": "ok"}
5 changes: 5 additions & 0 deletions scripts/healthcheck.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -e

# Simple health check using curl
curl --fail http://localhost:9000/health
14 changes: 14 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@

PATH = "/players/"

# GET /health/ -----------------------------------------------------------------

def test_given_get_when_request_path_is_health_then_response_status_code_should_be_200_ok(client):
"""
Given GET /health/
when request
then response Status Code should be 200 OK
"""
# Act
response = client.get("/health/")
# Assert
assert response.status_code == 200

Check warning on line 21 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert response.json() == {"status": "ok"}

Check warning on line 22 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# GET /players/ ----------------------------------------------------------------


Expand All @@ -20,8 +34,8 @@
response = client.get(PATH)

# Assert
assert "X-Cache" in response.headers

Check warning on line 37 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert response.headers.get("X-Cache") == "MISS"

Check warning on line 38 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

def test_given_get_when_request_is_subsequent_then_response_header_x_cache_should_be_hit(client):
"""
Expand All @@ -34,8 +48,8 @@
response = client.get(PATH) # subsequent (cached)

# Assert
assert "X-Cache" in response.headers

Check warning on line 51 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert response.headers.get("X-Cache") == "HIT"

Check warning on line 52 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

def test_given_get_when_request_path_has_no_id_then_response_status_code_should_be_200_ok(client):
"""
Expand All @@ -46,7 +60,7 @@
# Act
response = client.get(PATH)
# Assert
assert response.status_code == 200

Check warning on line 63 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_get_when_request_path_has_no_id_then_response_body_should_be_collection_of_players(client):
Expand All @@ -62,7 +76,7 @@
player_id = 0
for player in players:
player_id += 1
assert player["id"] == player_id

Check warning on line 79 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# GET /players/{player_id} -----------------------------------------------------

Expand All @@ -78,7 +92,7 @@
# Act
response = client.get(PATH + str(player_id))
# Assert
assert response.status_code == 404

Check warning on line 95 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_get_when_request_path_is_existing_id_then_response_status_code_should_be_200_ok(client):
Expand All @@ -92,7 +106,7 @@
# Act
response = client.get(PATH + str(player_id))
# Assert
assert response.status_code == 200

Check warning on line 109 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_get_when_request_path_is_existing_id_then_response_body_should_be_matching_player(client):
Expand All @@ -107,7 +121,7 @@
response = client.get(PATH + str(player_id))
# Assert
player = response.json()
assert player["id"] == player_id

Check warning on line 124 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# GET /players/squadnumber/{squad_number} --------------------------------------

Expand All @@ -123,7 +137,7 @@
# Act
response = client.get(PATH + "squadnumber" + "/" + str(squad_number))
# Assert
assert response.status_code == 404

Check warning on line 140 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_get_when_request_path_is_existing_squad_number_then_response_status_code_should_be_200_ok(client):
Expand All @@ -137,7 +151,7 @@
# Act
response = client.get(PATH + "squadnumber" + "/" + str(squad_number))
# Assert
assert response.status_code == 200

Check warning on line 154 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_get_when_request_path_is_existing_squad_number_then_response_body_should_be_matching_player(client):
Expand All @@ -152,7 +166,7 @@
response = client.get(PATH + "squadnumber" + "/" + str(squad_number))
# Assert
player = response.json()
assert player["squadNumber"] == squad_number

Check warning on line 169 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# POST /players/ ---------------------------------------------------------------

Expand All @@ -168,7 +182,7 @@
# Act
response = client.post(PATH, data=body)
# Assert
assert response.status_code == 422

Check warning on line 185 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_post_when_request_body_is_existing_player_then_response_status_code_should_be_409_conflict(client):
Expand All @@ -183,7 +197,7 @@
# Act
response = client.post(PATH, data=body)
# Assert
assert response.status_code == 409

Check warning on line 200 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_post_when_request_body_is_nonexistent_player_then_response_status_code_should_be_201_created(client):
Expand All @@ -198,7 +212,7 @@
# Act
response = client.post(PATH, data=body)
# Assert
assert response.status_code == 201

Check warning on line 215 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# PUT /players/{player_id} -----------------------------------------------------

Expand All @@ -215,7 +229,7 @@
# Act
response = client.put(PATH + str(player_id), data=body)
# Assert
assert response.status_code == 422

Check warning on line 232 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_put_when_request_path_is_unknown_id_then_response_status_code_should_be_404_not_found(client):
Expand All @@ -231,7 +245,7 @@
# Act
response = client.put(PATH + str(player_id), data=body)
# Assert
assert response.status_code == 404

Check warning on line 248 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_put_when_request_path_is_existing_id_then_response_status_code_should_be_204_no_content(client):
Expand All @@ -249,7 +263,7 @@
# Act
response = client.put(PATH + str(player_id), data=body)
# Assert
assert response.status_code == 204

Check warning on line 266 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# DELETE /players/{player_id} --------------------------------------------------

Expand All @@ -265,7 +279,7 @@
# Act
response = client.delete(PATH + str(player_id))
# Assert
assert response.status_code == 404

Check warning on line 282 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


def test_given_delete_when_request_path_is_existing_id_then_response_status_code_should_be__204_no_content(client):
Expand All @@ -279,4 +293,4 @@
# Act
response = client.delete(PATH + str(player_id))
# Assert
assert response.status_code == 204

Check warning on line 296 in tests/test_main.py

View check run for this annotation

Codeac.io / Codeac Code Quality

B101

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Loading