mercuto-client 0.2.8__py3-none-any.whl → 0.3.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.
Files changed (37) hide show
  1. mercuto_client/__init__.py +2 -24
  2. mercuto_client/_authentication.py +72 -0
  3. mercuto_client/_tests/test_ingester/test_parsers.py +67 -67
  4. mercuto_client/_tests/test_mocking/__init__.py +0 -0
  5. mercuto_client/_tests/test_mocking/conftest.py +13 -0
  6. mercuto_client/_tests/test_mocking/test_mock_identity.py +8 -0
  7. mercuto_client/acl.py +16 -10
  8. mercuto_client/client.py +53 -779
  9. mercuto_client/exceptions.py +5 -1
  10. mercuto_client/ingester/__main__.py +1 -1
  11. mercuto_client/ingester/mercuto.py +15 -16
  12. mercuto_client/ingester/parsers/__init__.py +3 -3
  13. mercuto_client/ingester/parsers/campbell.py +2 -2
  14. mercuto_client/ingester/parsers/generic_csv.py +5 -5
  15. mercuto_client/ingester/parsers/worldsensing.py +4 -3
  16. mercuto_client/mocks/__init__.py +92 -0
  17. mercuto_client/mocks/_utility.py +69 -0
  18. mercuto_client/mocks/mock_data.py +402 -0
  19. mercuto_client/mocks/mock_fatigue.py +30 -0
  20. mercuto_client/mocks/mock_identity.py +188 -0
  21. mercuto_client/modules/__init__.py +19 -0
  22. mercuto_client/modules/_util.py +18 -0
  23. mercuto_client/modules/core.py +674 -0
  24. mercuto_client/modules/data.py +623 -0
  25. mercuto_client/modules/fatigue.py +189 -0
  26. mercuto_client/modules/identity.py +254 -0
  27. mercuto_client/{ingester/util.py → util.py} +27 -11
  28. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/METADATA +10 -3
  29. mercuto_client-0.3.0.dist-info/RECORD +41 -0
  30. mercuto_client/_tests/test_mocking.py +0 -93
  31. mercuto_client/_util.py +0 -13
  32. mercuto_client/mocks.py +0 -203
  33. mercuto_client/types.py +0 -409
  34. mercuto_client-0.2.8.dist-info/RECORD +0 -30
  35. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/WHEEL +0 -0
  36. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
  37. {mercuto_client-0.2.8.dist-info → mercuto_client-0.3.0.dist-info}/top_level.txt +0 -0
mercuto_client/client.py CHANGED
@@ -1,121 +1,36 @@
1
- import base64
2
1
  import contextlib
2
+ import json as json_stdlib
3
3
  import logging
4
- import mimetypes
5
4
  import os
6
5
  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)
6
+ from typing import Any, Iterator, Mapping, Optional, Protocol, Type, TypeVar
12
7
 
13
8
  import requests
14
9
  import requests.cookies
15
10
 
16
- from ._util import timedelta_isoformat
11
+ from ._authentication import (IAuthenticationMethod,
12
+ create_authentication_method)
17
13
  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)
14
+ from .modules.core import MercutoCoreService
15
+ from .modules.data import MercutoDataService
16
+ from .modules.fatigue import MercutoFatigueService
17
+ from .modules.identity import MercutoIdentityService
27
18
 
28
19
  logger = logging.getLogger(__name__)
29
20
 
30
21
 
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:
22
+ class _ModuleBase(Protocol):
23
+ def __init__(self, client: 'MercutoClient', *args: Any, **kwargs: Any) -> None:
75
24
  pass
76
25
 
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
26
 
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')
27
+ _T = TypeVar('_T', bound=_ModuleBase)
112
28
 
113
29
 
114
30
  class MercutoClient:
115
31
  def __init__(self, url: Optional[str] = None, verify_ssl: bool = True, active_session: Optional[requests.Session] = None) -> None:
116
32
  if url is None:
117
- url = os.environ.get(
118
- 'MERCUTO_API_URL', 'https://api.rockfieldcloud.com.au')
33
+ url = os.environ.get('MERCUTO_API_URL', 'https://api.rockfieldcloud.com.au')
119
34
  assert isinstance(url, str)
120
35
 
121
36
  if url.endswith('/'):
@@ -135,7 +50,7 @@ class MercutoClient:
135
50
  self._auth_method: Optional[IAuthenticationMethod] = None
136
51
  self._cookies = requests.cookies.RequestsCookieJar()
137
52
 
138
- self._modules: dict[str, Module] = {}
53
+ self._modules: dict[str, _ModuleBase] = {}
139
54
 
140
55
  def url(self) -> str:
141
56
  return self._url
@@ -157,20 +72,21 @@ class MercutoClient:
157
72
  @contextlib.contextmanager
158
73
  def as_credentials(self, api_key: Optional[str] = None,
159
74
  service_token: Optional[str] = None,
75
+ bearer_token: Optional[str] = None,
160
76
  headers: Optional[Mapping[str, str]] = None) -> Iterator['MercutoClient']:
