lvi-api-client 0.2.1__tar.gz → 0.2.2__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1 @@
1
+ 0.2.2
@@ -3887,6 +3887,10 @@ class LogicVeinAPIClient:
3887
3887
  Calls Collection.addMonitorsToSet. The setId must be provided both as a top-level
3888
3888
  parameter and within each monitor object. Returns the saved monitors with assigned IDs.
3889
3889
 
3890
+ This always adds: the server stores a new monitor even for a name already in the
3891
+ set, or with the ID of an existing monitor, and leaves the old one as it was. Use
3892
+ :meth:`save_monitor` to change one.
3893
+
3890
3894
  Each monitor object should have:
3891
3895
  {
3892
3896
  "monitorId": int - Use -1 for new monitors; assigned ID returned in response,
@@ -3905,7 +3909,7 @@ class LogicVeinAPIClient:
3905
3909
 
3906
3910
  Args:
3907
3911
  set_id (int): The monitor set ID to add monitors to.
3908
- monitors (List[Dict[str, Any]]): List of monitor objects to add/update.
3912
+ monitors (List[Dict[str, Any]]): List of monitor objects to add.
3909
3913
 
3910
3914
  Returns:
3911
3915
  List[Dict[str, Any]]: List of saved monitor objects with assigned monitorIds.
@@ -3936,6 +3940,156 @@ class LogicVeinAPIClient:
3936
3940
  return True
3937
3941
  return False
3938
3942
 
3943
+ def create_monitor_set(
3944
+ self, set_name: str, managed_networks: List[str], smart_set: bool = False
3945
+ ) -> Optional[Dict[str, Any]]:
3946
+ """Create a monitor set.
3947
+
3948
+ A name already taken in those networks makes the server answer HTTP 500, which
3949
+ raises, and stores nothing.
3950
+
3951
+ Args:
3952
+ set_name (str): The set's name. Use :meth:`is_monitor_set_name_unique` to check it first.
3953
+ managed_networks (List[str]): The networks the set belongs to, at least one.
3954
+ smart_set (bool): Whether this is a smart set.
3955
+
3956
+ Returns:
3957
+ Dict[str, Any]: The saved set (``setId``, ``setName``, ``smartSet``,
3958
+ ``managedNetworks``), or None on failure.
3959
+
3960
+ Raises:
3961
+ ValueError: If no network is given. An empty list was not tried against a
3962
+ server, as a failed rule set save with no networks leaves a row that
3963
+ cannot be removed.
3964
+ """
3965
+ if not managed_networks:
3966
+ raise ValueError("A monitor set needs at least one managed network")
3967
+ return self._save_monitor_set(
3968
+ {"setId": -1, "setName": set_name, "smartSet": smart_set, "managedNetworks": managed_networks}
3969
+ )
3970
+
3971
+ def update_monitor_set(
3972
+ self,
3973
+ set_id: int,
3974
+ set_name: Optional[str] = None,
3975
+ managed_networks: Optional[List[str]] = None,
3976
+ smart_set: Optional[bool] = None,
3977
+ ) -> Optional[Dict[str, Any]]:
3978
+ """Change a monitor set, keeping what is not given.
3979
+
3980
+ Args:
3981
+ set_id (int): The ID of the set.
3982
+ set_name (Optional[str]): A new name, or None to keep it.
3983
+ managed_networks (Optional[List[str]]): New networks, at least one, or None to keep them.
3984
+ smart_set (Optional[bool]): Whether it is a smart set, or None to keep it.
3985
+
3986
+ Returns:
3987
+ Dict[str, Any]: The saved set, or None on failure.
3988
+
3989
+ Raises:
3990
+ ValueError: If the set does not exist, or an empty list of networks is given.
3991
+ """
3992
+ if managed_networks is not None and not managed_networks:
3993
+ raise ValueError("A monitor set needs at least one managed network")
3994
+ monitor_set = self.get_monitor_set(set_id)
3995
+ if not monitor_set:
3996
+ raise ValueError(f"Monitor set {set_id} does not exist")
3997
+ changes = {"setName": set_name, "managedNetworks": managed_networks, "smartSet": smart_set}
3998
+ return self._save_monitor_set({**monitor_set, **{k: v for k, v in changes.items() if v is not None}})
3999
+
4000
+ def _save_monitor_set(self, monitor_set: Dict[str, Any]) -> Optional[Dict[str, Any]]:
4001
+ """Save a monitor set record.
4002
+
4003
+ Args:
4004
+ monitor_set (Dict[str, Any]): The whole record. A ``setId`` of -1 creates a set.
4005
+
4006
+ Returns:
4007
+ Dict[str, Any]: The saved set, or None on failure.
4008
+ """
4009
+ response = self.call("Collection.saveMonitorSet", monitorSet=monitor_set)
4010
+ if response and isinstance(response, dict) and "result" in response:
4011
+ return response["result"]
4012
+ return None
4013
+
4014
+ def delete_monitor_sets(self, set_ids: List[int]) -> bool:
4015
+ """Delete monitor sets, with their monitors.
4016
+
4017
+ This also takes those monitors off the devices they were deployed to. The server
4018
+ answers success for a set that does not exist, so True does not show that one did.
4019
+
4020
+ Args:
4021
+ set_ids (List[int]): The IDs of the sets to delete.
4022
+
4023
+ Returns:
4024
+ bool: True if successful (result is null), otherwise False.
4025
+ """
4026
+ response = self.call("Collection.deleteMonitorSets", setIds=set_ids)
4027
+ return bool(response and isinstance(response, dict) and "result" in response and response["result"] is None)
4028
+
4029
+ def is_monitor_set_name_unique(self, set_name: str, managed_networks: List[str], ignore_set_id: int = -1) -> bool:
4030
+ """Check whether a monitor set name is free in some networks.
4031
+
4032
+ Args:
4033
+ set_name (str): The name to check.
4034
+ managed_networks (List[str]): The networks to check it in.
4035
+ ignore_set_id (int): A set to leave out, such as the one being renamed. -1 for none.
4036
+
4037
+ Returns:
4038
+ bool: True if the name is free.
4039
+
4040
+ Raises:
4041
+ ValueError: If the server's answer is not true or false.
4042
+ """
4043
+ response = self.call(
4044
+ "Collection.isMonitorSetNameUnique",
4045
+ setName=set_name,
4046
+ managedNetworks=managed_networks,
4047
+ ignoreSetId=ignore_set_id,
4048
+ )
4049
+ unique = response.get("result") if isinstance(response, dict) else None
4050
+ if not isinstance(unique, bool):
4051
+ raise ValueError(f"Collection.isMonitorSetNameUnique did not answer true or false for {set_name}")
4052
+ return unique
4053
+
4054
+ def save_monitor(self, monitor: Dict[str, Any]) -> Optional[Dict[str, Any]]:
4055
+ """Update a monitor in place.
4056
+
4057
+ The monitor keeps its ID, and a device it is deployed to gets the change.
4058
+
4059
+ Args:
4060
+ monitor (Dict[str, Any]): The whole record as :meth:`get_monitors_for_set` returns
4061
+ it, with the changes made.
4062
+
4063
+ Returns:
4064
+ Dict[str, Any]: The saved monitor, or None on failure.
4065
+ """
4066
+ response = self.call("Collection.saveMonitor", monitor=monitor)
4067
+ if response and isinstance(response, dict) and "result" in response:
4068
+ return response["result"]
4069
+ return None
4070
+
4071
+ def get_monitors_for_host(self, device_uuid: str, network: str) -> List[Dict[str, Any]]:
4072
+ """Get the monitors deployed to a device.
4073
+
4074
+ Args:
4075
+ device_uuid (str): UUID of the device.
4076
+ network (str): Network the device belongs to.
4077
+
4078
+ Returns:
4079
+ List[Dict[str, Any]]: The monitors, each with ``monitorId``, ``monitorName`` and
4080
+ ``setId``. The list is empty only when the server says the device has none,
4081
+ which it also says for a device that does not exist.
4082
+
4083
+ Raises:
4084
+ ValueError: If the server's answer is not a list. Callers check this list before
4085
+ deploying, so a failed read must not look like no monitors.
4086
+ """
4087
+ response = self.call("Collection.getMonitorsForHost", uuid=device_uuid, network=network)
4088
+ monitors = response.get("result") if isinstance(response, dict) else None
4089
+ if not isinstance(monitors, list):
4090
+ raise ValueError(f"Collection.getMonitorsForHost did not return a list for device {device_uuid}")
4091
+ return monitors
4092
+
3939
4093
  def validate_monitor_sets_against_device(
3940
4094
  self, monitor_set_ids: List[int], host_queries: List[str]
3941
4095
  ) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -329,6 +329,96 @@ class TestTriggers:
329
329
  assert server.sent("Scheduler.deleteTrigger") == {"triggerId": 3}
330
330
 
331
331
 
332
+ class TestMonitorSets:
333
+ SET = {"setId": 5, "setName": "s1", "smartSet": False, "managedNetworks": ["n1"]}
334
+
335
+ def test_a_new_set_is_saved_with_an_id_of_minus_one(self, connect):
336
+ client, server = connect({"Collection.saveMonitorSet": self.SET})
337
+ assert client.create_monitor_set("s1", ["n1"]) == self.SET
338
+ assert server.sent("Collection.saveMonitorSet") == {
339
+ "monitorSet": {"setId": -1, "setName": "s1", "smartSet": False, "managedNetworks": ["n1"]}
340
+ }
341
+
342
+ def test_a_new_smart_set_says_so(self, connect):
343
+ client, server = connect({"Collection.saveMonitorSet": self.SET})
344
+ client.create_monitor_set("s1", ["n1"], smart_set=True)
345
+ assert server.sent("Collection.saveMonitorSet")["monitorSet"]["smartSet"] is True
346
+
347
+ def test_a_new_set_with_no_networks_sends_nothing(self, connect):
348
+ client, server = connect()
349
+ with pytest.raises(ValueError):
350
+ client.create_monitor_set("s1", [])
351
+ assert server.requests == []
352
+
353
+ def test_an_update_keeps_what_it_is_not_given(self, connect):
354
+ client, server = connect({"Collection.getMonitorSet": self.SET, "Collection.saveMonitorSet": self.SET})
355
+ client.update_monitor_set(5, set_name="s2")
356
+ assert server.sent("Collection.saveMonitorSet") == {"monitorSet": {**self.SET, "setName": "s2"}}
357
+
358
+ def test_an_update_can_clear_smart_set(self, connect):
359
+ stored = {**self.SET, "smartSet": True}
360
+ client, server = connect({"Collection.getMonitorSet": stored, "Collection.saveMonitorSet": self.SET})
361
+ client.update_monitor_set(5, smart_set=False)
362
+ assert server.sent("Collection.saveMonitorSet")["monitorSet"]["smartSet"] is False
363
+
364
+ @pytest.mark.parametrize(
365
+ "stored, args",
366
+ [
367
+ pytest.param(None, {"set_name": "s2"}, id="no such set"),
368
+ pytest.param(SET, {"managed_networks": []}, id="no networks"),
369
+ ],
370
+ )
371
+ def test_an_update_that_cannot_apply_saves_nothing(self, connect, stored, args):
372
+ client, server = connect({"Collection.getMonitorSet": stored})
373
+ with pytest.raises(ValueError):
374
+ client.update_monitor_set(5, **args)
375
+ assert "Collection.saveMonitorSet" not in [name for name, _ in server.requests]
376
+
377
+ @pytest.mark.parametrize("answer, expected", [(None, True), (False, False)])
378
+ def test_a_delete_is_by_set_ids_and_null_is_success(self, connect, answer, expected):
379
+ client, server = connect({"Collection.deleteMonitorSets": answer})
380
+ assert client.delete_monitor_sets([5, 6]) is expected
381
+ assert server.sent("Collection.deleteMonitorSets") == {"setIds": [5, 6]}
382
+
383
+ @pytest.mark.parametrize("answer", [True, False])
384
+ def test_a_name_check_sends_its_scope_and_returns_the_answer(self, connect, answer):
385
+ client, server = connect({"Collection.isMonitorSetNameUnique": answer})
386
+ assert client.is_monitor_set_name_unique("s1", ["n1"], ignore_set_id=5) is answer
387
+ assert server.sent("Collection.isMonitorSetNameUnique") == {
388
+ "setName": "s1",
389
+ "managedNetworks": ["n1"],
390
+ "ignoreSetId": 5,
391
+ }
392
+
393
+ def test_a_name_check_with_no_answer_raises_rather_than_reading_as_taken(self, connect):
394
+ client, _ = connect({"Collection.isMonitorSetNameUnique": None})
395
+ with pytest.raises(ValueError):
396
+ client.is_monitor_set_name_unique("s1", ["n1"])
397
+
398
+ def test_a_monitor_is_saved_as_a_whole_record(self, connect):
399
+ monitor = {"monitorId": 3, "monitorName": "m1", "setId": 5, "json": {"port": 22}}
400
+ client, server = connect({"Collection.saveMonitor": monitor})
401
+ assert client.save_monitor(monitor) == monitor
402
+ assert server.sent("Collection.saveMonitor") == {"monitor": monitor}
403
+
404
+ def test_a_devices_monitors_are_read_by_uuid_and_network(self, connect):
405
+ rows = [{"monitorId": 3, "monitorName": "m1", "setId": 5}]
406
+ client, server = connect({"Collection.getMonitorsForHost": rows})
407
+ assert client.get_monitors_for_host("u1", "n1") == rows
408
+ assert server.sent("Collection.getMonitorsForHost") == {"uuid": "u1", "network": "n1"}
409
+
410
+ def test_a_device_with_no_monitors_gives_an_empty_list(self, connect):
411
+ client, _ = connect({"Collection.getMonitorsForHost": []})
412
+ assert client.get_monitors_for_host("u1", "n1") == []
413
+
414
+ @pytest.mark.parametrize("answer", [None, {"result": None}, {}, "<html>error</html>"])
415
+ def test_an_answer_that_is_not_a_list_raises_rather_than_reading_as_no_monitors(self, connect, answer):
416
+ client, _ = connect()
417
+ with patch.object(client, "call", return_value=answer):
418
+ with pytest.raises(ValueError):
419
+ client.get_monitors_for_host("u1", "n1")
420
+
421
+
332
422
  class TestCreateRuleSet:
333
423
  def test_an_agreeing_document_is_sent_unchanged(self, connect):
334
424
  client, server = connect(SAVED)
@@ -1 +0,0 @@
1
- 0.2.1
File without changes
File without changes
File without changes