mercuto-client 0.1.0__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.
Potentially problematic release.
This version of mercuto-client might be problematic. Click here for more details.
- mercuto_client/__init__.py +30 -0
- mercuto_client/_tests/__init__.py +0 -0
- mercuto_client/_tests/conftest.py +0 -0
- mercuto_client/_tests/test_ingester/__init__.py +0 -0
- mercuto_client/_tests/test_ingester/test_file_processor.py +210 -0
- mercuto_client/_tests/test_ingester/test_ftp.py +37 -0
- mercuto_client/_tests/test_ingester/test_parsers.py +145 -0
- mercuto_client/_tests/test_mocking.py +93 -0
- mercuto_client/_util.py +13 -0
- mercuto_client/acl.py +101 -0
- mercuto_client/client.py +903 -0
- mercuto_client/exceptions.py +15 -0
- mercuto_client/ingester/__init__.py +0 -0
- mercuto_client/ingester/__main__.py +287 -0
- mercuto_client/ingester/ftp.py +115 -0
- mercuto_client/ingester/parsers/__init__.py +42 -0
- mercuto_client/ingester/parsers/campbell.py +12 -0
- mercuto_client/ingester/parsers/generic_csv.py +114 -0
- mercuto_client/ingester/parsers/worldsensing.py +23 -0
- mercuto_client/ingester/processor.py +291 -0
- mercuto_client/ingester/util.py +64 -0
- mercuto_client/mocks.py +203 -0
- mercuto_client/py.typed +0 -0
- mercuto_client/types.py +409 -0
- mercuto_client-0.1.0.dist-info/METADATA +16 -0
- mercuto_client-0.1.0.dist-info/RECORD +29 -0
- mercuto_client-0.1.0.dist-info/WHEEL +5 -0
- mercuto_client-0.1.0.dist-info/licenses/LICENSE +619 -0
- mercuto_client-0.1.0.dist-info/top_level.txt +1 -0
mercuto_client/client.py
ADDED
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from contextlib import nullcontext
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import (Any, BinaryIO, Iterable, Iterator, List, Literal, Mapping,
|
|
11
|
+
Optional, Sequence, TextIO, Type, TypeVar)
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
import requests.cookies
|
|
15
|
+
|
|
16
|
+
from ._util import timedelta_isoformat
|
|
17
|
+
from .exceptions import MercutoClientException, MercutoHTTPException
|
|
18
|
+
from .types import (CHANNEL_CLASSIFICATION, AlertConfiguration,
|
|
19
|
+
AuthHealthcheckResult, Camera, Channel, Condition,
|
|
20
|
+
ContactGroup, Dashboards, Datalogger, DataRequest,
|
|
21
|
+
DataSample, Device, DeviceType, Event, FatigueConnection,
|
|
22
|
+
Image, ListAlertsResponseType, NewUserApiKey, Object,
|
|
23
|
+
PermissionGroup, Project, RainflowConfiguration,
|
|
24
|
+
ScheduledReport, ScheduledReportLog,
|
|
25
|
+
SystemHealthcheckResult, Tenant, Units, User,
|
|
26
|
+
UserContactMethod, UserDetails, VerifyMeResult, Video)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IAuthenticationMethod:
|
|
32
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def unique_key(self) -> str:
|
|
36
|
+
raise NotImplementedError(
|
|
37
|
+
f"unique_key not implemented for type {self.__class__.__name__}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ApiKeyAuthentication(IAuthenticationMethod):
|
|
41
|
+
def __init__(self, api_key: str) -> None:
|
|
42
|
+
self.api_key = api_key
|
|
43
|
+
|
|
44
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
45
|
+
header['X-Api-Key'] = self.api_key
|
|
46
|
+
|
|
47
|
+
def unique_key(self) -> str:
|
|
48
|
+
return f'api-key:{self.api_key}'
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ServiceTokenAuthentication(IAuthenticationMethod):
|
|
52
|
+
def __init__(self, service_token: str) -> None:
|
|
53
|
+
self.service_token = service_token
|
|
54
|
+
|
|
55
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
56
|
+
header['X-Service-Token'] = self.service_token
|
|
57
|
+
|
|
58
|
+
def unique_key(self) -> str:
|
|
59
|
+
return f'service-token:{self.service_token}'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AuthorizationHeaderAuthentication(IAuthenticationMethod):
|
|
63
|
+
def __init__(self, authorization_header: str) -> None:
|
|
64
|
+
self.authorization_header = authorization_header
|
|
65
|
+
|
|
66
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
67
|
+
header['Authorization'] = self.authorization_header
|
|
68
|
+
|
|
69
|
+
def unique_key(self) -> str:
|
|
70
|
+
return f'auth-bearer:{self.authorization_header}'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NullAuthenticationMethod(IAuthenticationMethod):
|
|
74
|
+
def update_header(self, header: dict[str, str]) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def unique_key(self) -> str:
|
|
78
|
+
return 'null-authentication'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_authentication_method(api_key: Optional[str] = None,
|
|
82
|
+
service_token: Optional[str] = None,
|
|
83
|
+
headers: Optional[Mapping[str, str]] = None) -> IAuthenticationMethod:
|
|
84
|
+
if api_key is not None and service_token is not None and headers is not None:
|
|
85
|
+
raise MercutoClientException(
|
|
86
|
+
"Only one of api_key or service_token can be provided")
|
|
87
|
+
authorization_header = None
|
|
88
|
+
if headers is not None:
|
|
89
|
+
api_key = headers.get('X-Api-Key', None)
|
|
90
|
+
service_token = headers.get('X-Service-Token', None)
|
|
91
|
+
authorization_header = headers.get('Authorization', None)
|
|
92
|
+
if api_key is not None:
|
|
93
|
+
return ApiKeyAuthentication(api_key)
|
|
94
|
+
elif service_token is not None:
|
|
95
|
+
return ServiceTokenAuthentication(service_token)
|
|
96
|
+
elif authorization_header is not None:
|
|
97
|
+
return AuthorizationHeaderAuthentication(authorization_header)
|
|
98
|
+
else:
|
|
99
|
+
return NullAuthenticationMethod()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Module:
|
|
103
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
104
|
+
self._client = client
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def client(self) -> 'MercutoClient':
|
|
108
|
+
return self._client
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
T = TypeVar('T', bound='Module')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MercutoClient:
|
|
115
|
+
def __init__(self, url: Optional[str] = None, verify_ssl: bool = True, active_session: Optional[requests.Session] = None) -> None:
|
|
116
|
+
if url is None:
|
|
117
|
+
url = os.environ.get(
|
|
118
|
+
'MERCUTO_API_URL', 'https://api.rockfieldcloud.com.au')
|
|
119
|
+
assert isinstance(url, str)
|
|
120
|
+
|
|
121
|
+
if url.endswith('/'):
|
|
122
|
+
url = url[:-1]
|
|
123
|
+
|
|
124
|
+
if not url.startswith('https://'):
|
|
125
|
+
raise ValueError(f'Url must be https, is {url}')
|
|
126
|
+
|
|
127
|
+
self._url = url
|
|
128
|
+
self._verify_ssl = verify_ssl
|
|
129
|
+
|
|
130
|
+
if active_session is None:
|
|
131
|
+
self._current_session = requests.Session()
|
|
132
|
+
else:
|
|
133
|
+
self._current_session = active_session
|
|
134
|
+
|
|
135
|
+
self._auth_method: Optional[IAuthenticationMethod] = None
|
|
136
|
+
self._cookies = requests.cookies.RequestsCookieJar()
|
|
137
|
+
|
|
138
|
+
self._modules: dict[str, Module] = {}
|
|
139
|
+
|
|
140
|
+
def url(self) -> str:
|
|
141
|
+
return self._url
|
|
142
|
+
|
|
143
|
+
def credentials_key(self) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Generate a unique key that identifies the current credentials set.
|
|
146
|
+
"""
|
|
147
|
+
if self._auth_method is None:
|
|
148
|
+
raise MercutoClientException("No credentials set")
|
|
149
|
+
return self._auth_method.unique_key()
|
|
150
|
+
|
|
151
|
+
def set_verify_ssl(self, verify_ssl: bool) -> None:
|
|
152
|
+
self._verify_ssl = verify_ssl
|
|
153
|
+
|
|
154
|
+
def copy(self) -> 'MercutoClient':
|
|
155
|
+
return MercutoClient(self._url, self._verify_ssl, self._current_session)
|
|
156
|
+
|
|
157
|
+
@contextlib.contextmanager
|
|
158
|
+
def as_credentials(self, api_key: Optional[str] = None,
|
|
159
|
+
service_token: Optional[str] = None,
|
|
160
|
+
headers: Optional[Mapping[str, str]] = None) -> Iterator['MercutoClient']:
|
|
161
|
+
"""
|
|
162
|
+
Same as .connect(), but as a context manager. Will automatically logout when exiting the context.
|
|
163
|
+
"""
|
|
164
|
+
# TODO: We are passing the current session along to re-use connections for speed. Will this cause security issues?
|
|
165
|
+
other = MercutoClient(self._url, self._verify_ssl,
|
|
166
|
+
self._current_session)
|
|
167
|
+
try:
|
|
168
|
+
yield other.connect(api_key=api_key, service_token=service_token, headers=headers)
|
|
169
|
+
finally:
|
|
170
|
+
other.logout()
|
|
171
|
+
|
|
172
|
+
def connect(self, *, api_key: Optional[str] = None,
|
|
173
|
+
service_token: Optional[str] = None,
|
|
174
|
+
headers: Optional[Mapping[str, str]] = None) -> 'MercutoClient':
|
|
175
|
+
"""
|
|
176
|
+
Attempt to connect using any available method.
|
|
177
|
+
if api_key is provided, use the api_key.
|
|
178
|
+
if service_token is provided, use the service_token.
|
|
179
|
+
if headers is provided, attempt to extract either api_key or service_token from given header set.
|
|
180
|
+
headers should be a dictionary of headers that would be sent in a request. Useful for using existing authenation mechanism for forwarding.
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
authentication = create_authentication_method(
|
|
184
|
+
api_key=api_key, service_token=service_token, headers=headers)
|
|
185
|
+
self.login(authentication)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def _update_headers(self, headers: dict[str, str]) -> dict[str, str]:
|
|
189
|
+
base: dict[str, str] = {}
|
|
190
|
+
|
|
191
|
+
if self._auth_method is not None:
|
|
192
|
+
self._auth_method.update_header(base)
|
|
193
|
+
base.update(headers)
|
|
194
|
+
return base
|
|
195
|
+
|
|
196
|
+
def build_url(self, path: str, **params: Any) -> str:
|
|
197
|
+
if path.startswith('/'):
|
|
198
|
+
path = path[1:]
|
|
199
|
+
if path.endswith('/'):
|
|
200
|
+
path = path[:-1]
|
|
201
|
+
return f"{self._url}/{path}/?{urllib.parse.urlencode(params)}"
|
|
202
|
+
|
|
203
|
+
def _request_json(self, method: str, url: str, *args: Any, **kwargs: Any) -> Any:
|
|
204
|
+
if 'timeout' not in kwargs:
|
|
205
|
+
kwargs['timeout'] = 10
|
|
206
|
+
kwargs['headers'] = self._update_headers(kwargs.get('headers', {}))
|
|
207
|
+
|
|
208
|
+
if 'verify' not in kwargs:
|
|
209
|
+
kwargs['verify'] = self._verify_ssl
|
|
210
|
+
|
|
211
|
+
if 'cookies' not in kwargs:
|
|
212
|
+
kwargs['cookies'] = self._cookies
|
|
213
|
+
return self._make_request(method, url, *args, **kwargs)
|
|
214
|
+
|
|
215
|
+
def _make_request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Any:
|
|
216
|
+
if url.startswith('/'):
|
|
217
|
+
url = url[1:]
|
|
218
|
+
full_url = f"{self._url}/{url}"
|
|
219
|
+
start = time.time()
|
|
220
|
+
resp = self._current_session.request(method, full_url, *args, **kwargs)
|
|
221
|
+
duration = time.time() - start
|
|
222
|
+
logger.debug("Made request to %s %s in %.2f seconds (code=%s)",
|
|
223
|
+
method, full_url, duration, resp.status_code)
|
|
224
|
+
if not resp.ok:
|
|
225
|
+
raise MercutoHTTPException(resp.text, resp.status_code)
|
|
226
|
+
if resp.headers.get('Content-Type', '') != 'application/json':
|
|
227
|
+
raise MercutoClientException(f"Response is not JSON: {resp.text}")
|
|
228
|
+
resp.cookies.update(self._cookies)
|
|
229
|
+
return resp.json()
|
|
230
|
+
|
|
231
|
+
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Any:
|
|
232
|
+
return self._request_json(method, url, *args, **kwargs)
|
|
233
|
+
|
|
234
|
+
def add_and_fetch_module(self, name: str, module: Type[T]) -> T:
|
|
235
|
+
if name not in self._modules:
|
|
236
|
+
self._modules[name] = module(self)
|
|
237
|
+
return self._modules[name] # type: ignore
|
|
238
|
+
|
|
239
|
+
def identity(self) -> 'MercutoIdentityService':
|
|
240
|
+
return self.add_and_fetch_module('identity', MercutoIdentityService)
|
|
241
|
+
|
|
242
|
+
def projects(self) -> 'MercutoProjectService':
|
|
243
|
+
return self.add_and_fetch_module('projects', MercutoProjectService)
|
|
244
|
+
|
|
245
|
+
def fatigue(self) -> 'MercutoFatigueService':
|
|
246
|
+
return self.add_and_fetch_module('fatigue', MercutoFatigueService)
|
|
247
|
+
|
|
248
|
+
def channels(self) -> 'MercutoChannelService':
|
|
249
|
+
return self.add_and_fetch_module('channels', MercutoChannelService)
|
|
250
|
+
|
|
251
|
+
def data(self) -> 'MercutoDataService':
|
|
252
|
+
return self.add_and_fetch_module('data', MercutoDataService)
|
|
253
|
+
|
|
254
|
+
def events(self) -> 'MercutoEventService':
|
|
255
|
+
return self.add_and_fetch_module('events', MercutoEventService)
|
|
256
|
+
|
|
257
|
+
def alerts(self) -> 'MercutoAlertService':
|
|
258
|
+
return self.add_and_fetch_module('alerts', MercutoAlertService)
|
|
259
|
+
|
|
260
|
+
def devices(self) -> 'MercutoDeviceService':
|
|
261
|
+
return self.add_and_fetch_module('devices', MercutoDeviceService)
|
|
262
|
+
|
|
263
|
+
def notifications(self) -> 'MercutoNotificationsService':
|
|
264
|
+
return self.add_and_fetch_module('notifications', MercutoNotificationsService)
|
|
265
|
+
|
|
266
|
+
def reports(self) -> 'MercutoReportingService':
|
|
267
|
+
return self.add_and_fetch_module('reports', MercutoReportingService)
|
|
268
|
+
|
|
269
|
+
def objects(self) -> 'MercutoObjectService':
|
|
270
|
+
return self.add_and_fetch_module('objects', MercutoObjectService)
|
|
271
|
+
|
|
272
|
+
def media(self) -> 'MercutoMediaService':
|
|
273
|
+
return self.add_and_fetch_module('media', MercutoMediaService)
|
|
274
|
+
|
|
275
|
+
def login(self, authentication: IAuthenticationMethod) -> None:
|
|
276
|
+
self._auth_method = authentication
|
|
277
|
+
|
|
278
|
+
def logout(self) -> None:
|
|
279
|
+
self._auth_method = None
|
|
280
|
+
|
|
281
|
+
def healthcheck(self) -> SystemHealthcheckResult:
|
|
282
|
+
return self._request_json('GET', '/healthcheck') # type: ignore[no-any-return]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class MercutoDataService(Module):
|
|
286
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
287
|
+
super().__init__(client)
|
|
288
|
+
|
|
289
|
+
def upload_samples(self, samples: Sequence[DataSample]) -> None:
|
|
290
|
+
self._client._request_json(
|
|
291
|
+
'POST', '/data/upload/samples', json=samples)
|
|
292
|
+
|
|
293
|
+
def upload_file(self, project: str, datatable: str, file: str | bytes | TextIO | BinaryIO, filename: Optional[str] = None) -> None:
|
|
294
|
+
if isinstance(file, str):
|
|
295
|
+
ctx = open(file, 'rb')
|
|
296
|
+
filename = filename or os.path.basename(file)
|
|
297
|
+
else:
|
|
298
|
+
ctx = nullcontext(file) # type: ignore
|
|
299
|
+
filename = filename or 'file.dat'
|
|
300
|
+
|
|
301
|
+
with ctx as f:
|
|
302
|
+
self._client._request_json('POST', '/files/upload/small', params={'project_code': project, 'datatable_code': datatable},
|
|
303
|
+
files={'file': (filename, f, 'text/csv')})
|
|
304
|
+
|
|
305
|
+
def get_data_url(
|
|
306
|
+
self,
|
|
307
|
+
project_code: str,
|
|
308
|
+
start_time: datetime | str | None = None,
|
|
309
|
+
end_time: datetime | str | None = None,
|
|
310
|
+
event_code: str | None = None,
|
|
311
|
+
channel_codes: Iterable[str] | None = None,
|
|
312
|
+
primary_channels: bool | None = None,
|
|
313
|
+
channels_like: str | None = None,
|
|
314
|
+
file_format: Literal['DAT', 'CSV', 'PARQUET', 'FEATHER'] = 'DAT',
|
|
315
|
+
frame_format: Literal['RECORDS', 'COLUMNS'] = 'COLUMNS',
|
|
316
|
+
channel_format: Literal['CODE', 'LABEL'] = 'LABEL',
|
|
317
|
+
timeout: timedelta | None = timedelta(seconds=20),
|
|
318
|
+
) -> str:
|
|
319
|
+
request = self.get_data_request(
|
|
320
|
+
project_code=project_code,
|
|
321
|
+
start_time=start_time,
|
|
322
|
+
end_time=end_time,
|
|
323
|
+
event_code=event_code,
|
|
324
|
+
channel_codes=channel_codes,
|
|
325
|
+
primary_channels=primary_channels,
|
|
326
|
+
channels_like=channels_like,
|
|
327
|
+
file_format=file_format,
|
|
328
|
+
frame_format=frame_format,
|
|
329
|
+
channel_format=channel_format,
|
|
330
|
+
timeout=timeout,
|
|
331
|
+
)
|
|
332
|
+
if request['presigned_url'] is None:
|
|
333
|
+
raise MercutoClientException("No presigned URL available")
|
|
334
|
+
|
|
335
|
+
return request['presigned_url']
|
|
336
|
+
|
|
337
|
+
def get_data_request(
|
|
338
|
+
self,
|
|
339
|
+
project_code: str,
|
|
340
|
+
start_time: datetime | str | None = None,
|
|
341
|
+
end_time: datetime | str | None = None,
|
|
342
|
+
event_code: str | None = None,
|
|
343
|
+
channel_codes: Iterable[str] | None = None,
|
|
344
|
+
primary_channels: bool | None = None,
|
|
345
|
+
channels_like: str | None = None,
|
|
346
|
+
file_format: Literal['DAT', 'CSV', 'PARQUET', 'FEATHER'] = 'DAT',
|
|
347
|
+
frame_format: Literal['RECORDS', 'COLUMNS'] = 'COLUMNS',
|
|
348
|
+
channel_format: Literal['CODE', 'LABEL'] = 'LABEL',
|
|
349
|
+
timeout: timedelta | None = timedelta(seconds=20),
|
|
350
|
+
) -> DataRequest:
|
|
351
|
+
params = {
|
|
352
|
+
'timeout': 0
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
body = {
|
|
356
|
+
'project_code': project_code,
|
|
357
|
+
'start_time': start_time.isoformat() if isinstance(start_time, datetime) else start_time,
|
|
358
|
+
'end_time': end_time.isoformat() if isinstance(end_time, datetime) else end_time,
|
|
359
|
+
'event_code': event_code,
|
|
360
|
+
'channel_codes': channel_codes,
|
|
361
|
+
'primary_channels': primary_channels,
|
|
362
|
+
'channels_like': channels_like,
|
|
363
|
+
'data_format': file_format,
|
|
364
|
+
'frame_format': frame_format,
|
|
365
|
+
'channel_format': channel_format,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
request: DataRequest = self._client._request_json(
|
|
369
|
+
'POST',
|
|
370
|
+
'/data/requests',
|
|
371
|
+
params=params,
|
|
372
|
+
json=body)
|
|
373
|
+
|
|
374
|
+
start = time.time()
|
|
375
|
+
while True:
|
|
376
|
+
if timeout is not None and time.time() - start > timeout.total_seconds():
|
|
377
|
+
raise MercutoClientException(
|
|
378
|
+
"Timeout waiting for data request")
|
|
379
|
+
if request['completed_at'] is not None:
|
|
380
|
+
return request
|
|
381
|
+
|
|
382
|
+
time.sleep(1)
|
|
383
|
+
request = self._client._request_json(
|
|
384
|
+
'GET', f'/data/requests/{request["code"]}')
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class MercutoEventService(Module):
|
|
388
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
389
|
+
super().__init__(client)
|
|
390
|
+
|
|
391
|
+
def list_events(self, project: str) -> list[Event]:
|
|
392
|
+
params: dict[str, str] = {'project_code': project}
|
|
393
|
+
return self._client._request_json('GET', '/events', params=params)
|
|
394
|
+
|
|
395
|
+
def get_nearest_event(
|
|
396
|
+
self,
|
|
397
|
+
project_code: str,
|
|
398
|
+
to: datetime | str,
|
|
399
|
+
maximum_delta: timedelta | None = None,
|
|
400
|
+
) -> Event:
|
|
401
|
+
params = {
|
|
402
|
+
'project_code': project_code,
|
|
403
|
+
'to': to.isoformat() if isinstance(to, datetime) else to,
|
|
404
|
+
'maximum_delta': timedelta_isoformat(maximum_delta) if maximum_delta is not None else None,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return self.client._request_json('GET', '/events/nearest', params=params)
|
|
408
|
+
|
|
409
|
+
def get_nearest_event_url(
|
|
410
|
+
self,
|
|
411
|
+
project_code: str,
|
|
412
|
+
to: datetime | str,
|
|
413
|
+
maximum_delta: timedelta | None = None,
|
|
414
|
+
file_format: Literal['DAT', 'CSV', 'PARQUET', 'FEATHER'] = 'DAT',
|
|
415
|
+
frame_format: Literal['COLUMNS', 'RECORDS'] = 'COLUMNS',
|
|
416
|
+
) -> str:
|
|
417
|
+
event = self.get_nearest_event(project_code, to, maximum_delta)
|
|
418
|
+
|
|
419
|
+
return self.client.data().get_data_url(
|
|
420
|
+
project_code=project_code,
|
|
421
|
+
event_code=event['code'],
|
|
422
|
+
primary_channels=True,
|
|
423
|
+
file_format=file_format,
|
|
424
|
+
frame_format=frame_format)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class MercutoAlertService(Module):
|
|
428
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
429
|
+
super().__init__(client)
|
|
430
|
+
|
|
431
|
+
def get_condition(self, code: str) -> Condition:
|
|
432
|
+
return self._client._request_json('GET', f'/alerts/conditions/{code}')
|
|
433
|
+
|
|
434
|
+
def create_condition(self, source: str, description: str, *,
|
|
435
|
+
lower_bound: Optional[float] = None,
|
|
436
|
+
upper_bound: Optional[float] = None,
|
|
437
|
+
neutral_position: float = 0) -> Condition:
|
|
438
|
+
json = {
|
|
439
|
+
'source_channel_code': source,
|
|
440
|
+
'description': description,
|
|
441
|
+
'neutral_position': neutral_position
|
|
442
|
+
}
|
|
443
|
+
if lower_bound is not None:
|
|
444
|
+
json['lower_inclusive_bound'] = lower_bound
|
|
445
|
+
if upper_bound is not None:
|
|
446
|
+
json['upper_exclusive_bound'] = upper_bound
|
|
447
|
+
return self._client._request_json('PUT', '/alerts/conditions', json=json)
|
|
448
|
+
|
|
449
|
+
def create_configuration(self, label: str, conditions: list[str], contact_group: Optional[str] = None) -> AlertConfiguration:
|
|
450
|
+
json = {
|
|
451
|
+
'label': label,
|
|
452
|
+
'conditions': conditions,
|
|
453
|
+
|
|
454
|
+
}
|
|
455
|
+
if contact_group is not None:
|
|
456
|
+
json['contact_group'] = contact_group
|
|
457
|
+
return self._client._request_json('PUT', '/alerts/configurations', json=json)
|
|
458
|
+
|
|
459
|
+
def get_alert_configuration(self, code: str) -> AlertConfiguration:
|
|
460
|
+
return self._client._request_json('GET', f'/alerts/configurations/{code}')
|
|
461
|
+
|
|
462
|
+
def list_logs(
|
|
463
|
+
self,
|
|
464
|
+
project: str | None = None,
|
|
465
|
+
configuration: str | None = None,
|
|
466
|
+
channels: list[str] | None = None,
|
|
467
|
+
start_time: datetime | str | None = None,
|
|
468
|
+
end_time: datetime | str | None = None,
|
|
469
|
+
limit: int = 10,
|
|
470
|
+
offset: int = 0,
|
|
471
|
+
latest_only: bool = False,
|
|
472
|
+
) -> ListAlertsResponseType:
|
|
473
|
+
params = {
|
|
474
|
+
'project': project,
|
|
475
|
+
'configuration_code': configuration,
|
|
476
|
+
'channels': channels,
|
|
477
|
+
'start_time': start_time.isoformat() if isinstance(start_time, datetime) else start_time,
|
|
478
|
+
'end_time': end_time.isoformat() if isinstance(end_time, datetime) else end_time,
|
|
479
|
+
'limit': limit,
|
|
480
|
+
'offset': offset,
|
|
481
|
+
'latest_only': latest_only,
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return self._client._request_json('GET', '/alerts/logs', params=params)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class MercutoProjectService(Module):
|
|
488
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
489
|
+
super().__init__(client)
|
|
490
|
+
|
|
491
|
+
def get_project(self, code: str) -> Project:
|
|
492
|
+
return self._client._request_json('GET', f'/projects/{code}')
|
|
493
|
+
|
|
494
|
+
def get_projects(self) -> list[Project]:
|
|
495
|
+
return self._client._request_json('GET', '/projects')
|
|
496
|
+
|
|
497
|
+
def create_project(self, name: str, project_number: str, description: str, tenant: str,
|
|
498
|
+
timezone: str, latitude: Optional[float] = None,
|
|
499
|
+
longitude: Optional[float] = None) -> Project:
|
|
500
|
+
|
|
501
|
+
return self._client._request_json('PUT', '/projects',
|
|
502
|
+
json={'name': name, 'project_number': project_number, 'description': description,
|
|
503
|
+
'tenant_code': tenant,
|
|
504
|
+
'timezone': timezone,
|
|
505
|
+
'latitude': latitude,
|
|
506
|
+
'longitude': longitude,
|
|
507
|
+
'project_type': 1,
|
|
508
|
+
'channels': []})
|
|
509
|
+
|
|
510
|
+
def ping_project(self, project: str, ip_address: str) -> None:
|
|
511
|
+
self._client._request_json(
|
|
512
|
+
'POST', f'/projects/{project}/ping', json={'ip_address': ip_address})
|
|
513
|
+
|
|
514
|
+
def create_channel(self, project: str, label: str, sampling_period: timedelta) -> Channel:
|
|
515
|
+
return self._client._request_json('PUT', '/channels', json={
|
|
516
|
+
'project_code': project,
|
|
517
|
+
'label': label,
|
|
518
|
+
'classification': 'SECONDARY',
|
|
519
|
+
'sampling_period': timedelta_isoformat(sampling_period),
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
def create_dashboard(self, project_code: str, dashboards: Dashboards) -> bool:
|
|
523
|
+
return self._client._request_json('POST', f'/projects/{project_code}/dashboard', json=dashboards) is None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class MercutoFatigueService(Module):
|
|
527
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
528
|
+
super().__init__(client)
|
|
529
|
+
|
|
530
|
+
def setup_rainflow(self, project: str,
|
|
531
|
+
max_bins: int,
|
|
532
|
+
bin_size: float,
|
|
533
|
+
multiplier: float,
|
|
534
|
+
channels: list[str],
|
|
535
|
+
reservoir_adjustment: bool = True,
|
|
536
|
+
enable_root_mean_cubed: bool = True,
|
|
537
|
+
enable_root_sum_cubed: bool = True) -> RainflowConfiguration:
|
|
538
|
+
return self.client._request_json('PUT', '/fatigue/rainflow/setup', json=dict(
|
|
539
|
+
project=project,
|
|
540
|
+
max_bins=max_bins,
|
|
541
|
+
bin_size=bin_size,
|
|
542
|
+
multiplier=multiplier,
|
|
543
|
+
reservoir_adjustment=reservoir_adjustment,
|
|
544
|
+
channels=channels,
|
|
545
|
+
enable_root_mean_cubed=enable_root_mean_cubed,
|
|
546
|
+
enable_root_sum_cubed=enable_root_sum_cubed
|
|
547
|
+
))
|
|
548
|
+
|
|
549
|
+
def add_connection(self, project: str, label: str,
|
|
550
|
+
multiplier: float, c_d: float, m: float, s_0: float,
|
|
551
|
+
bs7608_failure_probability: float, bs7608_detail_category: str,
|
|
552
|
+
initial_date: datetime, initial_damage: float,
|
|
553
|
+
sources: list[str]) -> FatigueConnection:
|
|
554
|
+
"""
|
|
555
|
+
Sources should be a list of Primary Channel codes.
|
|
556
|
+
"""
|
|
557
|
+
return self.client._request_json('PUT', '/fatigue/connections', json=dict(
|
|
558
|
+
project=project,
|
|
559
|
+
label=label,
|
|
560
|
+
multiplier=multiplier,
|
|
561
|
+
c_d=c_d,
|
|
562
|
+
m=m,
|
|
563
|
+
s_0=s_0,
|
|
564
|
+
bs7608_failure_probability=bs7608_failure_probability,
|
|
565
|
+
bs7608_detail_category=bs7608_detail_category,
|
|
566
|
+
initial_date=initial_date.isoformat(),
|
|
567
|
+
initial_damage=initial_damage,
|
|
568
|
+
sources=sources
|
|
569
|
+
))
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class MercutoChannelService(Module):
|
|
573
|
+
def __init__(self, client: MercutoClient) -> None:
|
|
574
|
+
super().__init__(client)
|
|
575
|
+
|
|
576
|
+
def get_units(self) -> List[Units]:
|
|
577
|
+
return self._client._request_json('GET', '/channels/units')
|
|
578
|
+
|
|
579
|
+
def create_units(self, name: str, unit: str) -> Units:
|
|
580
|
+
return self._client._request_json('PUT', '/channels/units', json=dict(name=name, unit=unit))
|
|
581
|
+
|
|
582
|
+
def create_channel(self, project_code: str, label: str, classification: CHANNEL_CLASSIFICATION, sampling_period: timedelta) -> Channel:
|
|
583
|
+
return self._client._request_json('PUT', '/channels', json=dict(
|
|
584
|
+
project_code=project_code,
|
|
585
|
+
label=label,
|
|
586
|
+
classification=classification,
|
|
587
|
+
sampling_period=timedelta_isoformat(sampling_period)))
|
|
588
|
+
|
|
589
|
+
def modify_channel(self,
|
|
590
|
+
channel: str,
|
|
591
|
+
label: Optional[str] = None,
|
|
592
|
+
units_code: Optional[str] = None,
|
|
593
|
+
device_code: Optional[str] = None,
|
|
594
|
+
metric: Optional[str] = None,
|
|
595
|
+
multiplier: float = 1,
|
|
596
|
+
offset: float = 0) -> Channel:
|
|
597
|
+
data: dict = {}
|
|
598
|
+
if label is not None:
|
|
599
|
+
data['label'] = label
|
|
600
|
+
if units_code is not None:
|
|
601
|
+
data['units_code'] = units_code
|
|
602
|
+
if metric is not None:
|
|
603
|
+
data['metric'] = metric
|
|
604
|
+
if device_code is not None:
|
|
605
|
+
data['device_code'] = device_code
|
|
606
|
+
data['multiplier'] = multiplier
|
|
607
|
+
data['offset'] = offset
|
|
608
|
+
return self._client._request_json('PATCH', f'/channels/{channel}', json=data)
|
|
609
|
+
|
|
610
|
+
def get_channels(
|
|
611
|
+
self,
|
|
612
|
+
project_code: str | None = None,
|
|
613
|
+
classification: CHANNEL_CLASSIFICATION | None = None,
|
|
614
|
+
aggregate: str | None = None,
|
|
615
|
+
metric: str | None = None,
|
|
616
|
+
) -> list[Channel]:
|
|
617
|
+
limit = 200
|
|
618
|
+
offset = 0
|
|
619
|
+
channels: list[Channel] = []
|
|
620
|
+
|
|
621
|
+
while True:
|
|
622
|
+
params = {
|
|
623
|
+
'project_code': project_code,
|
|
624
|
+
'classification': classification,
|
|
625
|
+
'aggregate': aggregate,
|
|
626
|
+
'metric': metric,
|
|
627
|
+
'limit': limit,
|
|
628
|
+
'offset': offset,
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
resp = self._client._request_json(
|
|
632
|
+
'GET', '/channels', params=params)
|
|
633
|
+
channels.extend(resp)
|
|
634
|
+
|
|
635
|
+
if len(resp) < limit:
|
|
636
|
+
break
|
|
637
|
+
|
|
638
|
+
offset += limit
|
|
639
|
+
|
|
640
|
+
return channels
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class MercutoDeviceService(Module):
|
|
644
|
+
def __init__(self, client: MercutoClient) -> None:
|
|
645
|
+
super().__init__(client)
|
|
646
|
+
|
|
647
|
+
def get_device_types(self) -> list[DeviceType]:
|
|
648
|
+
return self._client._request_json('GET', '/devices/types')
|
|
649
|
+
|
|
650
|
+
def create_device_type(self, description: str, manufacturer: str, model_number: str) -> DeviceType:
|
|
651
|
+
return self._client._request_json('PUT', '/devices/types', json=dict(
|
|
652
|
+
description=description,
|
|
653
|
+
manufacturer=manufacturer,
|
|
654
|
+
model_number=model_number))
|
|
655
|
+
|
|
656
|
+
def get_devices(self, project_code: str, limit: int, offset: int) -> list[Device]:
|
|
657
|
+
return self._client._request_json('GET', '/devices', params=dict(project_code=project_code, limit=limit, offset=offset))
|
|
658
|
+
|
|
659
|
+
def get_device(self, device_code: str) -> Device:
|
|
660
|
+
return self._client._request_json('GET', f'/devices/{device_code}')
|
|
661
|
+
|
|
662
|
+
def create_device(self,
|
|
663
|
+
project_code: str,
|
|
664
|
+
label: str,
|
|
665
|
+
device_type_code: str,
|
|
666
|
+
groups: list[str],
|
|
667
|
+
location_description: Optional[str] = None) -> Device:
|
|
668
|
+
return self._client._request_json('PUT', '/devices', json=dict(
|
|
669
|
+
project_code=project_code,
|
|
670
|
+
label=label,
|
|
671
|
+
device_type_code=device_type_code,
|
|
672
|
+
groups=groups,
|
|
673
|
+
location_description=location_description))
|
|
674
|
+
|
|
675
|
+
def list_dataloggers(self, project: str) -> list[Datalogger]:
|
|
676
|
+
return self._client._request_json('GET', '/dataloggers', params={'project_code': project})
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class MercutoIdentityService(Module):
|
|
680
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
681
|
+
super().__init__(client)
|
|
682
|
+
|
|
683
|
+
def create_tenant(self, name: str, description: str, logo_url: Optional[str] = None) -> Tenant:
|
|
684
|
+
return self._client._request_json('PUT', '/identity/tenants',
|
|
685
|
+
json={'name': name, 'description': description, 'logo_url': logo_url})
|
|
686
|
+
|
|
687
|
+
def list_tenants(self) -> list[Tenant]:
|
|
688
|
+
return self._client._request_json('GET', '/identity/tenants')
|
|
689
|
+
|
|
690
|
+
def get_tenant(self, code: str) -> Tenant:
|
|
691
|
+
return self._client._request_json('GET', f'/identity/tenants/{code}')
|
|
692
|
+
|
|
693
|
+
def create_user(self, username: str, tenant: str, description: str,
|
|
694
|
+
group: str, password: Optional[str] = None) -> User:
|
|
695
|
+
return self._client._request_json('PUT', '/identity/users',
|
|
696
|
+
json={'username': username, 'tenant_code': tenant, 'description': description,
|
|
697
|
+
'group_code': group, 'default_password': password})
|
|
698
|
+
|
|
699
|
+
def list_users(self, project: Optional[str] = None,
|
|
700
|
+
tenant: Optional[str] = None) -> list[User]:
|
|
701
|
+
params = {}
|
|
702
|
+
if project is not None:
|
|
703
|
+
params['project'] = project
|
|
704
|
+
if tenant is not None:
|
|
705
|
+
params['tenant'] = tenant
|
|
706
|
+
return self._client._request_json('GET', '/identity/users', params=params)
|
|
707
|
+
|
|
708
|
+
def get_user(self, code: str) -> User:
|
|
709
|
+
return self._client._request_json('GET', f'/identity/users/{code}')
|
|
710
|
+
|
|
711
|
+
def get_user_details(self, code: str) -> UserDetails:
|
|
712
|
+
return self._client._request_json('GET', f'/identity/users/{code}/details')
|
|
713
|
+
|
|
714
|
+
def edit_user_details(self, code: str, first_name: Optional[str], last_name: Optional[str],
|
|
715
|
+
email_address: Optional[str], mobile_number: Optional[str]) -> UserDetails:
|
|
716
|
+
return self._client._request_json('PATCH', f'/identity/users/{code}/details', json={
|
|
717
|
+
'first_name': first_name,
|
|
718
|
+
'last_name': last_name,
|
|
719
|
+
'email_address': email_address,
|
|
720
|
+
'mobile_number': mobile_number
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
def generate_api_key(self, user: str, description: str) -> NewUserApiKey:
|
|
724
|
+
return self._client._request_json('POST', f'/identity/users/{user}/api_keys',
|
|
725
|
+
json={'description': description})
|
|
726
|
+
|
|
727
|
+
def grant_user_permission(self, user: str, resource: str, action: str) -> None:
|
|
728
|
+
return self._client._request_json('POST', f'/identity/users/{user}/grant',
|
|
729
|
+
json={'resource': resource, 'action': action})
|
|
730
|
+
|
|
731
|
+
def create_permission_group(self, tenant: str, label: str,
|
|
732
|
+
acl_json: str) -> PermissionGroup:
|
|
733
|
+
return self._client._request_json('PUT', '/identity/permissions', json={
|
|
734
|
+
'tenant': tenant,
|
|
735
|
+
'label': label,
|
|
736
|
+
'acl_policy': acl_json
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
def list_permission_groups(self) -> list[PermissionGroup]:
|
|
740
|
+
return self._client._request_json('GET', '/identity/permissions')
|
|
741
|
+
|
|
742
|
+
def get_permission_group(self, code: str) -> PermissionGroup:
|
|
743
|
+
return self._client._request_json('GET', f'/identity/permissions/{code}')
|
|
744
|
+
|
|
745
|
+
def update_permission_group(self, code: str, label: str,
|
|
746
|
+
acl_json: str) -> None:
|
|
747
|
+
return self._client._request_json('PATCH', f'/identity/permissions/{code}', json={
|
|
748
|
+
'label': label,
|
|
749
|
+
'acl_policy': acl_json
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
def verify_me(self) -> VerifyMeResult:
|
|
753
|
+
return self._client._request_json('GET', '/identity/verify/me')
|
|
754
|
+
|
|
755
|
+
def healthcheck(self) -> AuthHealthcheckResult:
|
|
756
|
+
return self._client._request_json('GET', '/identity/healthcheck')
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class MercutoNotificationsService(Module):
|
|
760
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
761
|
+
super().__init__(client)
|
|
762
|
+
|
|
763
|
+
def list_contact_groups(self, project: Optional[str] = None) -> list[ContactGroup]:
|
|
764
|
+
params = {}
|
|
765
|
+
if project is not None:
|
|
766
|
+
params['project'] = project
|
|
767
|
+
return self._client._request_json('GET', '/notifications/contact_groups', params=params)
|
|
768
|
+
|
|
769
|
+
def get_contact_group(self, code: str) -> ContactGroup:
|
|
770
|
+
return self._client._request_json('GET', f'/notifications/contact_groups/{code}')
|
|
771
|
+
|
|
772
|
+
def create_contact_group(self, project: str, label: str, users: dict[str, list[UserContactMethod]]) -> ContactGroup:
|
|
773
|
+
return self._client._request_json('PUT', '/notifications/contact_groups',
|
|
774
|
+
json={
|
|
775
|
+
'project': project,
|
|
776
|
+
'label': label,
|
|
777
|
+
'users': users
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class MercutoReportingService(Module):
|
|
782
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
783
|
+
super().__init__(client)
|
|
784
|
+
|
|
785
|
+
def list_reports(self, project: Optional[str] = None) -> list['ScheduledReport']:
|
|
786
|
+
params = {}
|
|
787
|
+
if project is not None:
|
|
788
|
+
params['project'] = project
|
|
789
|
+
return self._client._request_json('GET', '/reports/scheduled', params=params)
|
|
790
|
+
|
|
791
|
+
def create_report(self, project: str, label: str, schedule: str, revision: str,
|
|
792
|
+
api_key: Optional[str] = None, contact_group: Optional[str] = None) -> ScheduledReport:
|
|
793
|
+
return self._client._request_json('PUT', '/reports/scheduled', json={
|
|
794
|
+
'project': project,
|
|
795
|
+
'label': label,
|
|
796
|
+
'schedule': schedule,
|
|
797
|
+
'revision': revision,
|
|
798
|
+
'execution_role_api_key': api_key,
|
|
799
|
+
'contact_group': contact_group
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
def generate_report(self, report: str, timestamp: datetime, mark_as_scheduled: bool = False) -> ScheduledReportLog:
|
|
803
|
+
return self._client._request_json('PUT', f'/reports/scheduled/{report}/generate', json={
|
|
804
|
+
'timestamp': timestamp.isoformat(),
|
|
805
|
+
'mark_as_scheduled': mark_as_scheduled
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
def list_report_logs(self, report: str, project: Optional[str] = None) -> list[ScheduledReportLog]:
|
|
809
|
+
params: dict[str, str] = {}
|
|
810
|
+
if project is not None:
|
|
811
|
+
params['project'] = project
|
|
812
|
+
return self._client._request_json('GET', f'/reports/scheduled/{report}/logs', params=params)
|
|
813
|
+
|
|
814
|
+
def get_report_log(self, report: str, log: str) -> ScheduledReportLog:
|
|
815
|
+
return self._client._request_json('GET', f'/reports/scheduled/{report}/logs/{log}')
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class MercutoObjectService(Module):
|
|
819
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
820
|
+
super().__init__(client)
|
|
821
|
+
|
|
822
|
+
def upload_file(self, project_code: str, file: str, event_code: Optional[str] = None,
|
|
823
|
+
mime_type: Optional[str] = None) -> Object:
|
|
824
|
+
with open(file, 'rb') as f:
|
|
825
|
+
if mime_type is None:
|
|
826
|
+
mime_type = mimetypes.guess_type(file, strict=False)[0]
|
|
827
|
+
if mime_type is None:
|
|
828
|
+
raise MercutoClientException(
|
|
829
|
+
f"Could not determine mime type for {file}")
|
|
830
|
+
base64_data = base64.b64encode(f.read()).decode('utf-8')
|
|
831
|
+
data_url = f'data:{mime_type};base64,{base64_data}'
|
|
832
|
+
|
|
833
|
+
return self._client._request_json('POST', '/objects/upload', params={
|
|
834
|
+
'project_code': project_code,
|
|
835
|
+
'event_code': event_code
|
|
836
|
+
}, json={
|
|
837
|
+
'filename': os.path.basename(file),
|
|
838
|
+
'mime_type': mime_type,
|
|
839
|
+
'data_url': data_url
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
class MercutoMediaService(Module):
|
|
844
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
845
|
+
super().__init__(client)
|
|
846
|
+
|
|
847
|
+
def list_cameras(self, project: str) -> list[Camera]:
|
|
848
|
+
params = {}
|
|
849
|
+
params['project_code'] = project
|
|
850
|
+
return self._client._request_json('GET', '/media/cameras', params=params)
|
|
851
|
+
|
|
852
|
+
def list_videos(self, project: Optional[str] = None, event: Optional[str] = None, camera: Optional[str] = None) -> list[Video]:
|
|
853
|
+
params = {}
|
|
854
|
+
if project is not None:
|
|
855
|
+
params['project'] = project
|
|
856
|
+
if event is not None:
|
|
857
|
+
params['event'] = event
|
|
858
|
+
if camera is not None:
|
|
859
|
+
params['camera'] = camera
|
|
860
|
+
return self._client._request_json('GET', '/media/videos', params=params)
|
|
861
|
+
|
|
862
|
+
def list_images(self, project: Optional[str] = None, event: Optional[str] = None, camera: Optional[str] = None) -> list[Image]:
|
|
863
|
+
params = {}
|
|
864
|
+
if project is not None:
|
|
865
|
+
params['project'] = project
|
|
866
|
+
if event is not None:
|
|
867
|
+
params['event'] = event
|
|
868
|
+
if camera is not None:
|
|
869
|
+
params['camera'] = camera
|
|
870
|
+
return self._client._request_json('GET', '/media/images', params=params)
|
|
871
|
+
|
|
872
|
+
def get_image(self, code: str) -> Image:
|
|
873
|
+
return self._client._request_json('GET', f'/media/images/{code}') # type: ignore[no-any-return]
|
|
874
|
+
|
|
875
|
+
def upload_image(self, project: str, file: str, event: Optional[str] = None,
|
|
876
|
+
camera: Optional[str] = None, timestamp: Optional[datetime] = None,
|
|
877
|
+
filename: Optional[str] = None) -> Image:
|
|
878
|
+
if timestamp is not None and timestamp.tzinfo is None:
|
|
879
|
+
raise MercutoClientException("Timestamp must be timezone aware")
|
|
880
|
+
|
|
881
|
+
mimetype, _ = mimetypes.guess_type(file, strict=False)
|
|
882
|
+
if mimetype is None or not mimetype.startswith('image/'):
|
|
883
|
+
raise MercutoClientException(f"File {file} is not an image")
|
|
884
|
+
|
|
885
|
+
if os.stat(file).st_size > 5_000_000:
|
|
886
|
+
raise MercutoClientException(f"File {file} is too large")
|
|
887
|
+
|
|
888
|
+
if filename is None:
|
|
889
|
+
filename = os.path.basename(file)
|
|
890
|
+
|
|
891
|
+
params = {}
|
|
892
|
+
params['project'] = project
|
|
893
|
+
if event is not None:
|
|
894
|
+
params['event'] = event
|
|
895
|
+
if camera is not None:
|
|
896
|
+
params['camera'] = camera
|
|
897
|
+
if timestamp is not None:
|
|
898
|
+
params['timestamp'] = timestamp.isoformat()
|
|
899
|
+
|
|
900
|
+
with open(file, 'rb') as f:
|
|
901
|
+
return self._client._request_json('PUT', '/media/images', params=params, files={
|
|
902
|
+
'file': (filename, f, mimetype)
|
|
903
|
+
})
|