pywebpush 2.2.0__tar.gz → 2.3.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.
- {pywebpush-2.2.0 → pywebpush-2.3.0}/CHANGELOG.md +12 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/PKG-INFO +1 -2
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pyproject.toml +1 -1
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush/__init__.py +23 -23
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush/__main__.py +4 -4
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush/tests/test_webpush.py +6 -6
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/PKG-INFO +1 -2
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/requires.txt +0 -1
- {pywebpush-2.2.0 → pywebpush-2.3.0}/requirements.txt +0 -1
- {pywebpush-2.2.0 → pywebpush-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/LICENSE +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/MANIFEST.in +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/README.md +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/README.rst +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/entry_points.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/local_test.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush/foo.py +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush/tests/__init__.py +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/SOURCES.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/dependency_links.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/entry_points.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/pywebpush.egg-info/top_level.txt +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/setup.cfg +0 -0
- {pywebpush-2.2.0 → pywebpush-2.3.0}/test-requirements.txt +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# I am terrible at keeping this up-to-date.
|
|
2
2
|
|
|
3
|
+
## 2.3.0 (2026-02-09)
|
|
4
|
+
|
|
5
|
+
- Cleanup from @Rotzbua
|
|
6
|
+
- Use modern typing for annotations
|
|
7
|
+
- Remove legacy python 2 import
|
|
8
|
+
- remove redundant :type annotations
|
|
9
|
+
- use [.dev] extras in CI
|
|
10
|
+
|
|
11
|
+
## 2.2.1 (2026-02-06)
|
|
12
|
+
|
|
13
|
+
- add'l typing info (Thanks @rotzbua)
|
|
14
|
+
|
|
3
15
|
## 2.2.0 (2026-)
|
|
4
16
|
|
|
5
17
|
- Update `rst` files to reflect `md` file changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywebpush
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: WebPush publication library
|
|
5
5
|
Author-email: JR Conlin <src+webpusher@jrconlin.com>
|
|
6
6
|
License: MPL-2.0
|
|
@@ -17,7 +17,6 @@ Requires-Dist: aiohttp
|
|
|
17
17
|
Requires-Dist: cryptography>=2.6.1
|
|
18
18
|
Requires-Dist: http-ece>=1.1.0
|
|
19
19
|
Requires-Dist: requests>=2.21.0
|
|
20
|
-
Requires-Dist: six>=1.15.0
|
|
21
20
|
Requires-Dist: py-vapid>=1.7.0
|
|
22
21
|
Provides-Extra: dev
|
|
23
22
|
Requires-Dist: black; extra == "dev"
|
|
@@ -34,11 +34,11 @@ class WebPushException(Exception):
|
|
|
34
34
|
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
def __init__(self, message, response=None):
|
|
37
|
+
def __init__(self, message, response=None) -> None:
|
|
38
38
|
self.message = message
|
|
39
39
|
self.response = response
|
|
40
40
|
|
|
41
|
-
def __str__(self):
|
|
41
|
+
def __str__(self) -> str:
|
|
42
42
|
extra = ""
|
|
43
43
|
if self.response is not None:
|
|
44
44
|
try:
|
|
@@ -46,8 +46,8 @@ class WebPushException(Exception):
|
|
|
46
46
|
self.response.text,
|
|
47
47
|
)
|
|
48
48
|
except AttributeError:
|
|
49
|
-
extra = ", Response {
|
|
50
|
-
return "WebPushException: {}{}"
|
|
49
|
+
extra = f", Response {self.response}"
|
|
50
|
+
return f"WebPushException: {self.message}{extra}"
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class NoData(Exception):
|
|
@@ -57,12 +57,12 @@ class NoData(Exception):
|
|
|
57
57
|
class CaseInsensitiveDict(dict):
|
|
58
58
|
"""A dictionary that has case-insensitive keys"""
|
|
59
59
|
|
|
60
|
-
def __init__(self, data={}, **kwargs):
|
|
60
|
+
def __init__(self, data={}, **kwargs) -> None:
|
|
61
61
|
for key in data:
|
|
62
62
|
dict.__setitem__(self, key.lower(), data[key])
|
|
63
63
|
self.update(kwargs)
|
|
64
64
|
|
|
65
|
-
def __contains__(self, key):
|
|
65
|
+
def __contains__(self, key) -> bool:
|
|
66
66
|
return dict.__contains__(self, key.lower())
|
|
67
67
|
|
|
68
68
|
def __setitem__(self, key, value):
|
|
@@ -80,7 +80,7 @@ class CaseInsensitiveDict(dict):
|
|
|
80
80
|
except KeyError:
|
|
81
81
|
return default
|
|
82
82
|
|
|
83
|
-
def update(self, data):
|
|
83
|
+
def update(self, data) -> None:
|
|
84
84
|
for key in data:
|
|
85
85
|
self.__setitem__(key, data[key])
|
|
86
86
|
|
|
@@ -137,7 +137,7 @@ class WebPusher:
|
|
|
137
137
|
requests_session: Union[None, requests.Session] = None,
|
|
138
138
|
aiohttp_session: Union[None, aiohttp.client.ClientSession] = None,
|
|
139
139
|
verbose: bool = False,
|
|
140
|
-
):
|
|
140
|
+
) -> None:
|
|
141
141
|
"""Initialize using the info provided by the client PushSubscription
|
|
142
142
|
object (See
|
|
143
143
|
https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe)
|
|
@@ -173,7 +173,7 @@ class WebPusher:
|
|
|
173
173
|
)
|
|
174
174
|
for k in ["p256dh", "auth"]:
|
|
175
175
|
if keys.get(k) is None:
|
|
176
|
-
raise WebPushException("Missing keys value: {}"
|
|
176
|
+
raise WebPushException(f"Missing keys value: {k}")
|
|
177
177
|
if isinstance(keys[k], str):
|
|
178
178
|
keys[k] = bytes(cast(str, keys[k]).encode("utf8"))
|
|
179
179
|
receiver_raw = base64.urlsafe_b64decode(
|
|
@@ -186,11 +186,11 @@ class WebPusher:
|
|
|
186
186
|
self._repad(cast(bytes, keys["auth"]))
|
|
187
187
|
)
|
|
188
188
|
|
|
189
|
-
def verb(self, msg: str, *args, **kwargs):
|
|
189
|
+
def verb(self, msg: str, *args, **kwargs) -> None:
|
|
190
190
|
if self.verbose:
|
|
191
191
|
logging.info(msg.format(*args, **kwargs))
|
|
192
192
|
|
|
193
|
-
def _repad(self, data: bytes):
|
|
193
|
+
def _repad(self, data: bytes) -> bytes:
|
|
194
194
|
"""Add base64 padding to the end of a string, if required"""
|
|
195
195
|
return data + b"===="[: len(data) % 4]
|
|
196
196
|
|
|
@@ -226,7 +226,7 @@ class WebPusher:
|
|
|
226
226
|
if content_encoding == "aesgcm":
|
|
227
227
|
self.verb("Generating salt for aesgcm...")
|
|
228
228
|
salt = os.urandom(16)
|
|
229
|
-
logging.debug("Salt: {}"
|
|
229
|
+
logging.debug(f"Salt: {salt}")
|
|
230
230
|
# The server key is an ephemeral ECDH key used only for this
|
|
231
231
|
# transaction
|
|
232
232
|
server_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
|
|
@@ -266,7 +266,7 @@ class WebPusher:
|
|
|
266
266
|
reply["salt"] = base64.urlsafe_b64encode(salt).strip(b"=")
|
|
267
267
|
return reply
|
|
268
268
|
|
|
269
|
-
def as_curl(self, endpoint: str, encoded_data: bytes, headers: Dict[str, str]):
|
|
269
|
+
def as_curl(self, endpoint: str, encoded_data: bytes, headers: Dict[str, str]) -> str:
|
|
270
270
|
"""Return the send as a curl command.
|
|
271
271
|
|
|
272
272
|
Useful for debugging. This will write out the encoded data to a local
|
|
@@ -282,7 +282,7 @@ class WebPusher:
|
|
|
282
282
|
|
|
283
283
|
"""
|
|
284
284
|
header_list = [
|
|
285
|
-
'-H "{}: {}" \\ \n'
|
|
285
|
+
f'-H "{key.lower()}: {val}" \\ \n' for key, val in headers.items()
|
|
286
286
|
]
|
|
287
287
|
data = ""
|
|
288
288
|
if encoded_data:
|
|
@@ -292,7 +292,7 @@ class WebPusher:
|
|
|
292
292
|
if "content-length" not in headers:
|
|
293
293
|
self.verb("Generating content-length header...")
|
|
294
294
|
header_list.append(
|
|
295
|
-
'-H "content-length: {}" \\ \n'
|
|
295
|
+
f'-H "content-length: {len(encoded_data)}" \\ \n'
|
|
296
296
|
)
|
|
297
297
|
return """curl -vX POST {url} \\\n{headers}{data}""".format(
|
|
298
298
|
url=endpoint, headers="".join(header_list), data=data
|
|
@@ -488,7 +488,7 @@ def webpush(
|
|
|
488
488
|
logging.info("Generating VAPID headers...")
|
|
489
489
|
if not vapid_claims.get("aud"):
|
|
490
490
|
url = urlparse(cast(str, subscription_info.get("endpoint")))
|
|
491
|
-
aud = "{}://{
|
|
491
|
+
aud = f"{url.scheme}://{url.netloc}"
|
|
492
492
|
vapid_claims["aud"] = aud
|
|
493
493
|
# Remember, passed structures are mutable in python.
|
|
494
494
|
# It's possible that a previously set `exp` field is no longer valid.
|
|
@@ -509,17 +509,17 @@ def webpush(
|
|
|
509
509
|
# Presume that key from file is handled correctly by
|
|
510
510
|
# py_vapid.
|
|
511
511
|
if verbose:
|
|
512
|
-
logging.info("Reading VAPID key from file {}"
|
|
512
|
+
logging.info(f"Reading VAPID key from file {vapid_private_key}")
|
|
513
513
|
vv = Vapid.from_file(private_key_file=vapid_private_key) # pragma no cover
|
|
514
514
|
else:
|
|
515
515
|
if verbose:
|
|
516
516
|
logging.info("Reading VAPID key from arguments")
|
|
517
517
|
vv = Vapid.from_string(private_key=vapid_private_key)
|
|
518
518
|
if verbose:
|
|
519
|
-
logging.info("\t claims: {}"
|
|
519
|
+
logging.info(f"\t claims: {vapid_claims}")
|
|
520
520
|
vapid_headers = vv.sign(vapid_claims)
|
|
521
521
|
if verbose:
|
|
522
|
-
logging.info("\t headers: {}"
|
|
522
|
+
logging.info(f"\t headers: {vapid_headers}")
|
|
523
523
|
headers.update(vapid_headers)
|
|
524
524
|
|
|
525
525
|
response = WebPusher(
|
|
@@ -625,7 +625,7 @@ async def webpush_async(
|
|
|
625
625
|
logging.info("Generating VAPID headers...")
|
|
626
626
|
if not vapid_claims.get("aud"):
|
|
627
627
|
url = urlparse(cast(str, subscription_info.get("endpoint")))
|
|
628
|
-
aud = "{}://{
|
|
628
|
+
aud = f"{url.scheme}://{url.netloc}"
|
|
629
629
|
vapid_claims["aud"] = aud
|
|
630
630
|
# Remember, passed structures are mutable in python.
|
|
631
631
|
# It's possible that a previously set `exp` field is no longer valid.
|
|
@@ -648,17 +648,17 @@ async def webpush_async(
|
|
|
648
648
|
# Presume that key from file is handled correctly by
|
|
649
649
|
# py_vapid.
|
|
650
650
|
if verbose:
|
|
651
|
-
logging.info("Reading VAPID key from file {}"
|
|
651
|
+
logging.info(f"Reading VAPID key from file {vapid_private_key}")
|
|
652
652
|
vv = Vapid.from_file(private_key_file=vapid_private_key) # pragma no cover
|
|
653
653
|
else:
|
|
654
654
|
if verbose:
|
|
655
655
|
logging.info("Reading VAPID key from arguments")
|
|
656
656
|
vv = Vapid.from_string(private_key=vapid_private_key)
|
|
657
657
|
if verbose:
|
|
658
|
-
logging.info("\t claims: {}"
|
|
658
|
+
logging.info(f"\t claims: {vapid_claims}")
|
|
659
659
|
vapid_headers = vv.sign(vapid_claims)
|
|
660
660
|
if verbose:
|
|
661
|
-
logging.info("\t headers: {}"
|
|
661
|
+
logging.info(f"\t headers: {vapid_headers}")
|
|
662
662
|
headers.update(vapid_headers)
|
|
663
663
|
|
|
664
664
|
response = await WebPusher(
|
|
@@ -76,15 +76,15 @@ def get_config():
|
|
|
76
76
|
args.claims = json.loads(r.read())
|
|
77
77
|
except JSONDecodeError as e:
|
|
78
78
|
raise WebPushException(
|
|
79
|
-
"Could not read the VAPID claims file {}"
|
|
79
|
+
f"Could not read the VAPID claims file {e}"
|
|
80
80
|
)
|
|
81
81
|
except Exception as ex:
|
|
82
|
-
logging.error("Couldn't read input {}."
|
|
82
|
+
logging.error(f"Couldn't read input {ex}.")
|
|
83
83
|
raise ex
|
|
84
84
|
return args
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
def main():
|
|
87
|
+
def main() -> None:
|
|
88
88
|
"""Send data"""
|
|
89
89
|
|
|
90
90
|
try:
|
|
@@ -101,7 +101,7 @@ def main():
|
|
|
101
101
|
)
|
|
102
102
|
print(result)
|
|
103
103
|
except Exception as ex:
|
|
104
|
-
logging.error("{}"
|
|
104
|
+
logging.error(f"{ex}")
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
if __name__ == "__main__":
|
|
@@ -319,7 +319,7 @@ class WebpushTestUtils(unittest.TestCase):
|
|
|
319
319
|
'-H "ttl: 0"',
|
|
320
320
|
'-H "content-length:',
|
|
321
321
|
]:
|
|
322
|
-
assert s in result, "missing: {}"
|
|
322
|
+
assert s in result, f"missing: {s}"
|
|
323
323
|
|
|
324
324
|
def test_ci_dict(self):
|
|
325
325
|
ci = CaseInsensitiveDict({"Foo": "apple", "bar": "banana"})
|
|
@@ -554,7 +554,7 @@ class WebPusherAsyncTestCase(WebpushTestUtils, unittest.IsolatedAsyncioTestCase)
|
|
|
554
554
|
'-H "ttl: 0"',
|
|
555
555
|
'-H "content-length:',
|
|
556
556
|
]:
|
|
557
|
-
assert s in result, "missing: {}"
|
|
557
|
+
assert s in result, f"missing: {s}"
|
|
558
558
|
|
|
559
559
|
|
|
560
560
|
class WebpushExceptionTestCase(unittest.TestCase):
|
|
@@ -562,7 +562,7 @@ class WebpushExceptionTestCase(unittest.TestCase):
|
|
|
562
562
|
from requests import Response
|
|
563
563
|
|
|
564
564
|
exp = WebPushException("foo")
|
|
565
|
-
assert "{}"
|
|
565
|
+
assert f"{exp}" == "WebPushException: foo"
|
|
566
566
|
# Really should try to load the response to verify, but this mock
|
|
567
567
|
# covers what we need.
|
|
568
568
|
response = Mock(spec=Response)
|
|
@@ -578,10 +578,10 @@ class WebpushExceptionTestCase(unittest.TestCase):
|
|
|
578
578
|
response.status_code = 401
|
|
579
579
|
response.reason = "Unauthorized"
|
|
580
580
|
exp = WebPushException("foo", response)
|
|
581
|
-
assert "{}"
|
|
581
|
+
assert f"{exp}" == "WebPushException: foo, Response {}".format(
|
|
582
582
|
response.text
|
|
583
583
|
)
|
|
584
|
-
assert "{
|
|
584
|
+
assert f"{exp.response}", "<Response [401]>"
|
|
585
585
|
assert cast(requests.Response, exp.response).json().get("errno") == 109
|
|
586
586
|
exp = WebPushException("foo", [1, 2, 3])
|
|
587
|
-
assert "{}"
|
|
587
|
+
assert f"{exp}" == "WebPushException: foo, Response [1, 2, 3]"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywebpush
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: WebPush publication library
|
|
5
5
|
Author-email: JR Conlin <src+webpusher@jrconlin.com>
|
|
6
6
|
License: MPL-2.0
|
|
@@ -17,7 +17,6 @@ Requires-Dist: aiohttp
|
|
|
17
17
|
Requires-Dist: cryptography>=2.6.1
|
|
18
18
|
Requires-Dist: http-ece>=1.1.0
|
|
19
19
|
Requires-Dist: requests>=2.21.0
|
|
20
|
-
Requires-Dist: six>=1.15.0
|
|
21
20
|
Requires-Dist: py-vapid>=1.7.0
|
|
22
21
|
Provides-Extra: dev
|
|
23
22
|
Requires-Dist: black; extra == "dev"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|