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.
@@ -0,0 +1,324 @@
1
+ import os
2
+ from datetime import timedelta
3
+ from typing import TYPE_CHECKING, Literal, Optional
4
+
5
+ from pydantic import TypeAdapter
6
+
7
+ if TYPE_CHECKING:
8
+ from ..client import MercutoClient
9
+
10
+ from datetime import datetime
11
+
12
+ from ..exceptions import MercutoHTTPException
13
+ from . import PayloadType
14
+ from ._util import BaseModel
15
+
16
+
17
+ class Image(BaseModel):
18
+ code: str
19
+ project: str
20
+ mime_type: str
21
+ size_bytes: int
22
+ name: str
23
+ access_url: str
24
+ access_expires: datetime
25
+ camera: Optional[str] = None
26
+ timestamp: Optional[datetime] = None
27
+ event: Optional[str] = None
28
+
29
+
30
+ class Video(BaseModel):
31
+ code: str
32
+ project: str
33
+ start_time: datetime
34
+ end_time: datetime
35
+
36
+ mime_type: str
37
+ size_bytes: int
38
+ name: str
39
+
40
+ access_url: str
41
+ access_expires: datetime
42
+
43
+ event: Optional[str] = None
44
+ camera: Optional[str] = None
45
+
46
+
47
+ class _VideoUploadInitializeResponse(BaseModel):
48
+ request_id: str
49
+ presigned_put_url: str
50
+ presigned_url_expires: datetime
51
+
52
+
53
+ class Healthcheck(BaseModel):
54
+ status: str
55
+
56
+
57
+ class CameraTrigger(BaseModel):
58
+ trigger_channel: str
59
+ pre_interval: timedelta = timedelta(seconds=10)
60
+ post_interval: timedelta = timedelta(seconds=10)
61
+ enabled: bool = True
62
+ trigger_greater_than: Optional[float] = None
63
+ trigger_less_than: Optional[float] = None
64
+
65
+
66
+ class Camera(BaseModel):
67
+ code: str
68
+ project: str
69
+ label: str
70
+ triggers: list[CameraTrigger]
71
+ camera_type: Literal['BOSCH', 'DIRECT_RTSP',
72
+ 'STATIC', 'ROCKFIELD-CAMERA-SERVER-VERSION-2']
73
+ encode_timestamp: bool = True
74
+ encode_blur: bool = False
75
+ blur_steps: Optional[int] = None
76
+ blur_sigma: Optional[float] = None
77
+ tunnel_address: Optional[str] = None
78
+ tunnel_port: Optional[int] = None
79
+ tunnel_username: Optional[str] = None
80
+ tunnel_password: Optional[str] = None
81
+ tunnel_key: Optional[str] = None
82
+ camera_ip: Optional[str] = None
83
+ camera_port: Optional[int] = None
84
+ camera_username: Optional[str] = None
85
+ camera_password: Optional[str] = None
86
+ camera_serial: Optional[str] = None
87
+ rtsp_url: Optional[str] = None
88
+
89
+
90
+ # --- TypeAdapters for lists ---
91
+ _ImagelistAdapter = TypeAdapter(list[Image])
92
+ _VideolistAdapter = TypeAdapter(list[Video])
93
+
94
+
95
+ class MercutoMediaService:
96
+ def __init__(self, client: 'MercutoClient', path: str = '/media') -> None:
97
+ self._client = client
98
+ self._path = path
99
+
100
+ def healthcheck(self) -> Healthcheck:
101
+ r = self._client.request(f"{self._path}/healthcheck", "GET")
102
+ return Healthcheck.model_validate_json(r.text)
103
+
104
+ # --- Images ---
105
+
106
+ def list_images(self, project: str,
107
+ camera: Optional[str] = None,
108
+ event: Optional[str] = None,
109
+ start_time: Optional[datetime] = None,
110
+ end_time: Optional[datetime] = None,
111
+ limit: int = 10,
112
+ offset: int = 0,
113
+ ascending: bool = True) -> list[Image]:
114
+ params: PayloadType = {
115
+ 'project': project,
116
+ 'limit': limit,
117
+ 'offset': offset,
118
+ 'ascending': ascending
119
+ }
120
+ if camera is not None:
121
+ params["camera"] = camera
122
+ if event is not None:
123
+ params["event"] = event
124
+ if start_time is not None:
125
+ if start_time.tzinfo is None:
126
+ raise ValueError("start_time must be timezone-aware")
127
+ params["start_time"] = start_time.isoformat()
128
+ if end_time is not None:
129
+ if end_time.tzinfo is None:
130
+ raise ValueError("end_time must be timezone-aware")
131
+ params["end_time"] = end_time.isoformat()
132
+ r = self._client.request(f"{self._path}/images", "GET", params=params)
133
+ return _ImagelistAdapter.validate_json(r.text)
134
+
135
+ def get_image(self, image_code: str) -> Image:
136
+ r = self._client.request(f"{self._path}/images/{image_code}", "GET")
137
+ return Image.model_validate_json(r.text)
138
+
139
+ def delete_image(self, image_code: str) -> None:
140
+ self._client.request(f"{self._path}/images/{image_code}", "DELETE")
141
+
142
+ def upload_image(self, filename: str, project: str,
143
+ camera: Optional[str] = None,
144
+ timestamp: Optional[datetime] = None,
145
+ event: Optional[str] = None,
146
+ filedata: Optional[bytes] = None) -> Image:
147
+ """
148
+ Upload an image to the media service.
149
+ Provide either filename (path to file) or filedata (bytes of file) + filename (reference only).
150
+ """
151
+ params: PayloadType = {
152
+ 'project': project
153
+ }
154
+ if camera is not None:
155
+ params["camera"] = camera
156
+ if timestamp is not None:
157
+ if timestamp.tzinfo is None:
158
+ raise ValueError("timestamp must be timezone-aware")
159
+ params["timestamp"] = timestamp.isoformat()
160
+ if event is not None:
161
+ params["event"] = event
162
+
163
+ if filedata is not None:
164
+ from io import BytesIO
165
+ r = self._client.request(
166
+ f"{self._path}/images", "PUT", params=params, files={'file': (filename, BytesIO(filedata))})
167
+ return Image.model_validate_json(r.text)
168
+ else:
169
+ with open(filename, 'rb') as f:
170
+ r = self._client.request(
171
+ f"{self._path}/images", "PUT", params=params, files={'file': f})
172
+ return Image.model_validate_json(r.text)
173
+
174
+ # --- Videos ---
175
+
176
+ def list_videos(self, project: str,
177
+ camera: Optional[str] = None,
178
+ event: Optional[str] = None,
179
+ start_time: Optional[datetime] = None,
180
+ end_time: Optional[datetime] = None,
181
+ limit: int = 10,
182
+ offset: int = 0,
183
+ ascending: bool = True) -> list[Video]:
184
+ params: PayloadType = {
185
+ 'project': project,
186
+ 'limit': limit,
187
+ 'offset': offset,
188
+ 'ascending': ascending
189
+ }
190
+ if camera is not None:
191
+ params["camera"] = camera
192
+ if event is not None:
193
+ params["event"] = event
194
+ if start_time is not None:
195
+ if start_time.tzinfo is None:
196
+ raise ValueError("start_time must be timezone-aware")
197
+ params["start_time"] = start_time.isoformat()
198
+ if end_time is not None:
199
+ if end_time.tzinfo is None:
200
+ raise ValueError("end_time must be timezone-aware")
201
+ params["end_time"] = end_time.isoformat()
202
+ r = self._client.request(f"{self._path}/videos", "GET", params=params)
203
+ return _VideolistAdapter.validate_json(r.text)
204
+
205
+ def get_video(self, video_code: str) -> Video:
206
+ r = self._client.request(f"{self._path}/videos/{video_code}", "GET")
207
+ return Video.model_validate_json(r.text)
208
+
209
+ def upload_video(self, filename: str, project: str,
210
+ start_time: datetime,
211
+ end_time: datetime,
212
+ camera: Optional[str] = None,
213
+ event: Optional[str] = None) -> str:
214
+ """
215
+ Upload a video file to the media service.
216
+ This is a multi-step process.
217
+ First, the request is initialized to get an upload URL.
218
+ Then, the file is uploaded to the provided URL.
219
+ Finally, the upload is finalized.
220
+
221
+ :returns: A request ID used to track the status of the uploaded video processing. Pass this to the /requests/{request_id} endpoint
222
+ to check the status and get the video_id once processing is complete.
223
+ """
224
+ import mimetypes
225
+ mime_type = mimetypes.guess_type(filename, strict=False)[0]
226
+ if mime_type is None:
227
+ raise ValueError(
228
+ f"Could not determine MIME type for file: {filename}")
229
+ if mime_type not in {'video/mp4', 'video/avi', 'video/mov', 'video/mkv'}:
230
+ raise ValueError(f"Unsupported video MIME type: {mime_type}")
231
+
232
+ # 1. Make the initialize upload request
233
+ init_payload: PayloadType = {
234
+ 'start_time': start_time.isoformat(),
235
+ 'end_time': end_time.isoformat(),
236
+ 'mime_type': mime_type,
237
+ 'filename': os.path.basename(filename)
238
+ }
239
+ if camera is not None:
240
+ init_payload['camera'] = camera
241
+ if event is not None:
242
+ init_payload['event'] = event
243
+ init_request = self._client.request(f"{self._path}/videos", "POST", json=init_payload,
244
+ params={'project': project, 'action': 'initialize'})
245
+ init_request_response = _VideoUploadInitializeResponse.model_validate_json(
246
+ init_request.text)
247
+
248
+ # 2. Upload the video file
249
+ with open(filename, 'rb') as f:
250
+ resp = self._client.session().put(init_request_response.presigned_put_url,
251
+ data=f, verify=self._client.verify_ssl)
252
+ if not resp.ok:
253
+ raise MercutoHTTPException(
254
+ f"Video upload failed: {resp.text}", resp.status_code)
255
+
256
+ # 3. Finalize the upload
257
+ self._client.request(f"{self._path}/videos", "POST", params={
258
+ 'project': project,
259
+ 'action': 'commit',
260
+ 'request_id': init_request_response.request_id
261
+ })
262
+
263
+ return init_request_response.request_id
264
+
265
+ # --- Cameras ---
266
+
267
+ def create_camera(self, project: str,
268
+ label: str,
269
+ triggers: list[CameraTrigger],
270
+ encode_timestamp: bool = True,
271
+ encode_blur: bool = False,
272
+ blur_steps: Optional[int] = None,
273
+ blur_sigma: Optional[float] = None,
274
+ tunnel_address: Optional[str] = None,
275
+ tunnel_port: Optional[int] = None,
276
+ tunnel_username: Optional[str] = None,
277
+ tunnel_password: Optional[str] = None,
278
+ tunnel_key: Optional[str] = None,
279
+ camera_ip: Optional[str] = None,
280
+ camera_port: Optional[int] = None,
281
+ camera_username: Optional[str] = None,
282
+ camera_password: Optional[str] = None,
283
+ camera_serial: Optional[str] = None,
284
+ camera_type: Literal['BOSCH', 'DIRECT_RTSP', 'STATIC',
285
+ 'ROCKFIELD-CAMERA-SERVER-VERSION-2'] = 'DIRECT_RTSP',
286
+ rtsp_url: Optional[str] = None,) -> Camera:
287
+ payload: PayloadType = {
288
+ 'label': label,
289
+ 'encode_timestamp': encode_timestamp,
290
+ 'encode_blur': encode_blur,
291
+ }
292
+ if blur_steps is not None:
293
+ payload['blur_steps'] = blur_steps
294
+ if blur_sigma is not None:
295
+ payload['blur_sigma'] = blur_sigma
296
+ if tunnel_address is not None:
297
+ tunnel_payload: PayloadType = {
298
+ 'address': tunnel_address,
299
+ 'port': tunnel_port,
300
+ 'username': tunnel_username,
301
+ 'password': tunnel_password,
302
+ 'key': tunnel_key
303
+ }
304
+ payload['ssh_tunnel'] = tunnel_payload
305
+ if camera_ip is not None:
306
+ payload['camera_ip'] = camera_ip
307
+ if camera_port is not None:
308
+ payload['camera_port'] = camera_port
309
+ if camera_username is not None:
310
+ payload['camera_username'] = camera_username
311
+ if camera_password is not None:
312
+ payload['camera_password'] = camera_password
313
+ if camera_serial is not None:
314
+ payload['camera_serial'] = camera_serial
315
+ payload['camera_type'] = camera_type
316
+ if rtsp_url is not None:
317
+ payload['rtsp_url'] = rtsp_url
318
+
319
+ if triggers:
320
+ payload['triggers'] = [trigger.model_dump(mode='json') for trigger in triggers] # type: ignore
321
+
322
+ r = self._client.request(
323
+ f"{self._path}/cameras", "PUT", json=payload, params={'project': project})
324
+ return Camera.model_validate_json(r.text)
@@ -0,0 +1,88 @@
1
+ from typing import TYPE_CHECKING, Literal, Optional
2
+
3
+ from pydantic import TypeAdapter
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import MercutoClient
7
+
8
+ from ._util import BaseModel
9
+
10
+ ContactMethod = Literal['EMAIL', 'SMS']
11
+
12
+
13
+ class ContactGroup(BaseModel):
14
+ code: str
15
+ project: str
16
+ label: str
17
+ users: dict[str, list[ContactMethod]]
18
+
19
+
20
+ class NotificationAttachment(BaseModel):
21
+ """
22
+ Attachment to include in the notification.
23
+ """
24
+ filename: str
25
+ presigned_url: str # Presigned URL to fetch the attachment from.
26
+ mime_type: str
27
+
28
+
29
+ class Healthcheck(BaseModel):
30
+ status: str
31
+
32
+
33
+ # --- TypeAdapters for lists ---
34
+ _ContactGroupListAdapter = TypeAdapter(list[ContactGroup])
35
+
36
+
37
+ class MercutoNotificationService:
38
+ def __init__(self, client: 'MercutoClient', path: str = '/notifications') -> None:
39
+ self._client = client
40
+ self._path = path
41
+
42
+ def healthcheck(self) -> Healthcheck:
43
+ r = self._client.request(f"{self._path}/healthcheck", "GET")
44
+ return Healthcheck.model_validate_json(r.text)
45
+
46
+ def list_contact_groups(self, project: str) -> list[ContactGroup]:
47
+ r = self._client.request(f"{self._path}/contact-groups", "GET", params={"project": project})
48
+ return _ContactGroupListAdapter.validate_json(r.text)
49
+
50
+ def get_contact_group(self, code: str) -> ContactGroup:
51
+ r = self._client.request(f"{self._path}/contact-groups/{code}", "GET")
52
+ return ContactGroup.model_validate_json(r.text)
53
+
54
+ def create_contact_group(self, project: str, label: str, users: dict[str, list[ContactMethod]]) -> ContactGroup:
55
+ r = self._client.request(f"{self._path}/contact-groups", "PUT", json={
56
+ "project": project,
57
+ "label": label,
58
+ "users": users
59
+ })
60
+ return ContactGroup.model_validate_json(r.text)
61
+
62
+ def issue_notification(self, contact_group: str, subject: str, html: str,
63
+ alternative_plaintext: Optional[str] = None,
64
+ attachments: Optional[list[NotificationAttachment]] = None,
65
+ unsubscribe_placeholder_text: Optional[str] = None) -> None:
66
+ """
67
+ Issue a notification to all contacts within a contact group, based on their contact preferences.
68
+
69
+ :param contact_group: The code of the contact group to send the notification to.
70
+ :param subject: The subject of the notification (i.e. email subject line).
71
+ :param html: The HTML content of the notification.
72
+ :param alternative_plaintext: Optional plaintext alternative for the notification.
73
+ Alternative plaintext is used for SMS notifications and for email clients that do not support HTML.
74
+ :param attachments: Optional list of attachments to include in the notification.
75
+ Only applicable for email notifications.
76
+ :param unsubscribe_placeholder_text: Optional placeholder text for unsubscribe links.
77
+ Any text matching this placeholder will be replaced with an unsubscribe link for email notifications.
78
+ :return: None
79
+ """
80
+
81
+ self._client.request(f"{self._path}/contact-groups/{contact_group}/notify", "POST", json={
82
+ "subject": subject,
83
+ "html": html,
84
+ "alternative_plaintext": alternative_plaintext,
85
+ "attachments": [attachment.model_dump() for attachment in attachments] if attachments else [],
86
+ "unsubscribe_placeholder_text": unsubscribe_placeholder_text
87
+ })
88
+ return
@@ -0,0 +1,222 @@
1
+ from datetime import datetime
2
+ from typing import (TYPE_CHECKING, BinaryIO, Literal, Optional, Protocol,
3
+ TypedDict)
4
+
5
+ from pydantic import AwareDatetime, TypeAdapter
6
+
7
+ from ..exceptions import MercutoHTTPException
8
+ from . import PayloadType
9
+ from ._util import BaseModel
10
+
11
+ if TYPE_CHECKING:
12
+ from ..client import MercutoClient
13
+
14
+
15
+ class ReportConfiguration(BaseModel):
16
+ code: str
17
+ project: str
18
+ label: str
19
+ revision: str
20
+ schedule: Optional[str] = None
21
+ contact_group: Optional[str] = None
22
+ last_scheduled: Optional[AwareDatetime] = None
23
+ custom_policy: Optional[str] = None
24
+
25
+
26
+ ReportLogStatus = Literal['PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED']
27
+
28
+
29
+ class ReportLog(BaseModel):
30
+ code: str
31
+ report_configuration: str
32
+ scheduled_start: Optional[AwareDatetime]
33
+ actual_start: AwareDatetime
34
+ actual_finish: Optional[AwareDatetime]
35
+ status: ReportLogStatus
36
+ message: Optional[str]
37
+ access_url: Optional[str]
38
+ mime_type: Optional[str]
39
+ filename: Optional[str]
40
+
41
+
42
+ class ReportSourceCodeRevision(BaseModel):
43
+ code: str
44
+ project: Optional[str]
45
+ revision_date: AwareDatetime
46
+ description: str
47
+ source_code_url: str
48
+
49
+
50
+ class Healthcheck(BaseModel):
51
+ status: str
52
+
53
+
54
+ _ReportConfigurationListAdapter = TypeAdapter(list[ReportConfiguration])
55
+ _ReportLogListAdapter = TypeAdapter(list[ReportLog])
56
+
57
+ """
58
+ The below types are used for defining report generation functions.
59
+ They are provided for type-checking and helpers for users writing custom reports.
60
+ """
61
+
62
+
63
+ class ReportHandlerResultLike(Protocol):
64
+ filename: str
65
+ mime_type: str
66
+ data: bytes
67
+
68
+
69
+ class ReportHandlerResult(BaseModel):
70
+ filename: str
71
+ mime_type: str
72
+ data: bytes
73
+
74
+
75
+ class HandlerRequest(TypedDict):
76
+ timestamp: datetime
77
+
78
+
79
+ class HandlerContext(TypedDict):
80
+ service_token: str
81
+ project_code: str
82
+ report_code: str
83
+ log_code: str
84
+ client: 'MercutoClient'
85
+
86
+
87
+ class ReportHandler(Protocol):
88
+ def __call__(self,
89
+ request: 'HandlerRequest',
90
+ context: 'HandlerContext') -> 'ReportHandlerResultLike':
91
+ ...
92
+
93
+
94
+ class MercutoReportService:
95
+ def __init__(self, client: 'MercutoClient', path: str = '/reports') -> None:
96
+ self._client = client
97
+ self._path = path
98
+
99
+ def healthcheck(self) -> Healthcheck:
100
+ r = self._client.request(f"{self._path}/healthcheck", "GET")
101
+ return Healthcheck.model_validate_json(r.text)
102
+
103
+ def list_report_configurations(self, project: str) -> list['ReportConfiguration']:
104
+ """
105
+ List scheduled reports for a specific project.
106
+ """
107
+ params: PayloadType = {
108
+ 'project': project
109
+ }
110
+ r = self._client.request(
111
+ f'{self._path}/configurations', 'GET', params=params)
112
+ return _ReportConfigurationListAdapter.validate_json(r.text)
113
+
114
+ def create_report_configuration(self, project: str, label: str, schedule: str, revision: str,
115
+ contact_group: Optional[str] = None, custom_policy: Optional[str] = None) -> ReportConfiguration:
116
+ """
117
+ Create a new scheduled report using the provided source code revision.
118
+ """
119
+ json: PayloadType = {
120
+ 'project': project,
121
+ 'label': label,
122
+ 'schedule': schedule,
123
+ 'revision': revision,
124
+ }
125
+ if contact_group is not None:
126
+ json['contact_group'] = contact_group
127
+ if custom_policy is not None:
128
+ json['custom_policy'] = custom_policy
129
+ r = self._client.request(
130
+ f'{self._path}/configurations', 'PUT', json=json)
131
+ return ReportConfiguration.model_validate_json(r.text)
132
+
133
+ def generate_report(self, report: str, timestamp: datetime, mark_as_scheduled: bool = False) -> ReportLog:
134
+ """
135
+ Trigger generation of a scheduled report for a specific timestamp.
136
+ """
137
+ r = self._client.request(f'{self._path}/configurations/{report}/generate', 'POST', json={
138
+ 'timestamp': timestamp.isoformat(),
139
+ 'mark_as_scheduled': mark_as_scheduled
140
+ })
141
+ return ReportLog.model_validate_json(r.text)
142
+
143
+ def list_report_logs(self, project: str, report: Optional[str] = None) -> list[ReportLog]:
144
+ """
145
+ List report log entries for a specific project.
146
+ """
147
+ params: PayloadType = {
148
+ 'project': project
149
+ }
150
+ if report is not None:
151
+ params['configuration'] = report
152
+ r = self._client.request(
153
+ f'{self._path}/logs', 'GET', params=params)
154
+ return _ReportLogListAdapter.validate_json(r.text)
155
+
156
+ def get_report_log(self, log: str) -> ReportLog:
157
+ """
158
+ Get a specific report log entry.
159
+ """
160
+ r = self._client.request(
161
+ f'{self._path}/logs/{log}', 'GET')
162
+ return ReportLog.model_validate_json(r.text)
163
+
164
+ def create_report_revision(self, revision_date: datetime,
165
+ description: str,
166
+ project: Optional[str],
167
+ source_code: BinaryIO) -> ReportSourceCodeRevision:
168
+ """
169
+ Create a new report source code revision.
170
+
171
+ A report should be a python file that defines a function called `generate_report`
172
+ that takes two arguments: `request` and `context`, and returns an object with
173
+ `filename`, `mime_type`, and `data` attributes. It can also be a package with __init__.py
174
+ defining the `generate_report` function.
175
+
176
+ You can use the `mercuto_client.modules.reports.ReportHandler` protocol
177
+ to type hint your report function. Example:
178
+ ```python
179
+ from mercuto_client.modules.reports import ReportHandler, HandlerRequest, HandlerContext, ReportHandlerResult
180
+ def generate_report(request: HandlerRequest, context: HandlerContext) -> ReportHandlerResult:
181
+ # Your report generation logic here
182
+ return ReportHandlerResult(
183
+ filename="report.pdf",
184
+ mime_type="application/pdf",
185
+ data=b"PDF binary data here"
186
+ )
187
+ ```
188
+ The request parameter contains information about the report generation request,
189
+ and the context parameter provides access to the Mercuto client and metadata about
190
+ the report being generated. The MercutoClient provided in the context can be used
191
+ to fetch any additional data required for the report. It will be authenticated
192
+ using a service token with VIEW_PROJECT permission and VIEW_TENANT permission.
193
+
194
+ Params:
195
+ project (str): The project code.
196
+ revision_date (datetime): The date of the revision.
197
+ description (str): A description of the revision.
198
+ source_code (io.BinaryIO): The report source code file, either a .py file or a .zip package.
199
+
200
+ """
201
+ # Create the revision metadata
202
+ json: PayloadType = {
203
+ 'revision_date': revision_date.isoformat(),
204
+ 'description': description,
205
+ }
206
+ if project is not None:
207
+ json['project'] = project
208
+ r = self._client.request(f'{self._path}/revisions', 'PUT', json=json)
209
+ revision = ReportSourceCodeRevision.model_validate_json(r.text)
210
+
211
+ # Upload the source code
212
+ r = self._client.request(
213
+ f'{self._path}/revisions/{revision.code}', 'PATCH')
214
+ upload_url = r.json()['target_source_code_url']
215
+ upload_url = self._client.session().put(upload_url,
216
+ data=source_code, verify=self._client.verify_ssl)
217
+ if not upload_url.ok:
218
+ raise MercutoHTTPException(
219
+ f"Failed to upload report source code: {upload_url.status_code} {upload_url.text}",
220
+ upload_url.status_code
221
+ )
222
+ return revision
mercuto_client/util.py CHANGED
@@ -53,7 +53,7 @@ def get_free_space_excluding_files(directory: str) -> int:
53
53
  :return: Free bytes available in the partition after subtracting file sizes.
54
54
  """
55
55
  # Get partition's free space
56
- total, used, free = shutil.disk_usage(directory)
56
+ _, _, free = shutil.disk_usage(directory)
57
57
 
58
58
  # Calculate the total size of files in the directory
59
59
  files_size = get_directory_size(directory)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mercuto-client
3
- Version: 0.3.0
3
+ Version: 0.3.4a3
4
4
  Summary: Library for interfacing with Rockfield's Mercuto API
5
5
  Author-email: Daniel Whipp <daniel.whipp@rocktech.com.au>
6
6
  License-Expression: AGPL-3.0-only