pywebpush 2.2.1__tar.gz → 2.4.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.1 → pywebpush-2.4.0}/CHANGELOG.md +8 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/MANIFEST.in +2 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/PKG-INFO +5 -3
- {pywebpush-2.2.1 → pywebpush-2.4.0}/README.rst +5 -5
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pyproject.toml +38 -4
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush/__init__.py +55 -100
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush/__main__.py +5 -7
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush/tests/test_webpush.py +19 -21
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/PKG-INFO +5 -3
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/SOURCES.txt +0 -3
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/requires.txt +3 -1
- {pywebpush-2.2.1 → pywebpush-2.4.0}/requirements.txt +1 -0
- pywebpush-2.2.1/local_test.txt +0 -3
- pywebpush-2.2.1/pywebpush/foo.py +0 -51
- pywebpush-2.2.1/test-requirements.txt +0 -4
- {pywebpush-2.2.1 → pywebpush-2.4.0}/CODE_OF_CONDUCT.md +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/LICENSE +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/README.md +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/entry_points.txt +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush/tests/__init__.py +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/dependency_links.txt +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/entry_points.txt +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/pywebpush.egg-info/top_level.txt +0 -0
- {pywebpush-2.2.1 → pywebpush-2.4.0}/setup.cfg +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 2.2.1 (2026-02-06)
|
|
4
12
|
|
|
5
13
|
- add'l typing info (Thanks @rotzbua)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywebpush
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: WebPush publication library
|
|
5
5
|
Author-email: JR Conlin <src+webpusher@jrconlin.com>
|
|
6
|
-
License: MPL-2.0
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/web-push-libs/pywebpush
|
|
8
8
|
Keywords: webpush,vapid,notification
|
|
9
9
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -14,11 +14,13 @@ Requires-Python: >=3.10
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: aiohttp
|
|
17
|
-
Requires-Dist: cryptography>=
|
|
17
|
+
Requires-Dist: cryptography>=47.0.0
|
|
18
18
|
Requires-Dist: http-ece>=1.1.0
|
|
19
19
|
Requires-Dist: requests>=2.21.0
|
|
20
20
|
Requires-Dist: py-vapid>=1.7.0
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: isort; extra == "dev"
|
|
23
|
+
Requires-Dist: bandit; extra == "dev"
|
|
22
24
|
Requires-Dist: black; extra == "dev"
|
|
23
25
|
Requires-Dist: mock; extra == "dev"
|
|
24
26
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -219,11 +219,11 @@ If you’re not really into coding your own solution, there’s also a
|
|
|
219
219
|
|
|
220
220
|
This uses two files:
|
|
221
221
|
|
|
222
|
-
-
|
|
223
|
-
|
|
224
|
-
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
- the *data* file, which contains the message to send, in whatever form
|
|
223
|
+
you like.
|
|
224
|
+
- the *subscription info* file, which contains the subscription
|
|
225
|
+
information as JSON encoded data. This is usually returned by the Push
|
|
226
|
+
``subscribe`` method and looks something like:
|
|
227
227
|
|
|
228
228
|
.. code:: json
|
|
229
229
|
|
|
@@ -9,9 +9,10 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "pywebpush"
|
|
12
|
-
version = "2.
|
|
12
|
+
version = "2.4.0"
|
|
13
|
+
# PYTHON_VER
|
|
13
14
|
requires-python = ">= 3.10"
|
|
14
|
-
license =
|
|
15
|
+
license = "MPL-2.0"
|
|
15
16
|
authors = [{ name = "JR Conlin", email = "src+webpusher@jrconlin.com" }]
|
|
16
17
|
description = "WebPush publication library"
|
|
17
18
|
readme = "README.md"
|
|
@@ -22,13 +23,20 @@ classifiers = [
|
|
|
22
23
|
"Programming Language :: Python",
|
|
23
24
|
"Programming Language :: Python :: 3",
|
|
24
25
|
]
|
|
25
|
-
|
|
26
|
+
dependencies = [
|
|
27
|
+
"aiohttp",
|
|
28
|
+
"cryptography>=47.0.0",
|
|
29
|
+
"http-ece>=1.1.0",
|
|
30
|
+
"requests>=2.21.0",
|
|
31
|
+
"py-vapid>=1.7.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
[project.urls]
|
|
28
36
|
Homepage = "https://github.com/web-push-libs/pywebpush"
|
|
29
37
|
|
|
30
38
|
[project.optional-dependencies]
|
|
31
|
-
dev = ["black", "mock", "pytest"]
|
|
39
|
+
dev = ["isort", "bandit", "black", "mock", "pytest"]
|
|
32
40
|
|
|
33
41
|
# create the `pywebpush` helper using `python -m pip install --editable .`
|
|
34
42
|
[project.scripts]
|
|
@@ -39,3 +47,29 @@ dependencies = { file = "requirements.txt" }
|
|
|
39
47
|
|
|
40
48
|
[tool.setuptools.packages.find]
|
|
41
49
|
include = ["pywebpush*"]
|
|
50
|
+
|
|
51
|
+
[tool.isort]
|
|
52
|
+
profile = "black"
|
|
53
|
+
skip_gitignore = true
|
|
54
|
+
|
|
55
|
+
[tool.bandit]
|
|
56
|
+
# skips asserts
|
|
57
|
+
# B101: https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html#
|
|
58
|
+
# skip false detect of hardcoded sql
|
|
59
|
+
# B608:https://bandit.readthedocs.io/en/latest/plugins/B608_hardcoded_sql_expressions.html#
|
|
60
|
+
skips = ["B101", "B608"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
disable_error_code = "attr-defined"
|
|
64
|
+
disallow_untyped_calls = false
|
|
65
|
+
follow_imports = "normal"
|
|
66
|
+
ignore_missing_imports = true
|
|
67
|
+
pretty = true
|
|
68
|
+
show_error_codes = true
|
|
69
|
+
strict_optional = true
|
|
70
|
+
warn_no_return = true
|
|
71
|
+
warn_redundant_casts = true
|
|
72
|
+
warn_return_any = true
|
|
73
|
+
warn_unused_ignores = true
|
|
74
|
+
warn_unreachable = true
|
|
75
|
+
check_untyped_defs = true
|
|
@@ -2,27 +2,22 @@
|
|
|
2
2
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
4
|
|
|
5
|
-
import asyncio
|
|
6
5
|
import base64
|
|
7
6
|
import json
|
|
7
|
+
import logging
|
|
8
8
|
import os
|
|
9
9
|
import time
|
|
10
|
-
import logging
|
|
11
10
|
from copy import deepcopy
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from urlparse import urlparse
|
|
16
|
-
except ImportError: # pragma nocover
|
|
17
|
-
from urllib.parse import urlparse
|
|
11
|
+
from types import ModuleType
|
|
12
|
+
from typing import Mapping, cast
|
|
13
|
+
from urllib.parse import urlparse
|
|
18
14
|
|
|
19
15
|
import aiohttp
|
|
20
16
|
import http_ece
|
|
21
17
|
import requests
|
|
22
18
|
from cryptography.hazmat.backends import default_backend
|
|
23
|
-
from cryptography.hazmat.primitives.asymmetric import ec
|
|
24
19
|
from cryptography.hazmat.primitives import serialization
|
|
25
|
-
from
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
26
21
|
from py_vapid import Vapid, Vapid01
|
|
27
22
|
from requests import Response
|
|
28
23
|
|
|
@@ -80,7 +75,9 @@ class CaseInsensitiveDict(dict):
|
|
|
80
75
|
except KeyError:
|
|
81
76
|
return default
|
|
82
77
|
|
|
83
|
-
|
|
78
|
+
# Skip mypy check on the following because the declaration is too
|
|
79
|
+
# abstract
|
|
80
|
+
def update(self, data: dict) -> None: # type: ignore
|
|
84
81
|
for key in data:
|
|
85
82
|
self.__setitem__(key, data[key])
|
|
86
83
|
|
|
@@ -120,22 +117,20 @@ class WebPusher:
|
|
|
120
117
|
|
|
121
118
|
"""
|
|
122
119
|
|
|
123
|
-
subscription_info = {}
|
|
124
|
-
valid_encodings = [
|
|
120
|
+
subscription_info: Mapping = {}
|
|
121
|
+
valid_encodings: list[str] = [
|
|
125
122
|
# "aesgcm128", # this is draft-0, but DO NOT USE.
|
|
126
123
|
"aesgcm", # draft-httpbis-encryption-encoding-01
|
|
127
124
|
"aes128gcm", # RFC8188 Standard encoding
|
|
128
125
|
]
|
|
129
|
-
verbose = False
|
|
126
|
+
verbose: bool = False
|
|
127
|
+
mod_or_session: ModuleType | requests.Session
|
|
130
128
|
|
|
131
|
-
# Note: the type declarations are not valid under python 3.8,
|
|
132
129
|
def __init__(
|
|
133
130
|
self,
|
|
134
|
-
subscription_info:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
requests_session: Union[None, requests.Session] = None,
|
|
138
|
-
aiohttp_session: Union[None, aiohttp.client.ClientSession] = None,
|
|
131
|
+
subscription_info: Mapping,
|
|
132
|
+
requests_session: None | requests.Session = None,
|
|
133
|
+
aiohttp_session: None | aiohttp.client.ClientSession = None,
|
|
139
134
|
verbose: bool = False,
|
|
140
135
|
) -> None:
|
|
141
136
|
"""Initialize using the info provided by the client PushSubscription
|
|
@@ -144,22 +139,16 @@ class WebPusher:
|
|
|
144
139
|
|
|
145
140
|
:param subscription_info: a dict containing the subscription_info from
|
|
146
141
|
the client.
|
|
147
|
-
:type subscription_info: dict
|
|
148
|
-
|
|
149
142
|
:param requests_session: a requests.Session object to optimize requests
|
|
150
143
|
to the same client.
|
|
151
|
-
:type requests_session: requests.Session
|
|
152
|
-
|
|
153
144
|
:param verbose: provide verbose feedback
|
|
154
|
-
:type verbose: bool
|
|
155
|
-
|
|
156
145
|
"""
|
|
157
146
|
|
|
158
147
|
self.verbose = verbose
|
|
159
148
|
if requests_session is None:
|
|
160
|
-
self.
|
|
149
|
+
self.mod_or_session = requests
|
|
161
150
|
else:
|
|
162
|
-
self.
|
|
151
|
+
self.mod_or_session = requests_session
|
|
163
152
|
|
|
164
153
|
self.aiohttp_session = aiohttp_session
|
|
165
154
|
|
|
@@ -168,8 +157,8 @@ class WebPusher:
|
|
|
168
157
|
self.subscription_info = deepcopy(subscription_info)
|
|
169
158
|
self.auth_key = self.receiver_key = None
|
|
170
159
|
if "keys" in subscription_info:
|
|
171
|
-
keys:
|
|
172
|
-
|
|
160
|
+
keys: dict[str, str | bytes] = cast(
|
|
161
|
+
dict[str, str | bytes], self.subscription_info["keys"]
|
|
173
162
|
)
|
|
174
163
|
for k in ["p256dh", "auth"]:
|
|
175
164
|
if keys.get(k) is None:
|
|
@@ -202,7 +191,6 @@ class WebPusher:
|
|
|
202
191
|
:param data: A serialized block of byte data (String, JSON, bit array,
|
|
203
192
|
etc.) Make sure that whatever you send, your client knows how
|
|
204
193
|
to understand it.
|
|
205
|
-
:type data: str
|
|
206
194
|
:param content_encoding: The content_encoding type to use to encrypt
|
|
207
195
|
the data. Defaults to RFC8188 "aes128gcm". The previous draft-01 is
|
|
208
196
|
"aesgcm", however this format is now deprecated.
|
|
@@ -217,7 +205,7 @@ class WebPusher:
|
|
|
217
205
|
if not self.auth_key or not self.receiver_key:
|
|
218
206
|
raise WebPushException("No keys specified in subscription info")
|
|
219
207
|
self.verb("Encoding data...")
|
|
220
|
-
salt = None
|
|
208
|
+
salt: bytes | None = None
|
|
221
209
|
if content_encoding not in self.valid_encodings:
|
|
222
210
|
raise WebPushException(
|
|
223
211
|
"Invalid content encoding specified. "
|
|
@@ -226,7 +214,7 @@ class WebPusher:
|
|
|
226
214
|
if content_encoding == "aesgcm":
|
|
227
215
|
self.verb("Generating salt for aesgcm...")
|
|
228
216
|
salt = os.urandom(16)
|
|
229
|
-
logging.debug(f"Salt: {salt}")
|
|
217
|
+
logging.debug(f"Salt: {salt!r}")
|
|
230
218
|
# The server key is an ephemeral ECDH key used only for this
|
|
231
219
|
# transaction
|
|
232
220
|
server_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
|
|
@@ -235,8 +223,6 @@ class WebPusher:
|
|
|
235
223
|
format=serialization.PublicFormat.UncompressedPoint,
|
|
236
224
|
)
|
|
237
225
|
|
|
238
|
-
if isinstance(data, str):
|
|
239
|
-
data = bytes(data.encode("utf8"))
|
|
240
226
|
if content_encoding == "aes128gcm":
|
|
241
227
|
self.verb("Encrypting to aes128gcm...")
|
|
242
228
|
encrypted = http_ece.encrypt(
|
|
@@ -266,20 +252,17 @@ class WebPusher:
|
|
|
266
252
|
reply["salt"] = base64.urlsafe_b64encode(salt).strip(b"=")
|
|
267
253
|
return reply
|
|
268
254
|
|
|
269
|
-
def as_curl(
|
|
255
|
+
def as_curl(
|
|
256
|
+
self, endpoint: str, encoded_data: bytes, headers: dict[str, str]
|
|
257
|
+
) -> str:
|
|
270
258
|
"""Return the send as a curl command.
|
|
271
259
|
|
|
272
260
|
Useful for debugging. This will write out the encoded data to a local
|
|
273
261
|
file named `encrypted.data`
|
|
274
262
|
|
|
275
263
|
:param endpoint: Push service endpoint URL
|
|
276
|
-
:type endpoint: basestring
|
|
277
264
|
:param encoded_data: byte array of encoded data
|
|
278
|
-
:type encoded_data: bytearray
|
|
279
265
|
:param headers: Additional headers for the send
|
|
280
|
-
:type headers: dict
|
|
281
|
-
:returns string
|
|
282
|
-
|
|
283
266
|
"""
|
|
284
267
|
header_list = [
|
|
285
268
|
f'-H "{key.lower()}: {val}" \\ \n' for key, val in headers.items()
|
|
@@ -291,17 +274,15 @@ class WebPusher:
|
|
|
291
274
|
data = "--data-binary @encrypted.data"
|
|
292
275
|
if "content-length" not in headers:
|
|
293
276
|
self.verb("Generating content-length header...")
|
|
294
|
-
header_list.append(
|
|
295
|
-
f'-H "content-length: {len(encoded_data)}" \\ \n'
|
|
296
|
-
)
|
|
277
|
+
header_list.append(f'-H "content-length: {len(encoded_data)}" \\ \n')
|
|
297
278
|
return """curl -vX POST {url} \\\n{headers}{data}""".format(
|
|
298
279
|
url=endpoint, headers="".join(header_list), data=data
|
|
299
280
|
)
|
|
300
281
|
|
|
301
282
|
def _prepare_send_data(
|
|
302
283
|
self,
|
|
303
|
-
data:
|
|
304
|
-
headers:
|
|
284
|
+
data: None | bytes = None,
|
|
285
|
+
headers: None | dict[str, str] = None,
|
|
305
286
|
ttl: int = 0,
|
|
306
287
|
content_encoding: str = "aes128gcm",
|
|
307
288
|
) -> dict:
|
|
@@ -310,19 +291,18 @@ class WebPusher:
|
|
|
310
291
|
:param data: A serialized block of data (see encode() ).
|
|
311
292
|
:type data: str
|
|
312
293
|
:param headers: A dictionary containing any additional HTTP headers.
|
|
313
|
-
:type headers: dict
|
|
314
294
|
:param ttl: The Time To Live in seconds for this message if the
|
|
315
295
|
recipient is not online. (Defaults to "0", which discards the
|
|
316
296
|
message immediately if the recipient is unavailable.)
|
|
317
|
-
:type ttl: int
|
|
318
297
|
:param content_encoding: ECE content encoding (defaults to "aes128gcm")
|
|
319
|
-
:type content_encoding: str
|
|
320
298
|
"""
|
|
321
299
|
# Encode the data.
|
|
322
300
|
if headers is None:
|
|
323
301
|
headers = dict()
|
|
324
302
|
encoded = CaseInsensitiveDict()
|
|
325
303
|
headers = CaseInsensitiveDict(headers)
|
|
304
|
+
if isinstance(data, str):
|
|
305
|
+
data = data.encode()
|
|
326
306
|
if data:
|
|
327
307
|
encoded = self.encode(data, content_encoding)
|
|
328
308
|
if "crypto_key" in encoded:
|
|
@@ -361,7 +341,7 @@ class WebPusher:
|
|
|
361
341
|
|
|
362
342
|
return {"endpoint": endpoint, "data": encoded_data, "headers": headers}
|
|
363
343
|
|
|
364
|
-
def send(self, *args, **kwargs) ->
|
|
344
|
+
def send(self, *args, **kwargs) -> Response | str:
|
|
365
345
|
"""Encode and send the data to the Push Service"""
|
|
366
346
|
timeout = kwargs.pop("timeout", 10000)
|
|
367
347
|
curl = kwargs.pop("curl", False)
|
|
@@ -374,7 +354,7 @@ class WebPusher:
|
|
|
374
354
|
headers = params["headers"]
|
|
375
355
|
return self.as_curl(endpoint, encoded_data=encoded_data, headers=headers)
|
|
376
356
|
|
|
377
|
-
resp = self.
|
|
357
|
+
resp = self.mod_or_session.post(
|
|
378
358
|
endpoint,
|
|
379
359
|
timeout=timeout,
|
|
380
360
|
**params,
|
|
@@ -383,11 +363,11 @@ class WebPusher:
|
|
|
383
363
|
"\nResponse:\n\tcode: {}\n\tbody: {}\n\theaders: {}",
|
|
384
364
|
resp.status_code,
|
|
385
365
|
resp.text or "Empty",
|
|
386
|
-
resp.headers or "None"
|
|
366
|
+
resp.headers or "None",
|
|
387
367
|
)
|
|
388
368
|
return resp
|
|
389
369
|
|
|
390
|
-
async def send_async(self, *args, **kwargs) ->
|
|
370
|
+
async def send_async(self, *args, **kwargs) -> aiohttp.ClientResponse | str:
|
|
391
371
|
timeout = kwargs.pop("timeout", 10000)
|
|
392
372
|
curl = kwargs.pop("curl", False)
|
|
393
373
|
|
|
@@ -414,22 +394,20 @@ class WebPusher:
|
|
|
414
394
|
|
|
415
395
|
|
|
416
396
|
def webpush(
|
|
417
|
-
subscription_info:
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
vapid_private_key: Union[None, Vapid, str] = None,
|
|
422
|
-
vapid_claims: Union[None, Dict[str, Union[str, int]]] = None,
|
|
397
|
+
subscription_info: Mapping,
|
|
398
|
+
data: None | str = None,
|
|
399
|
+
vapid_private_key: None | Vapid | str = None,
|
|
400
|
+
vapid_claims: None | dict[str, str | int] = None,
|
|
423
401
|
content_encoding: str = "aes128gcm",
|
|
424
402
|
curl: bool = False,
|
|
425
|
-
timeout:
|
|
403
|
+
timeout: None | float = None,
|
|
426
404
|
ttl: int = 0,
|
|
427
405
|
verbose: bool = False,
|
|
428
|
-
headers:
|
|
429
|
-
requests_session:
|
|
430
|
-
) ->
|
|
406
|
+
headers: None | dict[str, str | int | float] = None,
|
|
407
|
+
requests_session: None | requests.Session = None,
|
|
408
|
+
) -> str | requests.Response:
|
|
431
409
|
"""
|
|
432
|
-
One call solution to
|
|
410
|
+
One call solution to encode and send `data` to the endpoint
|
|
433
411
|
contained in `subscription_info` using optional VAPID auth headers.
|
|
434
412
|
|
|
435
413
|
in example:
|
|
@@ -453,28 +431,17 @@ def webpush(
|
|
|
453
431
|
`WebPushException`.
|
|
454
432
|
|
|
455
433
|
:param subscription_info: Provided by the client call
|
|
456
|
-
:type subscription_info: dict
|
|
457
434
|
:param data: Serialized data to send
|
|
458
|
-
:type data: str
|
|
459
435
|
:param vapid_private_key: Vapid instance or path to vapid private key PEM \
|
|
460
436
|
or encoded str
|
|
461
437
|
:type vapid_private_key: Union[Vapid, str]
|
|
462
438
|
:param vapid_claims: Dictionary of claims ('sub' required)
|
|
463
|
-
:type vapid_claims: dict
|
|
464
439
|
:param content_encoding: Optional content type string
|
|
465
|
-
:type content_encoding: str
|
|
466
440
|
:param curl: Return as "curl" string instead of sending
|
|
467
|
-
:type curl: bool
|
|
468
441
|
:param timeout: POST requests timeout
|
|
469
|
-
:type timeout: float
|
|
470
442
|
:param ttl: Time To Live
|
|
471
|
-
:type ttl: int
|
|
472
443
|
:param verbose: Provide verbose feedback
|
|
473
|
-
:type verbose: bool
|
|
474
|
-
:return requests.Response or string
|
|
475
444
|
:param headers: Dictionary of extra HTTP headers to include
|
|
476
|
-
:type headers: dict
|
|
477
|
-
|
|
478
445
|
"""
|
|
479
446
|
if headers is None:
|
|
480
447
|
headers = dict()
|
|
@@ -498,7 +465,9 @@ def webpush(
|
|
|
498
465
|
# encryption lives for 12 hours
|
|
499
466
|
vapid_claims["exp"] = int(time.time()) + (12 * 60 * 60)
|
|
500
467
|
if verbose:
|
|
501
|
-
logging.info(
|
|
468
|
+
logging.info(
|
|
469
|
+
"Setting VAPID expiry to {}...".format(vapid_claims["exp"])
|
|
470
|
+
)
|
|
502
471
|
if not vapid_private_key:
|
|
503
472
|
raise WebPushException("VAPID dict missing 'private_key'")
|
|
504
473
|
if isinstance(vapid_private_key, Vapid01):
|
|
@@ -544,23 +513,21 @@ def webpush(
|
|
|
544
513
|
|
|
545
514
|
|
|
546
515
|
async def webpush_async(
|
|
547
|
-
subscription_info:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
vapid_private_key: Union[None, Vapid, str] = None,
|
|
552
|
-
vapid_claims: Union[None, Dict[str, Union[str, int]]] = None,
|
|
516
|
+
subscription_info: dict[str, str | bytes | dict[str, str | bytes]],
|
|
517
|
+
data: None | str = None,
|
|
518
|
+
vapid_private_key: None | Vapid | str = None,
|
|
519
|
+
vapid_claims: None | dict[str, str | int] = None,
|
|
553
520
|
content_encoding: str = "aes128gcm",
|
|
554
521
|
curl: bool = False,
|
|
555
|
-
timeout:
|
|
522
|
+
timeout: None | float = None,
|
|
556
523
|
ttl: int = 0,
|
|
557
524
|
verbose: bool = False,
|
|
558
|
-
headers:
|
|
559
|
-
aiohttp_session:
|
|
560
|
-
) ->
|
|
525
|
+
headers: None | dict[str, str | int | float] = None,
|
|
526
|
+
aiohttp_session: None | aiohttp.ClientSession = None,
|
|
527
|
+
) -> str | aiohttp.ClientResponse:
|
|
561
528
|
"""
|
|
562
|
-
Async version of webpush function. One call solution to encode and send
|
|
563
|
-
`data` to the endpoint contained in `subscription_info` using optional
|
|
529
|
+
Async version of webpush function. One call solution to encode and send
|
|
530
|
+
`data` to the endpoint contained in `subscription_info` using optional
|
|
564
531
|
VAPID auth headers.
|
|
565
532
|
|
|
566
533
|
Example:
|
|
@@ -588,30 +555,18 @@ async def webpush_async(
|
|
|
588
555
|
`WebPushException`.
|
|
589
556
|
|
|
590
557
|
:param subscription_info: Provided by the client call
|
|
591
|
-
:type subscription_info: dict
|
|
592
558
|
:param data: Serialized data to send
|
|
593
|
-
:type data: str
|
|
594
559
|
:param vapid_private_key: Vapid instance or path to vapid private key PEM \
|
|
595
560
|
or encoded str
|
|
596
561
|
:type vapid_private_key: Union[Vapid, str]
|
|
597
562
|
:param vapid_claims: Dictionary of claims ('sub' required)
|
|
598
|
-
:type vapid_claims: dict
|
|
599
563
|
:param content_encoding: Optional content type string
|
|
600
|
-
:type content_encoding: str
|
|
601
564
|
:param curl: Return as "curl" string instead of sending
|
|
602
|
-
:type curl: bool
|
|
603
565
|
:param timeout: POST requests timeout
|
|
604
|
-
:type timeout: float
|
|
605
566
|
:param ttl: Time To Live
|
|
606
|
-
:type ttl: int
|
|
607
567
|
:param verbose: Provide verbose feedback
|
|
608
|
-
:type verbose: bool
|
|
609
568
|
:param headers: Dictionary of extra HTTP headers to include
|
|
610
|
-
:type headers: dict
|
|
611
569
|
:param aiohttp_session: Optional aiohttp ClientSession for connection reuse
|
|
612
|
-
:type aiohttp_session: aiohttp.ClientSession
|
|
613
|
-
:return aiohttp.ClientResponse or string
|
|
614
|
-
|
|
615
570
|
"""
|
|
616
571
|
if headers is None:
|
|
617
572
|
headers = dict()
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import os
|
|
3
2
|
import json
|
|
4
3
|
import logging
|
|
5
|
-
import
|
|
4
|
+
import os
|
|
6
5
|
|
|
7
6
|
from requests import JSONDecodeError
|
|
8
7
|
|
|
9
|
-
from pywebpush import
|
|
8
|
+
from pywebpush import WebPushException, webpush
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def get_config():
|
|
@@ -20,7 +19,8 @@ def get_config():
|
|
|
20
19
|
"--wns",
|
|
21
20
|
help="Include WNS cache header based on TTL",
|
|
22
21
|
default=False,
|
|
23
|
-
action="store_true"
|
|
22
|
+
action="store_true",
|
|
23
|
+
)
|
|
24
24
|
parser.add_argument(
|
|
25
25
|
"--curl",
|
|
26
26
|
help="Don't send, display as curl command",
|
|
@@ -75,9 +75,7 @@ def get_config():
|
|
|
75
75
|
try:
|
|
76
76
|
args.claims = json.loads(r.read())
|
|
77
77
|
except JSONDecodeError as e:
|
|
78
|
-
raise WebPushException(
|
|
79
|
-
f"Could not read the VAPID claims file {e}"
|
|
80
|
-
)
|
|
78
|
+
raise WebPushException(f"Could not read the VAPID claims file {e}")
|
|
81
79
|
except Exception as ex:
|
|
82
80
|
logging.error(f"Couldn't read input {ex}.")
|
|
83
81
|
raise ex
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
import unittest
|
|
5
4
|
import time
|
|
6
|
-
|
|
7
|
-
from
|
|
5
|
+
import unittest
|
|
6
|
+
from typing import cast
|
|
7
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
8
8
|
|
|
9
9
|
import http_ece
|
|
10
10
|
import py_vapid
|
|
11
11
|
import requests
|
|
12
|
-
from cryptography.hazmat.primitives.asymmetric import ec
|
|
13
|
-
from cryptography.hazmat.primitives import serialization
|
|
14
12
|
from cryptography.hazmat.backends import default_backend
|
|
13
|
+
from cryptography.hazmat.primitives import serialization
|
|
14
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
15
15
|
|
|
16
16
|
from pywebpush import (
|
|
17
|
-
|
|
17
|
+
CaseInsensitiveDict,
|
|
18
18
|
NoData,
|
|
19
|
+
WebPusher,
|
|
19
20
|
WebPushException,
|
|
20
|
-
CaseInsensitiveDict,
|
|
21
21
|
webpush,
|
|
22
22
|
webpush_async,
|
|
23
23
|
)
|
|
@@ -53,16 +53,16 @@ class WebpushTestUtils(unittest.TestCase):
|
|
|
53
53
|
|
|
54
54
|
def test_init(self):
|
|
55
55
|
# use static values so we know what to look for in the reply
|
|
56
|
-
subscription_info =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
subscription_info = dict(
|
|
57
|
+
endpoint="https://example.com/",
|
|
58
|
+
keys=dict(
|
|
59
|
+
p256dh=(
|
|
60
60
|
"BOrnIslXrUow2VAzKCUAE4sIbK00daEZCswOcf8m3T"
|
|
61
61
|
"F8V82B-OpOg5JbmYLg44kRcvQC1E2gMJshsUYA-_zMPR8"
|
|
62
62
|
),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
auth="k8JV6sjdbhAi1n3_LDBLvA",
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
66
|
rk_decode = (
|
|
67
67
|
b'\x04\xea\xe7"\xc9W\xadJ0\xd9P3(%\x00\x13\x8b'
|
|
68
68
|
b"\x08l\xad4u\xa1\x19\n\xcc\x0eq\xff&\xdd1"
|
|
@@ -192,7 +192,7 @@ class WebpushTestUtils(unittest.TestCase):
|
|
|
192
192
|
subscription_info = self._gen_subscription_info()
|
|
193
193
|
data = "Mary had a little lamb"
|
|
194
194
|
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
|
|
195
|
-
claims:
|
|
195
|
+
claims: dict[str, str | int] = dict(
|
|
196
196
|
sub="mailto:ops@example.com", aud="https://example.com"
|
|
197
197
|
)
|
|
198
198
|
webpush(
|
|
@@ -211,7 +211,7 @@ class WebpushTestUtils(unittest.TestCase):
|
|
|
211
211
|
subscription_info = self._gen_subscription_info()
|
|
212
212
|
data = "Mary had a little lamb"
|
|
213
213
|
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
|
|
214
|
-
claims = dict(
|
|
214
|
+
claims: dict[str, str | int] = dict(
|
|
215
215
|
sub="mailto:ops@example.com",
|
|
216
216
|
aud="https://example.com",
|
|
217
217
|
exp=int(time.time() - 48600),
|
|
@@ -456,7 +456,7 @@ class WebPusherAsyncTestCase(WebpushTestUtils, unittest.IsolatedAsyncioTestCase)
|
|
|
456
456
|
subscription_info = self._gen_subscription_info()
|
|
457
457
|
data = "Mary had a little lamb"
|
|
458
458
|
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
|
|
459
|
-
claims:
|
|
459
|
+
claims: dict[str, str | int] = dict(
|
|
460
460
|
sub="mailto:ops@example.com", aud="https://example.com"
|
|
461
461
|
)
|
|
462
462
|
await webpush_async(
|
|
@@ -477,7 +477,7 @@ class WebPusherAsyncTestCase(WebpushTestUtils, unittest.IsolatedAsyncioTestCase)
|
|
|
477
477
|
subscription_info = self._gen_subscription_info()
|
|
478
478
|
data = "Mary had a little lamb"
|
|
479
479
|
vapid_key = py_vapid.Vapid.from_string(self.vapid_key)
|
|
480
|
-
claims = dict(
|
|
480
|
+
claims: dict[str, str | int] = dict(
|
|
481
481
|
sub="mailto:ops@example.com",
|
|
482
482
|
aud="https://example.com",
|
|
483
483
|
exp=int(time.time() - 48600),
|
|
@@ -578,9 +578,7 @@ class WebpushExceptionTestCase(unittest.TestCase):
|
|
|
578
578
|
response.status_code = 401
|
|
579
579
|
response.reason = "Unauthorized"
|
|
580
580
|
exp = WebPushException("foo", response)
|
|
581
|
-
assert f"{exp}" == "WebPushException: foo, Response {}".format(
|
|
582
|
-
response.text
|
|
583
|
-
)
|
|
581
|
+
assert f"{exp}" == "WebPushException: foo, Response {}".format(response.text)
|
|
584
582
|
assert f"{exp.response}", "<Response [401]>"
|
|
585
583
|
assert cast(requests.Response, exp.response).json().get("errno") == 109
|
|
586
584
|
exp = WebPushException("foo", [1, 2, 3])
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywebpush
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: WebPush publication library
|
|
5
5
|
Author-email: JR Conlin <src+webpusher@jrconlin.com>
|
|
6
|
-
License: MPL-2.0
|
|
6
|
+
License-Expression: MPL-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/web-push-libs/pywebpush
|
|
8
8
|
Keywords: webpush,vapid,notification
|
|
9
9
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
@@ -14,11 +14,13 @@ Requires-Python: >=3.10
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: aiohttp
|
|
17
|
-
Requires-Dist: cryptography>=
|
|
17
|
+
Requires-Dist: cryptography>=47.0.0
|
|
18
18
|
Requires-Dist: http-ece>=1.1.0
|
|
19
19
|
Requires-Dist: requests>=2.21.0
|
|
20
20
|
Requires-Dist: py-vapid>=1.7.0
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: isort; extra == "dev"
|
|
23
|
+
Requires-Dist: bandit; extra == "dev"
|
|
22
24
|
Requires-Dist: black; extra == "dev"
|
|
23
25
|
Requires-Dist: mock; extra == "dev"
|
|
24
26
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -5,14 +5,11 @@ MANIFEST.in
|
|
|
5
5
|
README.md
|
|
6
6
|
README.rst
|
|
7
7
|
entry_points.txt
|
|
8
|
-
local_test.txt
|
|
9
8
|
pyproject.toml
|
|
10
9
|
requirements.txt
|
|
11
10
|
setup.cfg
|
|
12
|
-
test-requirements.txt
|
|
13
11
|
pywebpush/__init__.py
|
|
14
12
|
pywebpush/__main__.py
|
|
15
|
-
pywebpush/foo.py
|
|
16
13
|
pywebpush.egg-info/PKG-INFO
|
|
17
14
|
pywebpush.egg-info/SOURCES.txt
|
|
18
15
|
pywebpush.egg-info/dependency_links.txt
|
pywebpush-2.2.1/local_test.txt
DELETED
pywebpush-2.2.1/pywebpush/foo.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from pywebpush import webpush
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import datetime
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def send_push_notification(subscription, payload):
|
|
8
|
-
|
|
9
|
-
try:
|
|
10
|
-
|
|
11
|
-
# subscriptionData = json.loads(subscription)
|
|
12
|
-
|
|
13
|
-
# logger.error(subscriptionData)
|
|
14
|
-
|
|
15
|
-
webpush(
|
|
16
|
-
subscription_info={
|
|
17
|
-
"endpoint": subscription["endpoint"],
|
|
18
|
-
"keys": subscription["keys"],
|
|
19
|
-
},
|
|
20
|
-
data=json.dumps(payload),
|
|
21
|
-
vapid_claims={
|
|
22
|
-
"aud": "https://eshopper.africa",
|
|
23
|
-
"exp": int((datetime.datetime.now().timestamp())) + 86400,
|
|
24
|
-
"sub": "mailto:events@eshopper.africa",
|
|
25
|
-
},
|
|
26
|
-
vapid_private_key="UCUKEHn7Jd33QZx5lJFKBY4plOxGsJ6xJSOzE14jQlo",
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# subscription_info = { 'endpoint': subscription['endpoint'], 'keys': subscription['keys'] },
|
|
30
|
-
|
|
31
|
-
# data = json.loads(payload),
|
|
32
|
-
|
|
33
|
-
# headers = {}
|
|
34
|
-
|
|
35
|
-
# ttl = 0
|
|
36
|
-
|
|
37
|
-
# gcm_key = ''
|
|
38
|
-
|
|
39
|
-
# content_encoding="aes128gcm"
|
|
40
|
-
|
|
41
|
-
# reg_id=""
|
|
42
|
-
|
|
43
|
-
# WebPusher = webpush(subscription_info)
|
|
44
|
-
|
|
45
|
-
# WebPusher(subscription_info).send(data, headers, ttl, gcm_key, reg_id, content_encoding, timeout=None)
|
|
46
|
-
|
|
47
|
-
except Exception as inst:
|
|
48
|
-
print(f" webpush Notification Error : {inst}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
send_push_notification({"endpoint": "https://example.com", "keys": {}}, "laaaa")
|
|
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
|