mercuto-client 0.2.7__py3-none-any.whl → 0.3.0a0__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 +2 -24
- mercuto_client/_authentication.py +72 -0
- mercuto_client/_tests/test_ingester/test_parsers.py +67 -67
- mercuto_client/_tests/test_mocking/__init__.py +0 -0
- mercuto_client/_tests/test_mocking/conftest.py +13 -0
- mercuto_client/_tests/test_mocking/test_mock_identity.py +8 -0
- mercuto_client/acl.py +16 -10
- mercuto_client/client.py +53 -779
- mercuto_client/exceptions.py +5 -1
- mercuto_client/ingester/__main__.py +1 -1
- mercuto_client/ingester/mercuto.py +15 -16
- mercuto_client/ingester/parsers/__init__.py +3 -3
- mercuto_client/ingester/parsers/campbell.py +2 -2
- mercuto_client/ingester/parsers/generic_csv.py +5 -5
- mercuto_client/ingester/parsers/worldsensing.py +4 -3
- mercuto_client/mocks/__init__.py +92 -0
- mercuto_client/mocks/_utility.py +69 -0
- mercuto_client/mocks/mock_data.py +402 -0
- mercuto_client/mocks/mock_fatigue.py +30 -0
- mercuto_client/mocks/mock_identity.py +188 -0
- mercuto_client/modules/__init__.py +19 -0
- mercuto_client/modules/_util.py +18 -0
- mercuto_client/modules/core.py +674 -0
- mercuto_client/modules/data.py +623 -0
- mercuto_client/modules/fatigue.py +189 -0
- mercuto_client/modules/identity.py +254 -0
- mercuto_client/{ingester/util.py → util.py} +27 -11
- mercuto_client-0.3.0a0.dist-info/METADATA +72 -0
- mercuto_client-0.3.0a0.dist-info/RECORD +41 -0
- mercuto_client/_tests/test_mocking.py +0 -93
- mercuto_client/_util.py +0 -13
- mercuto_client/mocks.py +0 -203
- mercuto_client/types.py +0 -409
- mercuto_client-0.2.7.dist-info/METADATA +0 -20
- mercuto_client-0.2.7.dist-info/RECORD +0 -30
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/WHEEL +0 -0
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
- {mercuto_client-0.2.7.dist-info → mercuto_client-0.3.0a0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import TypeAdapter
|
|
7
|
+
|
|
8
|
+
from . import _PayloadType
|
|
9
|
+
from ._util import BaseModel, serialise_timedelta
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..client import MercutoClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ProjectStatus(BaseModel):
|
|
16
|
+
last_ping: Optional[str]
|
|
17
|
+
ip_address: Optional[str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Project(BaseModel):
|
|
21
|
+
code: str
|
|
22
|
+
name: str
|
|
23
|
+
project_number: str
|
|
24
|
+
active: bool
|
|
25
|
+
description: str
|
|
26
|
+
latitude: Optional[float]
|
|
27
|
+
longitude: Optional[float]
|
|
28
|
+
timezone: str
|
|
29
|
+
display_timezone: Optional[str]
|
|
30
|
+
tenant: str
|
|
31
|
+
status: ProjectStatus
|
|
32
|
+
commission_date: datetime
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WidgetConfig(BaseModel):
|
|
36
|
+
type: str
|
|
37
|
+
config: dict[Any, Any]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WidgetColumn(BaseModel):
|
|
41
|
+
size: Optional[str | int]
|
|
42
|
+
widget: WidgetConfig
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WidgetRow(BaseModel):
|
|
46
|
+
columns: list[WidgetColumn]
|
|
47
|
+
height: int
|
|
48
|
+
title: str
|
|
49
|
+
breakpoint: Optional[str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Dashboard(BaseModel):
|
|
53
|
+
icon: Optional[str]
|
|
54
|
+
name: Optional[str]
|
|
55
|
+
banner_image: Optional[str]
|
|
56
|
+
widgets: Optional[list[WidgetRow]]
|
|
57
|
+
fullscreen: Optional[bool]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Dashboards(BaseModel):
|
|
61
|
+
dashboards: list[Dashboard]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProjectEventDetection(BaseModel):
|
|
65
|
+
enabled: bool
|
|
66
|
+
datatables: list[str]
|
|
67
|
+
max_duration: timedelta
|
|
68
|
+
max_files: int
|
|
69
|
+
maximise: bool
|
|
70
|
+
overlap_period: timedelta
|
|
71
|
+
split_interval_cron: Optional[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ItemCode(BaseModel):
|
|
75
|
+
code: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class EventTag(BaseModel):
|
|
79
|
+
tag: str
|
|
80
|
+
value: Any | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Object(BaseModel):
|
|
84
|
+
code: str
|
|
85
|
+
mime_type: str
|
|
86
|
+
size_bytes: int
|
|
87
|
+
name: str
|
|
88
|
+
event: ItemCode | None
|
|
89
|
+
project: ItemCode
|
|
90
|
+
access_url: str | None
|
|
91
|
+
access_expires: datetime | None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Event(BaseModel):
|
|
95
|
+
code: str
|
|
96
|
+
project: ItemCode
|
|
97
|
+
start_time: datetime
|
|
98
|
+
end_time: datetime
|
|
99
|
+
objects: list[Object]
|
|
100
|
+
tags: list[EventTag]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
UserContactMethod = Literal['EMAIL', 'SMS']
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ContactGroup(BaseModel):
|
|
107
|
+
project: str
|
|
108
|
+
code: str
|
|
109
|
+
label: str
|
|
110
|
+
users: dict[str, list[UserContactMethod]]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Condition(BaseModel):
|
|
114
|
+
code: str
|
|
115
|
+
source: str
|
|
116
|
+
description: str
|
|
117
|
+
upper_exclusive_bound: Optional[float]
|
|
118
|
+
lower_inclusive_bound: Optional[float]
|
|
119
|
+
neutral_position: float
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AlertConfiguration(BaseModel):
|
|
123
|
+
code: str
|
|
124
|
+
project: str
|
|
125
|
+
label: str
|
|
126
|
+
conditions: list[Condition]
|
|
127
|
+
contact_group: Optional[ContactGroup]
|
|
128
|
+
retrigger_interval: Optional[datetime]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AlertLogConditionEntry(BaseModel):
|
|
132
|
+
condition: Condition
|
|
133
|
+
start_value: float
|
|
134
|
+
start_time: str
|
|
135
|
+
start_percentile: float
|
|
136
|
+
|
|
137
|
+
peak_value: float
|
|
138
|
+
peak_time: str
|
|
139
|
+
peak_percentile: float
|
|
140
|
+
|
|
141
|
+
end_value: float
|
|
142
|
+
end_time: str
|
|
143
|
+
end_percentile: float
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class AlertLogComment(BaseModel):
|
|
147
|
+
user_code: str
|
|
148
|
+
comment: str
|
|
149
|
+
created_at: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class AlertLog(BaseModel):
|
|
153
|
+
code: str
|
|
154
|
+
project: str
|
|
155
|
+
event: Optional[str]
|
|
156
|
+
acknowledged: bool
|
|
157
|
+
fired_at: str
|
|
158
|
+
configuration: str
|
|
159
|
+
conditions: list[AlertLogConditionEntry]
|
|
160
|
+
comments: list[AlertLogComment]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class AlertSummary(BaseModel):
|
|
164
|
+
alerts: list[AlertLog]
|
|
165
|
+
total: int
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Healthcheck(BaseModel):
|
|
169
|
+
ephemeral_warehouse: str
|
|
170
|
+
ephemeral_document_store: str
|
|
171
|
+
cache: str
|
|
172
|
+
database: str
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class DeviceType(BaseModel):
|
|
176
|
+
code: str
|
|
177
|
+
description: str
|
|
178
|
+
manufacturer: str
|
|
179
|
+
model_number: str
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class DeviceChannel(BaseModel):
|
|
183
|
+
channel: str
|
|
184
|
+
field: str
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Device(BaseModel):
|
|
188
|
+
code: str
|
|
189
|
+
project: ItemCode
|
|
190
|
+
label: str
|
|
191
|
+
location_description: Optional[str]
|
|
192
|
+
device_type: DeviceType
|
|
193
|
+
groups: list[str]
|
|
194
|
+
channels: list[DeviceChannel]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class EventAggregate(BaseModel):
|
|
198
|
+
aggregate: Literal["max", "greatest", "min", "median", "abs-max", "mean", "rms", "peak-to-peak", "daf"]
|
|
199
|
+
enabled: bool = True
|
|
200
|
+
options: Optional[dict[str, Any]] = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class Camera(BaseModel):
|
|
204
|
+
code: str
|
|
205
|
+
project: str
|
|
206
|
+
label: str
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Video(BaseModel):
|
|
210
|
+
code: str
|
|
211
|
+
project: str
|
|
212
|
+
camera: str | None
|
|
213
|
+
start_time: str
|
|
214
|
+
end_time: str
|
|
215
|
+
mime_type: str
|
|
216
|
+
size_bytes: int
|
|
217
|
+
name: str
|
|
218
|
+
event: str | None
|
|
219
|
+
access_url: str | None
|
|
220
|
+
access_expires: str
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class Image(BaseModel):
|
|
224
|
+
code: str
|
|
225
|
+
project: str
|
|
226
|
+
camera: str | None
|
|
227
|
+
timestamp: str | None
|
|
228
|
+
mime_type: str
|
|
229
|
+
size_bytes: int
|
|
230
|
+
name: str
|
|
231
|
+
event: str | None
|
|
232
|
+
access_url: str | None
|
|
233
|
+
access_expires: str
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ScheduledReport(BaseModel):
|
|
237
|
+
code: str
|
|
238
|
+
project: str
|
|
239
|
+
label: str
|
|
240
|
+
revision: str
|
|
241
|
+
schedule: Optional[str]
|
|
242
|
+
contact_group: Optional[str]
|
|
243
|
+
last_scheduled: Optional[str]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class ScheduledReportLog(BaseModel):
|
|
247
|
+
code: str
|
|
248
|
+
report: str
|
|
249
|
+
scheduled_start: Optional[str]
|
|
250
|
+
actual_start: str
|
|
251
|
+
actual_finish: Optional[str]
|
|
252
|
+
status: Literal['IN_PROGRESS', 'COMPLETED', 'FAILED']
|
|
253
|
+
message: Optional[str]
|
|
254
|
+
access_url: Optional[str]
|
|
255
|
+
mime_type: Optional[str]
|
|
256
|
+
filename: Optional[str]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class ReportSourceCodeRevision(BaseModel):
|
|
260
|
+
code: str
|
|
261
|
+
revision_date: datetime
|
|
262
|
+
description: str
|
|
263
|
+
source_code_url: str
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
_ProjectListAdapter = TypeAdapter(list[Project])
|
|
267
|
+
_EventsListAdapter = TypeAdapter(list[Event])
|
|
268
|
+
_DevicesListAdapter = TypeAdapter(list[Device])
|
|
269
|
+
_DeviceTypeListAdapter = TypeAdapter(list[DeviceType])
|
|
270
|
+
_ImageListAdapter = TypeAdapter(list[Image])
|
|
271
|
+
_VideoListAdapter = TypeAdapter(list[Video])
|
|
272
|
+
_CameraListAdapter = TypeAdapter(list[Camera])
|
|
273
|
+
_ContactGroupListAdapter = TypeAdapter(list[ContactGroup])
|
|
274
|
+
_ScheduledReportListAdapter = TypeAdapter(list[ScheduledReport])
|
|
275
|
+
_ScheduledReportLogListAdapter = TypeAdapter(list[ScheduledReportLog])
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class MercutoCoreService:
|
|
279
|
+
def __init__(self, client: 'MercutoClient') -> None:
|
|
280
|
+
self._client = client
|
|
281
|
+
|
|
282
|
+
def healthcheck(self) -> Healthcheck:
|
|
283
|
+
r = self._client._http_request("/healthcheck", "GET")
|
|
284
|
+
return Healthcheck.model_validate_json(r.text)
|
|
285
|
+
|
|
286
|
+
# Projects
|
|
287
|
+
|
|
288
|
+
def get_project(self, code: str) -> Project:
|
|
289
|
+
if len(code) == 0:
|
|
290
|
+
raise ValueError("Project code must not be empty")
|
|
291
|
+
r = self._client._http_request(f'/projects/{code}', 'GET')
|
|
292
|
+
return Project.model_validate_json(r.text)
|
|
293
|
+
|
|
294
|
+
def list_projects(self) -> list[Project]:
|
|
295
|
+
r = self._client._http_request('/projects', 'GET')
|
|
296
|
+
return _ProjectListAdapter.validate_json(r.text)
|
|
297
|
+
|
|
298
|
+
def create_project(self, name: str, project_number: str, description: str, tenant: str,
|
|
299
|
+
timezone: str,
|
|
300
|
+
latitude: Optional[float] = None,
|
|
301
|
+
longitude: Optional[float] = None) -> Project:
|
|
302
|
+
|
|
303
|
+
payload: _PayloadType = {
|
|
304
|
+
'name': name,
|
|
305
|
+
'project_number': project_number,
|
|
306
|
+
'description': description,
|
|
307
|
+
'tenant_code': tenant,
|
|
308
|
+
'timezone': timezone,
|
|
309
|
+
}
|
|
310
|
+
if latitude is not None:
|
|
311
|
+
payload['latitude'] = latitude
|
|
312
|
+
if longitude is not None:
|
|
313
|
+
payload['longitude'] = longitude
|
|
314
|
+
|
|
315
|
+
r = self._client._http_request('/projects', 'PUT', json=payload)
|
|
316
|
+
return Project.model_validate_json(r.text)
|
|
317
|
+
|
|
318
|
+
def ping_project(self, project: str, ip_address: str) -> None:
|
|
319
|
+
self._client._http_request(f'/projects/{project}/ping', 'POST', json={'ip_address': ip_address})
|
|
320
|
+
|
|
321
|
+
def create_dashboard(self, project_code: str, dashboards: Dashboards) -> None:
|
|
322
|
+
json = dashboards.model_dump()
|
|
323
|
+
self._client._http_request(f'/projects/{project_code}/dashboard', 'POST', json=json)
|
|
324
|
+
|
|
325
|
+
def set_project_event_detection(self, project: str, datatables: list[str]) -> ProjectEventDetection:
|
|
326
|
+
if len(datatables) == 0:
|
|
327
|
+
raise ValueError('At least one datatable must be provided to enable event detection')
|
|
328
|
+
|
|
329
|
+
params: _PayloadType = {
|
|
330
|
+
"enabled": True,
|
|
331
|
+
"datatables": datatables
|
|
332
|
+
}
|
|
333
|
+
r = self._client._http_request(f'/projects/{project}/event-detection', 'POST', json=params)
|
|
334
|
+
return ProjectEventDetection.model_validate_json(r.text)
|
|
335
|
+
|
|
336
|
+
# EVENTS
|
|
337
|
+
|
|
338
|
+
def create_event(self, project: str, start_time: datetime, end_time: datetime) -> Event:
|
|
339
|
+
if start_time.tzinfo is None or end_time.tzinfo is None:
|
|
340
|
+
raise ValueError("Timestamp must be timezone aware")
|
|
341
|
+
|
|
342
|
+
json: _PayloadType = {
|
|
343
|
+
'project': project,
|
|
344
|
+
'start_time': start_time.isoformat(),
|
|
345
|
+
'end_time': end_time.isoformat(),
|
|
346
|
+
}
|
|
347
|
+
r = self._client._http_request('/events', 'PUT', json=json)
|
|
348
|
+
return Event.model_validate_json(r.text)
|
|
349
|
+
|
|
350
|
+
def list_events(self, project: str) -> list[Event]:
|
|
351
|
+
params: _PayloadType = {'project_code': project}
|
|
352
|
+
r = self._client._http_request('/events', 'GET', params=params)
|
|
353
|
+
return _EventsListAdapter.validate_json(r.text)
|
|
354
|
+
|
|
355
|
+
def get_event(self, event: str) -> Event:
|
|
356
|
+
r = self._client._http_request(f'/events/{event}', 'GET')
|
|
357
|
+
return Event.model_validate_json(r.text)
|
|
358
|
+
|
|
359
|
+
def delete_event(self, event: str) -> None:
|
|
360
|
+
self._client._http_request(f'/events/{event}', 'DELETE')
|
|
361
|
+
|
|
362
|
+
def get_nearest_event(
|
|
363
|
+
self,
|
|
364
|
+
project_code: str,
|
|
365
|
+
to: datetime,
|
|
366
|
+
maximum_delta: timedelta | None = None,
|
|
367
|
+
) -> Event:
|
|
368
|
+
params: _PayloadType = {
|
|
369
|
+
'project_code': project_code,
|
|
370
|
+
'to': to.isoformat(),
|
|
371
|
+
}
|
|
372
|
+
if maximum_delta is not None:
|
|
373
|
+
params['maximum_delta'] = serialise_timedelta(maximum_delta)
|
|
374
|
+
|
|
375
|
+
r = self._client._http_request('/events/nearest', 'GET', params=params)
|
|
376
|
+
return Event.model_validate_json(r.text)
|
|
377
|
+
|
|
378
|
+
def set_event_aggregates(self, project: str, aggregates: list[EventAggregate]) -> None:
|
|
379
|
+
self._client._http_request('/aggregates', 'PUT',
|
|
380
|
+
json=[agg.model_dump(mode='json') for agg in aggregates], # type: ignore
|
|
381
|
+
params={'project_code': project})
|
|
382
|
+
|
|
383
|
+
# ALERTS
|
|
384
|
+
|
|
385
|
+
def get_condition(self, code: str) -> Condition:
|
|
386
|
+
r = self._client._http_request(f'/alerts/conditions/{code}', 'GET')
|
|
387
|
+
return Condition.model_validate_json(r.text)
|
|
388
|
+
|
|
389
|
+
def create_condition(self, source: str, description: str, *,
|
|
390
|
+
lower_bound: Optional[float] = None,
|
|
391
|
+
upper_bound: Optional[float] = None,
|
|
392
|
+
neutral_position: float = 0) -> Condition:
|
|
393
|
+
json: _PayloadType = {
|
|
394
|
+
'source_channel_code': source,
|
|
395
|
+
'description': description,
|
|
396
|
+
'neutral_position': neutral_position
|
|
397
|
+
}
|
|
398
|
+
if lower_bound is not None:
|
|
399
|
+
json['lower_inclusive_bound'] = lower_bound
|
|
400
|
+
if upper_bound is not None:
|
|
401
|
+
json['upper_exclusive_bound'] = upper_bound
|
|
402
|
+
r = self._client._http_request('/alerts/conditions', 'PUT', json=json)
|
|
403
|
+
return Condition.model_validate_json(r.text)
|
|
404
|
+
|
|
405
|
+
def create_alert_configuration(self, label: str,
|
|
406
|
+
conditions: list[str],
|
|
407
|
+
contact_group: Optional[str] = None) -> AlertConfiguration:
|
|
408
|
+
json: _PayloadType = {
|
|
409
|
+
'label': label,
|
|
410
|
+
'conditions': conditions,
|
|
411
|
+
|
|
412
|
+
}
|
|
413
|
+
if contact_group is not None:
|
|
414
|
+
json['contact_group'] = contact_group
|
|
415
|
+
r = self._client._http_request('/alerts/configurations', 'PUT', json=json)
|
|
416
|
+
return AlertConfiguration.model_validate_json(r.text)
|
|
417
|
+
|
|
418
|
+
def get_alert_configuration(self, code: str) -> AlertConfiguration:
|
|
419
|
+
r = self._client._http_request(f'/alerts/configurations/{code}', 'GET')
|
|
420
|
+
return AlertConfiguration.model_validate_json(r.text)
|
|
421
|
+
|
|
422
|
+
def list_alert_logs(
|
|
423
|
+
self,
|
|
424
|
+
project: str | None = None,
|
|
425
|
+
configuration: str | None = None,
|
|
426
|
+
channels: list[str] | None = None,
|
|
427
|
+
start_time: datetime | str | None = None,
|
|
428
|
+
end_time: datetime | str | None = None,
|
|
429
|
+
limit: int = 10,
|
|
430
|
+
offset: int = 0,
|
|
431
|
+
latest_only: bool = False,
|
|
432
|
+
) -> AlertSummary:
|
|
433
|
+
params: _PayloadType = {
|
|
434
|
+
'limit': limit,
|
|
435
|
+
'offset': offset,
|
|
436
|
+
'latest_only': latest_only,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if project is not None:
|
|
440
|
+
params['project'] = project
|
|
441
|
+
if configuration is not None:
|
|
442
|
+
params['configuration_code'] = configuration
|
|
443
|
+
if channels is not None:
|
|
444
|
+
params['channels'] = channels
|
|
445
|
+
if start_time is not None:
|
|
446
|
+
params['start_time'] = start_time.isoformat() if isinstance(start_time, datetime) else start_time
|
|
447
|
+
if end_time is not None:
|
|
448
|
+
params['end_time'] = end_time.isoformat() if isinstance(end_time, datetime) else end_time
|
|
449
|
+
|
|
450
|
+
r = self._client._http_request('/alerts/logs', 'GET', params=params)
|
|
451
|
+
return AlertSummary.model_validate_json(r.text)
|
|
452
|
+
|
|
453
|
+
# DEVICES
|
|
454
|
+
|
|
455
|
+
def list_device_types(self) -> list[DeviceType]:
|
|
456
|
+
r = self._client._http_request('/devices/types', 'GET')
|
|
457
|
+
return _DeviceTypeListAdapter.validate_json(r.text)
|
|
458
|
+
|
|
459
|
+
def create_device_type(self, description: str, manufacturer: str, model_number: str) -> DeviceType:
|
|
460
|
+
json: _PayloadType = {
|
|
461
|
+
'description': description,
|
|
462
|
+
'manufacturer': manufacturer,
|
|
463
|
+
'model_number': model_number
|
|
464
|
+
}
|
|
465
|
+
r = self._client._http_request('/devices/types', 'PUT', json=json)
|
|
466
|
+
return DeviceType.model_validate_json(r.text)
|
|
467
|
+
|
|
468
|
+
def list_devices(self, project_code: str, limit: int, offset: int) -> list[Device]:
|
|
469
|
+
params: _PayloadType = {
|
|
470
|
+
'project_code': project_code,
|
|
471
|
+
'limit': limit,
|
|
472
|
+
'offset': offset
|
|
473
|
+
}
|
|
474
|
+
r = self._client._http_request('/devices', 'GET', params=params)
|
|
475
|
+
return _DevicesListAdapter.validate_json(r.text)
|
|
476
|
+
|
|
477
|
+
def get_device(self, device_code: str) -> Device:
|
|
478
|
+
r = self._client._http_request(f'/devices/{device_code}', 'GET')
|
|
479
|
+
return Device.model_validate_json(r.text)
|
|
480
|
+
|
|
481
|
+
def create_device(self,
|
|
482
|
+
project_code: str,
|
|
483
|
+
label: str,
|
|
484
|
+
device_type_code: str,
|
|
485
|
+
groups: list[str],
|
|
486
|
+
location_description: Optional[str] = None,
|
|
487
|
+
channels: Optional[list[DeviceChannel]] = None) -> Device:
|
|
488
|
+
json: _PayloadType = {
|
|
489
|
+
'project_code': project_code,
|
|
490
|
+
'label': label,
|
|
491
|
+
'device_type_code': device_type_code,
|
|
492
|
+
'groups': groups,
|
|
493
|
+
}
|
|
494
|
+
if location_description is not None:
|
|
495
|
+
json['location_description'] = location_description
|
|
496
|
+
if channels is not None:
|
|
497
|
+
json['channels'] = [channel.model_dump(mode='json') for channel in channels] # type: ignore[assignment]
|
|
498
|
+
r = self._client._http_request('/devices', 'PUT', json=json)
|
|
499
|
+
return Device.model_validate_json(r.text)
|
|
500
|
+
|
|
501
|
+
# MEDIA
|
|
502
|
+
|
|
503
|
+
def list_cameras(self, project: str) -> list[Camera]:
|
|
504
|
+
params: _PayloadType = {}
|
|
505
|
+
params['project_code'] = project
|
|
506
|
+
r = self._client._http_request('/media/cameras', 'GET', params=params)
|
|
507
|
+
return _CameraListAdapter.validate_json(r.text)
|
|
508
|
+
|
|
509
|
+
def list_videos(self, project: Optional[str] = None, event: Optional[str] = None, camera: Optional[str] = None) -> list[Video]:
|
|
510
|
+
params: _PayloadType = {}
|
|
511
|
+
if project is not None:
|
|
512
|
+
params['project'] = project
|
|
513
|
+
if event is not None:
|
|
514
|
+
params['event'] = event
|
|
515
|
+
if camera is not None:
|
|
516
|
+
params['camera'] = camera
|
|
517
|
+
r = self._client._http_request('/media/videos', 'GET', params=params)
|
|
518
|
+
return _VideoListAdapter.validate_json(r.text)
|
|
519
|
+
|
|
520
|
+
def get_video(self, code: str) -> Video:
|
|
521
|
+
r = self._client._http_request(f'/media/videos/{code}', 'GET')
|
|
522
|
+
return Video.model_validate_json(r.text)
|
|
523
|
+
|
|
524
|
+
def list_images(self, project: Optional[str] = None, event: Optional[str] = None, camera: Optional[str] = None) -> list[Image]:
|
|
525
|
+
params = {}
|
|
526
|
+
if project is not None:
|
|
527
|
+
params['project'] = project
|
|
528
|
+
if event is not None:
|
|
529
|
+
params['event'] = event
|
|
530
|
+
if camera is not None:
|
|
531
|
+
params['camera'] = camera
|
|
532
|
+
r = self._client._http_request('/media/images', 'GET', params=params)
|
|
533
|
+
return _ImageListAdapter.validate_json(r.text)
|
|
534
|
+
|
|
535
|
+
def get_image(self, code: str) -> Image:
|
|
536
|
+
r = self._client._http_request(f'/media/images/{code}', 'GET')
|
|
537
|
+
return Image.model_validate_json(r.text)
|
|
538
|
+
|
|
539
|
+
def upload_image(self, project: str, file: str, event: Optional[str] = None,
|
|
540
|
+
camera: Optional[str] = None, timestamp: Optional[datetime] = None,
|
|
541
|
+
filename: Optional[str] = None) -> Image:
|
|
542
|
+
if timestamp is not None and timestamp.tzinfo is None:
|
|
543
|
+
raise ValueError("Timestamp must be timezone aware")
|
|
544
|
+
|
|
545
|
+
mimetype, _ = mimetypes.guess_type(file, strict=False)
|
|
546
|
+
if mimetype is None or not mimetype.startswith('image/'):
|
|
547
|
+
raise ValueError(f"File {file} is not an image")
|
|
548
|
+
|
|
549
|
+
if os.stat(file).st_size > 5_000_000:
|
|
550
|
+
raise ValueError(f"File {file} is too large")
|
|
551
|
+
|
|
552
|
+
if filename is None:
|
|
553
|
+
filename = os.path.basename(file)
|
|
554
|
+
|
|
555
|
+
params: _PayloadType = {}
|
|
556
|
+
params['project'] = project
|
|
557
|
+
if event is not None:
|
|
558
|
+
params['event'] = event
|
|
559
|
+
if camera is not None:
|
|
560
|
+
params['camera'] = camera
|
|
561
|
+
if timestamp is not None:
|
|
562
|
+
params['timestamp'] = timestamp.isoformat()
|
|
563
|
+
|
|
564
|
+
with open(file, 'rb') as f:
|
|
565
|
+
r = self._client._http_request('/media/images', 'PUT',
|
|
566
|
+
params=params,
|
|
567
|
+
files={
|
|
568
|
+
'file': (filename, f, mimetype)
|
|
569
|
+
})
|
|
570
|
+
return Image.model_validate_json(r.text)
|
|
571
|
+
|
|
572
|
+
def upload_video(self, project: str, file: str,
|
|
573
|
+
start_time: datetime, end_time: datetime,
|
|
574
|
+
event: Optional[str] = None,
|
|
575
|
+
filename: Optional[str] = None) -> Video:
|
|
576
|
+
if start_time.tzinfo is None:
|
|
577
|
+
raise ValueError("Timestamp must be timezone aware")
|
|
578
|
+
if end_time.tzinfo is None:
|
|
579
|
+
raise ValueError("Timestamp must be timezone aware")
|
|
580
|
+
|
|
581
|
+
mimetype, _ = mimetypes.guess_type(file, strict=False)
|
|
582
|
+
if mimetype is None or not mimetype.startswith('video/'):
|
|
583
|
+
raise ValueError(f"File {file} is not a video")
|
|
584
|
+
|
|
585
|
+
if os.stat(file).st_size > 5_000_000:
|
|
586
|
+
raise ValueError(f"File {file} is too large")
|
|
587
|
+
|
|
588
|
+
if filename is None:
|
|
589
|
+
filename = os.path.basename(file)
|
|
590
|
+
|
|
591
|
+
params: _PayloadType = {}
|
|
592
|
+
params['project'] = project
|
|
593
|
+
params['start_time'] = start_time.isoformat()
|
|
594
|
+
params['end_time'] = end_time.isoformat()
|
|
595
|
+
if event is not None:
|
|
596
|
+
params['event'] = event
|
|
597
|
+
|
|
598
|
+
with open(file, 'rb') as f:
|
|
599
|
+
r = self._client._http_request('/media/videos', 'PUT',
|
|
600
|
+
params=params,
|
|
601
|
+
files={
|
|
602
|
+
'file': (filename, f, mimetype)
|
|
603
|
+
})
|
|
604
|
+
return Video.model_validate_json(r.text)
|
|
605
|
+
|
|
606
|
+
# Contacts
|
|
607
|
+
|
|
608
|
+
def list_contact_groups(self, project: Optional[str] = None) -> list[ContactGroup]:
|
|
609
|
+
params: _PayloadType = {}
|
|
610
|
+
if project is not None:
|
|
611
|
+
params['project'] = project
|
|
612
|
+
r = self._client._http_request('/notifications/contact_groups', 'GET', params=params)
|
|
613
|
+
return _ContactGroupListAdapter.validate_json(r.text)
|
|
614
|
+
|
|
615
|
+
def get_contact_group(self, code: str) -> ContactGroup:
|
|
616
|
+
r = self._client._http_request(f'/notifications/contact_groups/{code}', 'GET')
|
|
617
|
+
return ContactGroup.model_validate_json(r.text)
|
|
618
|
+
|
|
619
|
+
def create_contact_group(self, project: str, label: str, users: dict[str, list[UserContactMethod]]) -> ContactGroup:
|
|
620
|
+
r = self._client._http_request('/notifications/contact_groups', 'PUT',
|
|
621
|
+
json={
|
|
622
|
+
'project': project,
|
|
623
|
+
'label': label,
|
|
624
|
+
'users': users
|
|
625
|
+
})
|
|
626
|
+
return ContactGroup.model_validate_json(r.text)
|
|
627
|
+
|
|
628
|
+
# Reports
|
|
629
|
+
def list_reports(self, project: Optional[str] = None) -> list['ScheduledReport']:
|
|
630
|
+
params: _PayloadType = {}
|
|
631
|
+
if project is not None:
|
|
632
|
+
params['project'] = project
|
|
633
|
+
r = self._client._http_request('/reports/scheduled', 'GET', params=params)
|
|
634
|
+
return _ScheduledReportListAdapter.validate_json(r.text)
|
|
635
|
+
|
|
636
|
+
def create_report(self, project: str, label: str, schedule: str, revision: str,
|
|
637
|
+
api_key: Optional[str] = None, contact_group: Optional[str] = None) -> ScheduledReport:
|
|
638
|
+
json: _PayloadType = {
|
|
639
|
+
'project': project,
|
|
640
|
+
'label': label,
|
|
641
|
+
'schedule': schedule,
|
|
642
|
+
'revision': revision,
|
|
643
|
+
'execution_role_api_key': api_key,
|
|
644
|
+
'contact_group': contact_group
|
|
645
|
+
}
|
|
646
|
+
r = self._client._http_request('/reports/scheduled', 'PUT', json=json)
|
|
647
|
+
return ScheduledReport.model_validate_json(r.text)
|
|
648
|
+
|
|
649
|
+
def generate_report(self, report: str, timestamp: datetime, mark_as_scheduled: bool = False) -> ScheduledReportLog:
|
|
650
|
+
r = self._client._http_request(f'/reports/scheduled/{report}/generate', 'PUT', json={
|
|
651
|
+
'timestamp': timestamp.isoformat(),
|
|
652
|
+
'mark_as_scheduled': mark_as_scheduled
|
|
653
|
+
})
|
|
654
|
+
return ScheduledReportLog.model_validate_json(r.text)
|
|
655
|
+
|
|
656
|
+
def list_report_logs(self, report: str, project: Optional[str] = None) -> list[ScheduledReportLog]:
|
|
657
|
+
params: _PayloadType = {}
|
|
658
|
+
if project is not None:
|
|
659
|
+
params['project'] = project
|
|
660
|
+
r = self._client._http_request(f'/reports/scheduled/{report}/logs', 'GET', params=params)
|
|
661
|
+
return _ScheduledReportLogListAdapter.validate_json(r.text)
|
|
662
|
+
|
|
663
|
+
def get_report_log(self, report: str, log: str) -> ScheduledReportLog:
|
|
664
|
+
r = self._client._http_request(f'/reports/scheduled/{report}/logs/{log}', 'GET')
|
|
665
|
+
return ScheduledReportLog.model_validate_json(r.text)
|
|
666
|
+
|
|
667
|
+
def create_report_revision(self, project: str, revision_date: datetime, description: str, source_code_data_url: str) -> ReportSourceCodeRevision:
|
|
668
|
+
json = {
|
|
669
|
+
'revision_date': revision_date.isoformat(),
|
|
670
|
+
'description': description,
|
|
671
|
+
'source_code_data_url': source_code_data_url,
|
|
672
|
+
}
|
|
673
|
+
r = self._client._http_request('/reports/revisions', 'PUT', json=json, params={'project': project})
|
|
674
|
+
return ReportSourceCodeRevision.model_validate_json(r.text)
|