first commit

This commit is contained in:
2026-07-24 07:49:59 +02:00
parent edb261ee5c
commit c02110a82b
9 changed files with 452 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
name: CI/CD
on:
push:
branches: [main]
jobs:
# ---------------------------------------------------------------------
# 1. Unit tests — moeten slagen voordat er iets gebouwd wordt
# ---------------------------------------------------------------------
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Installeer dependencies
run: pip install -r requirements.txt
- name: Lint (ruff)
run: ruff check .
- name: Unit tests (pytest)
run: pytest tests/ -v
# ---------------------------------------------------------------------
# 2. SonarQube — codekwaliteit-gate
# ---------------------------------------------------------------------
sonarqube:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube wil de volledige git-historie zien
- name: SonarQube scan
uses: sonarsource/sonarqube-scan-action@v4
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
# ---------------------------------------------------------------------
# 3. Image bouwen + Trivy security-scan + naar Gitea-registry pushen
# ---------------------------------------------------------------------
build-and-scan:
runs-on: ubuntu-latest
needs: [test, sonarqube]
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t gitea.thuis.local/willem/glucose-dashboard:${{ gitea.sha }} .
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: gitea.thuis.local/willem/glucose-dashboard:${{ gitea.sha }}
severity: CRITICAL,HIGH
exit-code: "1" # laat de pipeline falen bij kritieke kwetsbaarheden
- name: Login bij Gitea-registry
run: echo "${{ secrets.GITEA_TOKEN }}" | docker login gitea.thuis.local -u willem --password-stdin
- name: Tag en push
run: |
docker tag gitea.thuis.local/willem/glucose-dashboard:${{ gitea.sha }} gitea.thuis.local/willem/glucose-dashboard:latest
docker push gitea.thuis.local/willem/glucose-dashboard:${{ gitea.sha }}
docker push gitea.thuis.local/willem/glucose-dashboard:latest
# ---------------------------------------------------------------------
# 4. Deploy — SSH naar de LXC en herstart met de nieuwe image
# ---------------------------------------------------------------------
deploy:
runs-on: ubuntu-latest
needs: build-and-scan
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /opt/glucose-dashboard
docker compose pull
docker compose up -d
+18
View File
@@ -0,0 +1,18 @@
FROM python:3.12-slim
WORKDIR /app
# Systeem-dependency voor de HEALTHCHECK hieronder
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
+34
View File
@@ -0,0 +1,34 @@
from datetime import date
import streamlit as st
from data_processing import filter_by_date, hourly_average, load_data
st.set_page_config(page_title="Glucose dashboard", page_icon="📊", layout="centered")
st.title("Glucose per uur")
df = load_data()
min_date = df["timestamp"].dt.date.min() if not df.empty else date.today()
max_date = df["timestamp"].dt.date.max() if not df.empty else date.today()
selected_date = st.date_input(
"Kies een datum",
value=max_date,
min_value=min_date,
max_value=max_date,
)
day_data = filter_by_date(df, selected_date)
if day_data.empty:
st.info("Geen metingen gevonden voor deze datum.")
else:
hourly = hourly_average(day_data)
st.bar_chart(
hourly.set_index("hour")["glucose_value"],
x_label="Uur van de dag",
y_label="Gemiddelde glucosewaarde",
)
st.caption(f"{len(day_data)} metingen op {selected_date.strftime('%d-%m-%Y')}")
+193
View File
@@ -0,0 +1,193 @@
timestamp,glucose_value
2026-07-22 00:00:00,6.7
2026-07-22 00:15:00,6.7
2026-07-22 00:30:00,7.3
2026-07-22 00:45:00,7.9
2026-07-22 01:00:00,7.5
2026-07-22 01:15:00,7.7
2026-07-22 01:30:00,8.5
2026-07-22 01:45:00,8.3
2026-07-22 02:00:00,7.8
2026-07-22 02:15:00,8.2
2026-07-22 02:30:00,7.7
2026-07-22 02:45:00,7.6
2026-07-22 03:00:00,7.7
2026-07-22 03:15:00,6.6
2026-07-22 03:30:00,6.4
2026-07-22 03:45:00,6.6
2026-07-22 04:00:00,6.1
2026-07-22 04:15:00,6.3
2026-07-22 04:30:00,5.6
2026-07-22 04:45:00,5.1
2026-07-22 05:00:00,6.0
2026-07-22 05:15:00,5.2
2026-07-22 05:30:00,5.1
2026-07-22 05:45:00,4.5
2026-07-22 06:00:00,4.8
2026-07-22 06:15:00,5.1
2026-07-22 06:30:00,4.7
2026-07-22 06:45:00,5.4
2026-07-22 07:00:00,5.2
2026-07-22 07:15:00,5.5
2026-07-22 07:30:00,5.7
2026-07-22 07:45:00,6.9
2026-07-22 08:00:00,6.5
2026-07-22 08:15:00,6.4
2026-07-22 08:30:00,7.4
2026-07-22 08:45:00,6.8
2026-07-22 09:00:00,7.6
2026-07-22 09:15:00,7.0
2026-07-22 09:30:00,7.4
2026-07-22 09:45:00,8.0
2026-07-22 10:00:00,8.3
2026-07-22 10:15:00,8.0
2026-07-22 10:30:00,7.8
2026-07-22 10:45:00,7.6
2026-07-22 11:00:00,7.0
2026-07-22 11:15:00,7.0
2026-07-22 11:30:00,6.9
2026-07-22 11:45:00,7.2
2026-07-22 12:00:00,6.6
2026-07-22 12:15:00,5.5
2026-07-22 12:30:00,6.1
2026-07-22 12:45:00,5.5
2026-07-22 13:00:00,5.2
2026-07-22 13:15:00,5.5
2026-07-22 13:30:00,5.5
2026-07-22 13:45:00,5.4
2026-07-22 14:00:00,4.7
2026-07-22 14:15:00,4.9
2026-07-22 14:30:00,5.2
2026-07-22 14:45:00,5.6
2026-07-22 15:00:00,5.2
2026-07-22 15:15:00,5.6
2026-07-22 15:30:00,5.5
2026-07-22 15:45:00,5.7
2026-07-22 16:00:00,6.8
2026-07-22 16:15:00,7.3
2026-07-22 16:30:00,7.0
2026-07-22 16:45:00,7.7
2026-07-22 17:00:00,7.7
2026-07-22 17:15:00,7.5
2026-07-22 17:30:00,8.0
2026-07-22 17:45:00,8.6
2026-07-22 18:00:00,8.0
2026-07-22 18:15:00,8.6
2026-07-22 18:30:00,6.8
2026-07-22 18:45:00,8.1
2026-07-22 19:00:00,7.6
2026-07-22 19:15:00,7.2
2026-07-22 19:30:00,7.1
2026-07-22 19:45:00,6.0
2026-07-22 20:00:00,6.4
2026-07-22 20:15:00,6.4
2026-07-22 20:30:00,6.5
2026-07-22 20:45:00,5.5
2026-07-22 21:00:00,5.1
2026-07-22 21:15:00,5.1
2026-07-22 21:30:00,5.5
2026-07-22 21:45:00,5.2
2026-07-22 22:00:00,4.8
2026-07-22 22:15:00,5.2
2026-07-22 22:30:00,5.2
2026-07-22 22:45:00,5.6
2026-07-22 23:00:00,5.2
2026-07-22 23:15:00,5.5
2026-07-22 23:30:00,5.8
2026-07-22 23:45:00,5.6
2026-07-23 00:00:00,6.6
2026-07-23 00:15:00,6.9
2026-07-23 00:30:00,7.1
2026-07-23 00:45:00,7.2
2026-07-23 01:00:00,7.0
2026-07-23 01:15:00,7.6
2026-07-23 01:30:00,7.7
2026-07-23 01:45:00,7.7
2026-07-23 02:00:00,7.9
2026-07-23 02:15:00,8.1
2026-07-23 02:30:00,8.6
2026-07-23 02:45:00,7.8
2026-07-23 03:00:00,7.7
2026-07-23 03:15:00,7.3
2026-07-23 03:30:00,6.3
2026-07-23 03:45:00,6.8
2026-07-23 04:00:00,6.5
2026-07-23 04:15:00,7.2
2026-07-23 04:30:00,5.8
2026-07-23 04:45:00,5.8
2026-07-23 05:00:00,5.4
2026-07-23 05:15:00,4.8
2026-07-23 05:30:00,5.6
2026-07-23 05:45:00,5.3
2026-07-23 06:00:00,5.3
2026-07-23 06:15:00,4.7
2026-07-23 06:30:00,5.7
2026-07-23 06:45:00,4.7
2026-07-23 07:00:00,5.7
2026-07-23 07:15:00,6.5
2026-07-23 07:30:00,5.5
2026-07-23 07:45:00,6.0
2026-07-23 08:00:00,6.5
2026-07-23 08:15:00,6.6
2026-07-23 08:30:00,6.5
2026-07-23 08:45:00,7.4
2026-07-23 09:00:00,7.1
2026-07-23 09:15:00,7.9
2026-07-23 09:30:00,7.5
2026-07-23 09:45:00,8.6
2026-07-23 10:00:00,7.7
2026-07-23 10:15:00,7.8
2026-07-23 10:30:00,8.2
2026-07-23 10:45:00,7.3
2026-07-23 11:00:00,7.7
2026-07-23 11:15:00,7.9
2026-07-23 11:30:00,6.4
2026-07-23 11:45:00,6.9
2026-07-23 12:00:00,6.6
2026-07-23 12:15:00,6.5
2026-07-23 12:30:00,5.4
2026-07-23 12:45:00,5.1
2026-07-23 13:00:00,5.6
2026-07-23 13:15:00,5.4
2026-07-23 13:30:00,5.2
2026-07-23 13:45:00,5.2
2026-07-23 14:00:00,4.7
2026-07-23 14:15:00,5.1
2026-07-23 14:30:00,5.2
2026-07-23 14:45:00,5.0
2026-07-23 15:00:00,6.2
2026-07-23 15:15:00,5.9
2026-07-23 15:30:00,5.4
2026-07-23 15:45:00,6.5
2026-07-23 16:00:00,6.1
2026-07-23 16:15:00,7.1
2026-07-23 16:30:00,7.5
2026-07-23 16:45:00,7.0
2026-07-23 17:00:00,7.9
2026-07-23 17:15:00,7.9
2026-07-23 17:30:00,8.2
2026-07-23 17:45:00,8.7
2026-07-23 18:00:00,7.9
2026-07-23 18:15:00,7.7
2026-07-23 18:30:00,7.5
2026-07-23 18:45:00,7.4
2026-07-23 19:00:00,7.5
2026-07-23 19:15:00,7.5
2026-07-23 19:30:00,7.2
2026-07-23 19:45:00,7.1
2026-07-23 20:00:00,6.5
2026-07-23 20:15:00,6.8
2026-07-23 20:30:00,5.8
2026-07-23 20:45:00,6.8
2026-07-23 21:00:00,5.7
2026-07-23 21:15:00,4.9
2026-07-23 21:30:00,4.7
2026-07-23 21:45:00,5.2
2026-07-23 22:00:00,4.9
2026-07-23 22:15:00,5.3
2026-07-23 22:30:00,5.3
2026-07-23 22:45:00,5.2
2026-07-23 23:00:00,5.1
2026-07-23 23:15:00,5.1
2026-07-23 23:30:00,5.7
2026-07-23 23:45:00,6.5
1 timestamp glucose_value
2 2026-07-22 00:00:00 6.7
3 2026-07-22 00:15:00 6.7
4 2026-07-22 00:30:00 7.3
5 2026-07-22 00:45:00 7.9
6 2026-07-22 01:00:00 7.5
7 2026-07-22 01:15:00 7.7
8 2026-07-22 01:30:00 8.5
9 2026-07-22 01:45:00 8.3
10 2026-07-22 02:00:00 7.8
11 2026-07-22 02:15:00 8.2
12 2026-07-22 02:30:00 7.7
13 2026-07-22 02:45:00 7.6
14 2026-07-22 03:00:00 7.7
15 2026-07-22 03:15:00 6.6
16 2026-07-22 03:30:00 6.4
17 2026-07-22 03:45:00 6.6
18 2026-07-22 04:00:00 6.1
19 2026-07-22 04:15:00 6.3
20 2026-07-22 04:30:00 5.6
21 2026-07-22 04:45:00 5.1
22 2026-07-22 05:00:00 6.0
23 2026-07-22 05:15:00 5.2
24 2026-07-22 05:30:00 5.1
25 2026-07-22 05:45:00 4.5
26 2026-07-22 06:00:00 4.8
27 2026-07-22 06:15:00 5.1
28 2026-07-22 06:30:00 4.7
29 2026-07-22 06:45:00 5.4
30 2026-07-22 07:00:00 5.2
31 2026-07-22 07:15:00 5.5
32 2026-07-22 07:30:00 5.7
33 2026-07-22 07:45:00 6.9
34 2026-07-22 08:00:00 6.5
35 2026-07-22 08:15:00 6.4
36 2026-07-22 08:30:00 7.4
37 2026-07-22 08:45:00 6.8
38 2026-07-22 09:00:00 7.6
39 2026-07-22 09:15:00 7.0
40 2026-07-22 09:30:00 7.4
41 2026-07-22 09:45:00 8.0
42 2026-07-22 10:00:00 8.3
43 2026-07-22 10:15:00 8.0
44 2026-07-22 10:30:00 7.8
45 2026-07-22 10:45:00 7.6
46 2026-07-22 11:00:00 7.0
47 2026-07-22 11:15:00 7.0
48 2026-07-22 11:30:00 6.9
49 2026-07-22 11:45:00 7.2
50 2026-07-22 12:00:00 6.6
51 2026-07-22 12:15:00 5.5
52 2026-07-22 12:30:00 6.1
53 2026-07-22 12:45:00 5.5
54 2026-07-22 13:00:00 5.2
55 2026-07-22 13:15:00 5.5
56 2026-07-22 13:30:00 5.5
57 2026-07-22 13:45:00 5.4
58 2026-07-22 14:00:00 4.7
59 2026-07-22 14:15:00 4.9
60 2026-07-22 14:30:00 5.2
61 2026-07-22 14:45:00 5.6
62 2026-07-22 15:00:00 5.2
63 2026-07-22 15:15:00 5.6
64 2026-07-22 15:30:00 5.5
65 2026-07-22 15:45:00 5.7
66 2026-07-22 16:00:00 6.8
67 2026-07-22 16:15:00 7.3
68 2026-07-22 16:30:00 7.0
69 2026-07-22 16:45:00 7.7
70 2026-07-22 17:00:00 7.7
71 2026-07-22 17:15:00 7.5
72 2026-07-22 17:30:00 8.0
73 2026-07-22 17:45:00 8.6
74 2026-07-22 18:00:00 8.0
75 2026-07-22 18:15:00 8.6
76 2026-07-22 18:30:00 6.8
77 2026-07-22 18:45:00 8.1
78 2026-07-22 19:00:00 7.6
79 2026-07-22 19:15:00 7.2
80 2026-07-22 19:30:00 7.1
81 2026-07-22 19:45:00 6.0
82 2026-07-22 20:00:00 6.4
83 2026-07-22 20:15:00 6.4
84 2026-07-22 20:30:00 6.5
85 2026-07-22 20:45:00 5.5
86 2026-07-22 21:00:00 5.1
87 2026-07-22 21:15:00 5.1
88 2026-07-22 21:30:00 5.5
89 2026-07-22 21:45:00 5.2
90 2026-07-22 22:00:00 4.8
91 2026-07-22 22:15:00 5.2
92 2026-07-22 22:30:00 5.2
93 2026-07-22 22:45:00 5.6
94 2026-07-22 23:00:00 5.2
95 2026-07-22 23:15:00 5.5
96 2026-07-22 23:30:00 5.8
97 2026-07-22 23:45:00 5.6
98 2026-07-23 00:00:00 6.6
99 2026-07-23 00:15:00 6.9
100 2026-07-23 00:30:00 7.1
101 2026-07-23 00:45:00 7.2
102 2026-07-23 01:00:00 7.0
103 2026-07-23 01:15:00 7.6
104 2026-07-23 01:30:00 7.7
105 2026-07-23 01:45:00 7.7
106 2026-07-23 02:00:00 7.9
107 2026-07-23 02:15:00 8.1
108 2026-07-23 02:30:00 8.6
109 2026-07-23 02:45:00 7.8
110 2026-07-23 03:00:00 7.7
111 2026-07-23 03:15:00 7.3
112 2026-07-23 03:30:00 6.3
113 2026-07-23 03:45:00 6.8
114 2026-07-23 04:00:00 6.5
115 2026-07-23 04:15:00 7.2
116 2026-07-23 04:30:00 5.8
117 2026-07-23 04:45:00 5.8
118 2026-07-23 05:00:00 5.4
119 2026-07-23 05:15:00 4.8
120 2026-07-23 05:30:00 5.6
121 2026-07-23 05:45:00 5.3
122 2026-07-23 06:00:00 5.3
123 2026-07-23 06:15:00 4.7
124 2026-07-23 06:30:00 5.7
125 2026-07-23 06:45:00 4.7
126 2026-07-23 07:00:00 5.7
127 2026-07-23 07:15:00 6.5
128 2026-07-23 07:30:00 5.5
129 2026-07-23 07:45:00 6.0
130 2026-07-23 08:00:00 6.5
131 2026-07-23 08:15:00 6.6
132 2026-07-23 08:30:00 6.5
133 2026-07-23 08:45:00 7.4
134 2026-07-23 09:00:00 7.1
135 2026-07-23 09:15:00 7.9
136 2026-07-23 09:30:00 7.5
137 2026-07-23 09:45:00 8.6
138 2026-07-23 10:00:00 7.7
139 2026-07-23 10:15:00 7.8
140 2026-07-23 10:30:00 8.2
141 2026-07-23 10:45:00 7.3
142 2026-07-23 11:00:00 7.7
143 2026-07-23 11:15:00 7.9
144 2026-07-23 11:30:00 6.4
145 2026-07-23 11:45:00 6.9
146 2026-07-23 12:00:00 6.6
147 2026-07-23 12:15:00 6.5
148 2026-07-23 12:30:00 5.4
149 2026-07-23 12:45:00 5.1
150 2026-07-23 13:00:00 5.6
151 2026-07-23 13:15:00 5.4
152 2026-07-23 13:30:00 5.2
153 2026-07-23 13:45:00 5.2
154 2026-07-23 14:00:00 4.7
155 2026-07-23 14:15:00 5.1
156 2026-07-23 14:30:00 5.2
157 2026-07-23 14:45:00 5.0
158 2026-07-23 15:00:00 6.2
159 2026-07-23 15:15:00 5.9
160 2026-07-23 15:30:00 5.4
161 2026-07-23 15:45:00 6.5
162 2026-07-23 16:00:00 6.1
163 2026-07-23 16:15:00 7.1
164 2026-07-23 16:30:00 7.5
165 2026-07-23 16:45:00 7.0
166 2026-07-23 17:00:00 7.9
167 2026-07-23 17:15:00 7.9
168 2026-07-23 17:30:00 8.2
169 2026-07-23 17:45:00 8.7
170 2026-07-23 18:00:00 7.9
171 2026-07-23 18:15:00 7.7
172 2026-07-23 18:30:00 7.5
173 2026-07-23 18:45:00 7.4
174 2026-07-23 19:00:00 7.5
175 2026-07-23 19:15:00 7.5
176 2026-07-23 19:30:00 7.2
177 2026-07-23 19:45:00 7.1
178 2026-07-23 20:00:00 6.5
179 2026-07-23 20:15:00 6.8
180 2026-07-23 20:30:00 5.8
181 2026-07-23 20:45:00 6.8
182 2026-07-23 21:00:00 5.7
183 2026-07-23 21:15:00 4.9
184 2026-07-23 21:30:00 4.7
185 2026-07-23 21:45:00 5.2
186 2026-07-23 22:00:00 4.9
187 2026-07-23 22:15:00 5.3
188 2026-07-23 22:30:00 5.3
189 2026-07-23 22:45:00 5.2
190 2026-07-23 23:00:00 5.1
191 2026-07-23 23:15:00 5.1
192 2026-07-23 23:30:00 5.7
193 2026-07-23 23:45:00 6.5
+52
View File
@@ -0,0 +1,52 @@
"""
Data processing logic voor het glucose-dashboard.
Bewust gescheiden van app.py: Streamlit-UI-code kun je niet zomaar los
unit-testen (die heeft een draaiende Streamlit-context nodig). Door de
logica (inladen, aggregeren) in een los module te zetten, kun je die
logica wel gewoon met pytest testen, en blijft app.py alleen de UI-laag.
"""
from pathlib import Path
import pandas as pd
DATA_PATH = Path(__file__).parent / "data" / "glucose_readings.csv"
def load_data(path: Path = DATA_PATH) -> pd.DataFrame:
"""Laad de ruwe metingen in.
Verwacht kolommen: 'timestamp' (parsebaar als datetime) en
'glucose_value' (numeriek). Vervang deze functie later door je
eigen databron (database, CGM-export, API-call, etc.) — de rest
van de app blijft dan ongewijzigd werken.
"""
df = pd.read_csv(path, parse_dates=["timestamp"])
return df
def filter_by_date(df: pd.DataFrame, selected_date) -> pd.DataFrame:
"""Filter de dataset op een enkele kalenderdag."""
return df[df["timestamp"].dt.date == selected_date]
def hourly_average(df: pd.DataFrame) -> pd.DataFrame:
"""Groepeer per uur (0-23) en bereken de gemiddelde waarde.
Geeft een DataFrame terug met kolommen 'hour' en 'glucose_value',
één rij per uur waarin daadwerkelijk data zit.
"""
if df.empty:
return pd.DataFrame(columns=["hour", "glucose_value"])
working = df.copy()
working["hour"] = working["timestamp"].dt.hour
result = (
working.groupby("hour")["glucose_value"]
.mean()
.round(1)
.reset_index()
.sort_values("hour")
)
return result
+9
View File
@@ -0,0 +1,9 @@
services:
glucose-dashboard:
image: gitea.thuis.local/willem/glucose-dashboard:latest
container_name: glucose-dashboard
restart: unless-stopped
ports:
- "8501:8501"
volumes:
- ./data:/app/data # zet hier je eigen databron neer i.p.v. de voorbeelddata
+4
View File
@@ -0,0 +1,4 @@
streamlit>=1.38,<2.0
pandas>=2.2,<3.0
pytest>=8.0,<9.0
ruff>=0.6,<1.0
+6
View File
@@ -0,0 +1,6 @@
sonar.projectKey=glucose-dashboard
sonar.projectName=Glucose Dashboard
sonar.sources=.
sonar.exclusions=data/**,tests/**
sonar.tests=tests
sonar.python.version=3.12
+52
View File
@@ -0,0 +1,52 @@
import pandas as pd
import pytest
from data_processing import filter_by_date, hourly_average
@pytest.fixture
def sample_df():
return pd.DataFrame(
{
"timestamp": pd.to_datetime(
[
"2026-07-22 08:05",
"2026-07-22 08:40",
"2026-07-22 09:10",
"2026-07-23 08:05", # andere dag, moet uitgefilterd worden
]
),
"glucose_value": [6.0, 8.0, 7.0, 12.0],
}
)
def test_filter_by_date_keeps_only_selected_day(sample_df):
result = filter_by_date(sample_df, pd.Timestamp("2026-07-22").date())
assert len(result) == 3
assert (result["timestamp"].dt.date == pd.Timestamp("2026-07-22").date()).all()
def test_hourly_average_computes_mean_per_hour(sample_df):
filtered = filter_by_date(sample_df, pd.Timestamp("2026-07-22").date())
result = hourly_average(filtered)
hour_8 = result.loc[result["hour"] == 8, "glucose_value"].iloc[0]
hour_9 = result.loc[result["hour"] == 9, "glucose_value"].iloc[0]
assert hour_8 == pytest.approx(7.0) # gemiddelde van 6.0 en 8.0
assert hour_9 == pytest.approx(7.0)
def test_hourly_average_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "glucose_value"])
result = hourly_average(empty)
assert result.empty
assert list(result.columns) == ["hour", "glucose_value"]
def test_hourly_average_only_returns_hours_with_data(sample_df):
filtered = filter_by_date(sample_df, pd.Timestamp("2026-07-22").date())
result = hourly_average(filtered)
# alleen uur 8 en 9 hebben data op deze dag, dus precies 2 rijen
assert len(result) == 2