Skip to content

Commit cd955e4

Browse files
Merge pull request #83 from GabrielPalmar/test
chore(kustomize): Added Kustomize + JSON stream to shorten call time
2 parents fb36d4c + 482ec1d commit cd955e4

14 files changed

Lines changed: 464 additions & 17 deletions

app/opensense.py

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'''Module to get entries from OpenSenseMap API and get the average temperature'''
2+
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
23
from datetime import datetime, timezone, timedelta
3-
import re
4+
import json
45
import requests
56
import redis
67
from app.config import create_redis_client, CACHE_TTL
@@ -26,16 +27,43 @@ def classify_temperature(average):
2627

2728
return "Unknown" # Default case
2829

30+
def _parse_partial_json_array(text: str):
31+
"""Parse as many full objects as possible from a (possibly truncated) JSON array."""
32+
decoder = json.JSONDecoder()
33+
items = []
34+
i = text.find('[')
35+
if i == -1:
36+
return items
37+
i += 1 # past '['
38+
n = len(text)
39+
while i < n:
40+
while i < n and text[i].isspace():
41+
i += 1
42+
if i >= n or text[i] == ']':
43+
break
44+
try:
45+
obj, end = decoder.raw_decode(text, i)
46+
except json.JSONDecodeError:
47+
# truncated object at the end; stop with what we have
48+
break
49+
items.append(obj)
50+
i = end
51+
while i < n and text[i].isspace():
52+
i += 1
53+
if i < n and text[i] == ',':
54+
i += 1
55+
return items
56+
2957
def get_temperature():
3058
'''Function to get the average temperature from OpenSenseMap API.'''
3159
if REDIS_AVAILABLE:
3260
try:
3361
cached_data = redis_client.get("temperature_data")
3462
if cached_data:
3563
print("Using cached data from Redis.")
36-
# Return cached data with default stats (since we don't have fresh stats)
64+
cached_result = cached_data.decode('utf-8')
3765
default_stats = {"total_sensors": 0, "null_count": 0}
38-
return cached_data, default_stats
66+
return cached_result, default_stats
3967
except redis.RedisError as e:
4068
print(f"Redis error: {e}. Proceeding without cache.")
4169

@@ -49,41 +77,86 @@ def get_temperature():
4977
"format": "json"
5078
}
5179

80+
# Streaming configuration
81+
max_mb = 0.5
82+
max_bytes = int(max_mb * 1024 * 1024)
83+
5284
print('Getting data from OpenSenseMap API...')
85+
5386
try:
87+
# Stream the response and count bytes
5488
response = requests.get(
5589
"https://api.opensensemap.org/boxes",
5690
params=params,
57-
timeout=(3, 10)
91+
stream=True,
92+
timeout=(180, 60)
5893
)
59-
print('Data retrieved successfully!')
94+
response.raise_for_status()
95+
96+
downloaded = 0
97+
chunks = []
98+
truncated = False
99+
100+
for chunk in response.iter_content(chunk_size=64 * 1024): # 64 KB
101+
if not chunk:
102+
break
103+
chunks.append(chunk)
104+
downloaded += len(chunk)
105+
if downloaded >= max_bytes:
106+
print(f"Reached {max_mb} MB limit ({downloaded:,} bytes), stopping download")
107+
truncated = True
108+
response.close()
109+
break
110+
111+
print(f'Bytes downloaded: {downloaded:,}')
112+
print('Data retrieved successfully!' + (" (partial)" if truncated else ""))
113+
114+
# Build body and parse JSON
115+
body = b"".join(chunks)
116+
text = body.decode(response.encoding or "utf-8", errors="replace")
117+
118+
try:
119+
data = json.loads(text)
120+
except json.JSONDecodeError:
121+
if not truncated:
122+
print("Warning: Unexpected JSON parse error. Trying partial parse.")
123+
data = _parse_partial_json_array(text)
124+
if not data:
125+
return "Error: Failed to parse JSON and no partial objects found\n", {
126+
"total_sensors": 0,
127+
"null_count": 0
128+
}
129+
60130
except requests.Timeout:
61131
print("API request timed out")
62132
return "Error: API request timed out\n", {"total_sensors": 0, "null_count": 0}
63133
except requests.RequestException as e:
64134
print(f"API request failed: {e}")
65135
return f"Error: API request failed - {e}\n", {"total_sensors": 0, "null_count": 0}
66136

