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.
- {simplepush-3.6.0 → simplepush-3.7.0}/PKG-INFO +4 -3
- {simplepush-3.6.0 → simplepush-3.7.0}/README.md +3 -2
- {simplepush-3.6.0 → simplepush-3.7.0}/pyproject.toml +1 -1
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/api.py +43 -19
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/client.py +4 -2
- {simplepush-3.6.0 → simplepush-3.7.0}/.gitignore +0 -0
- {simplepush-3.6.0 → simplepush-3.7.0}/LICENSE +0 -0
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/__init__.py +0 -0
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/_extras.py +0 -0
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/crypto.py +0 -0
- {simplepush-3.6.0 → simplepush-3.7.0}/src/simplepush/decrypt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simplepush
|
|
3
|
-
Version: 3.
|
|
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); `
|
|
117
|
-
|
|
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); `
|
|
67
|
-
|
|
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.
|
|
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
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|