Skip to content

Commit 266a7a7

Browse files
committed
feat: add fathomnet-rest-api 2.3.0 endpoints
1 parent 676895c commit 266a7a7

File tree

5 files changed

+207
-0
lines changed

5 files changed

+207
-0
lines changed

src/fathomnet/api/boundingboxes.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ def count_total_by_concept() -> List[dto.ByConceptCount]:
3636
return list(map(dto.ByConceptCount.from_dict, res_json))
3737

3838

39+
def count_total_by_observer() -> List[dto.ByObserverCount]:
40+
"""Get a count of bounding boxes for each observer."""
41+
res_json = BoundingBoxes.get("list/counts/observer")
42+
return list(map(dto.ByObserverCount.from_dict, res_json))
43+
44+
45+
def count_total_by_reviewer() -> List[dto.ByReviewerCount]:
46+
"""Get a count of bounding boxes for each reviewer."""
47+
res_json = BoundingBoxes.get("list/counts/reviewer")
48+
return list(map(dto.ByReviewerCount.from_dict, res_json))
49+
50+
3951
def find_observers() -> List[str]:
4052
"""Get a list of all observers."""
4153
res_json = BoundingBoxes.get("list/observers")
@@ -194,3 +206,45 @@ def audit_by_observer(
194206
"audit/observer/{}".format(quote(observer)), params=params
195207
)
196208
return list(map(dto.BoundingBoxDTO.from_dict, res_json))
209+
210+
211+
def bulk_rename(
212+
boxes: List[dto.BoundingBoxDTO],
213+
auth_header: Optional[dto.AuthHeader] = None,
214+
) -> List[dto.BoundingBoxDTO]:
215+
"""Bulk rename bounding boxes."""
216+
res_json = BoundingBoxes.put(
217+
"bulk/rename",
218+
json=[box.to_dict() for box in boxes],
219+
auth=auth_header,
220+
)
221+
return list(map(dto.BoundingBoxDTO.from_dict, res_json))
222+
223+
224+
def bulk_review(
225+
boxes: List[dto.BoundingBoxDTO],
226+
auth_header: Optional[dto.AuthHeader] = None,
227+
) -> List[dto.BoundingBoxDTO]:
228+
"""Bulk review bounding boxes."""
229+
res_json = BoundingBoxes.put(
230+
"bulk/review",
231+
json=[box_review.to_dict() for box_review in boxes],
232+
auth=auth_header,
233+
)
234+
return list(map(dto.BoundingBoxDTO.from_dict, res_json))
235+
236+
237+
def find(
238+
constraints: dto.BoundingBoxConstraintsDTO,
239+
) -> List[dto.BoundingBoxDTO]:
240+
"""Query bounding boxes by constraints."""
241+
res_json = BoundingBoxes.post("query", json=constraints.to_dict())
242+
return list(map(dto.BoundingBoxDTO.from_dict, res_json.get("content", [])))
243+
244+
245+
def count(
246+
constraints: dto.BoundingBoxConstraintsDTO,
247+
) -> dto.Count:
248+
"""Count bounding boxes by constraints."""
249+
res_json = BoundingBoxes.post("query/count", json=constraints.to_dict())
250+
return dto.Count.from_dict(res_json)

src/fathomnet/api/stats.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# stats.py (fathomnet-py)
22
from typing import List
33

4+
from fathomnet import dto
45
from fathomnet.api import EndpointManager
56

67

@@ -12,3 +13,15 @@ def most_popular_searches() -> List[str]:
1213
"""Get a list of the most popular searches."""
1314
res_json = Stats.get("list/popular/searches")
1415
return res_json
16+
17+
18+
def image_set_upload_positions() -> List[dto.ImageSetUploadPosition]:
19+
"""Get a list of image set upload positions."""
20+
res_json = Stats.get("list/upload/positions")
21+
return [dto.ImageSetUploadPosition.from_dict(item) for item in res_json]
22+
23+
24+
def contribution_stats() -> List[dto.ContributionStats]:
25+
"""Get contribution stats by contributing institution."""
26+
res_json = Stats.get("list/contributions")
27+
return [dto.ContributionStats.from_dict(item) for item in res_json]

src/fathomnet/dto.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,62 @@ class BoundingBox(DTO):
204204
lastReviewedTimestamp: Optional[str] = None
205205

206206

207+
class BoundingBoxSort(DTO):
208+
class Field(str, Enum):
209+
UUID = "UUID"
210+
CONCEPT = "CONCEPT"
211+
CREATED_AT = "CREATED_AT"
212+
UPDATED = "UPDATED"
213+
REVIEWED = "REVIEWED"
214+
IMAGE_URL = "IMAGE_URL"
215+
ROI_SIZE = "ROI_SIZE"
216+
217+
class Direction(str, Enum):
218+
ASC = "ASC"
219+
DESC = "DESC"
220+
221+
field: Optional[Field] = None
222+
direction: Optional[Direction] = None
223+
224+
225+
class BoundingBoxConstraintsDTO(DTO):
226+
concept: Optional[str] = None
227+
taxaProviderName: Optional[str] = None
228+
contributorsEmails: Optional[List[str]] = None
229+
startTimestamp: Optional[str] = None
230+
endTimestamp: Optional[str] = None
231+
includeUnverified: Optional[bool] = None
232+
includeVerified: Optional[bool] = None
233+
includeRejected: Optional[bool] = None
234+
includeUnknown: Optional[bool] = None
235+
minLongitude: Optional[float] = None
236+
maxLongitude: Optional[float] = None
237+
minLatitude: Optional[float] = None
238+
maxLatitude: Optional[float] = None
239+
minDepth: Optional[float] = None
240+
maxDepth: Optional[float] = None
241+
ownerInstitutionCodes: Optional[List[str]] = None
242+
tagKeys: Optional[List[str]] = None
243+
limit: Optional[int] = None
244+
offset: Optional[int] = None
245+
sort: Optional[BoundingBoxSort] = None
246+
247+
207248
class ByConceptCount(DTO):
208249
concept: Optional[str] = None
209250
count: Optional[int] = None
210251