67-
_sensor_stats["total_sensors"] = sum(
68-
1 for line in response.text.splitlines() if re.search(r'^\s*"sensors"\s*:\s*\[', line)
69-
)
70-
71-
res = [d.get('sensors') for d in response.json() if 'sensors' in d]
137+
# Process the data (keeping the existing logic)
138+
_sensor_stats["total_sensors"] = sum(1 for d in data if isinstance(d, dict) and "sensors" in d)
139+
res = [d.get('sensors') for d in data if isinstance(d, dict) and 'sensors' in d]
72140

73141
temp_list = []
74-
_sensor_stats["null_count"] = 0 # Initialize counter for null measurements
142+
_sensor_stats["null_count"] = 0
75143

76144
for sensor_list in res:
77145
for measure in sensor_list:
78146
if measure.get('unit') == "°C" and 'lastMeasurement' in measure:
79-
last_measurement = measure['lastMeasurement']
80-
if last_measurement is not None and 'value' in last_measurement:
81-
last_measurement_int = float(last_measurement['value'])
82-
temp_list.append(last_measurement_int)
147+
last = measure['lastMeasurement']
148+
if last is not None and isinstance(last, dict) and 'value' in last:
149+
try:
150+
temp_list.append(float(last['value']))
151+
except (TypeError, ValueError):
152+
_sensor_stats["null_count"] += 1
83153
else:
84154
_sensor_stats["null_count"] += 1
85155

86-
average = sum(temp_list) / len(temp_list) if temp_list else 0
156+
average = sum(temp_list) / len(temp_list) if temp_list else 0.0
157+
158+
if not temp_list:
159+
print("Warning: No valid temperature readings found")
87160

88161
# Use the dictionary-based classification
89162
status = classify_temperature(average)

kustomize/base/cronjob.yaml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
apiVersion: batch/v1
2+
kind: CronJob
3+
metadata:
4+
name: temperature-storage-cronjob
5+
labels:
6+
app: hivebox-cronjob
7+
spec:
8+
schedule: "*/5 * * * *"
9+
concurrencyPolicy: Forbid
10+
jobTemplate:
11+
spec:
12+
template:
13+
spec:
14+
restartPolicy: OnFailure
15+
securityContext:
16+
fsGroup: 1000
17+
runAsNonRoot: true
18+
runAsUser: 1000
19+
runAsGroup: 1000
20+
initContainers:
21+
- name: wait-for-start
22+
image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
23+
command: ["/bin/sh", "-c"]
24+
args:
25+
- |
26+
set -eu
27+
while true; do
28+
if curl -sSf -m 3 http://hivebox-service/version >/dev/null; then
29+
echo "Hivebox service is up!"
30+
exit 0
31+
else
32+
echo "Waiting for Hivebox service to be available..."
33+
sleep 5
34+
fi
35+
done
36+
securityContext:
37+
allowPrivilegeEscalation: false
38+
readOnlyRootFilesystem: true
39+
runAsNonRoot: true
40+
runAsGroup: 1000
41+
runAsUser: 1000
42+
capabilities:
43+
drop: ["ALL"]
44+
containers:
45+
- name: temperature-storage
46+
image: curlimages/curl:8.15.0@sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
47+
command: ["curl"]
48+
args:
49+
- "-f"
50+
- "-s"
51+
- "-S"
52+
- "--max-time"
53+
- "60"
54+
- "http://hivebox-service/store"
55+
securityContext:
56+
allowPrivilegeEscalation: false
57+
readOnlyRootFilesystem: true
58+
runAsNonRoot: true
59+
runAsGroup: 1000
60+
runAsUser: 1000
61+
capabilities:
62+
drop: ["ALL"]
63+
resources:
64+
limits: { memory: "32Mi", cpu: "50m" }
65+
requests: { memory: "16Mi", cpu: "10m" }
66+
successfulJobsHistoryLimit: 3
67+
failedJobsHistoryLimit: 1

