Skip to content

Commit ac31425

Browse files
Merge pull request #77 from GabrielPalmar/test
chore(app): Added ijson support
2 parents f570e9d + f4be78f commit ac31425

6 files changed

Lines changed: 506 additions & 243 deletions

File tree

‎.github/workflows/pylint.yml‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ jobs:
2626
- name: Install dependencies
2727
run: |
2828
python -m pip install --upgrade pip
29-
pip install pylint
30-
pip install flask
31-
pip install requests
32-
pip install vcrpy
33-
pip install prometheus_client
34-
pip install redis
35-
pip install minio
29+
pip install pylint
30+
pip install flask
31+
pip install requests
32+
pip install vcrpy
33+
pip install prometheus_client
34+
pip install redis
35+
pip install minio
36+
pip install ijson
3637
- name: Analysing the code with pylint
3738
run: |
3839
# Set PYTHONPATH so pylint can find the app module

‎app/opensense.py‎

Lines changed: 103 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,140 @@
1-
'''Module to get entries from OpenSenseMap API and get the average temperature'''
1+
"""Module to get entries from OpenSenseMap API and get the average temperature"""
22
from datetime import datetime, timezone, timedelta
3-
import re
3+
import json
4+
from typing import Iterable, Dict, Tuple, Optional
45
import requests
56
import redis
7+
import ijson
8+
from ijson.common import JSONError as IjsonJSONError
69
from app.config import create_redis_client, CACHE_TTL
710

8-
# Use shared Redis client
911
redis_client, REDIS_AVAILABLE = create_redis_client()
1012

1113
_sensor_stats = {"total_sensors": 0, "null_count": 0}
1214

1315
def classify_temperature(average):
14-
'''Classify temperature based on ranges using dictionary approach'''
15-
# Define temperature ranges and their classifications
16+
"""Classify temperature based on ranges using dictionary approach"""
1617
temp_classifications = {
1718
"cold": (float('-inf'), 10, "Warning: Too cold"),
18-
"good": (10, 36, "Good"),
19-
"hot": (36, float('inf'), "Warning: Too hot")
19+
"good": (10, 36, "Good"),
20+
"hot": (36, float('inf'), "Warning: Too hot"),
2021
}
21-
22-
# Find the appropriate classification
2322
for _, (min_temp, max_temp, status) in temp_classifications.items():
2423
if min_temp < average <= max_temp:
2524
return status
25+
return "Unknown"
2626

27-
return "Unknown" # Default case
27+
def _iter_sensors_from_stream(stream) -> Iterable[dict]:
28+
"""Yield sensors from a streaming JSON array of boxes."""
29+
yield from ijson.items(stream, 'item.sensors.item')
30+
31+
def _iter_sensors_from_json(boxes: Iterable[dict]) -> Iterable[dict]:
32+
"""Yield sensors from a loaded list of boxes."""
33+
for box in boxes:
34+
yield from box.get("sensors", [])
35+
36+
def _compute_stats(sensors: Iterable[dict]) -> Tuple[float, int, Dict[str, int]]:
37+
"""Compute temperature sum/count and stats from an iterable of sensors."""
38+
temp_sum = 0.0
39+
temp_count = 0
40+
stats = {"total_sensors": 0, "null_count": 0}
41+
42+
for sensor in sensors:
43+
stats["total_sensors"] += 1
44+
if sensor.get('unit') != "°C":
45+
continue
46+
47+
last = sensor.get('lastMeasurement')
48+
if not last or 'value' not in last:
49+
stats["null_count"] += 1
50+
continue
2851

29-
def get_temperature():
30-
'''Function to get the average temperature from OpenSenseMap API.'''
31-
if REDIS_AVAILABLE:
3252
try:
33-
cached_data = redis_client.get("temperature_data")
34-
if cached_data:
35-
print("Using cached data from Redis.")
36-
# Return cached data with default stats (since we don't have fresh stats)
37-
default_stats = {"total_sensors": 0, "null_count": 0}
38-
return cached_data, default_stats
39-
except redis.RedisError as e:
40-
print(f"Redis error: {e}. Proceeding without cache.")
41-
42-
print("Fetching new data from OpenSenseMap API...")
43-
44-
# Ensuring that data is not older than 1 hour.
45-
time_iso = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat().replace("+00:00", "Z")
53+
temp_sum += float(last['value'])
54+
temp_count += 1
55+
except (TypeError, ValueError):
56+
stats["null_count"] += 1
4657

47-
params = {
48-
"date": time_iso,
49-
"format": "json"
50-
}
58+
return temp_sum, temp_count, stats
5159

