lvi-api-client 0.2.7__tar.gz → 0.2.9__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.7
3
+ Version: 0.2.9
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.9
@@ -3,7 +3,7 @@ import json
3
3
  import logging
4
4
  import os
5
5
  import re
6
- from typing import Any, Dict, List, MutableMapping, Optional, Sequence, Tuple, Union
6
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
7
7
  from xml.etree import ElementTree
8
8
  from xml.sax.saxutils import escape
9
9
 
@@ -211,7 +211,10 @@ class LogicVeinAPIClient:
211
211
  f"LogicVeinAPIClient initialized: host={self.host}, username={self.username}, using_token={bool(self.api_token)}, ignore_env={ignore_env}"
212
212
  )
213
213
  self.base_url = f"https://{self.host}/rest"
214
- self.headers: MutableMapping[str, Union[str, bytes]] = {"Content-Type": "application/json"}
214
+ # The session holds this same mapping, so its headers are exactly these (requests' defaults are not sent).
215
+ self.headers: requests.structures.CaseInsensitiveDict[str] = requests.structures.CaseInsensitiveDict(
216
+ {"Content-Type": "application/json"}
217
+ )
215
218
  if self.api_token:
216
219
  self.headers["Authorization"] = f"Bearer {self.api_token}"
217
220
  self.lvi_client = requests.Session()
@@ -4394,41 +4397,69 @@ class LogicVeinAPIClient:
4394
4397
  return None
4395
4398
 
4396
4399
  def delete_alert_policy(self, policy_id: int, reassign_policy_id: int = -1) -> bool:
4397
- """Delete an alert policy, with its actions.
4400
+ """Delete an alert policy, with its actions, refusing one still in use.
4398
4401
 
4399
- Calls Collection.deleteAlertPolicy, with the parameters the web UI sends. The server
4400
- refuses ``policyId`` alone. The UI asks first whether the policy is referenced
4401
- (:meth:`is_alert_policy_referenced`); what the server does with the references of a
4402
- referenced policy deleted with ``reassign_policy_id`` -1 has not been measured.
4402
+ A policy that alert triggers still use (:meth:`is_alert_policy_referenced`) may not be
4403
+ deleted on its own: as in the web UI, which asks for an alternate policy to take those
4404
+ triggers, ``reassign_policy_id`` must name another existing policy. Otherwise this
4405
+ raises before deleting anything.
4403
4406
 
4404
- The server's answer is not relied on: the policies are read back afterwards. An answer
4405
- that cannot show whether the policy is gone raises rather than reading as deleted, and
4406
- errors from either call (``LogicVeinAPIError``, ``requests`` errors) propagate.
4407
+ Calls Collection.deleteAlertPolicy with ``policyId`` and ``reassignPolicyId``, as the UI
4408
+ does; the server refuses ``policyId`` alone. It answers true when it deletes the policy
4409
+ and false for an unknown ID. The policies are then read back, and the result is whether
4410
+ this one is gone: an answer that cannot show it raises rather than reading as deleted.
4411
+ Errors from the calls themselves (``LogicVeinAPIError``, ``requests`` errors) propagate.
4407
4412
 
4408
4413
  Args:
4409
4414
  policy_id (int): The ID of the policy to delete.
4410
- reassign_policy_id (int): The policy to move its references to, or -1 for none.
4415
+ reassign_policy_id (int): The policy to move its triggers to, required when the
4416
+ policy is in use; -1 for none.
4411
4417
 
4412
4418
  Returns:
4413
4419
  bool: True if the policy is gone afterwards.
4414
4420
 
4415
4421
  Raises:
4416
- ValueError: If the read-back answer has no list of policies, or a policy without an ID.
4417
- """
4422
+ ValueError: If the policy is in use and no other existing policy is given to take
4423
+ its triggers, if the policies cannot be read (an answer with no list of policies,
4424
+ or a policy without an ID), or if the reference check does not answer true or false.
4425
+ """
4426
+ if reassign_policy_id == policy_id:
4427
+ raise ValueError(f"Alert policy {policy_id} cannot take its own triggers; name another policy")
4428
+ in_use = self.is_alert_policy_referenced(policy_id)
4429
+ if reassign_policy_id != -1:
4430
+ if reassign_policy_id not in self._alert_policy_ids():
4431
+ raise ValueError(f"Alert policy {reassign_policy_id}, to take the triggers, does not exist")
4432
+ elif in_use:
4433
+ raise ValueError(
4434
+ f"Alert policy {policy_id} is used by alert triggers, so it may not be deleted until another "
4435
+ "policy takes them: pass reassign_policy_id"
4436
+ )
4418
4437
  self.call("Collection.deleteAlertPolicy", policyId=policy_id, reassignPolicyId=reassign_policy_id)
