simplepush 3.6.0__tar.gz → 3.7.0__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.5
2
2
  Name: simplepush
3
- Version: 3.6.0
3
+ Version: 3.7.0
4
4
  Summary: Python client for Simplepush (tasks, notifications, events, end-to-end encryption).
5
5
  Project-URL: Homepage, https://simplepu.sh
6
6
  Project-URL: Documentation, https://simplepu.sh/guide/python-sdk
@@ -113,8 +113,9 @@ form at once (by default each filled input arrives as an intermediate
113
113
  set); `reply=ReplyMode.STICKY` (or `"one-shot"` / `"one-time-per-user"`) shows
114
114
  recipients an in-thread reply composer (collect via `replies()`);
115
115
  `content_format=ContentFormat.MARKDOWN` renders a task's `content` as
116
- Markdown (notifications are always plain); `critical=True` sends an iOS
117
- Critical Alert.
116
+ Markdown (notifications are always plain); `priority=` sets how loudly the
117
+ push interrupts, 1 (minimal) to 5 (critical; sounds even on a muted phone),
118
+ default 3, with `critical_volume=` (0 to 1) for level 5 on iOS.
118
119
 
119
120
  A task can have **subtasks** appended to its chain. A subtask inherits the
120
121
  parent's recipients and encryption (no target, no password); its `inputs()` /
@@ -63,8 +63,9 @@ form at once (by default each filled input arrives as an intermediate
63
63
  set); `reply=ReplyMode.STICKY` (or `"one-shot"` / `"one-time-per-user"`) shows
64
64
  recipients an in-thread reply composer (collect via `replies()`);
65
65
  `content_format=ContentFormat.MARKDOWN` renders a task's `content` as
66
- Markdown (notifications are always plain); `critical=True` sends an iOS
67
- Critical Alert.
66
+ Markdown (notifications are always plain); `priority=` sets how loudly the
67
+ push interrupts, 1 (minimal) to 5 (critical; sounds even on a muted phone),
68
+ default 3, with `critical_volume=` (0 to 1) for level 5 on iOS.
68
69
 
69
70
  A task can have **subtasks** appended to its chain. A subtask inherits the
70
71
  parent's recipients and encryption (no target, no password); its `inputs()` /
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simplepush"
7
- version = "3.6.0"
7
+ version = "3.7.0"
8
8
  description = "Python client for Simplepush (tasks, notifications, events, end-to-end encryption)."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -62,6 +62,27 @@ from .client import (
62
62
 
63
63
  # --- Wire serialization helpers ---
64
64
 
65
+ def _priority_wire(priority, critical, critical_volume) -> dict:
66
+ """The `priority` / `criticalVolume` fields of a create request. `priority`
67
+ is 1 (minimal) to 5 (critical), absent = 3; the deprecated `critical` flag
68
+ means 5 when no priority is given. `critical_volume` (0 < v <= 1) is the
69
+ iOS critical alert volume and is only valid with level 5."""
70
+ level = priority if priority is not None else (5 if critical else None)
71
+ if level is not None and (not isinstance(level, int) or isinstance(level, bool) or not 1 <= level <= 5):
72
+ raise ValueError("priority must be an integer between 1 and 5")
73
+ if critical_volume is not None:
74
+ if level != 5:
75
+ raise ValueError("critical_volume applies to priority 5 only")
76
+ if not 0 < critical_volume <= 1:
77
+ raise ValueError("critical_volume must be greater than 0 and at most 1")
78
+ out: dict = {}
79
+ if level is not None:
80
+ out["priority"] = level
81
+ if critical_volume is not None:
82
+ out["criticalVolume"] = critical_volume
83
+ return out
84
+
85
+
65
86
  def _expires_at_wire(value) -> str:
66
87
  """Serialize a task deadline to wire ISO-8601. A naive datetime is
67
88
  rejected rather than guessed at — the server compares against UTC."""
@@ -595,6 +616,8 @@ class _BaseClient:
595
616
  password: str | None = None,
596
617
  tag: str | None = None,
597
618
  critical: bool = False,
619
+ priority: int | None = None,
620
+ critical_volume: float | None = None,
598
621
  reply: ReplyMode | Literal["one-shot", "sticky", "one-time-per-user"] | None = None,
599
622
  content_format: ContentFormat | Literal["plain", "markdown"] | None = None,
600
623
  shared: bool = False,
@@ -667,7 +690,7 @@ class _BaseClient:
667
690
  links=links, files=files,
668
691
  auto_commit=auto_commit,
669
692
  password=password if password is not None else self._send_password(topic),
670
- tag=tag, critical=critical, reply=reply,
693
+ tag=tag, critical=critical, priority=priority, critical_volume=critical_volume, reply=reply,
671
694
  content_format=content_format, shared=shared,
672
695
  expires_at=expires_at,
673
696
  )
@@ -687,6 +710,8 @@ class _BaseClient:
687
710
  password: str | None = None,
688
711
  tag: str | None = None,
