Skip to content
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/env
/.venv
/__pycache__
/out.txt
/tests/__pycache__
/.pytest_cache
/.pytest_cache
/local
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13.5-alpine@sha256:37b14db89f587f9eaa890e4a442a3fe55db452b69cca1403cc730bd0fbdc8aaf
FROM python:3.13.6-alpine@sha256:f196fd275fdad7287ccb4b0a85c2e402bb8c794d205cf6158909041c1ee9f38d

RUN addgroup -S appgroup && adduser -S -G appgroup appuser

Expand Down
27 changes: 27 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'''Shared configuration module'''
import os
import redis

# Redis configuration
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_DB = int(os.environ.get('REDIS_DB', 0))
CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))

def create_redis_client():
'''Create and return Redis client with error handling'''
try:
redis_client = redis.StrictRedis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True,
socket_connect_timeout=240,
socket_timeout=240
)
redis_client.ping()
print("Connected to Redis successfully!")
return redis_client, True
except (redis.ConnectionError, redis.TimeoutError) as e:
print(f"Could not connect to Redis: {e}")
return None, False
17 changes: 16 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from app import opensense
from app import storage
from app import readiness

app = Flask(__name__)

Expand All @@ -24,7 +25,8 @@ def print_version():
@app.route('/temperature')
def get_temperature():
'''Function to get the current temperature.'''
return opensense.get_temperature() + f"From: {IPADDR}\n"
result, _ = opensense.get_temperature()
return result + f"From: {IPADDR}\n"

@app.route('/metrics')
def metrics():
Expand All @@ -36,5 +38,18 @@ def store():
'''Function to store results in MinIO.'''
return storage.store_temperature_data()

@app.route('/readyz')
def readyz():
'''Readiness probe endpoint'''
status_code = readiness.readiness_check()

if status_code == 200:
return {"status": "ready"}, 200

return {
"status": "not ready",
"error": "More than 50% of sensors unreachable and cache expired"
}, 503

if __name__ == "__main__":
app.run()
75 changes: 40 additions & 35 deletions app/opensense.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
'''Module to get entries from OpenSenseMap API and get the average temperature'''
from datetime import datetime, timezone, timedelta
import os
import re
import requests
import redis
from app.config import create_redis_client, CACHE_TTL

REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
REDIS_PORT = int(os.environ.get('REDIS_PORT', 6379))
REDIS_DB = int(os.environ.get('REDIS_DB', 0))
CACHE_TTL = int(os.environ.get('CACHE_TTL', 300))

try:
redis_client = redis.StrictRedis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
decode_responses=True,
socket_connect_timeout=240,
socket_timeout=240
)
redis_client.ping()
REDIS_AVAILABLE = True
print("Connected to Redis successfully!")
except (redis.ConnectionError, redis.TimeoutError) as e:
REDIS_AVAILABLE = False
print("Could not connect to Redis.")
# Use shared Redis client
redis_client, REDIS_AVAILABLE = create_redis_client()

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

def classify_temperature(average):
'''Classify temperature based on ranges using dictionary approach'''
# Define temperature ranges and their classifications
temp_classifications = {
"cold": (float('-inf'), 10, "Warning: Too cold"),
"good": (10, 36, "Good"),
"hot": (36, float('inf'), "Warning: Too hot")
}

# Find the appropriate classification
for _, (min_temp, max_temp, status) in temp_classifications.items():
if min_temp < average <= max_temp:
return status

return "Unknown" # Default case

def get_temperature():
'''Function to get the average temperature from OpenSenseMap API.'''
cache_key = "temperature_data"
if REDIS_AVAILABLE:
try:
cached_data = redis_client.get(cache_key)
cached_data = redis_client.get("temperature_data")
if cached_data:
print("Using cached data from Redis.")
return cached_data
# Return cached data with default stats (since we don't have fresh stats)
default_stats = {"total_sensors": 0, "null_count": 0}
return cached_data, default_stats
except redis.RedisError as e:
print(f"Redis error: {e}. Proceeding without cache.")

Expand All @@ -52,33 +54,36 @@ def get_temperature():
response = requests.get("https://api.opensensemap.org/boxes", params=params, timeout=300)
print('Data retrieved successfully!')

