Skip to content

Commit ac3e175

Browse files
authored
Introduces Poses table and related endpoints udpates (#531)
* Introduced poses and its fk (nullable before migration) * Created poses schema * Updated camaras and detections schemas * Created crud_pose * Updated crud Init.py * updated api router and dependencies * Created endpoints for poses * updated endpoints detections * Updated camera endpoints * Updated conftest * Added tests for poses * Updates detections and cameras tests * Updated client * Updates db diagram * update test end to end * mypy fixes * fix codacy * fix mypy * fix codacy and mypy with more explicit import * quick fix test client * revert client updates * fix typos db diagram docs * updates patrol_id type from str to int * updated tests patrol_id str to int * style * fix to respect code convention * wip updates client * typo * Specify None * optional pose id * resolve merge conflict merging main * client: add pose_id param optionnal in create_detection * update client tests
1 parent 36942b0 commit ac3e175

File tree

21 files changed

+694
-73
lines changed

21 files changed

+694
-73
lines changed

client/pyroclient/client.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.
55

66
from enum import Enum
7-
from typing import Dict, List, Tuple
7+
from typing import Dict, List, Optional, Tuple
88
from urllib.parse import urljoin
99

1010
import requests
@@ -22,6 +22,9 @@ class ClientRoute(str, Enum):
2222
CAMERAS_HEARTBEAT = "cameras/heartbeat"
2323
CAMERAS_IMAGE = "cameras/image"
2424
CAMERAS_FETCH = "cameras/"
25+
# POSES
26+
POSES_CREATE = "poses/"
27+
POSES_BY_ID = "poses/{pose_id}"
2528
# DETECTIONS
2629
DETECTIONS_CREATE = "detections/"
2730
DETECTIONS_FETCH = "detections"
@@ -148,37 +151,102 @@ def update_last_image(self, media: bytes) -> Response:
148151
timeout=self.timeout,
149152
)
150153

154+
# POSES
155+
def create_pose(
156+
self,
157+
camera_id: int,
158+
azimuth: float,
159+
patrol_id: int | None = None,
160+
) -> Response:
161+
"""Create a pose for a camera
162+
163+
>>> api_client.create_pose(camera_id=1, azimuth=120.5, patrol_id=3)
164+
"""
165+
payload = {
166+
"camera_id": camera_id,
167+
"azimuth": azimuth,
168+
}
169+
if patrol_id is not None:
170+
payload["patrol_id"] = patrol_id
171+
172+
return requests.post(
173+
urljoin(self._route_prefix, ClientRoute.POSES_CREATE),
174+
headers=self.headers,
175+
json=payload,
176+
timeout=self.timeout,
177+
)
178+
179+
def patch_pose(
180+
self,
181+
pose_id: int,
182+
azimuth: float | None = None,
183+
patrol_id: int | None = None,
184+
) -> Response:
185+
"""Update a pose
186+
187+
>>> api_client.patch_pose(pose_id=1, azimuth=90.0)
188+
"""
189+
payload = {}
190+
if azimuth is not None:
191+
payload["azimuth"] = azimuth
192+
if patrol_id is not None:
193+
payload["patrol_id"] = patrol_id
194+
195+
return requests.patch(
196+
urljoin(self._route_prefix, ClientRoute.POSES_BY_ID.format(pose_id=pose_id)),
197+
headers=self.headers,
198+
json=payload,
199+
timeout=self.timeout,
200+
)
201+
202+
def delete_pose(self, pose_id: int) -> Response:
203+
"""Delete a pose
204+
205+
>>> api_client.delete_pose(pose_id=1)
206+
"""
207+
return requests.delete(
208+
urljoin(self._route_prefix, ClientRoute.POSES_BY_ID.format(pose_id=pose_id)),
209+
headers=self.headers,
210+
timeout=self.timeout,
211+
)
212+
151213
# DETECTIONS
214+
152215
def create_detection(
153216
self,
154217
media: bytes,
155218
azimuth: float,
156219
bboxes: List[Tuple[float, float, float, float, float]],
220+
pose_id: Optional[int] = None,
157221
) -> Response:
158222
"""Notify the detection of a wildfire on the picture taken by a camera.
159223
160224
>>> from pyroclient import Client
161225
>>> api_client = Client("MY_CAM_TOKEN")
162226
>>> with open("path/to/my/file.ext", "rb") as f: data = f.read()
163-
>>> response = api_client.create_detection(data, azimuth=124.2, bboxes=[(.1,.1,.5,.8,.5)])
227+
>>> response = api_client.create_detection(data, azimuth=124.2, bboxes=[(.1,.1,.5,.8,.5)], pose_id=12)
164228
165229
Args:
166230
media: byte data of the picture
167231
azimuth: the azimuth of the camera when the picture was taken
168232
bboxes: list of tuples where each tuple is a relative coordinate in order xmin, ymin, xmax, ymax, conf
233+
pose_id: optional, pose_id of the detection
169234
170235
Returns:
171236
HTTP response
172237
"""
173238
if not isinstance(bboxes, (list, tuple)) or len(bboxes) == 0 or len(bboxes) > 5:
174239
raise ValueError("bboxes must be a non-empty list of tuples with a maximum of 5 boxes")
240+
data = {
241+
"azimuth": azimuth,
242+
"bboxes": _dump_bbox_to_json(bboxes),
243+
}
244+
if pose_id is not None:
245+
data["pose_id"] = pose_id
175246
return requests.post(
176247
urljoin(self._route_prefix, ClientRoute.DETECTIONS_CREATE),
177248
headers=self.headers,
178-
data={
179-
"azimuth": azimuth,
180-
"bboxes": _dump_bbox_to_json(bboxes),
181-
},
249+
data=data,
182250
timeout=self.timeout,
183251
files={"file": ("logo.png", media, "image/png")},
184252
)