161
77
  """
162
78
  Same as .connect(), but as a context manager. Will automatically logout when exiting the context.
163
79
  """
164
80
  # 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)
81
+ other = MercutoClient(self._url, self._verify_ssl, self._current_session)
167
82
  try:
168
- yield other.connect(api_key=api_key, service_token=service_token, headers=headers)
83
+ yield other.connect(api_key=api_key, service_token=service_token, bearer_token=bearer_token, headers=headers)
169
84
  finally:
170
85
  other.logout()
171
86
 
172
87
  def connect(self, *, api_key: Optional[str] = None,
173
88
  service_token: Optional[str] = None,
89
+ bearer_token: Optional[str] = None,
174
90
  headers: Optional[Mapping[str, str]] = None) -> 'MercutoClient':
175
91
  """
176
92
  Attempt to connect using any available method.
@@ -180,8 +96,7 @@ class MercutoClient:
180
96
  headers should be a dictionary of headers that would be sent in a request. Useful for using existing authenation mechanism for forwarding.
181
97
 
182
98
  """
183
- authentication = create_authentication_method(
184
- api_key=api_key, service_token=service_token, headers=headers)
99
+ authentication = create_authentication_method(api_key=api_key, service_token=service_token, bearer_token=bearer_token, headers=headers)
185
100
  self.login(authentication)
186
101
  return self
187
102
 
@@ -193,14 +108,15 @@ class MercutoClient:
193
108
  base.update(headers)
194
109
  return base
195
110
 
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)}"
111
+ def _http_request(self, url: str, method: str,
112
+ params: Optional[dict[str, Any]] = None,
113
+ json: Optional[dict[str, Any]] = None,
114
+ raise_for_status: bool = True,
115
+ **kwargs: Any) -> requests.Response:
116
+ if url.startswith('/'):
117
+ url = url[1:]
118
+ full_url = f"{self._url}/{url}"
202
119
 
203
- def _request_json(self, method: str, url: str, *args: Any, **kwargs: Any) -> Any:
204
120
  if 'timeout' not in kwargs:
205
121
  kwargs['timeout'] = 10
206
122
  kwargs['headers'] = self._update_headers(kwargs.get('headers', {}))
@@ -210,67 +126,46 @@ class MercutoClient:
210
126
 
211
127
  if 'cookies' not in kwargs:
212
128
  kwargs['cookies'] = self._cookies
213
- return self._make_request(method, url, *args, **kwargs)
214
129
 
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}"
130
+ # Custom parsing json to support NAN
131
+ if json is not None and kwargs.get('data') is None:
132
+ kwargs['data'] = json_stdlib.dumps(json, allow_nan=True)
133
+ kwargs['headers']['Content-Type'] = 'application/json'
134
+ json = None
135
+
219
136
  start = time.time()
220
- resp = self._current_session.request(method, full_url, *args, **kwargs)
137
+ resp = self._current_session.request(method, full_url, params=params, json=json, **kwargs)
221
138
  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}")
139
+ logger.debug("Made request to %s %s in %.2f seconds (code=%s)", method, full_url, duration, resp.status_code)
140
+ if raise_for_status and not resp.ok:
141
+ try:
142
+ error_json = resp.json()
143
+ except Exception:
144
+ raise MercutoHTTPException(resp.text, resp.status_code)
145
+ else:
146
+ if 'detail' in error_json and isinstance(error_json['detail'], str):
147
+ raise MercutoHTTPException(error_json['detail'], resp.status_code)
148
+ else:
149
+ raise MercutoHTTPException(resp.text, resp.status_code)
228
150
  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)
151
+ return resp
233
152
 
234
- def add_and_fetch_module(self, name: str, module: Type[T]) -> T:
153
+ def _add_and_fetch_module(self, name: str, module: Type[_T]) -> _T:
235
154
  if name not in self._modules:
236
155
  self._modules[name] = module(self)
237
156
  return self._modules[name] # type: ignore
238
157
 
239
158
  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)
159
+ return self._add_and_fetch_module('identity', MercutoIdentityService)
244
160
 
245
161
  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)
162
+ return self._add_and_fetch_module('fatigue', MercutoFatigueService)
250
163
 
251
164
  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)
165
+ return self._add_and_fetch_module('data', MercutoDataService)
265
166
 
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)
167
+ def core(self) -> 'MercutoCoreService':
168
+ return self._add_and_fetch_module('core', MercutoCoreService)
274
169
 
275
170
  def login(self, authentication: IAuthenticationMethod) -> None:
276
171
  self._auth_method = authentication
@@ -278,626 +173,5 @@ class MercutoClient:
278
173
  def logout(self) -> None:
279
174
  self._auth_method = None
280
175
 
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
- })
176
+ def is_logged_in(self) -> bool:
177
+ return self._auth_method is not None