Skip to content
This repository was archived by the owner on May 22, 2026. It is now read-only.
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
44 changes: 16 additions & 28 deletions .github/actions/python-setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
name: Python Poetry Setup
description: Set up Python + Poetry, cache dependencies, and install project deps
name: Python uv Setup
description: Set up Python + uv, cache dependencies, and install project deps

inputs:
python-version:
description: Python version to install
required: false
default: "3.14"
poetry-version:
description: Poetry version to install
uv-version:
description: uv version to install
required: false
default: "2.3.2"
default: "0.11.14"
working-directory:
description: Project directory containing pyproject.toml
required: false
default: "app_python"
lockfile-path:
description: Path to poetry.lock for cache key invalidation
description: Path to uv.lock for cache key invalidation
required: false
default: "app_python/poetry.lock"
default: "app_python/uv.lock"
install-args:
description: Extra arguments passed to poetry install
description: Extra arguments passed to uv sync
required: false
default: "--with dev --no-interaction --no-ansi"
default: "--locked"

runs:
using: composite
Expand All @@ -31,27 +31,15 @@ runs:
with:
python-version: ${{ inputs.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
- name: Setup uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ inputs.poetry-version }}

- name: Configure Poetry virtualenv location
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry config virtualenvs.in-project true

- name: Cache Poetry dependencies
uses: actions/cache@v5
with:
path: |
~/.cache/pypoetry
${{ inputs.working-directory }}/.venv
key: ${{ runner.os }}-py${{ inputs.python-version }}-poetry${{ inputs.poetry-version }}-${{ hashFiles(inputs.lockfile-path) }}
restore-keys: |
${{ runner.os }}-py${{ inputs.python-version }}-poetry${{ inputs.poetry-version }}-
version: ${{ inputs.uv-version }}
python-version: ${{ inputs.python-version }}
enable-cache: true
cache-dependency-glob: ${{ inputs.lockfile-path }}

- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry install ${{ inputs.install-args }}
run: uv sync ${{ inputs.install-args }}
12 changes: 6 additions & 6 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
fail-fast: false
matrix:
python-version: [3.14]
poetry-version: [2.3.2]
uv-version: [0.11.14]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
defaults:
Expand All @@ -32,16 +32,16 @@ jobs:
uses: ./.github/actions/python-setup
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ matrix.poetry-version }}
uv-version: ${{ matrix.uv-version }}
working-directory: app_python
lockfile-path: app_python/poetry.lock
install-args: --with dev --no-interaction --no-ansi
lockfile-path: app_python/uv.lock
install-args: --locked
- name: Lint with flake8
run: poetry run flake8 src tests
run: uv run flake8 src tests
- name: Test using pytest with coverage report
run: |
mkdir -p test-results
poetry run pytest \
uv run pytest \
--junitxml=test-results/pytest-report.xml \
--cov=src \
--cov-report=term-missing \
Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/python-snyk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,38 @@ jobs:
- uses: actions/checkout@v6
- name: Setup Python tooling and dependencies
uses: ./.github/actions/python-setup
with:
uv-version: 0.11.14
install-args: --locked --no-dev
- name: Setup Snyk CLI
uses: snyk/actions/setup@master
- name: Run Snyk dependency scan (or skip)
- name: Verify Snyk requirements export
run: |
uv export \
--locked \
--no-dev \
--no-annotate \
--no-header \
--no-hashes \
--format requirements.txt \
--output-file /tmp/snyk-requirements.txt \
> /dev/null
diff -u requirements.txt /tmp/snyk-requirements.txt
- name: Prepare Snyk scan environment
run: |
uv venv --seed .snyk-venv
uv pip install --python .snyk-venv/bin/python -r requirements.txt
- name: Run Snyk dependency scan
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
if [ -z "${SNYK_TOKEN:-}" ]; then
echo "SNYK_TOKEN secret not set; skipping Snyk dependency scan."
exit 0
fi
snyk test --severity-threshold=medium --fail-on=upgradable
snyk test \
--file=requirements.txt \
--package-manager=pip \
--command=.snyk-venv/bin/python \
--severity-threshold=medium \
--fail-on=upgradable
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ __pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
app_python/.venv/
app_python/.snyk-venv/
app_python/test-results/

# IDE
.vscode/
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Master **production-grade DevOps practices** through hands-on labs. Build, conta
3. **Start with Lab 1** and progress sequentially
4. **Submit PRs** for each lab (details below)

## Module Directories

- `app_python/`: Python DevOps Info Service.
- `app_go/`: Go DevOps Info Service.
- `k8s/`: Kubernetes, Helm, GitOps, rollout, storage, and monitoring labs.
- `edge-api/`: Lab 17 Cloudflare Workers API.
- `nix/`: Lab 18 reproducible builds with Nix, uv, flakes, and `dockerTools`.

---

## Course Roadmap
Expand Down
2 changes: 1 addition & 1 deletion app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
*
!src/**
!pyproject.toml
!poetry.lock
!uv.lock
!gunicorn.conf.py
30 changes: 19 additions & 11 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
# syntax=docker/dockerfile:1
FROM python:3.14-alpine
RUN apk upgrade -U

COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /uvx /bin/

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV POETRY_VERSION=2.3.2

RUN pip install --no-cache-dir "poetry==$POETRY_VERSION" \
&& addgroup appgroup \
&& adduser --disabled-password --gecos "" --no-create-home -s /bin/sh appuser -G appgroup
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_PROJECT_ENVIRONMENT=/app/.venv
ENV HOME=/home/appuser
ENV PATH="/app/.venv/bin:$PATH"

WORKDIR /app

COPY pyproject.toml poetry.lock gunicorn.conf.py ./
RUN poetry config virtualenvs.create false \
&& poetry install --only main --no-interaction --no-ansi --no-root
RUN apk upgrade -U \
&& addgroup -S appgroup \
&& adduser -S -D -h /home/appuser -s /sbin/nologin -G appgroup appuser \
&& mkdir -p /data \
&& chown -R appuser:appgroup /app /data /home/appuser

COPY pyproject.toml uv.lock gunicorn.conf.py ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project

COPY src ./src
COPY --chown=appuser:appgroup src ./src

ENV PORT=5000
ENV HOST="0.0.0.0"

USER appuser
CMD ["sh", "-c", "gunicorn --config /app/gunicorn.conf.py src.main:app"]
CMD ["gunicorn", "--config", "/app/gunicorn.conf.py", "src.main:app"]
23 changes: 15 additions & 8 deletions app_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ Small Flask web service that reports service metadata, system information, runti
## Prerequisites

- Python 3.14+
- Poetry
- uv

## Installation

```bash
poetry install
uv sync --locked
```

`uv.lock` is the source of truth for Python dependencies. `requirements.txt` is generated from it for Snyk's pip-compatible scan path and should be refreshed with:

```bash
uv export --locked --no-dev --no-annotate --no-header --no-hashes --format requirements.txt --output-file requirements.txt > /dev/null
```

### Docker
Expand All @@ -29,16 +35,17 @@ poetry install
```
The Docker build installs dependencies with:
```bash
poetry install --only main --no-root
uv sync --locked --no-dev --no-install-project
```
The image uses the pinned uv binary image and runs as `appuser` with `HOME=/home/appuser`.

## Running the Application

Production-style local run with Gunicorn:

```bash
poetry run gunicorn --config gunicorn.conf.py src.main:app
HOST=127.0.0.1 PORT=8080 poetry run gunicorn --config gunicorn.conf.py src.main:app
uv run gunicorn --config gunicorn.conf.py src.main:app
HOST=127.0.0.1 PORT=8080 uv run gunicorn --config gunicorn.conf.py src.main:app
```

Gunicorn access logs are emitted as JSON so Loki can parse request fields cleanly.
Expand Down Expand Up @@ -86,8 +93,8 @@ For Lab 12, run the monitoring stack with a writable `/data` volume for the Pyth
The project uses `pytest` for unit tests.

```bash
poetry install --with dev
poetry run pytest --cov=src --cov-report=term-missing
uv sync --locked
uv run pytest --cov=src --cov-report=term-missing
```

The test suite covers:
Expand All @@ -101,5 +108,5 @@ The test suite covers:
## Linting

```bash
poetry run flake8 src tests
uv run flake8 src tests
```
Loading