lvi-api-client 0.2.0__tar.gz → 0.2.1__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.0
3
+ Version: 0.2.1
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.1
@@ -2907,8 +2907,14 @@ class LogicVeinAPIClient:
2907
2907
  time_zone: str = "Etc/GMT",
2908
2908
  paused: bool = False,
2909
2909
  filter_name: str = "",
2910
+ trigger_id: Optional[int] = None,
2910
2911
  ) -> Optional[Dict[str, Any]]:
2911
- """Schedule a trigger for an existing job.
2912
+ """Schedule a trigger for an existing job, or change one it has.
2913
+
2914
+ Without ``trigger_id`` this always adds a trigger, even one identical to
2915
+ a trigger the job already has, so each repeat adds another schedule. Use
2916
+ :meth:`get_job_triggers` to check first. With ``trigger_id`` it updates
2917
+ that trigger in place, and every field is set from these arguments.
2912
2918
 
2913
2919
  For a one-time run, pass an ISO datetime string as ``cron_expression``
2914
2920
  (e.g. ``"2026-03-17T09:00"``). Standard cron syntax works for recurring
@@ -2918,29 +2924,65 @@ class LogicVeinAPIClient:
2918
2924
  job_id (int): ID of the job to schedule (returned by :meth:`save_job`).
2919
2925
  trigger_name (str): Display name for the trigger.
2920
2926
  cron_expression (str): ISO datetime (one-time) or cron expression (recurring).
2921
- time_zone (str): Timezone name (e.g. ``"America/Halifax"``). Default ``"UTC"``.
2927
+ time_zone (str): Timezone name (e.g. ``"America/Halifax"``). Default ``"Etc/GMT"``.
2922
2928
  paused (bool): Whether to create the trigger in a paused state.
2923
2929
  filter_name (str): Optional filter name; pass empty string for none.
2930
+ trigger_id (Optional[int]): ID of the trigger to update, or None to add one.
2924
2931
 
2925
2932
  Returns:
2926
2933
  Dict[str, Any]: The saved trigger record (contains ``triggerId``,
2927
2934
  ``nextExpectedFireTime``, etc.), or None on failure.