52-
print('Getting data from OpenSenseMap API...')
60+
def _empty_stats() -> Dict[str, int]:
61+
return {"total_sensors": 0, "null_count": 0}
62+
63+
def _get_cached_temperature() -> Optional[str]:
64+
if not REDIS_AVAILABLE:
65+
return None
66+
try:
67+
cached = redis_client.get("temperature_data")
68+
if cached:
69+
return cached.decode("utf-8") if isinstance(cached, (bytes, bytearray)) else cached
70+
except redis.RedisError:
71+
return None
72+
return None
73+
74+
def _set_cached_temperature(value: str) -> None:
75+
if not REDIS_AVAILABLE:
76+
return
5377
try:
54-
response = requests.get(
78+
redis_client.setex("temperature_data", CACHE_TTL, value)
79+
except redis.RedisError:
80+
pass
81+
82+
def _build_params() -> Dict[str, str]:
83+
time_iso = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat().replace("+00:00", "Z")
84+
return {"date": time_iso, "format": "json"}
85+
86+
def _request_boxes(params: Dict[str, str]):
87+
try:
88+
resp = requests.get(
5589
"https://api.opensensemap.org/boxes",
5690
params=params,
57-
timeout=(3, 10)
91+
stream=True,
92+
timeout=(60, 90)
5893
)
59-
print('Data retrieved successfully!')
94+
if hasattr(resp, "raise_for_status"):
95+
resp.raise_for_status()
96+
if hasattr(resp, "raw") and hasattr(resp.raw, "decode_content"):
97+
resp.raw.decode_content = True
98+
return resp, None
6099
except requests.Timeout:
61-
print("API request timed out")
62-
return "Error: API request timed out\n", {"total_sensors": 0, "null_count": 0}
100+
return None, "Error: API request timed out\n"
63101
except requests.RequestException as e:
64-
print(f"API request failed: {e}")
65-
return f"Error: API request failed - {e}\n", {"total_sensors": 0, "null_count": 0}
102+
return None, f"Error: API request failed - {e}\n"
66103

67-
_sensor_stats["total_sensors"] = sum(
68-
1 for line in response.text.splitlines() if re.search(r'^\s*"sensors"\s*:\s*\[', line)
69-
)
104+
def _make_sensor_iter(response) -> Tuple[Optional[Iterable[dict]], Optional[str]]:
105+
# Prefer streaming if raw is available; otherwise load JSON once.
106+
if hasattr(response, "raw") and getattr(response, "raw", None):
107+
return _iter_sensors_from_stream(response.raw), None
108+
try:
109+
boxes = response.json()
110+
except (json.JSONDecodeError, ValueError) as e:
111+
return None, f"Error: parse failed - {e}\n"
112+
return _iter_sensors_from_json(boxes), None
70113

71-
res = [d.get('sensors') for d in response.json() if 'sensors' in d]
114+
def get_temperature():
115+
'''Function to get the average temperature from OpenSenseMap API.'''
116+
cached = _get_cached_temperature()
117+
if cached:
118+
return cached, _empty_stats()
72119

73-
temp_list = []
74-
_sensor_stats["null_count"] = 0 # Initialize counter for null measurements
120+
params = _build_params()
121+
response, err = _request_boxes(params)
122+
if err:
123+
return err, _empty_stats()
75124

76-
for sensor_list in res:
77-
for measure in sensor_list:
78-
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)
83-
else:
84-
_sensor_stats["null_count"] += 1
125+
sensors_iter, err = _make_sensor_iter(response)
126+
if err or sensors_iter is None:
127+
return err or "Error: parser unavailable\n", _empty_stats()
85128

86-
average = sum(temp_list) / len(temp_list) if temp_list else 0
129+
try:
130+
temp_sum, temp_count, stats = _compute_stats(sensors_iter)
131+
except IjsonJSONError as e:
132+
return f"Error: parse failed - {e}\n", _empty_stats()
87133

88-
# Use the dictionary-based classification
134+
average = (temp_sum / temp_count) if temp_count else 0.0
89135
status = classify_temperature(average)
90136
result = f'Average temperature: {average:.2f} °C ({status})\n'
91137

92-
if REDIS_AVAILABLE:
93-
try:
94-
redis_client.setex("temperature_data", CACHE_TTL, result)
95-
print("Data cached in Redis.")
96-
except redis.RedisError as e:
97-
print(f"Redis error while caching data: {e}")
98-
138+
_set_cached_temperature(result)
139+
_sensor_stats.update(stats)
99140
return result, _sensor_stats

‎helm-chart/templates/deployment.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ spec:
3434
httpGet:
3535
path: /readyz
3636
port: 5000
37-
initialDelaySeconds: 30
37+
initialDelaySeconds: 10
3838
timeoutSeconds: 480
3939
failureThreshold: 3
4040
periodSeconds: 600

‎requirements.in‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ Flask==3.1.2
22
requests==2.32.5
33
prometheus-client==0.22.1
44
redis==6.4.0
5-
minio==7.2.16
5+
minio==7.2.16
6+
ijson==3.4.0

0 commit comments

Comments
 (0)