mercuto-client 0.3.0__py3-none-any.whl → 0.3.4a3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,9 @@
1
- import mimetypes
2
- import os
3
1
  from datetime import datetime, timedelta
4
2
  from typing import TYPE_CHECKING, Any, Literal, Optional
5
3
 
6
4
  from pydantic import TypeAdapter
7
5
 
8
- from . import _PayloadType
6
+ from . import PayloadType
9
7
  from ._util import BaseModel, serialise_timedelta
10
8
 
11
9
  if TYPE_CHECKING:
@@ -100,6 +98,15 @@ class Event(BaseModel):
100
98
  tags: list[EventTag]
101
99
 
102
100
 
101
+ class EventStatisticsOut(BaseModel):
102
+ n_events_last_week: int
103
+ n_events_last_month: int
104
+ n_events_last_year: int
105
+ n_events_all_time: int
106
+ n_events_in_range: int
107
+ last_event: Optional[Event] = None
108
+
109
+
103
110
  UserContactMethod = Literal['EMAIL', 'SMS']
104
111
 
105
112
 
@@ -124,7 +131,7 @@ class AlertConfiguration(BaseModel):
124
131
  project: str
125
132
  label: str
126
133
  conditions: list[Condition]
127
- contact_group: Optional[ContactGroup]
134
+ contact_group: Optional[str]
128
135
  retrigger_interval: Optional[datetime]
129
136
 
130
137
 
@@ -194,85 +201,27 @@ class Device(BaseModel):
194
201
  channels: list[DeviceChannel]
195
202
 
196
203
 
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):
204
+ class DeviceGroup(BaseModel):
210
205
  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
206
+ project: ItemCode
239
207
  label: str
240
- revision: str
241
- schedule: Optional[str]
242
- contact_group: Optional[str]
243
- last_scheduled: Optional[str]
208
+ description: str
209
+ group_label: Optional[str] = None
244
210
 
245
211
 
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
212
+ class EventAggregate(BaseModel):
213
+ aggregate: Literal["max", "greatest", "min", "median",
214
+ "abs-max", "mean", "rms", "peak-to-peak", "daf"]
215
+ enabled: bool = True
216
+ options: Optional[dict[str, Any]] = None
264
217
 
265
218
 
266
219
  _ProjectListAdapter = TypeAdapter(list[Project])
267
220
  _EventsListAdapter = TypeAdapter(list[Event])
268
221
  _DevicesListAdapter = TypeAdapter(list[Device])
269
222
  _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])
223
+ _DeviceGroupListAdapter = TypeAdapter(list[DeviceGroup])
224
+ _ConditionListAdapter = TypeAdapter(list[Condition])
276
225
 
277
226
 
278
227
  class MercutoCoreService:
@@ -280,7 +229,7 @@ class MercutoCoreService:
280
229
  self._client = client
281
230
 
282
231
  def healthcheck(self) -> Healthcheck:
283
- r = self._client._http_request("/healthcheck", "GET")
232
+ r = self._client.request("/healthcheck", "GET")
284
233
  return Healthcheck.model_validate_json(r.text)
285
234
 
286
235
  # Projects
@@ -288,11 +237,11 @@ class MercutoCoreService:
288
237
  def get_project(self, code: str) -> Project:
289
238
  if len(code) == 0:
290
239
  raise ValueError("Project code must not be empty")
291
- r = self._client._http_request(f'/projects/{code}', 'GET')
240
+ r = self._client.request(f'/projects/{code}', 'GET')
292
241
  return Project.model_validate_json(r.text)
293
242
 
294
243
  def list_projects(self) -> list[Project]:
295
- r = self._client._http_request('/projects', 'GET')
244
+ r = self._client.request('/projects', 'GET')
296
245
  return _ProjectListAdapter.validate_json(r.text)
297
246
 
298
247
  def create_project(self, name: str, project_number: str, description: str, tenant: str,
@@ -300,7 +249,7 @@ class MercutoCoreService:
300
249
  latitude: Optional[float] = None,
301
250
  longitude: Optional[float] = None) -> Project:
302
251
 