4419
- remaining = self._search_alert_policies([], [], "policyName", False, 500, strict=True)
4420
- ids = [policy.get("policyId") if isinstance(policy, dict) else None for policy in remaining]
4421
- if not all(isinstance(found, int) for found in ids):
4438
+ return policy_id not in self._alert_policy_ids()
4439
+
4440
+ def _alert_policy_ids(self) -> List[int]:
4441
+ """Read the IDs of every alert policy, failing on an answer that cannot list them.
4442
+
4443
+ Returns:
4444
+ List[int]: The IDs.
4445
+
4446
+ Raises:
4447
+ ValueError: If the answer has no list of policies, or a policy without an ID.
4448
+ """
4449
+ policies = self._search_alert_policies([], [], "policyName", False, 500, strict=True)
4450
+ ids = [policy.get("policyId") if isinstance(policy, dict) else None for policy in policies]
4451
+ known = [found for found in ids if isinstance(found, int)]
4452
+ if len(known) != len(ids):
4422
4453
  raise ValueError("Collection.searchAlertPolicies answered with a policy that has no ID")
4423
- return policy_id not in ids
4454
+ return known
4424
4455
 
4425
4456
  def is_alert_policy_referenced(self, policy_id: int) -> bool:
4426
4457
  """Check whether anything refers to an alert policy.
4427
4458
 
4428
- Calls Collection.isAlertPolicyReferenced, which the web UI calls before deleting one.
4429
- Monitor triggers name the policy they alert through, but they are not the only
4430
- references counted: on the lab, a policy no trigger names also answered true. An
4431
- unknown ID answers false.
4459
+ Calls Collection.isAlertPolicyReferenced, which the web UI calls before deleting one: a
4460
+ policy in use may only be deleted with another policy to take its alert triggers (see
4461
+ :meth:`delete_alert_policy`). On the lab, a policy no monitor trigger names also
4462
+ answered true, so more than monitor triggers count. An unknown ID answers false.
4432
4463
 
4433
4464
  Args:
4434
4465
  policy_id (int): The ID of the policy.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -7,6 +7,7 @@ import pytest
7
7
 
8
8
  _PATH = Path(__file__).resolve().parent.parent / "scripts" / "ci" / "check_version_bump.py"
9
9
  _SPEC = importlib.util.spec_from_file_location("check_version_bump", _PATH)
10
+ assert _SPEC is not None and _SPEC.loader is not None, _PATH
10
11
  check = importlib.util.module_from_spec(_SPEC)
11
12
  _SPEC.loader.exec_module(check)
12
13
 
@@ -48,6 +48,16 @@ class TestLogicVeinAPIClientUnit:
48
48
  assert "Authorization" in client.headers
49
49
  assert client.headers["Authorization"] == "Bearer test-token-123"
50
50
 
51
+ def test_the_session_sends_only_the_clients_headers(self, mock_env):
52
+ """The session holds the client's own header mapping, with no requests defaults added."""
53
+ client = LogicVeinAPIClient()
54
+ assert client.lvi_client.headers is client.headers
55
+ assert dict(client.lvi_client.headers) == {
56
+ "Content-Type": "application/json",
57
+ "Authorization": "Bearer test-token-123",
58
+ }
59
+ assert client.lvi_client.headers["content-type"] == "application/json"
60
+
51
61
  def test_initialization_with_kwargs(self, monkeypatch):
52
62
  """Test client initialization with kwargs instead of env vars."""
53
63
  # Clear env vars so kwargs take precedence
@@ -625,31 +625,77 @@ class TestDeployingASet:
625
625
  class TestAlertPolicies:
626
626
  """Deleting alert policies and the checks the web UI makes around saving and deleting them."""
627
627
 