_sensor_stats["total_sensors"] = sum(
1 for line in response.text.splitlines() if re.search(r'^\s*"sensors"\s*:\s*\[', line)
)

res = [d.get('sensors') for d in response.json() if 'sensors' in d]

temp_list = []
_sensor_stats["null_count"] = 0 # Initialize counter for null measurements

for sensor_list in res:
for measure in sensor_list:
if measure.get('title') == "Temperatur" and 'lastMeasurement' in measure:
if measure.get('unit') == "\u00b0C" and 'lastMeasurement' in measure:
last_measurement = measure['lastMeasurement']
if last_measurement is not None and 'value' in last_measurement:
last_measurement_int = float(last_measurement['value'])
temp_list.append(last_measurement_int)
else:
_sensor_stats["null_count"] += 1

total_sum = sum(temp_list)
average = total_sum / len(temp_list) if temp_list else 0
average = sum(temp_list) / len(temp_list) if temp_list else 0

if average <= 10:
result = f'Average temperature: {average:.2f} °C (Warning: Too cold)\n'
elif 10 < average <= 36:
result = f'Average temperature: {average:.2f} °C (Good)\n'
else:
result = f'Average temperature: {average:.2f} °C (Warning: Too hot)\n'
# Use the dictionary-based classification
status = classify_temperature(average)
result = f'Average temperature: {average:.2f} °C ({status})\n'

if REDIS_AVAILABLE:
try:
redis_client.setex(cache_key, CACHE_TTL, result)
redis_client.setex("temperature_data", CACHE_TTL, result)
print("Data cached in Redis.")
except redis.RedisError as e:
print(f"Redis error while caching data: {e}")

return result
return result, _sensor_stats
38 changes: 38 additions & 0 deletions app/readiness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'''Module to check the readiness of the stored information'''
from app.opensense import get_temperature
from app.config import create_redis_client

redis_client, REDIS_AVAILABLE = create_redis_client()

def check_caching():
'''Check if caching content is older than 5 minutes'''
if not REDIS_AVAILABLE:
return True

cache_key = "temperature_data"
ttl = redis_client.ttl(cache_key)

if ttl in (-2, -1):
return True

return False

def reachable_boxes():
'''Check if 50% + 1 of boxes are reachable'''
_, sensor_stats = get_temperature()
total = sensor_stats["total_sensors"]
null_count = sensor_stats["null_count"]
if total > 0 and null_count > (total * 0.5):
print("Warning: More than 50% of sensors are unreachable")
return 400
return 200

def readiness_check():
'''Combined readiness check for the /readyz endpoint'''
boxes_status = reachable_boxes()
cache_is_old = check_caching()

if boxes_status == 400 and cache_is_old:
return 503

return 200
10 changes: 5 additions & 5 deletions app/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ def store_temperature_data():
bucket_name = "temperature-data"
destination_file = f"temperature_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S%f')}.txt"

# Get the original temperature data format
temperature_data = opensense.get_temperature()
# Get the temperature data - unpack the tuple
temperature_result, _ = opensense.get_temperature()

text_bytes = temperature_data.encode('utf-8')
text_bytes = temperature_result.encode('utf-8')
text_stream = io.BytesIO(text_bytes)

# Make the bucket if it doesn't exist.
Expand Down Expand Up @@ -63,5 +63,5 @@ def store_temperature_data():
return error_msg

if __name__ == "__main__":
result = store_temperature_data()
print(result)
RESULT = store_temperature_data()
print(RESULT)
8 changes: 4 additions & 4 deletions k8s/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ spec:
failureThreshold: 3
readinessProbe:
httpGet:
path: /version
path: /readyz
port: 5000
initialDelaySeconds: 60
periodSeconds: 120
timeoutSeconds: 10
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
volumeMounts:
- name: tmp-volume
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/vcr_cassettes/version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interactions:
uri: http://127.0.0.1:5000/version
response:
body:
string: "Current app version: 0.5.0"
string: "Current app version: 0.7.0"
headers:
Connection:
- close
Expand Down
Loading