211252

253+
class ByObserverCount(DTO):
254+
observer: Optional[str] = None
255+
count: Optional[int] = None
256+
257+
258+
class ByReviewerCount(DTO):
259+
reviewer: Optional[str] = None
260+
count: Optional[int] = None
261+
262+
212263
class ByContributorCount(DTO):
213264
contributorsEmail: Optional[str] = None
214265
count: Optional[int] = None
@@ -570,3 +621,15 @@ class WormsNames(DTO):
570621
name: Optional[str] = None
571622
acceptedName: Optional[str] = None
572623
alternateNames: Optional[List[str]] = None
624+
625+
626+
class ImageSetUploadPosition(DTO):
627+
latitude: Optional[float] = None
628+
longitude: Optional[float] = None
629+
imageSetUploadUuid: Optional[str] = None
630+
631+
632+
class ContributionStats(DTO):
633+
ownerInstitutionCode: Optional[str] = None
634+
totalUploads: Optional[int] = None
635+
totalImages: Optional[int] = None

test/test_boundingboxes.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest import SkipTest, TestCase
22

3+
from fathomnet import dto
34
from fathomnet.api import boundingboxes
45

56
from . import skipIfNoAuth
@@ -31,6 +32,26 @@ def test_count_total_by_concept(self):
3132
else:
3233
self.fail()
3334

35+
def test_count_total_by_observer(self):
36+
observer_counts = boundingboxes.count_total_by_observer()
37+
self.assertIsNotNone(observer_counts)
38+
self.assertGreater(len(observer_counts), 0)
39+
# Verify the structure of the returned data
40+
for observer_count in observer_counts:
41+
self.assertIsNotNone(observer_count.observer)
42+
self.assertIsNotNone(observer_count.count)
43+
self.assertGreater(observer_count.count, 0)
44+
45+
def test_count_total_by_reviewer(self):
46+
reviewer_counts = boundingboxes.count_total_by_reviewer()
47+
self.assertIsNotNone(reviewer_counts)
48+
self.assertGreater(len(reviewer_counts), 0)
49+
# Verify the structure of the returned data
50+
for reviewer_count in reviewer_counts:
51+
self.assertIsNotNone(reviewer_count.reviewer)
52+
self.assertIsNotNone(reviewer_count.count)
53+
self.assertGreater(reviewer_count.count, 0)
54+
3455
def test_find_observers(self):
3556
observers = boundingboxes.find_observers()
3657
self.assertIsNotNone(observers)
@@ -111,3 +132,35 @@ def test_audit_by_observer(self):
111132
observer = "brian@mbari.org"
112133
boxes = boundingboxes.audit_by_observer(observer)
113134
self.assertIsNotNone(boxes)
135+
136+
@skipIfNoAuth
137+
def test_bulk_rename(self):
138+
raise SkipTest("Write tests not yet implemented") # TODO bulk_rename test
139+
140+
@skipIfNoAuth
141+
def test_bulk_review(self):
142+
raise SkipTest("Write tests not yet implemented") # TODO bulk_review test
143+
144+
def test_find(self):
145+
# Test with a simple concept constraint
146+
constraints = dto.BoundingBoxConstraintsDTO(
147+
concept="Bathochordaeus",
148+
limit=10,
149+
)
150+
boxes = boundingboxes.find(constraints)
151+
self.assertIsNotNone(boxes)
152+
# Should return a list (might be empty if no results)
153+
self.assertIsInstance(boxes, list)
154+
# If there are results, verify the concept matches
155+
for box in boxes:
156+
self.assertEqual(box.concept, "Bathochordaeus")
157+
158+
def test_count(self):
159+
# Test with a simple concept constraint
160+
constraints = dto.BoundingBoxConstraintsDTO(
161+
concept="Bathochordaeus",
162+
limit=10,
163+
)
164+
count = boundingboxes.count(constraints)
165+
self.assertIsNotNone(count)
166+
self.assertGreater(count.count, 0)

test/test_stats.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,27 @@ def test_most_popular_searches(self):
88
results = stats.most_popular_searches()
99
self.assertIsNotNone(results)
1010
self.assertGreater(len(results), 0)
11+
12+
def test_image_set_upload_positions(self):
13+
positions = stats.image_set_upload_positions()
14+
self.assertIsNotNone(positions)
15+
# Should return a list
16+
self.assertIsInstance(positions, list)
17+
# If there are results, verify the structure
18+
if len(positions) > 0:
19+
position = positions[0]
20+
self.assertIsNotNone(position.latitude)
21+
self.assertIsNotNone(position.longitude)
22+
self.assertIsNotNone(position.imageSetUploadUuid)
23+
24+
def test_contribution_stats(self):
25+
stats_list = stats.contribution_stats()
26+
self.assertIsNotNone(stats_list)
27+
# Should return a list
28+
self.assertIsInstance(stats_list, list)
29+
# If there are results, verify the structure
30+
if len(stats_list) > 0:
31+
contribution_stat = stats_list[0]
32+
self.assertIsNotNone(contribution_stat.ownerInstitutionCode)
33+
self.assertIsNotNone(contribution_stat.totalUploads)
34+
self.assertIsNotNone(contribution_stat.totalImages)

0 commit comments

Comments
 (0)