client/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ def cam_token():
4040
response = requests.post(urljoin(API_URL, "cameras"), json=payload, headers=admin_headers, timeout=5)
4141
assert response.status_code == 201
4242
cam_id = response.json()["id"]
43+
# create a pose related to the cam
44+
payload = {"azimuth": 359, "patrol_id": 1, "camera_id": cam_id}
45+
response = requests.post(urljoin(API_URL, "poses"), json=payload, headers=admin_headers, timeout=5)
46+
assert response.status_code == 201
47+
4348
# Create a cam token
4449
return requests.post(urljoin(API_URL, f"cameras/{cam_id}/token"), headers=admin_headers, timeout=5).json()[
4550
"access_token"

client/tests/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_cam_workflow(cam_token, mock_img):
4040
cam_client.create_detection(mock_img, 123.2, None)
4141
with pytest.raises(ValueError, match="bboxes must be a non-empty list of tuples"):
4242
cam_client.create_detection(mock_img, 123.2, [])
43-
response = cam_client.create_detection(mock_img, 123.2, [(0, 0, 1.0, 0.9, 0.5)])
43+
response = cam_client.create_detection(mock_img, 123.2, [(0, 0, 1.0, 0.9, 0.5)], pose_id=1)
4444
assert response.status_code == 201, response.__dict__
4545
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)])
4646
assert response.status_code == 201, response.__dict__

scripts/dbdiagram.txt

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,37 @@ Enum "userrole" {
44
"user"
55
}
66