689
712
  critical: bool = False,
713
+ priority: int | None = None,
714
+ critical_volume: float | None = None,
690
715
  shared: bool = False,
691
716
  ) -> "Notification | NotificationGroup":
692
717
  """Send a notification and return a handle to its event stream.
@@ -726,10 +751,10 @@ class _BaseClient:
726
751
  audio: Optional single audio clip (URL or local path), like `image`.
727
752
  Plays inline on iOS; Android has no inline audio. Mutually
728
753
  exclusive with `image` (a notification shows at most one media).
729
- link: Optional URL shown as an "Open link" button on the push when
730
- the notification has no input. Any scheme: an https URL opens
731
- the browser, an app's deep link (`unifi-protect://...`) opens
732
- that app.
754
+ link: Optional URL shown as an "Open link" button on the push. Any
755
+ scheme: an https URL opens the browser, an app's deep link
756
+ (`unifi-protect://...`) opens that app. Mutually exclusive
757
+ with `input` (the input's buttons take the action slots).
733
758
  password: Optional password to encrypt body fields with. Requires a
734
759
  topic (used as the salt for key derivation). Falls back to
735
760
  the client's default `password` when omitted. Not accepted
@@ -757,13 +782,13 @@ class _BaseClient:
757
782
  title=title, content=content, input=input,
758
783
  image=image, audio=audio, link=link,
759
784
  password=password if password is not None else self._send_password(topic),
760
- tag=tag, critical=critical, shared=shared,
785
+ tag=tag, critical=critical, priority=priority, critical_volume=critical_volume, shared=shared,
761
786
  )
762
787
 
763
788
  def _create_task(self, *, topic=None, member=None, broadcast=False,
764
789
  title=None, content=None, inputs=None, links=None,
765
790
  files=None,
766
- auto_commit=False, password=None, tag=None, critical=False,
791
+ auto_commit=False, password=None, tag=None, critical=False, priority=None, critical_volume=None,
767
792
  reply=None, content_format=None, shared=False,
768
793
  expires_at=None) -> "Task | TaskGroup":
769
794
  if not content and not inputs:
@@ -893,8 +918,7 @@ class _BaseClient:
893
918
  body["title"] = title
894
919
  if content is not None:
895
920
  body["content"] = content
896
- if critical:
897
- body["critical"] = critical
921
+ body.update(_priority_wire(priority, critical, critical_volume))
898
922
  if reply is not None:
899
923
  body["reply"] = reply.value if isinstance(reply, ReplyMode) else reply
900
924
  if content_format is not None:
@@ -971,7 +995,7 @@ class _BaseClient:
971
995
 
972
996
  def _create_notification(self, *, topic=None, member=None, broadcast=False,
973
997
  title=None, content=None, input=None, image=None, audio=None,
974
- link=None, password=None, tag=None, critical=False,
998
+ link=None, password=None, tag=None, critical=False, priority=None, critical_volume=None,
975
999
  shared=False) -> "Notification | NotificationGroup":
976
1000
  if not content and input is None:
977
1001
  raise ValueError("Either content or an input must be provided")
@@ -995,6 +1019,8 @@ class _BaseClient:
995
1019
  _validate_actions(input.actions)
996
1020
  if image is not None and audio is not None:
997
1021
  raise ValueError("a notification can carry at most one media attachment (image or audio, not both)")
1022
+ if link is not None and input is not None:
1023
+ raise ValueError("a notification carries either an input or a link, not both: the input's buttons take the action slots, so the link would never be shown")
998
1024
 
999
1025
  # A notification carries at most one input, sent as `textInput {}` XOR
1000
1026
  # `choiceInput {options}` XOR `actionInput {actions}`. For actions, both
@@ -1096,8 +1122,7 @@ class _BaseClient:
1096
1122
  payload["choiceInput"] = {"options": choice_options}
1097
1123
  elif action_defs is not None:
1098
1124
  payload["actionInput"] = {"actions": action_defs}
1099
- if critical:
1100
- payload["critical"] = critical
1125
+ payload.update(_priority_wire(priority, critical, critical_volume))
1101
1126
  if encryption_dict is not None:
1102
1127
  payload["encryption"] = encryption_dict
1103
1128
  if shared:
@@ -1225,7 +1250,7 @@ class _BaseClient:
1225
1250
 
1226
1251
  def _build_subtask_data(self, send_key, *, title=None, content=None, inputs=None,
1227
1252
  links=None, files=None,
1228
- auto_commit=False, critical=False,
1253
+ auto_commit=False, critical=False, priority=None, critical_volume=None,
1229
1254
  reply=None, content_format=None) -> "tuple[dict, list]":
1230
1255
  """Build the (encrypted) `data` dict of a subtask append plus the
1231
1256
  prepared local attachments awaiting upload — shared by the single-task
@@ -1318,8 +1343,7 @@ class _BaseClient:
1318
1343
  data["title"] = title
1319
1344
  if content is not None:
1320
1345
  data["content"] = content