2928
2935
  """
2929
- response = self.call(
2930
- "Scheduler.saveTrigger",
2931
- triggerData={
2932
- "triggerName": trigger_name,
2933
- "paused": paused,
2934
- "cronExpression": cron_expression,
2935
- "timeZone": time_zone,
2936
- "filterName": filter_name,
2937
- "jobId": job_id,
2938
- },
2939
- )
2936
+ trigger_data: Dict[str, Any] = {
2937
+ "triggerName": trigger_name,
2938
+ "paused": paused,
2939
+ "cronExpression": cron_expression,
2940
+ "timeZone": time_zone,
2941
+ "filterName": filter_name,
2942
+ "jobId": job_id,
2943
+ }
2944
+ if trigger_id is not None:
2945
+ trigger_data["triggerId"] = trigger_id
2946
+ response = self.call("Scheduler.saveTrigger", triggerData=trigger_data)
2940
2947
  if response and isinstance(response, dict) and "result" in response:
2941
2948
  return response["result"]
2942
2949
  return None
2943
2950
 
2951
+ def get_job_triggers(self, job_id: int) -> List[Dict[str, Any]]:
2952
+ """Get the triggers of a job.
2953
+
2954
+ Args:
2955
+ job_id (int): ID of the job. An unknown job has no triggers.
2956
+
2957
+ Returns:
2958
+ List[Dict[str, Any]]: The trigger records (``triggerId``, ``jobId``,
2959
+ ``triggerName``, ``cronExpression``, ``timeZone``, ``paused``,
2960
+ ``filterName``, ``nextExpectedFireTime``, ``accessLimited``). The list
2961
+ is empty only when the server says the job has none.
2962
+
2963
+ Raises:
2964
+ ValueError: If the server's answer is not a list. Callers check this list
2965
+ before adding a trigger, so a failed read must not look like no triggers.
2966
+ """
2967
+ response = self.call("Scheduler.getTriggersOfJob", jobId=job_id)
2968
+ triggers = response.get("result") if isinstance(response, dict) else None
2969
+ if not isinstance(triggers, list):
2970
+ raise ValueError(f"Scheduler.getTriggersOfJob did not return a list for job {job_id}")
2971
+ return triggers
2972
+
2973
+ def delete_trigger(self, trigger_id: int) -> bool:
2974
+ """Delete a trigger.
2975
+
2976
+ Args:
2977
+ trigger_id (int): ID of the trigger to delete.
2978
+
2979
+ Returns:
2980
+ bool: The server's answer. It answers True for a trigger that is
2981
+ already gone, so this does not show that one existed.
2982
+ """
2983
+ response = self.call("Scheduler.deleteTrigger", triggerId=trigger_id)
2984
+ return bool(response and isinstance(response, dict) and response.get("result"))
2985
+
2944
2986
  def create_command_runner_job(
2945
2987
  self,
2946
2988
  device_ip: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lvi-api-client
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: LogicVein API Client Library
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -281,6 +281,54 @@ XML = (
281
281
  SAVED = {"Compliance.saveRuleSet": {"ruleSetId": 5, "ruleSetName": "rs1"}}
282
282
 
283
283
 
284
+ class TestTriggers:
285
+ TRIGGER = {
286
+ "triggerName": "t1",
287
+ "paused": True,
288
+ "cronExpression": "0 2 * * *",
289
+ "timeZone": "Etc/GMT",
290
+ "filterName": "",
291
+ "jobId": 7,
292
+ }
293
+
294
+ def save(self, connect, **extra):
295
+ client, server = connect({"Scheduler.saveTrigger": {"triggerId": 3}})
296
+ saved = client.save_trigger(job_id=7, trigger_name="t1", cron_expression="0 2 * * *", paused=True, **extra)
297
+ return saved, server.sent("Scheduler.saveTrigger")
298
+
299
+ def test_a_save_without_an_id_sends_none_so_the_server_adds_one(self, connect):
300
+ saved, sent = self.save(connect)
301
+ assert saved == {"triggerId": 3}
302
+ assert sent == {"triggerData": self.TRIGGER}
303
+
304
+ def test_a_save_with_an_id_sends_it_so_the_server_updates_that_trigger(self, connect):
305
+ _, sent = self.save(connect, trigger_id=3)
306
+ assert sent == {"triggerData": {**self.TRIGGER, "triggerId": 3}}
307
+
308
+ def test_triggers_of_a_job_are_read_by_job_id(self, connect):
309
+ rows = [{"triggerId": 3, "triggerName": "t1"}]
310
+ client, server = connect({"Scheduler.getTriggersOfJob": rows})
311
+ assert client.get_job_triggers(7) == rows
312
+ assert server.sent("Scheduler.getTriggersOfJob") == {"jobId": 7}
313
+
314
+ def test_a_job_with_no_triggers_gives_an_empty_list(self, connect):
315
+ client, _ = connect({"Scheduler.getTriggersOfJob": []})
316
+ assert client.get_job_triggers(7) == []
317
+
318
+ @pytest.mark.parametrize("answer", [None, {"result": None}, {}, "<html>error</html>"])
319
+ def test_an_answer_that_is_not_a_list_raises_rather_than_reading_as_no_triggers(self, connect, answer):
320
+ client, _ = connect()
321
+ with patch.object(client, "call", return_value=answer):
322
+ with pytest.raises(ValueError):
323
+ client.get_job_triggers(7)
324
+
325
+ @pytest.mark.parametrize("answer", [True, False])
326
+ def test_a_delete_is_by_trigger_id_and_returns_the_answer(self, connect, answer):
327
+ client, server = connect({"Scheduler.deleteTrigger": answer})
328
+ assert client.delete_trigger(3) is answer
329
+ assert server.sent("Scheduler.deleteTrigger") == {"triggerId": 3}
330
+
331
+
284
332
  class TestCreateRuleSet:
285
333
  def test_an_agreeing_document_is_sent_unchanged(self, connect):
286
334
  client, server = connect(SAVED)
@@ -1 +0,0 @@
1
- 0.2.0
File without changes
File without changes
File without changes