303
- payload: _PayloadType = {
252
+ payload: PayloadType = {
304
253
  'name': name,
305
254
  'project_number': project_number,
306
255
  'description': description,
@@ -312,25 +261,29 @@ class MercutoCoreService:
312
261
  if longitude is not None:
313
262
  payload['longitude'] = longitude
314
263
 
315
- r = self._client._http_request('/projects', 'PUT', json=payload)
264
+ r = self._client.request('/projects', 'PUT', json=payload)
316
265
  return Project.model_validate_json(r.text)
317
266
 
318
267
  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})
268
+ self._client.request(
269
+ f'/projects/{project}/ping', 'POST', json={'ip_address': ip_address})
320
270
 
321
271
  def create_dashboard(self, project_code: str, dashboards: Dashboards) -> None:
322
272
  json = dashboards.model_dump()
323
- self._client._http_request(f'/projects/{project_code}/dashboard', 'POST', json=json)
273
+ self._client.request(
274
+ f'/projects/{project_code}/dashboard', 'POST', json=json)
324
275
 
325
276
  def set_project_event_detection(self, project: str, datatables: list[str]) -> ProjectEventDetection:
326
277
  if len(datatables) == 0:
327
- raise ValueError('At least one datatable must be provided to enable event detection')
278
+ raise ValueError(
279
+ 'At least one datatable must be provided to enable event detection')
328
280
 
329
- params: _PayloadType = {
281
+ params: PayloadType = {
330
282
  "enabled": True,
331
283
  "datatables": datatables
332
284
  }
333
- r = self._client._http_request(f'/projects/{project}/event-detection', 'POST', json=params)
285
+ r = self._client.request(
286
+ f'/projects/{project}/event-detection', 'POST', json=params)
334
287
  return ProjectEventDetection.model_validate_json(r.text)
335
288
 
336
289
  # EVENTS
@@ -339,25 +292,47 @@ class MercutoCoreService:
339
292
  if start_time.tzinfo is None or end_time.tzinfo is None:
340
293
  raise ValueError("Timestamp must be timezone aware")
341
294
 
342
- json: _PayloadType = {
295
+ json: PayloadType = {
343
296
  'project': project,
344
297
  'start_time': start_time.isoformat(),
345
298
  'end_time': end_time.isoformat(),
346
299
  }
347
- r = self._client._http_request('/events', 'PUT', json=json)
300
+ r = self._client.request('/events', 'PUT', json=json)
348
301
  return Event.model_validate_json(r.text)
349
302
 
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)
303
+ def list_events(self, project: str,
304
+ start_time: Optional[datetime] = None,
305
+ end_time: Optional[datetime] = None,
306
+ limit: Optional[int] = None, offset: Optional[int] = 0,
307
+ ascending: bool = True) -> list[Event]:
308
+ """
309
+ Lists events for a project, optionally filtered by time range.
310
+ :param project: Project code to list events for.
311
+ :param start_time: Optional start time to filter events from.
312
+ :param end_time: Optional end time to filter events to.
313
+ :param limit: Optional maximum number of events to return. Default is set by API (usually 10).
314
+ :param offset: Optional offset for pagination.
315
+ :param ascending: Whether to sort events in ascending order by start time.
316
+ :return: List of Event objects.
317
+ """
318
+ params: PayloadType = {'project_code': project, 'ascending': ascending}
319
+ if start_time is not None:
320
+ params['start_time'] = start_time.isoformat()
321
+ if end_time is not None:
322
+ params['end_time'] = end_time.isoformat()
323
+ if limit is not None:
324
+ params['limit'] = limit
325
+ if offset is not None:
326
+ params['offset'] = offset
327
+ r = self._client.request('/events', 'GET', params=params)
353
328
  return _EventsListAdapter.validate_json(r.text)
354
329
 
355
330
  def get_event(self, event: str) -> Event:
356
- r = self._client._http_request(f'/events/{event}', 'GET')
331
+ r = self._client.request(f'/events/{event}', 'GET')
357
332
  return Event.model_validate_json(r.text)
358
333
 
359
334
  def delete_event(self, event: str) -> None:
360
- self._client._http_request(f'/events/{event}', 'DELETE')
335
+ self._client.request(f'/events/{event}', 'DELETE')
361
336
 
362
337
  def get_nearest_event(
363
338
  self,
@@ -365,32 +340,55 @@ class MercutoCoreService:
365
340
  to: datetime,
366
341
  maximum_delta: timedelta | None = None,
367
342
  ) -> Event:
368
- params: _PayloadType = {
343
+ params: PayloadType = {
369
344
  'project_code': project_code,
370
345
  'to': to.isoformat(),
371
346
  }
372
347
  if maximum_delta is not None:
373
348
  params['maximum_delta'] = serialise_timedelta(maximum_delta)
374
349
 
375
- r = self._client._http_request('/events/nearest', 'GET', params=params)
350
+ r = self._client.request('/events/nearest', 'GET', params=params)
376
351
  return Event.model_validate_json(r.text)
377
352
 
353
+ def get_event_statistics(
354
+ self,
355
+ project_code: str,
356
+ start_time: datetime,
357
+ end_time: datetime,
358
+ ) -> EventStatisticsOut:
359
+ params: PayloadType = {
360
+ 'project_code': project_code,
361
+ 'start_time': start_time.isoformat(),
362
+ 'end_time': end_time.isoformat(),
363
+ }
364
+
365
+ r = self._client.request('/events/statistics', 'GET', params=params)
366
+ return EventStatisticsOut.model_validate_json(r.text)
367
+
378
368
  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})