7-
Table "User" as U {
7+
Enum "annotationtype" {
8+
"wildfire_smoke"
9+
"other_smoke"
10+
"other"
11+
}
12+
13+
Table "organizations" as O {
14+
"id" int [not null]
15+
"name" varchar [not null]
16+
"telegram_id" varchar
17+
"slack_hook" varchar
18+
19+
Indexes {
20+
(id) [pk]
21+
}
22+
}
23+
24+
Table "users" as U {
825
"id" int [not null]
926
"organization_id" int [ref: > O.id, not null]
1027
"role" userrole [not null]
1128
"login" varchar [not null]
1229
"hashed_password" varchar [not null]
1330
"created_at" timestamp [not null]
31+
1432
Indexes {
1533
(id, login) [pk]
1634
}
1735
}
1836

19-
Table "Camera" as C {
37+
Table "cameras" as C {
2038
"id" int [not null]
2139
"organization_id" int [ref: > O.id, not null]
2240
"name" varchar [not null]
@@ -28,49 +46,56 @@ Table "Camera" as C {
2846
"created_at" timestamp [not null]
2947
"last_active_at" timestamp
3048
"last_image" varchar
49+
50+
Indexes {
51+
(id) [pk]
52+
}
53+
}
54+
55+
Table "poses" as P {
56+
"id" int [not null]
57+
"camera_id" int [ref: > C.id, not null]
58+
"azimuth" float [not null]
59+
"patrol_id" int
60+
3161
Indexes {
3262
(id) [pk]
3363
}
3464
}
3565

36-
Table "Sequence" as S {
66+
Table "sequences" as S {
3767
"id" int [not null]
3868
"camera_id" int [ref: > C.id, not null]
69+
"pose_id" int [ref: > P.id]
3970
"azimuth" float [not null]
40-
"is_wildfire" AnnotationType
71+
"is_wildfire" annotationtype
4172
"started_at" timestamp [not null]
4273
"last_seen_at" timestamp [not null]
74+
4375
Indexes {
4476
(id) [pk]
4577
}
4678
}
4779

48-
Table "Detection" as D {
80+
Table "detections" as D {
4981
"id" int [not null]
5082
"camera_id" int [ref: > C.id, not null]
83+
"pose_id" int [ref: > P.id]
5184
"sequence_id" int [ref: > S.id]
5285
"azimuth" float [not null]
5386
"bucket_key" varchar [not null]
5487
"bboxes" varchar [not null]
5588
"created_at" timestamp [not null]
56-
Indexes {
57-
(id) [pk]
58-
}
59-
}
6089

61-
Table "Organization" as O {
62-
"id" int [not null]
63-
"name" varchar [not null]
64-
"telegram_id" varchar
6590
Indexes {
6691
(id) [pk]
6792
}
6893
}
6994

70-
71-
Table "Webhook" as W {
95+
Table "webhooks" as W {
7296
"id" int [not null]
7397
"url" varchar [not null]
98+
7499
Indexes {
75100
(id) [pk]
76101
}

scripts/test_e2e.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ def main(args):
8989

9090
cam_auth = {"Authorization": f"Bearer {cam_token}"}
9191

92+
# Create a camera pose
93+
payload = {
94+
"camera_id": cam_id,
95+
"azimuth": 45,
96+
}
97+
pose_id = api_request("post", f"{args.endpoint}/poses/", agent_auth, payload)["id"]
98+
9299
# Take a picture
93100
file_bytes = requests.get("https://pyronear.org/img/logo.png", timeout=5).content
94101
# Update cam last image
@@ -110,7 +117,7 @@ def main(args):
110117
response = requests.post(
111118
f"{args.endpoint}/detections",
112119
headers=cam_auth,
113-
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]"},
120+
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]", "pose_id": pose_id},
114121
files={"file": ("logo.png", file_bytes, "image/png")},
115122
timeout=5,
116123
)
@@ -126,14 +133,14 @@ def main(args):
126133
det_id_2 = requests.post(
127134
f"{args.endpoint}/detections",
128135
headers=cam_auth,
129-
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]"},
136+
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]", "pose_id": pose_id},
130137
files={"file": ("logo.png", file_bytes, "image/png")},
131138
timeout=5,
132139
).json()["id"]
133140
det_id_3 = requests.post(
134141
f"{args.endpoint}/detections",
135142
headers=cam_auth,
136-
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]"},
143+
data={"azimuth": 45.6, "bboxes": "[(0.1,0.1,0.8,0.8,0.5)]", "pose_id": pose_id},
137144
files={"file": ("logo.png", file_bytes, "image/png")},
138145
timeout=5,
139146
).json()["id"]
@@ -173,6 +180,7 @@ def main(args):
173180
api_request("delete", f"{args.endpoint}/detections/{det_id_2}/", superuser_auth)
174181
api_request("delete", f"{args.endpoint}/detections/{det_id_3}/", superuser_auth)
175182
api_request("delete", f"{args.endpoint}/sequences/{sequence['id']}/", superuser_auth)
183+
api_request("delete", f"{args.endpoint}/poses/{pose_id}/", superuser_auth)
176184
api_request("delete", f"{args.endpoint}/cameras/{cam_id}/", superuser_auth)
177185
api_request("delete", f"{args.endpoint}/users/{user_id}/", superuser_auth)
178186
api_request("delete", f"{args.endpoint}/organizations/{org_id}/", superuser_auth)

0 commit comments

Comments
 (0)