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
10 changes: 7 additions & 3 deletions containers/init_script/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
FROM python:3.8.16-slim
FROM python:3.11-slim

# hadolint ignore=DL3008
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Copy requirements file
COPY requirements.txt /tmp/

# hadolint ignore=DL3013
RUN pip install --no-cache-dir pandas python-dotenv==1.0.1 boto3==1.34.90 requests
RUN pip install --no-cache-dir -r /tmp/requirements.txt

# Copy your initialization script into the container
# Copy initialization scripts into the container
COPY init_script.py /usr/local/bin/
COPY utils.py /usr/local/bin/

# Set execute permission on the script
RUN chmod +x /usr/local/bin/init_script.py
Expand Down
213 changes: 168 additions & 45 deletions containers/init_script/init_script.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,34 @@
#  TODO REFACTOR : use the init scripts which are in the test_update_pi directory

#!/usr/bin/env python

from typing import Any, Dict, Optional
import pandas as pd
import sys
import logging
import os
import glob
import itertools
import random
import io
import pandas as pd
import requests
import json
from PIL import Image
from pyroclient import Client

# Import utility functions
from utils import (
get_token,
api_request,
read_json_file,
write_json_file,
download_images_if_needed,
)

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")

# Base URL for Client initialization (like in notebook: "http://api:5050")
base_url = os.environ.get("API_URL")
# API URL with /api/v1 for direct API requests
api_url = base_url + "/api/v1"

def get_token(api_url: str, login: str, pwd: str) -> str:
response = requests.post(
f"{api_url}/login/creds",
data={"username": login, "password": pwd},
timeout=5,
)
if response.status_code != 200:
raise ValueError(response.json()["detail"])
return response.json()["access_token"]


def api_request(
method_type: str,
route: str,
headers=Dict[str, str],
payload: Optional[Dict[str, Any]] = None,
):
kwargs = {"json": payload} if isinstance(payload, dict) else {}

response = getattr(requests, method_type)(route, headers=headers, **kwargs)
try:
detail = response.json()
except (requests.exceptions.JSONDecodeError, KeyError):
detail = response.text
assert response.status_code // 100 == 2, print(detail)
return response.json()


# Function to read JSON data from a file
def read_json_file(file_path):
with open(file_path, "r") as file:
return json.load(file)


# Function to write JSON data to a file
def write_json_file(file_path, data):
with open(file_path, "w") as file:
json.dump(data, file, indent=4)


api_url = os.environ.get("API_URL") + "/api/v1"
superuser_login = os.environ.get("SUPERADMIN_LOGIN")
superuser_pwd = os.environ.get("SUPERADMIN_PWD")
slack_hook = os.environ.get("SLACK_HOOK")
Expand All @@ -74,6 +49,10 @@ def write_json_file(file_path, data):
cameras = cameras.fillna("")
poses = pd.read_csv(f"data/csv/API_DATA{sub_path} - poses.csv")

# ============================================================================
# ORGAS CREATION
# ============================================================================

for orga in organizations.itertuples(index=False):
logging.info(f"saving orga : {orga.name}")
payload = {"name": orga.name}
Expand All @@ -90,6 +69,9 @@ def write_json_file(file_path, data):
payload,
)

# ============================================================================
# USERS CREATION
# ============================================================================

for user in users.itertuples(index=False):
logging.info(f"saving user : {user.login}")
Expand All @@ -105,6 +87,10 @@ def write_json_file(file_path, data):
data = read_json_file(credentials_path)
data_wildfire = read_json_file(credentials_path_etl)

# ============================================================================
# CAMERAS CREATION
# ============================================================================