369
+ self._client.request('/aggregates', 'PUT',
370
+ json=[agg.model_dump(mode='json') for agg in aggregates], # type: ignore
371
+ params={'project_code': project})
382
372
 
383
373
  # ALERTS
374
+ def list_conditions(self, project: str, limit: int = 100, offset: int = 0) -> list[Condition]:
375
+ params: PayloadType = {
376
+ 'project': project,
377
+ 'limit': limit,
378
+ 'offset': offset
379
+ }
380
+ r = self._client.request('/alerts/conditions', 'GET', params=params)
381
+ return _ConditionListAdapter.validate_json(r.text)
384
382
 
385
383
  def get_condition(self, code: str) -> Condition:
386
- r = self._client._http_request(f'/alerts/conditions/{code}', 'GET')
384
+ r = self._client.request(f'/alerts/conditions/{code}', 'GET')
387
385
  return Condition.model_validate_json(r.text)
388
386
 
389
387
  def create_condition(self, source: str, description: str, *,
390
388
  lower_bound: Optional[float] = None,
391
389
  upper_bound: Optional[float] = None,
392
390
  neutral_position: float = 0) -> Condition:
393
- json: _PayloadType = {
391
+ json: PayloadType = {
394
392
  'source_channel_code': source,
395
393
  'description': description,
396
394
  'neutral_position': neutral_position
@@ -399,24 +397,25 @@ class MercutoCoreService:
399
397
  json['lower_inclusive_bound'] = lower_bound
400
398
  if upper_bound is not None:
401
399
  json['upper_exclusive_bound'] = upper_bound
402
- r = self._client._http_request('/alerts/conditions', 'PUT', json=json)
400
+ r = self._client.request('/alerts/conditions', 'PUT', json=json)
403
401
  return Condition.model_validate_json(r.text)
404
402
 
405
403
  def create_alert_configuration(self, label: str,
406
404
  conditions: list[str],
407
405
  contact_group: Optional[str] = None) -> AlertConfiguration:
408
- json: _PayloadType = {
406
+ json: PayloadType = {
409
407
  'label': label,
410
408
  'conditions': conditions,
411
409
 
412
410
  }
413
411
  if contact_group is not None:
414
412
  json['contact_group'] = contact_group
415
- r = self._client._http_request('/alerts/configurations', 'PUT', json=json)
413
+ r = self._client.request(
414
+ '/alerts/configurations', 'PUT', json=json)
416
415
  return AlertConfiguration.model_validate_json(r.text)
417
416
 
418
417
  def get_alert_configuration(self, code: str) -> AlertConfiguration:
419
- r = self._client._http_request(f'/alerts/configurations/{code}', 'GET')
418
+ r = self._client.request(f'/alerts/configurations/{code}', 'GET')
420
419
  return AlertConfiguration.model_validate_json(r.text)
421
420
 
422
421
  def list_alert_logs(
@@ -430,7 +429,7 @@ class MercutoCoreService:
430
429
  offset: int = 0,
431
430
  latest_only: bool = False,
432
431
  ) -> AlertSummary:
433
- params: _PayloadType = {
432
+ params: PayloadType = {
434
433
  'limit': limit,
435
434
  'offset': offset,
436
435
  'latest_only': latest_only,
@@ -443,39 +442,41 @@ class MercutoCoreService:
443
442
  if channels is not None:
444
443
  params['channels'] = channels
445
444
  if start_time is not None:
446
- params['start_time'] = start_time.isoformat() if isinstance(start_time, datetime) else start_time
445
+ params['start_time'] = start_time.isoformat() if isinstance(
446
+ start_time, datetime) else start_time
447
447
  if end_time is not None:
448
- params['end_time'] = end_time.isoformat() if isinstance(end_time, datetime) else end_time
448
+ params['end_time'] = end_time.isoformat() if isinstance(
449
+ end_time, datetime) else end_time
449
450
 
450
- r = self._client._http_request('/alerts/logs', 'GET', params=params)
451
+ r = self._client.request('/alerts/logs', 'GET', params=params)
451
452
  return AlertSummary.model_validate_json(r.text)
452
453
 
453
454
  # DEVICES
454
455
 
455
456
  def list_device_types(self) -> list[DeviceType]:
456
- r = self._client._http_request('/devices/types', 'GET')
457
+ r = self._client.request('/devices/types', 'GET')
457
458
  return _DeviceTypeListAdapter.validate_json(r.text)
458
459
 
459
460
  def create_device_type(self, description: str, manufacturer: str, model_number: str) -> DeviceType:
460
- json: _PayloadType = {
461
+ json: PayloadType = {
461
462
  'description': description,
462
463
  'manufacturer': manufacturer,
463
464
  'model_number': model_number
464
465
  }
465
- r = self._client._http_request('/devices/types', 'PUT', json=json)
466
+ r = self._client.request('/devices/types', 'PUT', json=json)
466
467
  return DeviceType.model_validate_json(r.text)
467
468
 
468
469
  def list_devices(self, project_code: str, limit: int, offset: int) -> list[Device]:
469
- params: _PayloadType = {
470
+ params: PayloadType = {
470
471
  'project_code': project_code,
471
472
  'limit': limit,
472
473
  'offset': offset
473
474
  }
474
- r = self._client._http_request('/devices', 'GET', params=params)
475
+ r = self._client.request('/devices', 'GET', params=params)
475
476
  return _DevicesListAdapter.validate_json(r.text)
476
477
 
477
478
  def get_device(self, device_code: str) -> Device:
478
- r = self._client._http_request(f'/devices/{device_code}', 'GET')
479
+ r = self._client.request(f'/devices/{device_code}', 'GET')
479
480
  return Device.model_validate_json(r.text)
480
481
 
481
482
  def create_device(self,
@@ -485,7 +486,7 @@ class MercutoCoreService:
485
486
  groups: list[str],
486
487
  location_description: Optional[str] = None,
487
488
  channels: Optional[list[DeviceChannel]] = None) -> Device:
488
- json: _PayloadType = {
489
+ json: PayloadType = {
489
490
  'project_code': project_code,
490
491
  'label': label,
491
492
  'device_type_code': device_type_code,
@@ -495,180 +496,9 @@ class MercutoCoreService:
495
496
  json['location_description'] = location_description
496
497
  if channels is not None:
497
498
  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
+ r = self._client.request('/devices', 'PUT', json=json)
499
500
  return Device.model_validate_json(r.text)
500
501
 
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)
502
+ def list_device_groups(self, project: str) -> list[DeviceGroup]:
503
+ r = self._client.request('/devices/groups', 'GET', params={'project_code': project})
504
+ return _DeviceGroupListAdapter.validate_json(r.text)