pywebpush 2.3.0__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.
Files changed (24) hide show
  1. {pywebpush-2.3.0 → pywebpush-2.4.0}/MANIFEST.in +2 -0
  2. {pywebpush-2.3.0 → pywebpush-2.4.0}/PKG-INFO +5 -3
  3. {pywebpush-2.3.0 → pywebpush-2.4.0}/README.rst +5 -5
  4. {pywebpush-2.3.0 → pywebpush-2.4.0}/pyproject.toml +38 -4
  5. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush/__init__.py +55 -100
  6. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush/__main__.py +5 -7
  7. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush/tests/test_webpush.py +19 -21
  8. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/PKG-INFO +5 -3
  9. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/SOURCES.txt +0 -3
  10. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/requires.txt +3 -1
  11. {pywebpush-2.3.0 → pywebpush-2.4.0}/requirements.txt +1 -0
  12. pywebpush-2.3.0/local_test.txt +0 -3
  13. pywebpush-2.3.0/pywebpush/foo.py +0 -51
  14. pywebpush-2.3.0/test-requirements.txt +0 -4
  15. {pywebpush-2.3.0 → pywebpush-2.4.0}/CHANGELOG.md +0 -0
  16. {pywebpush-2.3.0 → pywebpush-2.4.0}/CODE_OF_CONDUCT.md +0 -0
  17. {pywebpush-2.3.0 → pywebpush-2.4.0}/LICENSE +0 -0
  18. {pywebpush-2.3.0 → pywebpush-2.4.0}/README.md +0 -0
  19. {pywebpush-2.3.0 → pywebpush-2.4.0}/entry_points.txt +0 -0
  20. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush/tests/__init__.py +0 -0
  21. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/dependency_links.txt +0 -0
  22. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/entry_points.txt +0 -0
  23. {pywebpush-2.3.0 → pywebpush-2.4.0}/pywebpush.egg-info/top_level.txt +0 -0
  24. {pywebpush-2.3.0 → pywebpush-2.4.0}/setup.cfg +0 -0
@@ -3,3 +3,5 @@ include *.txt
3
3
  include setup.*
4
4
  include LICENSE
5
5
  recursive-include pywebpush *.py
6
+ global-exclude */__pycache__/*
7
+ global-exclude *.pyc
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywebpush
3
- Version: 2.3.0
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>=2.6.1
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
- - 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
226
- Push ``subscribe`` method and looks something like:
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.3.0"
12
+ version = "2.4.0"
13
+ # PYTHON_VER
13
14
  requires-python = ">= 3.10"
14
- license = { text = "MPL-2.0" }
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
- dynamic = ["dependencies"]
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 typing import cast, Union, Dict
13
-
14
- try:
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 functools import partial
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
- def update(self, data) -> None:
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: Dict[
135
- str, Union[Union[str, bytes], Dict[str, Union[str, bytes]]]
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.requests_method = requests
149
+ self.mod_or_session = requests
161
150
  else:
162
- self.requests_method = requests_session
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: Dict[str, Union[str, bytes]] = cast(
172
- Dict[str, Union[str, bytes]], self.subscription_info["keys"]
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(self, endpoint: str, encoded_data: bytes, headers: Dict[str, str]) -> str:
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: Union[None, bytes] = None,
304
- headers: Union[None, Dict[str, str]] = None,
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) -> Union[Response, str]:
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.requests_method.post(
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) -> Union[aiohttp.ClientResponse, str]:
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: Dict[
418
- str, Union[Union[str, bytes], Dict[str, Union[str, bytes]]]
419
- ],
420
- data: Union[None, str] = None,
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: Union[None, float] = None,
403
+ timeout: None | float = None,
426
404
  ttl: int = 0,
427
405
  verbose: bool = False,
428
- headers: Union[None, Dict[str, Union[str, int, float]]] = None,
429
- requests_session: Union[None, requests.Session] = None,
430
- ) -> Union[str, requests.Response]:
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 endcode and send `data` to the endpoint
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("Setting VAPID expry to {}...".format(vapid_claims["exp"]))
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: Dict[
548
- str, Union[Union[str, bytes], Dict[str, Union[str, bytes]]]
549
- ],
550
- data: Union[None, str] = None,
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: Union[None, float] = None,
522
+ timeout: None | float = None,
556
523
  ttl: int = 0,
557
524
  verbose: bool = False,
558
- headers: Union[None, Dict[str, Union[str, int, float]]] = None,
559
- aiohttp_session: Union[None, aiohttp.ClientSession] = None,
560
- ) -> Union[str, aiohttp.ClientResponse]:
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 math
4
+ import os
6
5
 
7
6
  from requests import JSONDecodeError
8
7
 
9
- from pywebpush import webpush, WebPushException
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
- from typing import cast, Union, Dict
7
- from unittest.mock import patch, Mock, AsyncMock
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
- WebPusher,
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
- "endpoint": "https://example.com/",
58
- "keys": {
59
- "p256dh": (
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
- "auth": "k8JV6sjdbhAi1n3_LDBLvA",
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: Dict[str, Union[str, int]] = dict(
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: Dict[str, Union[str, int]] = dict(
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.0
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>=2.6.1
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
@@ -1,10 +1,12 @@
1
1
  aiohttp
2
- cryptography>=2.6.1
2
+ cryptography>=47.0.0
3
3
  http-ece>=1.1.0
4
4
  requests>=2.21.0
5
5
  py-vapid>=1.7.0
6
6
 
7
7
  [dev]
8
+ isort
9
+ bandit
8
10
  black
9
11
  mock
10
12
  pytest
@@ -1,3 +1,4 @@
1
+ # NOTE: Requirements are now in pyproject.toml
1
2
  aiohttp
2
3
  cryptography>=2.6.1
3
4
  http-ece>=1.1.0
@@ -1,3 +0,0 @@
1
- Amidst the mists and coldest frosts I thrust my fists against the
2
- posts and still demand to see the ghosts.
3
-
@@ -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")
@@ -1,4 +0,0 @@
1
- -r requirements.txt
2
- black
3
- mock
4
- pytest
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes