Skip to content
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
50 changes: 50 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ---- 1. Builder Stage: Prepare the environment ----
# This stage has all the tools needed to build our dependencies correctly.
FROM ghcr.io/astral-sh/uv:python3.13-alpine AS builder

WORKDIR /app

# Create a virtual environment. This is a crucial step.
RUN uv venv

# Copy only the dependency files to leverage layer caching.
COPY pyproject.toml uv.lock ./

# Install dependencies into the virtual environment. This will install the
# correct, musl-compatible version of pydantic-core.
RUN uv sync --locked

# Copy the rest of your application code.
COPY . .

# ---- 2. Final Stage: Run the application ----
# Start from a clean, lightweight Python base image.
FROM python:3.13-alpine AS final

# Create the non-root user for security.
RUN addgroup -S app && adduser -S -G app app

WORKDIR /app

# Copy the pre-built virtual environment from the builder stage.
# No installation happens here, we just copy the finished product.
COPY --from=builder /app/.venv ./.venv

# Copy the application source code.
COPY --from=builder /app .

# Fix ownership so the non-root user can access the files.
RUN chown -R app:app /app

# Switch to the non-root user.
USER app

# **THE KEY FIX**: Activate the virtual environment by adding it to the PATH.
ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8000

# **THE SECOND KEY FIX**: Call uvicorn directly.
# It's now on the PATH, and we are no longer using `uv run`.
# This prevents any runtime re-installation.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
26 changes: 26 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
node {
String containerTag = 'myapp-hivebox'

stage('Build') {
checkout scm
linting()
sh "docker build -t ${containerTag} ."
}
stage('Test') {
sh "docker run --rm ${containerTag} pytest"
}
stage('Deploy') {
withCredentials([
usernamePassword(credentialsId: 'docker-hub-repo', passwordVariable: 'PASS', usernameVariable: 'USER')
]) {
sh "echo ${PASS} | docker login --username ${USER} --password-stdin"
sh "docker tag ${containerTag} ${USER}/my-repo:1.0.0"
}
}
}

void linting() {
sh "docker run --rm -v ${pwd()}:/src ghcr.io/astral-sh/ruff check ."
sh "docker run --rm -v ${pwd()}:/src ghcr.io/astral-sh/ruff format ."
sh 'docker run --rm -i hadolint/hadolint < Dockerfile'
}
Binary file added __pycache__/_version.cpython-312.pyc
Binary file not shown.
Binary file added __pycache__/main.cpython-312.pyc
Binary file not shown.
Empty file added app/__init__.py
Empty file.
Binary file added app/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added app/__pycache__/_version.cpython-312.pyc
Binary file not shown.
Binary file added app/__pycache__/config_logger.cpython-312.pyc
Binary file not shown.
Binary file added app/__pycache__/hivebox_api.cpython-312.pyc
Binary file not shown.
Binary file added app/__pycache__/main.cpython-312.pyc
Binary file not shown.
Binary file not shown.
20 changes: 20 additions & 0 deletions app/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import tomllib
from pathlib import Path


def get_project_version() -> str:
"""Reads the project version from the pyproject.toml file."""
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"

try:
with open(pyproject_path, "rb") as f:
pyproject_data = tomllib.load(f)

return pyproject_data["project"]["version"]
except (FileNotFoundError, KeyError):
# Handle cases where the file or key doesn't exist
return "0.0.0-dev"


# You can now use this version in your application
__version__ = get_project_version()
5 changes: 5 additions & 0 deletions app/config_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import logging

logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
51 changes: 51 additions & 0 deletions app/hivebox_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import requests
import logging

logger = logging.getLogger(__name__)


senseBox_api_base_url = "https://api.opensensemap.org/boxes/"
senseBox_ids = [
"5eba5fbad46fb8001b799786",
"5c21ff8f919bf8001adf2488",
"5ade1acf223bd80019a1011c",
]


def get_senseBox_data(box_id):
url = f"{senseBox_api_base_url}{box_id}"
response = requests.get(url)
if response.status_code == 200:
logging.info(f"Successfully fetched data for box ID: {box_id}")
return response.json()
else:
logging.info(
f"Failed to fetch data for box ID: {box_id}. Status code: {response.status_code}"
)
return None


def get_senseBox_temp(box_id):
senseBox_data = get_senseBox_data(box_id)
if senseBox_data:
all_sensor_data = senseBox_data.get("sensors")
for sensor_data in all_sensor_data:
if sensor_data.get("title") == "Temperatur":
logging.info(
f"Successfully fetched temperature for box ID: {box_id} with temperature: {sensor_data.get('lastMeasurement').get('value')}"
)
return float(sensor_data.get("lastMeasurement").get("value"))
logging.info(f"Failed to fetch temperature for box ID: {box_id}")
return None


def get_avg_senseBox_temp():
logging.info("Fetching data for all senseBox IDs:")
senseBox_temp = []
for senseBox_id in senseBox_ids:
senseBox_temp.append(get_senseBox_temp(senseBox_id))
logging.info(f"Found total of {len(senseBox_temp)} sensebox temp points")
if len(senseBox_temp) > 0:
return sum(senseBox_temp) / len(senseBox_temp)
logging.info("Failed to fetch temperature for any senseBox")
return None
23 changes: 23 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import app.config_logger
import logging
from app.hivebox_api import get_avg_senseBox_temp
from fastapi import FastAPI
from app._version import __version__

logger = logging.getLogger(__name__)

app = FastAPI(title="My Awesome API")


@app.get("/version")
async def get_version():
return {"version": __version__}


@app.get("/temperature")
async def get_temperature():
avg_temp = get_avg_senseBox_temp()
if avg_temp is not None:
return {"temperature": avg_temp}
else:
return {"error": "Failed to fetch temperature"}
17 changes: 17 additions & 0 deletions app/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi.testclient import TestClient
from .main import app

client = TestClient(app)


def test_get_version():
response = client.get("/version")
assert response.status_code == 200
assert response.json() == {"version": "0.1.0"}


def test_get_temperature():
response = client.get("/temperature")
assert response.status_code == 200
assert "temperature" in response.json()
assert isinstance(response.json()["temperature"], (int, float))
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "devops-hands-on-project-hivebox"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.116.1",
"httpx>=0.28.1",
"pytest>=8.4.1",
"requests>=2.32.4",
"ruff>=0.12.9",
"uvicorn[standard]>=0.35.0",
]
Loading