for camera in cameras.itertuples(index=False):
logging.info(f"saving camera : {camera.name}")
payload = {
Expand Down Expand Up @@ -132,6 +118,10 @@ def write_json_file(file_path, data):
write_json_file(credentials_path, data)
write_json_file(credentials_path_etl, data_wildfire)

# ============================================================================
# POSES CREATION
# ============================================================================

logging.info("creating poses")
for pose in poses.itertuples(index=False):
payload = {
Expand All @@ -142,6 +132,139 @@ def write_json_file(file_path, data):
api_request("post", f"{api_url}/poses/", superuser_auth, payload)


# ============================================================================
# CAMERA STATUS UPDATES AND POSE IMAGE UPLOADS
# ============================================================================

# Constants
BASE_DIRECTORY = "data"
SAMPLE_PATH = "last_image_cameras"
IMAGES_URL = "https://github.com/pyronear/pyro-envdev/releases/download/v0.0.1/last_image_cameras.zip"

# Predefined list of image filenames for pose images
# (to be filled with actual filenames)
POSE_IMAGE_FILES = [
"11-20251001151013-d6de7b82.jpg",
"17-20251001143249-dc04ad6f.jpg",
"22-20251001150858-6578cf9e.jpg",
"59-20251001151203-354621da.jpg",
"69-20251003170645-721ec72e.jpg",
]

# Cameras to skip for pose images (one per organization)
# Format: {organization_id: camera_id_to_skip}
CAMERAS_TO_SKIP_POSE_IMAGES = {
2: 8, # Organization 2: skip camera 1 (videlles-01)
3: 14, # Organization 3: skip camera 9 (brison-01)
}

# Download images if needed
logging.info("Checking for image files...")
download_images_if_needed(BASE_DIRECTORY, SAMPLE_PATH, IMAGES_URL)

# Get all image files
image_dir = os.path.join(BASE_DIRECTORY, SAMPLE_PATH)
all_images = glob.glob(os.path.join(image_dir, "*.jpg"))

if not all_images:
logging.warning(
"No images found in directory, skipping camera and pose image updates"
)
else:
# Cycle images for camera last image updates
images_cycle = itertools.cycle(all_images)

# Prepare pose images
pose_images_full_paths = [
os.path.join(image_dir, fname)
for fname in POSE_IMAGE_FILES
if os.path.exists(os.path.join(image_dir, fname))
]

if not pose_images_full_paths:
logging.warning("No valid pose images found in predefined list")

logging.info("Updating cameras (heartbeat, last image) and pose images...")

# Create admin client once for all operations
admin_token = superuser_auth["Authorization"].replace("Bearer ", "")
admin_client = Client(admin_token, base_url)

# Fetch all cameras using pyroclient
cameras_response = admin_client.fetch_cameras().json()

# Process each camera
for camera_data in cameras_response:
camera_id = camera_data["id"]
camera_name = camera_data["name"]
org_id = camera_data["organization_id"]

logging.info(f"Processing camera {camera_name} (id: {camera_id})...")

try:
result = api_request(
"post", f"{api_url}/cameras/{camera_id}/token", superuser_auth
)
camera_token = result["access_token"]

camera_client = Client(camera_token, base_url)

# Update heartbeat
camera_client.heartbeat()
logging.info(" ✓ Heartbeat updated")

# Update camera last image
img_file = next(images_cycle)
stream = io.BytesIO()
im = Image.open(img_file)
im.save(stream, format="JPEG", quality=80)
stream.seek(0)
camera_client.update_last_image(stream.getvalue())
logging.info(" ✓ Last image updated")

# Fetch camera details including poses
camera_details = requests.get(
f"{api_url}/cameras/{camera_id}", headers=superuser_auth, timeout=10
).json()

camera_poses = camera_details.get("poses", [])

if not camera_poses:
logging.info(" No poses for this camera")
elif CAMERAS_TO_SKIP_POSE_IMAGES.get(org_id) == camera_id:
logging.info(f" Skipping pose images (org {org_id} skip rule)")
elif not pose_images_full_paths:
logging.info(" No pose images available")
else:
# Upload pose images
num_poses = len(camera_poses)
num_images = min(num_poses, len(pose_images_full_paths))
selected_images = random.sample(pose_images_full_paths, num_images)

logging.info(f" Uploading {num_images} pose images...")
for pose_data, image_path in zip(camera_poses, selected_images):
try:
with open(image_path, "rb") as f:
image_data = f.read()

# time.sleep(2)
admin_client.update_pose_image(pose_data["id"], image_data)
logging.info(
f"✓ Pose {pose_data['id']} (azimuth {pose_data['azimuth']})"
)
except Exception as e:
logging.error(f" ✗ Pose {pose_data['id']}: {e}")

logging.info(f" Completed camera {camera_name}")

except Exception as e:
logging.error(f" Error processing camera {camera_name}: {e}")
continue

logging.info("All camera and pose updates completed")

logging.info("Initialization script completed successfully")

# Load environment variables from .env file
# load_dotenv()

Expand Down
6 changes: 6 additions & 0 deletions containers/init_script/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pandas
python-dotenv==1.0.1
boto3==1.34.90
requests
pillow
pyroclient @ git+https://github.com/pyronear/pyro-api.git@main#subdirectory=client
Loading