1321
- if critical:
1322
- data["critical"] = critical
1346
+ data.update(_priority_wire(priority, critical, critical_volume))
1323
1347
  if reply is not None:
1324
1348
  data["reply"] = reply.value if isinstance(reply, ReplyMode) else reply
1325
1349
  if content_format is not None:
@@ -1330,14 +1354,14 @@ class _BaseClient:
1330
1354
 
1331
1355
  def _append_subtask(self, task, *, title=None, content=None, inputs=None,
1332
1356
  links=None, files=None,
1333
- auto_commit=False, critical=False,
1357
+ auto_commit=False, critical=False, priority=None, critical_volume=None,
1334
1358
  reply=None, content_format=None) -> Subtask:
1335
1359
  if not task.append_token:
1336
1360
  raise RuntimeError("this task has no append token; cannot append a subtask")
1337
1361
  # Inherit the parent's key (subtask encryption must match the chain's).
1338
1362
  data, prepared = self._build_subtask_data(
1339
1363
  task._send_key, title=title, content=content, inputs=inputs,
1340
- links=links, files=files, auto_commit=auto_commit, critical=critical,
1364
+ links=links, files=files, auto_commit=auto_commit, critical=critical, priority=priority, critical_volume=critical_volume,
1341
1365
  reply=reply, content_format=content_format,
1342
1366
  )
1343
1367
 
@@ -1367,7 +1391,7 @@ class _BaseClient:
1367
1391
 
1368
1392
  def _append_subtasks_to_group(self, group, *, instances=None, title=None,
1369
1393
  content=None, inputs=None, links=None, files=None,
1370
- auto_commit=False, critical=False,
1394
+ auto_commit=False, critical=False, priority=None, critical_volume=None,
1371
1395
  reply=None, content_format=None) -> "list[Subtask]":
1372
1396
  """Append one subtask per member instance to a task group's chains,
1373
1397
  atomically, via the group append token. `instances` (task ids) restricts
@@ -1379,7 +1403,7 @@ class _BaseClient:
1379
1403
  raise RuntimeError("this task group has no append token; cannot append a subtask")
1380
1404
  data, prepared = self._build_subtask_data(
1381
1405
  group._send_key, title=title, content=content, inputs=inputs,
1382
- links=links, files=files, auto_commit=auto_commit, critical=critical,
1406
+ links=links, files=files, auto_commit=auto_commit, critical=critical, priority=priority, critical_volume=critical_volume,
1383
1407
  reply=reply, content_format=content_format,
1384
1408
  )
1385
1409
 
@@ -1472,6 +1472,7 @@ class Task:
1472
1472
  inputs=None, links: list[str] | None = None,
1473
1473
  files: "list[str | os.PathLike] | None" = None,
1474
1474
  auto_commit: bool = False, critical: bool = False,
1475
+ priority: int | None = None, critical_volume: float | None = None,
1475
1476
  reply: "ReplyMode | Literal['one-shot', 'sticky', 'one-time-per-user'] | None" = None,
1476
1477
  content_format: "ContentFormat | Literal['plain', 'markdown'] | None" = None) -> "Subtask":
1477
1478
  """Append a subtask to this task's chain and return a `Subtask` handle.
@@ -1486,7 +1487,7 @@ class Task:
1486
1487
  return self._client._append_subtask(
1487
1488
  self, title=title, content=content, inputs=inputs,
1488
1489
  links=links, files=files,
1489
- auto_commit=auto_commit, critical=critical, reply=reply,
1490
+ auto_commit=auto_commit, critical=critical, priority=priority, critical_volume=critical_volume, reply=reply,
1490
1491
  content_format=content_format,
1491
1492
  )
1492
1493
 
@@ -1644,6 +1645,7 @@ class TaskGroup:
1644
1645
  files: "list[str | os.PathLike] | None" = None,
1645
1646
  instances: "list[str | Task] | None" = None,
1646
1647
  auto_commit: bool = False, critical: bool = False,
1648
+ priority: int | None = None, critical_volume: float | None = None,
1647
1649
  reply: "ReplyMode | Literal['one-shot', 'sticky', 'one-time-per-user'] | None" = None,
1648
1650
  content_format: "ContentFormat | Literal['plain', 'markdown'] | None" = None) -> "list[Subtask]":
1649
1651
  """Append a subtask to every member instance's chain atomically — or
@@ -1661,7 +1663,7 @@ class TaskGroup:
1661
1663
  ids = [t.task_id if isinstance(t, Task) else t for t in instances]
1662
1664
  return self._client._append_subtasks_to_group(
1663
1665
  self, instances=ids, title=title, content=content, inputs=inputs,
1664
- links=links, files=files, auto_commit=auto_commit, critical=critical,
1666
+ links=links, files=files, auto_commit=auto_commit, critical=critical, priority=priority, critical_volume=critical_volume,
1665
1667
  reply=reply, content_format=content_format,
1666
1668
  )
1667
1669
 
File without changes
File without changes