Skip to content

Commit

Permalink
Merge branch 'main' into rs/use-organization-bucket
Browse files Browse the repository at this point in the history
  • Loading branch information
frgfm committed Aug 23, 2024
2 parents 348c603 + 7441114 commit 72d7a43
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 166 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
docker push $IMAGE_ID:latest
deploy-dev:
needs: dockerhub
needs: docker
runs-on: ubuntu-latest
steps:
- uses: appleboy/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
- id: debug-statements
language_version: python3
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.5.2'
rev: 'v0.6.2'
hooks:
- id: ruff
args:
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ stop:
test:
poetry export -f requirements.txt --without-hashes --with test --output requirements.txt
docker compose -f docker-compose.dev.yml up -d --build --wait
- docker compose exec -T backend pytest --cov=app
- docker compose -f docker-compose.dev.yml exec -T backend pytest --cov=app
docker compose -f docker-compose.dev.yml down

build-client:
pip install -e client/.

# Run tests for the Python client
# the "-" are used to launch the next command even if a command fail
test-client:
test-client: build-client
poetry export -f requirements.txt --without-hashes --output requirements.txt
docker compose -f docker-compose.dev.yml up -d --build --wait
- cd client && pytest --cov=pyroclient tests/ && cd ..
Expand Down
54 changes: 30 additions & 24 deletions client/pyroclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

from typing import Dict, List, Tuple, Union

from typing import Dict, List, Tuple
from urllib.parse import urljoin

import requests
Expand Down Expand Up @@ -38,30 +39,30 @@
}