628
- def test_a_delete_sends_what_the_ui_sends_and_reads_the_policies_back(self, connect):
628
+ @staticmethod
629
+ def listing(*ids):
630
+ return {"policies": [{"policyId": i, "policyName": f"p{i}"} for i in ids]}
631
+
632
+ def test_a_delete_of_a_policy_not_in_use_sends_what_the_ui_sends_and_reads_the_policies_back(self, connect):
629
633
  client, server = connect(
630
634
  {
631
- "Collection.deleteAlertPolicy": None,
632
- "Collection.searchAlertPolicies": {"policies": [{"policyId": 1, "policyName": "kept"}]},
635
+ "Collection.isAlertPolicyReferenced": False,
636
+ "Collection.deleteAlertPolicy": True,
637
+ "Collection.searchAlertPolicies": self.listing(1),
633
638
  }
634
639
  )
635
640
  assert client.delete_alert_policy(65) is True
641
+ assert server.sent("Collection.isAlertPolicyReferenced") == {"policyId": 65}
636
642
  assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": -1}
637
643
  assert server.sent("Collection.searchAlertPolicies")["managedNetworks"] == []
638
644
 
639
645
  def test_a_policy_still_listed_after_a_delete_is_not_deleted(self, connect):
640
646
  client, _ = connect(
641
647
  {
648
+ "Collection.isAlertPolicyReferenced": False,
649
+ "Collection.deleteAlertPolicy": False,
650
+ "Collection.searchAlertPolicies": self.listing(65),
651
+ }
652
+ )
653
+ assert client.delete_alert_policy(65) is False
654
+
655
+ def test_a_policy_in_use_is_not_deleted_without_a_policy_to_take_its_triggers(self, connect):
656
+ client, server = connect({"Collection.isAlertPolicyReferenced": True})
657
+ with pytest.raises(ValueError, match="used by alert triggers"):
658
+ client.delete_alert_policy(65)
659
+ assert [method for method, _ in server.requests] == ["Collection.isAlertPolicyReferenced"]
660
+
661
+ def test_a_policy_in_use_is_deleted_with_another_to_take_its_triggers(self, connect):
662
+ client, server = connect(
663
+ {
664
+ "Collection.isAlertPolicyReferenced": True,
642
665
  "Collection.deleteAlertPolicy": True,
643
- "Collection.searchAlertPolicies": {"policies": [{"policyId": 65, "policyName": "p"}]},
666
+ "Collection.searchAlertPolicies": self.listing(1, 65),
644
667
  }
645
668
  )
646
- assert client.delete_alert_policy(65, reassign_policy_id=1) is False
669
+ # The read-back still lists 65 because this fake does not delete; what matters is the request.
670
+ client.delete_alert_policy(65, reassign_policy_id=1)
671
+ assert server.sent("Collection.deleteAlertPolicy") == {"policyId": 65, "reassignPolicyId": 1}
672
+
673
+ @pytest.mark.parametrize("reassign", [65, 2])
674
+ def test_a_policy_to_take_the_triggers_must_be_another_that_exists(self, connect, reassign):
675
+ client, server = connect(
676
+ {"Collection.isAlertPolicyReferenced": True, "Collection.searchAlertPolicies": self.listing(1, 65)}
677
+ )
678
+ with pytest.raises(ValueError):
679
+ client.delete_alert_policy(65, reassign_policy_id=reassign)
680
+ assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
681
+
682
+ def test_a_reference_check_with_no_answer_deletes_nothing(self, connect):
683
+ client, server = connect({"Collection.isAlertPolicyReferenced": None})
684
+ with pytest.raises(ValueError):
685
+ client.delete_alert_policy(65)
686
+ assert "Collection.deleteAlertPolicy" not in [method for method, _ in server.requests]
647
687
 
648
688
  @pytest.mark.parametrize(
649
689
  "answer", [None, {}, {"policies": None}, {"policies": [{}]}, {"policies": [{"policyId": None}, "x"]}]
650
690
  )
651
691
  def test_a_failed_read_back_raises_rather_than_reading_as_deleted(self, connect, answer):
652
- client, _ = connect({"Collection.deleteAlertPolicy": None, "Collection.searchAlertPolicies": answer})
692
+ client, _ = connect(
693
+ {
694
+ "Collection.isAlertPolicyReferenced": False,
695
+ "Collection.deleteAlertPolicy": True,
696
+ "Collection.searchAlertPolicies": answer,
697
+ }
698
+ )
653
699
  with pytest.raises(ValueError):
654
700
  client.delete_alert_policy(65)
655
701
 
@@ -1 +0,0 @@
1
- 0.2.7
File without changes
File without changes
File without changes