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.
@@ -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, str] | dict[str, float] | dict[str, int] | dict[str, _PayloadValueType]
8
- _PayloadType = dict[str, _PayloadValueType | _PayloadListType | _PayloadDictType]
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 _raise_for_response(r: requests.Response) -> None:
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