def convert_loc_to_str(
localization: Union[List[Tuple[float, float, float, float, float]], None] = None,
max_num_boxes: int = 5,
def _to_str(coord: float) -> str:
"""Format string conditionally"""
return f"{coord:.0f}" if coord == round(coord) else f"{coord:.3f}"


def _dump_bbox_to_json(
bboxes: List[Tuple[float, float, float, float, float]],
) -> str:
"""Performs a custom JSON dump for list of coordinates
Args:
localization: list of tuples where each tuple is a relative coordinate in order xmin, ymin, xmax, ymax, conf
max_num_boxes: maximum allowed number of bounding boxes
bboxes: list of tuples where each tuple is a relative coordinate in order xmin, ymin, xmax, ymax, conf
Returns:
the JSON string dump with 2 decimal precision
the JSON string dump with 3 decimal precision
"""
if isinstance(localization, list) and len(localization) > 0:
if any(coord > 1 or coord < 0 for bbox in localization for coord in bbox):
raise ValueError("coordinates are expected to be relative")
if any(len(bbox) != 5 for bbox in localization):
raise ValueError("Each bbox is expected to be in format xmin, ymin, xmax, ymax, conf")
if len(localization) > max_num_boxes:
raise ValueError(f"Please limit the number of boxes to {max_num_boxes}")
box_list = tuple(
f"[{xmin:.3f},{ymin:.3f},{xmax:.3f},{ymax:.3f},{conf:.3f}]" for xmin, ymin, xmax, ymax, conf in localization
)
return f"[{','.join(box_list)}]"
return "[]"
if any(coord > 1 or coord < 0 for bbox in bboxes for coord in bbox):
raise ValueError("coordinates are expected to be relative")
if any(len(bbox) != 5 for bbox in bboxes):
raise ValueError("Each bbox is expected to be in format xmin, ymin, xmax, ymax, conf")
box_strs = (
f"({_to_str(xmin)},{_to_str(ymin)},{_to_str(xmax)},{_to_str(ymax)},{_to_str(conf)})"
for xmin, ymin, xmax, ymax, conf in bboxes
)
return f"[{','.join(box_strs)}]"


class Client:
Expand Down Expand Up @@ -119,27 +120,32 @@ def create_detection(
self,
media: bytes,
azimuth: float,
localization: Union[List[Tuple[float, float, float, float, float]], None],
bboxes: List[Tuple[float, float, float, float, float]],
) -> Response:
"""Notify the detection of a wildfire on the picture taken by a camera.
>>> from pyroclient import Client
>>> api_client = Client("MY_CAM_TOKEN")
>>> with open("path/to/my/file.ext", "rb") as f: data = f.read()
>>> response = api_client.create_detection(data, azimuth=124.2, localizationn"xyxy")
>>> response = api_client.create_detection(data, azimuth=124.2, bboxes=[(.1,.1,.5,.8,.5)])
Args:
media: byte data of the picture
azimuth: the azimuth of the camera when the picture was taken
localization: bounding box of the detected fire
bboxes: list of tuples where each tuple is a relative coordinate in order xmin, ymin, xmax, ymax, conf
Returns:
HTTP response
"""
if not isinstance(bboxes, (list, tuple)) or len(bboxes) == 0 or len(bboxes) > 5:
raise ValueError("bboxes must be a non-empty list of tuples with a maximum of 5 boxes")
return requests.post(
self.routes["detections-create"],
headers=self.headers,
data={"azimuth": azimuth, "localization": convert_loc_to_str(localization)},
data={
"azimuth": azimuth,
"bboxes": _dump_bbox_to_json(bboxes),
},
timeout=self.timeout,
files={"file": ("logo.png", media, "image/png")},
)
Expand Down Expand Up @@ -221,7 +227,7 @@ def fetch_unlabeled_detections(self, from_date: str) -> Response:
>>> from pyroclient import client
>>> api_client = Client("MY_USER_TOKEN")
>>> response = api_client.fetch_unacknowledged_detections("2023-07-04")
>>> response = api_client.fetch_unacknowledged_detections("2023-07-04T00:00:00")
Returns:
HTTP response
Expand Down
2 changes: 2 additions & 0 deletions client/pyroclient/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from typing import Union

__all__ = ["HTTPRequestError"]


class HTTPRequestError(Exception):
def __init__(self, status_code: int, response_message: Union[str, None] = None) -> None:
Expand Down
24 changes: 16 additions & 8 deletions client/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from requests.exceptions import ConnectionError, ReadTimeout
from requests.exceptions import ConnectionError as ConnError
from requests.exceptions import ReadTimeout

from pyroclient.client import Client
from pyroclient.exceptions import HTTPRequestError
Expand All @@ -9,7 +10,7 @@
("token", "host", "timeout", "expected_error"),
[
("invalid_token", "http://localhost:5050", 10, HTTPRequestError),
(pytest.admin_token, "http://localhost:8003", 10, ConnectionError),
(pytest.admin_token, "http://localhost:8003", 10, ConnError),
(pytest.admin_token, "http://localhost:5050", 0.00001, ReadTimeout),
(pytest.admin_token, "http://localhost:5050", 10, None),
],
Expand All @@ -26,24 +27,31 @@ def test_client_constructor(token, host, timeout, expected_error):
def test_cam_workflow(cam_token, mock_img):
cam_client = Client(cam_token, "http://localhost:5050", timeout=10)
assert cam_client.heartbeat().status_code == 200
response = cam_client.create_detection(mock_img, 123.2, None)
assert response.status_code == 201, print(response.__dict__)
# Check that adding bboxes works
with pytest.raises(ValueError, match="bboxes must be a non-empty list of tuples"):
cam_client.create_detection(mock_img, 123.2, None)
with pytest.raises(ValueError, match="bboxes must be a non-empty list of tuples"):
cam_client.create_detection(mock_img, 123.2, [])
response = cam_client.create_detection(mock_img, 123.2, [(0, 0, 1.0, 0.9, 0.5)])
assert response.status_code == 201, response.__dict__
response = cam_client.create_detection(mock_img, 123.2, [(0, 0, 1.0, 0.9, 0.5), (0.2, 0.2, 0.7, 0.7, 0.8)])
assert response.status_code == 201, response.__dict__
return response.json()["id"]


def test_agent_workflow(test_cam_workflow, agent_token):
# Agent workflow
agent_client = Client(agent_token, "http://localhost:5050", timeout=10)
response = agent_client.label_detection(test_cam_workflow, True)
assert response.status_code == 200, print(response.__dict__)
assert response.status_code == 200, response.__dict__


def test_user_workflow(test_cam_workflow, user_token):
# User workflow
user_client = Client(user_token, "http://localhost:5050", timeout=10)
response = user_client.get_detection_url(test_cam_workflow)
assert response.status_code == 200, print(response.__dict__)
assert response.status_code == 200, response.__dict__
response = user_client.fetch_detections()
assert response.status_code == 200, print(response.__dict__)
assert response.status_code == 200, response.__dict__
response = user_client.fetch_unlabeled_detections("2018-06-06T00:00:00")
assert response.status_code == 200, print(response.__dict__)
assert response.status_code == 200, response.__dict__
Loading

0 comments on commit 72d7a43

Please sign in to comment.