kustomize/base/deployment.yaml

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: hivebox
5+
labels:
6+
app: hivebox
7+
spec:
8+
replicas: 2
9+
selector:
10+
matchLabels:
11+
app: hivebox
12+
template:
13+
metadata:
14+
labels:
15+
app: hivebox
16+
spec:
17+
securityContext:
18+
fsGroup: 1000
19+
runAsNonRoot: true
20+
runAsUser: 1000
21+
runAsGroup: 1000
22+
containers:
23+
- name: hivebox
24+
image: ghcr.io/gabrielpalmar/hivebox:latest@sha256:c731999c3fd9b757e2fd816e3c9dcf645dba56647d8a921cb567ece3cf378dc3
25+
ports:
26+
- containerPort: 5000
27+
env:
28+
- name: REDIS_HOST
29+
value: redis-service
30+
- name: MINIO_HOST
31+
value: minio-service
32+
securityContext:
33+
allowPrivilegeEscalation: false
34+
readOnlyRootFilesystem: true
35+
capabilities:
36+
drop: ["ALL"]
37+
resources:
38+
limits: { memory: "512Mi", cpu: "500m" }
39+
requests: { memory: "256Mi", cpu: "250m" }
40+
readinessProbe:
41+
httpGet:
42+
path: /readyz
43+
port: 5000
44+
initialDelaySeconds: 30
45+
timeoutSeconds: 480
46+
failureThreshold: 3
47+
periodSeconds: 600
48+
livenessProbe:
49+
httpGet:
50+
path: /version
51+
port: 5000
52+
timeoutSeconds: 3
53+
failureThreshold: 3
54+
periodSeconds: 60
55+
volumeMounts:
56+
- name: tmp-volume
57+
mountPath: /tmp
58+
volumes:
59+
- name: tmp-volume
60+
emptyDir: {}
61+
62+
---
63+
apiVersion: apps/v1
64+
kind: Deployment
65+
metadata:
66+
name: redis
67+
labels:
68+
app: redis
69+
spec:
70+
replicas: 1
71+
selector:
72+
matchLabels:
73+
app: redis
74+
template:
75+
metadata:
76+
labels:
77+
app: redis
78+
spec:
79+
securityContext:
80+
fsGroup: 1000
81+
runAsNonRoot: true
82+
runAsUser: 1000
83+
runAsGroup: 1000
84+
containers:
85+
- name: valkey
86+
image: valkey/valkey:8-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
87+
ports:
88+
- containerPort: 6379
89+
command: ["valkey-server"]
90+
args: ["--save", "", "--appendonly", "no"]
91+
securityContext:
92+
allowPrivilegeEscalation: false
93+
readOnlyRootFilesystem: true
94+
runAsNonRoot: true
95+
runAsGroup: 1000
96+
runAsUser: 1000
97+
capabilities:
98+
drop: ["ALL"]
99+
resources:
100+
limits: { memory: "256Mi", cpu: "250m" }
101+
requests: { memory: "128Mi", cpu: "100m" }
102+
volumeMounts:
103+
- name: valkey-data
104+
mountPath: /data
105+
volumes:
106+
- name: valkey-data
107+
emptyDir: {}
108+
109+
---
110+
apiVersion: apps/v1
111+
kind: Deployment
112+
metadata:
113+
name: minio
114+
labels:
115+
app: minio
116+
spec:
117+
replicas: 1
118+
selector:
119+
matchLabels:
120+
app: minio
121+
template:
122+
metadata:
123+
labels:
124+
app: minio
125+
spec:
126+
securityContext:
127+
fsGroup: 1000
128+
runAsNonRoot: true
129+
runAsUser: 1000
130+
runAsGroup: 1000
131+
containers:
132+
- name: minio
133+
image: minio/minio:RELEASE.2025-07-23T15-54-02Z@sha256:d249d1fb6966de4d8ad26c04754b545205ff15a62e4fd19ebd0f26fa5baacbc0
134+
ports:
135+
- containerPort: 9000
136+
command: ["minio", "server", "/data"]
137+
env:
138+
- name: MINIO_ROOT_USER
139+
value: minioadmin
140+
- name: MINIO_ROOT_PASSWORD
141+
value: minioadmin
142+
securityContext:
143+
allowPrivilegeEscalation: false
144+
readOnlyRootFilesystem: true
145+
runAsNonRoot: true
146+
runAsGroup: 1000
147+
runAsUser: 1000
148+
capabilities:
149+
drop: ["ALL"]
150+
resources:
151+
limits: { memory: "256Mi", cpu: "250m" }
152+
requests: { memory: "128Mi", cpu: "100m" }
153+
volumeMounts:
154+
- name: minio-data
155+
mountPath: /data
156+
volumes:
157+
- name: minio-data
158+
emptyDir: {}

0 commit comments

Comments
 (0)