mercuto-client 0.3.0__py3-none-any.whl → 0.3.4a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mercuto_client/_tests/test_ingester/test_file_processor.py +171 -13
- mercuto_client/acl.py +20 -0
- mercuto_client/client.py +41 -8
- mercuto_client/ingester/__main__.py +11 -8
- mercuto_client/ingester/processor.py +57 -37
- mercuto_client/mocks/__init__.py +76 -2
- mercuto_client/mocks/_utility.py +6 -0
- mercuto_client/mocks/mock_core.py +44 -0
- mercuto_client/mocks/mock_media.py +117 -0
- mercuto_client/mocks/mock_notifications.py +65 -0
- mercuto_client/modules/__init__.py +8 -4
- mercuto_client/modules/core.py +117 -287
- mercuto_client/modules/data.py +39 -41
- mercuto_client/modules/fatigue.py +19 -19
- mercuto_client/modules/identity.py +31 -31
- mercuto_client/modules/media.py +324 -0
- mercuto_client/modules/notifications.py +88 -0
- mercuto_client/modules/reports.py +222 -0
- mercuto_client/util.py +1 -1
- {mercuto_client-0.3.0.dist-info → mercuto_client-0.3.4a3.dist-info}/METADATA +1 -1
- {mercuto_client-0.3.0.dist-info → mercuto_client-0.3.4a3.dist-info}/RECORD +24 -18
- {mercuto_client-0.3.0.dist-info → mercuto_client-0.3.4a3.dist-info}/WHEEL +0 -0
- {mercuto_client-0.3.0.dist-info → mercuto_client-0.3.4a3.dist-info}/licenses/LICENSE +0 -0
- {mercuto_client-0.3.0.dist-info → mercuto_client-0.3.4a3.dist-info}/top_level.txt +0 -0
mercuto_client/mocks/_utility.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import logging
|
|
2
3
|
from types import FunctionType
|
|
3
4
|
from typing import Any, Callable
|
|
@@ -67,3 +68,8 @@ class EnforceOverridesMeta(type):
|
|
|
67
68
|
return error_method
|
|
68
69
|
|
|
69
70
|
setattr(cls, attr, make_error_method(attr))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_data_url(mime_type: str, data: bytes) -> str:
|
|
74
|
+
encoded_data = base64.b64encode(data).decode('utf-8')
|
|
75
|
+
return f"data:{mime_type};base64,{encoded_data}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ..client import MercutoClient
|
|
7
|
+
from ..exceptions import MercutoHTTPException
|
|
8
|
+
from ..modules.core import Event, ItemCode, MercutoCoreService
|
|
9
|
+
from ._utility import EnforceOverridesMeta
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MockMercutoCoreService(MercutoCoreService, metaclass=EnforceOverridesMeta):
|
|
15
|
+
def __init__(self, client: 'MercutoClient'):
|
|
16
|
+
super().__init__(client=client)
|
|
17
|
+
self._events: dict[str, Event] = {}
|
|
18
|
+
|
|
19
|
+
def create_event(self, project: str, start_time: datetime, end_time: datetime) -> Event:
|
|
20
|
+
event = Event(code=str(uuid.uuid4()), project=ItemCode(code=project), start_time=start_time, end_time=end_time, objects=[], tags=[])
|
|
21
|
+
self._events[event.code] = event
|
|
22
|
+
return event
|
|
23
|
+
|
|
24
|
+
def get_event(self, event: str) -> Event:
|
|
25
|
+
if event not in self._events:
|
|
26
|
+
raise MercutoHTTPException(status_code=404, message=f"Event {event} not found")
|
|
27
|
+
return self._events[event]
|
|
28
|
+
|
|
29
|
+
def list_events(self, project: str,
|
|
30
|
+
start_time: Optional[datetime] = None,
|
|
31
|
+
end_time: Optional[datetime] = None,
|
|
32
|
+
limit: Optional[int] = None, offset: Optional[int] = 0,
|
|
33
|
+
ascending: bool = True) -> list[Event]:
|
|
34
|
+
filtered = [event for event in self._events.values() if event.project.code == project]
|
|
35
|
+
if start_time is not None:
|
|
36
|
+
filtered = [event for event in filtered if event.start_time >= start_time]
|
|
37
|
+
if end_time is not None:
|
|
38
|
+
filtered = [event for event in filtered if event.end_time <= end_time]
|
|
39
|
+
filtered.sort(key=lambda e: e.start_time, reverse=not ascending)
|
|
40
|
+
if offset is not None:
|
|
41
|
+
filtered = filtered[offset:]
|
|
42
|
+
if limit is not None:
|
|
43
|
+
filtered = filtered[:limit]
|
|
44
|
+
return filtered
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import mimetypes
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..client import MercutoClient
|
|
9
|
+
from ..exceptions import MercutoHTTPException
|
|
10
|
+
from ..modules.media import Image, MercutoMediaService, Video
|
|
11
|
+
from ._utility import EnforceOverridesMeta, create_data_url
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MockMercutoMediaService(MercutoMediaService, metaclass=EnforceOverridesMeta):
|
|
17
|
+
def __init__(self, client: 'MercutoClient'):
|
|
18
|
+
super().__init__(client=client)
|
|
19
|
+
self._events: dict[str, Image] = {}
|
|
20
|
+
self._videos: dict[str, Video] = {}
|
|
21
|
+
|
|
22
|
+
def upload_image(self, filename: str, project: str,
|
|
23
|
+
camera: Optional[str] = None,
|
|
24
|
+
timestamp: Optional[datetime] = None,
|
|
25
|
+
event: Optional[str] = None,
|
|
26
|
+
filedata: Optional[bytes] = None) -> Image:
|
|
27
|
+
code = str(uuid.uuid4())
|
|
28
|
+
mime_type, _ = mimetypes.guess_type(filename, strict=False)
|
|
29
|
+
if mime_type is None:
|
|
30
|
+
raise ValueError("Could not determine MIME type for file")
|
|
31
|
+
if mime_type not in ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff']:
|
|
32
|
+
raise MercutoHTTPException(f"Unsupported image MIME type: {mime_type}", 400)
|
|
33
|
+
if filedata is not None:
|
|
34
|
+
data = filedata
|
|
35
|
+
else:
|
|
36
|
+
with open(filename, 'rb') as f:
|
|
37
|
+
data = f.read()
|
|
38
|
+
data_url = create_data_url(mime_type, data)
|
|
39
|
+
image = Image(code=code, project=project, camera=camera, timestamp=timestamp, event=event, access_url=data_url,
|
|
40
|
+
mime_type=mime_type, access_expires=datetime.now(timezone.utc) + timedelta(days=365),
|
|
41
|
+
size_bytes=len(data), name=os.path.basename(filename))
|
|
42
|
+
self._events[code] = image
|
|
43
|
+
return image
|
|
44
|
+
|
|
45
|
+
def list_images(self, project: str,
|
|
46
|
+
camera: Optional[str] = None,
|
|
47
|
+
event: Optional[str] = None,
|
|
48
|
+
start_time: Optional[datetime] = None,
|
|
49
|
+
end_time: Optional[datetime] = None,
|
|
50
|
+
limit: int = 10,
|
|
51
|
+
offset: int = 0,
|
|
52
|
+
ascending: bool = True) -> list[Image]:
|
|
53
|
+
results = [img for img in self._events.values() if img.project == project]
|
|
54
|
+
if camera:
|
|
55
|
+
results = [img for img in results if img.camera == camera]
|
|
56
|
+
if event:
|
|
57
|
+
results = [img for img in results if img.event == event]
|
|
58
|
+
if start_time:
|
|
59
|
+
results = [img for img in results if img.timestamp and img.timestamp >= start_time]
|
|
60
|
+
if end_time:
|
|
61
|
+
results = [img for img in results if img.timestamp and img.timestamp <= end_time]
|
|
62
|
+
results.sort(key=lambda img: img.timestamp or datetime.min, reverse=not ascending)
|
|
63
|
+
return results[offset:offset + limit]
|
|
64
|
+
|
|
65
|
+
def get_image(self, image_code: str) -> Image:
|
|
66
|
+
try:
|
|
67
|
+
return self._events[image_code]
|
|
68
|
+
except KeyError:
|
|
69
|
+
raise MercutoHTTPException(f"Image not found: {image_code}", 404)
|
|
70
|
+
|
|
71
|
+
def delete_image(self, image_code: str) -> None:
|
|
72
|
+
try:
|
|
73
|
+
del self._events[image_code]
|
|
74
|
+
except KeyError:
|
|
75
|
+
raise MercutoHTTPException(f"Image not found: {image_code}", 404)
|
|
76
|
+
|
|
77
|
+
def upload_video(self, filename: str, project: str, start_time: datetime, end_time: datetime,
|
|
78
|
+
camera: str | None = None, event: str | None = None) -> str:
|
|
79
|
+
code = str(uuid.uuid4())
|
|
80
|
+
mime_type, _ = mimetypes.guess_type(filename, strict=False)
|
|
81
|
+
if mime_type is None:
|
|
82
|
+
raise ValueError("Could not determine MIME type for file")
|
|
83
|
+
with open(filename, 'rb') as f:
|
|
84
|
+
data = f.read()
|
|
85
|
+
data_url = create_data_url(mime_type, data)
|
|
86
|
+
video = Video(code=code, project=project, camera=camera, start_time=start_time, end_time=end_time, event=event,
|
|
87
|
+
access_url=data_url, mime_type=mime_type,
|
|
88
|
+
access_expires=datetime.now(timezone.utc) + timedelta(days=365),
|
|
89
|
+
size_bytes=len(data), name=os.path.basename(filename))
|
|
90
|
+
self._videos[code] = video
|
|
91
|
+
return code
|
|
92
|
+
|
|
93
|
+
def list_videos(self, project: str,
|
|
94
|
+
camera: Optional[str] = None,
|
|
95
|
+
event: Optional[str] = None,
|
|
96
|
+
start_time: Optional[datetime] = None,
|
|
97
|
+
end_time: Optional[datetime] = None,
|
|
98
|
+
limit: int = 10,
|
|
99
|
+
offset: int = 0,
|
|
100
|
+
ascending: bool = True) -> list[Video]:
|
|
101
|
+
results = [vid for vid in self._videos.values() if vid.project == project]
|
|
102
|
+
if camera:
|
|
103
|
+
results = [vid for vid in results if vid.camera == camera]
|
|
104
|
+
if event:
|
|
105
|
+
results = [vid for vid in results if vid.event == event]
|
|
106
|
+
if start_time:
|
|
107
|
+
results = [vid for vid in results if vid.start_time and vid.start_time >= start_time]
|
|
108
|
+
if end_time:
|
|
109
|
+
results = [vid for vid in results if vid.end_time and vid.end_time <= end_time]
|
|
110
|
+
results.sort(key=lambda vid: vid.start_time or datetime.min, reverse=not ascending)
|
|
111
|
+
return results[offset:offset + limit]
|
|
112
|
+
|
|
113
|
+
def get_video(self, video_code: str) -> Video:
|
|
114
|
+
try:
|
|
115
|
+
return self._videos[video_code]
|
|
116
|
+
except KeyError:
|
|
117
|
+
raise MercutoHTTPException(f"Video not found: {video_code}", 404)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ..client import MercutoClient
|
|
9
|
+
from ..exceptions import MercutoHTTPException
|
|
10
|
+
from ..modules.notifications import (ContactGroup, ContactMethod,
|
|
11
|
+
MercutoNotificationService,
|
|
12
|
+
NotificationAttachment)
|
|
13
|
+
from ._utility import EnforceOverridesMeta
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IssuedNotification(BaseModel):
|
|
19
|
+
contact_group: ContactGroup
|
|
20
|
+
subject: str
|
|
21
|
+
html: str
|
|
22
|
+
alternative_plaintext: Optional[str] = None
|
|
23
|
+
attachments: Optional[list[NotificationAttachment]] = None
|
|
24
|
+
unsubscribe_placeholder_text: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
issued_on: datetime
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MockMercutoNotificationService(MercutoNotificationService, metaclass=EnforceOverridesMeta):
|
|
30
|
+
def __init__(self, client: 'MercutoClient'):
|
|
31
|
+
super().__init__(client=client)
|
|
32
|
+
self.contact_groups: dict[str, ContactGroup] = {}
|
|
33
|
+
self.issued_notifications: list[IssuedNotification] = []
|
|
34
|
+
|
|
35
|
+
def list_contact_groups(self, project: str) -> list[ContactGroup]:
|
|
36
|
+
return [group for group in self.contact_groups.values() if group.project == project]
|
|
37
|
+
|
|
38
|
+
def get_contact_group(self, code: str) -> ContactGroup:
|
|
39
|
+
if code not in self.contact_groups:
|
|
40
|
+
raise MercutoHTTPException(
|
|
41
|
+
f"Contact group with code {code} not found", 404)
|
|
42
|
+
return self.contact_groups[code]
|
|
43
|
+
|
|
44
|
+
def create_contact_group(self, project: str, label: str, users: dict[str, list[ContactMethod]]) -> ContactGroup:
|
|
45
|
+
code = str(uuid.uuid4())
|
|
46
|
+
contact_group = ContactGroup(
|
|
47
|
+
code=code, project=project, label=label, users=users)
|
|
48
|
+
self.contact_groups[code] = contact_group
|
|
49
|
+
return contact_group
|
|
50
|
+
|
|
51
|
+
def issue_notification(self, contact_group: str, subject: str, html: str,
|
|
52
|
+
alternative_plaintext: Optional[str] = None,
|
|
53
|
+
attachments: Optional[list[NotificationAttachment]] = None,
|
|
54
|
+
unsubscribe_placeholder_text: Optional[str] = None) -> None:
|
|
55
|
+
group = self.get_contact_group(contact_group)
|
|
56
|
+
issued_notification = IssuedNotification(
|
|
57
|
+
contact_group=group,
|
|
58
|
+
subject=subject,
|
|
59
|
+
html=html,
|
|
60
|
+
alternative_plaintext=alternative_plaintext,
|
|
61
|
+
attachments=attachments,
|
|
62
|
+
unsubscribe_placeholder_text=unsubscribe_placeholder_text,
|
|
63
|
+
issued_on=datetime.now(timezone.utc)
|
|
64
|
+
)
|
|
65
|
+
self.issued_notifications.append(issued_notification)
|
|
@@ -2,13 +2,14 @@ import requests
|
|
|
2
2
|
|
|
3
3
|
from ..exceptions import MercutoClientException, MercutoHTTPException
|
|
4
4
|
|
|
5
|
-
_PayloadValueType = str | float | int | None
|
|
5
|
+
_PayloadValueType = str | float | int | None | bool
|
|
6
6
|
_PayloadListType = list[str] | list[float] | list[int] | list[_PayloadValueType]
|
|
7
|
-
_PayloadDictType = dict[str,
|
|
8
|
-
|
|
7
|
+
_PayloadDictType = dict[str, '_PayloadValueType | _PayloadListType | _PayloadDictType']
|
|
8
|
+
PayloadType = dict[str, _PayloadValueType | _PayloadListType | _PayloadDictType]
|
|
9
|
+
_PayloadType = PayloadType # For backwards compatibility
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def
|
|
12
|
+
def raise_for_response(r: requests.Response) -> None:
|
|
12
13
|
if 500 <= r.status_code < 600:
|
|
13
14
|
raise MercutoClientException(f"Server error: {r.text}")
|
|
14
15
|
if not (200 <= r.status_code < 300):
|
|
@@ -17,3 +18,6 @@ def _raise_for_response(r: requests.Response) -> None:
|
|
|
17
18
|
except Exception:
|
|
18
19
|
detail = str(r)
|
|
19
20
|
raise MercutoHTTPException(detail, r.status_code)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_raise_for_response = raise_for_response # For backwards compatibility
|