From 059773e24f928926eae23843255ab632f7e7a893 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Mon, 14 Oct 2024 08:52:28 -0700 Subject: [PATCH 01/14] release: 2.0.1 * Fix license as classifier (Thanks, @sevdog!) #170 --- setup.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index f8756d1..112e754 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -__version__ = "2.0.0" +__version__ = "2.0.1" def read_from(file): @@ -44,6 +44,7 @@ def read_from(file): "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", "Programming Language :: Python :: 3", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", ], keywords="push webpush publication", author="JR Conlin", @@ -58,8 +59,6 @@ def read_from(file): # be created, and still will if you run # `python setup.py develop` entry_points={ - "console_scripts": [ - "pywebpush=pywebpush.__main__:main" - ], - } + "console_scripts": ["pywebpush=pywebpush.__main__:main"], + }, ) From d81acfd8e1e34664298f4a1a8203b5463e0bbdef Mon Sep 17 00:00:00 2001 From: jrconlin Date: Mon, 14 Oct 2024 08:57:12 -0700 Subject: [PATCH 02/14] chore: tag 2.0.1 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0999302..31531a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # I am terrible at keeping this up-to-date. +## 2.0.1 (2024-10-14) +docs: Use License classifiers in pyproject.toml (thanks @sevdog) + ## 2.0.0 (2024-01-02) chore: Update to modern python practices * include pyproject.toml file From c59bf166f1a4dcbebfae43967ab460aecce19da7 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Mon, 5 Jan 2026 09:06:11 -0800 Subject: [PATCH 03/14] update README.rst --- README.rst | 60 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 7584bf0..77e8af4 100644 --- a/README.rst +++ b/README.rst @@ -14,12 +14,8 @@ make of that what you will. Installation ------------ -You’ll need to run ``python -m venv venv``. Then - -.. code:: bash - - venv/bin/pip install -r requirements.txt - venv/bin/python -m pip install -e . +To work with this repo locally, you’ll need to run +``python -m venv venv``. Then ``venv/bin/pip install --editable .`` Usage ----- @@ -35,7 +31,10 @@ As illustration, a ``subscription_info`` object may look like: .. code:: json - {"endpoint": "https://updates.push.services.mozilla.com/push/v1/gAA...", "keys": {"auth": "k8J...", "p256dh": "BOr..."}} + { + "endpoint": "https://updates.push.services.mozilla.com/push/v1/gAA...", + "keys": { "auth": "k8J...", "p256dh": "BOr..." } + } How you send the PushSubscription data to your backend, store it referenced to the user who requested it, and recall it when there’s a @@ -60,7 +59,8 @@ This will encode ``data``, add the appropriate VAPID auth headers if required and send it to the push server identified in the ``subscription_info`` block. -**Parameters** +Parameters +'''''''''' *subscription_info* - The ``dict`` of the subscription info (described above). @@ -78,7 +78,9 @@ standard form. authorization (See `py_vapid `__ for more details). If ``aud`` is not specified, pywebpush will attempt -to auto-fill from the ``endpoint``. +to auto-fill from the ``endpoint``. If ``exp`` is not specified or set +in the past, it will be set to 12 hours from now. In both cases, the +passed ``dict`` **will be mutated** after the call. *vapid_private_key* - Either a path to a VAPID EC2 private key PEM file, or a string containing the DER representation. (See @@ -93,7 +95,8 @@ e.g. the output of: openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem -**Example** +Example +''''''' .. code:: python @@ -140,7 +143,10 @@ The following methods are available: Send the data using additional parameters. On error, returns a ``WebPushException`` -**Parameters** +.. _parameters-1: + +Parameters +'''''''''' *data* Binary string of data to send @@ -165,7 +171,10 @@ purposes. *timeout* timeout for requests POST query. See `requests documentation `__. -**Example** +.. _example-1: + +Example +''''''' to send from Chrome using the old GCM mode: @@ -179,13 +188,24 @@ to send from Chrome using the old GCM mode: Encode the ``data`` for future use. On error, returns a ``WebPushException`` -**Parameters** +.. _parameters-2: + +Parameters +'''''''''' *data* Binary string of data to send *content_encoding* ECE content encoding type (defaults to “aes128gcm”) -**Example** +*Note* This will return a ``NoData`` exception if the data is not +present or empty. It is completely valid to send a WebPush notification +with no data, but encoding is a no-op in that case. Best not to call it +if you don’t have data. + +.. _example-2: + +Example +''''''' .. code:: python @@ -207,11 +227,13 @@ This uses two files: .. code:: json - {"endpoint": "https://push...", - "keys": { - "auth": "ab01...", - "p256dh": "aa02..." - }} + { + "endpoint": "https://push...", + "keys": { + "auth": "ab01...", + "p256dh": "aa02..." + } + } If you’re interested in just testing your applications WebPush interface, you could use the Command Line: From f87b083a2aa4fe262917f6711b15428ada258dfa Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 21 Jul 2026 15:51:28 -0700 Subject: [PATCH 04/14] chore: Updates for 2026-07 * Add Makefile for easy, consistant checks * Add Black,itool,bandit for formatting checks * Increase type checks * clean up --- .gitignore | 7 +++- Makefile | 27 +++++++++++++++ pyproject.toml | 28 +++++++++++++++- pywebpush/__init__.py | 58 +++++++++++++++------------------ pywebpush/__main__.py | 12 +++---- pywebpush/tests/__init__.py | 0 pywebpush/tests/test_webpush.py | 34 +++++++++---------- 7 files changed, 108 insertions(+), 58 deletions(-) create mode 100644 Makefile create mode 100644 pywebpush/tests/__init__.py diff --git a/.gitignore b/.gitignore index d7a9866..fc94328 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ downloads/ eggs/ .eggs/ include/ +ignore/ local/ lib/ lib64/ @@ -26,6 +27,7 @@ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg +.installed *.egg MANIFEST @@ -53,6 +55,8 @@ coverage.xml .hypothesis/ .pytest_cache/ cover/ +ltest/ +.circleci/ # Translations *.mo @@ -125,6 +129,7 @@ celerybeat.pid # Environments .env .venv +.envrc env/ venv/ ENV/ @@ -162,4 +167,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -.vscode/ \ No newline at end of file +.vscode/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..efd7bd4 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# Simple Makefile to help with things like formatting +# checks and installs + +.PHONY: build +build: .installed + +.PHONY: install +install: .installed +.installed: + pip install ".[dev]" + touch .installed + +.PHONY: test +test: .installed + pytest + +lint: .installed + isort --sp pyproject.toml -c pywebpush + black --quiet --config pyproject.toml --check --target-version py314 pywebpush + bandit --quiet -r -c pyproject.toml pywebpush + +format: .installed + isort --sp pyproject.toml pywebpush + black --quiet --config pyproject.toml --target-version py314 pywebpush + bandit --quiet -r -c pyproject.toml pywebpush + + diff --git a/pyproject.toml b/pyproject.toml index 78e46ac..f9e9f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dynamic = ["dependencies"] Homepage = "https://github.com/web-push-libs/pywebpush" [project.optional-dependencies] -dev = ["black", "mock", "pytest"] +dev = ["isort", "bandit", "black", "mock", "pytest"] # create the `pywebpush` helper using `python -m pip install --editable .` [project.scripts] @@ -39,3 +39,29 @@ dependencies = { file = "requirements.txt" } [tool.setuptools.packages.find] include = ["pywebpush*"] + +[tool.isort] +profile = "black" +skip_gitignore = true + +[tool.bandit] +# skips asserts +# B101: https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html# +# skip false detect of hardcoded sql +# B608:https://bandit.readthedocs.io/en/latest/plugins/B608_hardcoded_sql_expressions.html# +skips = ["B101", "B608"] + +[tool.mypy] +disable_error_code = "attr-defined" +disallow_untyped_calls = false +follow_imports = "normal" +ignore_missing_imports = true +pretty = true +show_error_codes = true +strict_optional = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unused_ignores = true +warn_unreachable = true +check_untyped_defs = true diff --git a/pywebpush/__init__.py b/pywebpush/__init__.py index ca5ef2d..457dd3c 100644 --- a/pywebpush/__init__.py +++ b/pywebpush/__init__.py @@ -2,23 +2,22 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -import asyncio import base64 import json +import logging import os import time -import logging from copy import deepcopy -from typing import cast, Union, Dict +from types import ModuleType +from typing import Mapping, cast from urllib.parse import urlparse import aiohttp import http_ece import requests from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import serialization -from functools import partial +from cryptography.hazmat.primitives.asymmetric import ec from py_vapid import Vapid, Vapid01 from requests import Response @@ -76,7 +75,9 @@ def get(self, key, default=None): except KeyError: return default - def update(self, data) -> None: + # Skip mypy check on the following because the declaration is too + # abstract + def update(self, data: dict) -> None: # type: ignore for key in data: self.__setitem__(key, data[key]) @@ -116,19 +117,18 @@ class WebPusher: """ - subscription_info = {} - valid_encodings = [ + subscription_info: Mapping = {} + valid_encodings: list[str] = [ # "aesgcm128", # this is draft-0, but DO NOT USE. "aesgcm", # draft-httpbis-encryption-encoding-01 "aes128gcm", # RFC8188 Standard encoding ] - verbose = False + verbose: bool = False + mod_or_session: ModuleType | requests.Session def __init__( self, - subscription_info: dict[ - str, str | bytes | dict[str, str | bytes] - ], + subscription_info: Mapping, requests_session: None | requests.Session = None, aiohttp_session: None | aiohttp.client.ClientSession = None, verbose: bool = False, @@ -146,9 +146,9 @@ def __init__( self.verbose = verbose if requests_session is None: - self.requests_method = requests + self.mod_or_session = requests else: - self.requests_method = requests_session + self.mod_or_session = requests_session self.aiohttp_session = aiohttp_session @@ -205,7 +205,7 @@ def encode( if not self.auth_key or not self.receiver_key: raise WebPushException("No keys specified in subscription info") self.verb("Encoding data...") - salt = None + salt: bytes | None = None if content_encoding not in self.valid_encodings: raise WebPushException( "Invalid content encoding specified. " @@ -214,7 +214,7 @@ def encode( if content_encoding == "aesgcm": self.verb("Generating salt for aesgcm...") salt = os.urandom(16) - logging.debug(f"Salt: {salt}") + logging.debug(f"Salt: {salt!r}") # The server key is an ephemeral ECDH key used only for this # transaction server_key = ec.generate_private_key(ec.SECP256R1(), default_backend()) @@ -223,8 +223,6 @@ def encode( format=serialization.PublicFormat.UncompressedPoint, ) - if isinstance(data, str): - data = bytes(data.encode("utf8")) if content_encoding == "aes128gcm": self.verb("Encrypting to aes128gcm...") encrypted = http_ece.encrypt( @@ -254,7 +252,9 @@ def encode( reply["salt"] = base64.urlsafe_b64encode(salt).strip(b"=") return reply - def as_curl(self, endpoint: str, encoded_data: bytes, headers: dict[str, str]) -> str: + def as_curl( + self, endpoint: str, encoded_data: bytes, headers: dict[str, str] + ) -> str: """Return the send as a curl command. Useful for debugging. This will write out the encoded data to a local @@ -274,9 +274,7 @@ def as_curl(self, endpoint: str, encoded_data: bytes, headers: dict[str, str]) - data = "--data-binary @encrypted.data" if "content-length" not in headers: self.verb("Generating content-length header...") - header_list.append( - f'-H "content-length: {len(encoded_data)}" \\ \n' - ) + header_list.append(f'-H "content-length: {len(encoded_data)}" \\ \n') return """curl -vX POST {url} \\\n{headers}{data}""".format( url=endpoint, headers="".join(header_list), data=data ) @@ -303,6 +301,8 @@ def _prepare_send_data( headers = dict() encoded = CaseInsensitiveDict() headers = CaseInsensitiveDict(headers) + if isinstance(data, str): + data = data.encode() if data: encoded = self.encode(data, content_encoding) if "crypto_key" in encoded: @@ -354,7 +354,7 @@ def send(self, *args, **kwargs) -> Response | str: headers = params["headers"] return self.as_curl(endpoint, encoded_data=encoded_data, headers=headers) - resp = self.requests_method.post( + resp = self.mod_or_session.post( endpoint, timeout=timeout, **params, @@ -363,7 +363,7 @@ def send(self, *args, **kwargs) -> Response | str: "\nResponse:\n\tcode: {}\n\tbody: {}\n\theaders: {}", resp.status_code, resp.text or "Empty", - resp.headers or "None" + resp.headers or "None", ) return resp @@ -394,9 +394,7 @@ async def send_async(self, *args, **kwargs) -> aiohttp.ClientResponse | str: def webpush( - subscription_info: dict[ - str, str | bytes | dict[str, str | bytes] - ], + subscription_info: Mapping, data: None | str = None, vapid_private_key: None | Vapid | str = None, vapid_claims: None | dict[str, str | int] = None, @@ -409,7 +407,7 @@ def webpush( requests_session: None | requests.Session = None, ) -> str | requests.Response: """ - One call solution to endcode and send `data` to the endpoint + One call solution to encode and send `data` to the endpoint contained in `subscription_info` using optional VAPID auth headers. in example: @@ -513,9 +511,7 @@ def webpush( async def webpush_async( - subscription_info: dict[ - str, str | bytes | dict[str, str | bytes] - ], + subscription_info: dict[str, str | bytes | dict[str, str | bytes]], data: None | str = None, vapid_private_key: None | Vapid | str = None, vapid_claims: None | dict[str, str | int] = None, diff --git a/pywebpush/__main__.py b/pywebpush/__main__.py index b367c0e..42cfdc1 100644 --- a/pywebpush/__main__.py +++ b/pywebpush/__main__.py @@ -1,12 +1,11 @@ import argparse -import os import json import logging -import math +import os from requests import JSONDecodeError -from pywebpush import webpush, WebPushException +from pywebpush import WebPushException, webpush def get_config(): @@ -20,7 +19,8 @@ def get_config(): "--wns", help="Include WNS cache header based on TTL", default=False, - action="store_true") + action="store_true", + ) parser.add_argument( "--curl", help="Don't send, display as curl command", @@ -75,9 +75,7 @@ def get_config(): try: args.claims = json.loads(r.read()) except JSONDecodeError as e: - raise WebPushException( - f"Could not read the VAPID claims file {e}" - ) + raise WebPushException(f"Could not read the VAPID claims file {e}") except Exception as ex: logging.error(f"Couldn't read input {ex}.") raise ex diff --git a/pywebpush/tests/__init__.py b/pywebpush/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pywebpush/tests/test_webpush.py b/pywebpush/tests/test_webpush.py index 633534e..ecab2bd 100644 --- a/pywebpush/tests/test_webpush.py +++ b/pywebpush/tests/test_webpush.py @@ -1,23 +1,23 @@ import base64 import json import os -import unittest import time +import unittest from typing import cast -from unittest.mock import patch, Mock, AsyncMock +from unittest.mock import AsyncMock, Mock, patch import http_ece import py_vapid import requests -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec from pywebpush import ( - WebPusher, + CaseInsensitiveDict, NoData, + WebPusher, WebPushException, - CaseInsensitiveDict, webpush, webpush_async, ) @@ -53,16 +53,16 @@ def _get_pubkey_str(self, priv_key): def test_init(self): # use static values so we know what to look for in the reply - subscription_info = { - "endpoint": "https://example.com/", - "keys": { - "p256dh": ( + subscription_info = dict( + endpoint="https://example.com/", + keys=dict( + p256dh=( "BOrnIslXrUow2VAzKCUAE4sIbK00daEZCswOcf8m3T" "F8V82B-OpOg5JbmYLg44kRcvQC1E2gMJshsUYA-_zMPR8" ), - "auth": "k8JV6sjdbhAi1n3_LDBLvA", - }, - } + auth="k8JV6sjdbhAi1n3_LDBLvA", + ), + ) rk_decode = ( b'\x04\xea\xe7"\xc9W\xadJ0\xd9P3(%\x00\x13\x8b' b"\x08l\xad4u\xa1\x19\n\xcc\x0eq\xff&\xdd1" @@ -211,7 +211,7 @@ def test_webpush_vapid_exp(self, vapid_sign, pusher_send): subscription_info = self._gen_subscription_info() data = "Mary had a little lamb" vapid_key = py_vapid.Vapid.from_string(self.vapid_key) - claims = dict( + claims: dict[str, str | int] = dict( sub="mailto:ops@example.com", aud="https://example.com", exp=int(time.time() - 48600), @@ -477,7 +477,7 @@ async def test_webpush_async_vapid_exp(self, vapid_sign, pusher_send): subscription_info = self._gen_subscription_info() data = "Mary had a little lamb" vapid_key = py_vapid.Vapid.from_string(self.vapid_key) - claims = dict( + claims: dict[str, str | int] = dict( sub="mailto:ops@example.com", aud="https://example.com", exp=int(time.time() - 48600), @@ -578,9 +578,7 @@ def test_exception(self): response.status_code = 401 response.reason = "Unauthorized" exp = WebPushException("foo", response) - assert f"{exp}" == "WebPushException: foo, Response {}".format( - response.text - ) + assert f"{exp}" == "WebPushException: foo, Response {}".format(response.text) assert f"{exp.response}", "" assert cast(requests.Response, exp.response).json().get("errno") == 109 exp = WebPushException("foo", [1, 2, 3]) From fe06d8017a487837b44dab25c9635846a1f38fff Mon Sep 17 00:00:00 2001 From: jrconlin Date: Thu, 23 Jul 2026 10:54:08 -0700 Subject: [PATCH 05/14] f add workflow (first try) --- .github/workflows/python-package.yml | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..51385ce --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,96 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "main", "ga" ] + pull_request: + branches: [ "main", "ga" ] + +jobs: + lint_and_test: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install black pytest + python -m pip install ".[dev]" + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint + run: | + # stop the build if there are Python syntax errors or undefined names + black --quiet --diff --config pyproject.toml --check pywebpush + bandit --quiet -c pyproject.toml pywebpush + - name: Test with pytest + run: | + pytest pywebpush + + release-build: + runs-on: ubuntu-latest + needs: lint_and_test + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + # NOTE: put your own distribution build steps here. + python -m pip install --upgrade pip + python -m pip install build + python -m build + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: release-dists + path: dist/ + + pypi-publish: + if: ${{ github.event_name == "push" && ( github.ref_name == "main" || startsWith(github.ref, "refs/tags/")) }} + runs-on: ubuntu-latest + needs: + - release-build + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: pypi + # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: + url: https://pypi.org/p/pywebpush + # + # ALTERNATIVE: if your GitHub Release name is the PyPI project version string + # ALTERNATIVE: exactly, uncomment the following line instead: + # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} + + steps: + - name: Retrieve release distributions + uses: actions/download-artifact@v4 + with: + name: release-dists + path: dist/ + + # - name: Publish release distributions to PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # packages-dir: dist/ From 484e840686f240bc858cf38f9af75dcdba744f2e Mon Sep 17 00:00:00 2001 From: jrconlin Date: Thu, 23 Jul 2026 11:23:48 -0700 Subject: [PATCH 06/14] f remove old circleci dir --- .circleci/config.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 6d95e53..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,29 +0,0 @@ -# SOP Python circleci file - -version: 2.1 - -orbs: - python: circleci/python@2.1.1 - -jobs: - build_and_test: - executor: python/default - steps: - - checkout - - python/install-packages: - pkg-manager: pip - - run: - name: Build - command: pip3 install ".[dev]" - - run: - name: Run tests - command: python -m pytest pywebpush - - persist_to_workspace: - root: ~/project - paths: - - . - -workflows: - build_and_test: - jobs: - - build_and_test From eb018090ac4c64d10c2c79b85f3dc546e49eecdc Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:02:47 -0700 Subject: [PATCH 07/14] f correct package --- .github/workflows/python-package.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 51385ce..9cd960d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -64,7 +64,8 @@ jobs: path: dist/ pypi-publish: - if: ${{ github.event_name == "push" && ( github.ref_name == "main" || startsWith(github.ref, "refs/tags/")) }} + # Remember, matching strings need to be in single quotes. + if: ${{ github.event_name == 'push' && ( github.ref_name == 'main' || startsWith(github.ref, 'refs/tags/')) }} runs-on: ubuntu-latest needs: - release-build @@ -89,7 +90,9 @@ jobs: with: name: release-dists path: dist/ - + - name: validate build + run: | + ls -aFl dist/ # - name: Publish release distributions to PyPI # uses: pypa/gh-action-pypi-publish@release/v1 # with: From 95acb1b8b91a7ca2074825f10b71da6e0c7eb4ed Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:07:26 -0700 Subject: [PATCH 08/14] f PYTHON_VER --- .github/workflows/python-package.yml | 3 ++- pyproject.toml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9cd960d..a343ff3 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.13", "3.14"] + # PYTHON_VER + python-version: ["3.10", "3.11", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index f9e9f8c..63c3acb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ build-backend = "setuptools.build_meta" [project] name = "pywebpush" version = "2.3.0" +# PYTHON_VER requires-python = ">= 3.10" license = { text = "MPL-2.0" } authors = [{ name = "JR Conlin", email = "src+webpusher@jrconlin.com" }] From 57cf80bd3efa3c0a1922dc9642846ec35a764f0f Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:11:59 -0700 Subject: [PATCH 09/14] f fix license (again) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 63c3acb..680bb96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ name = "pywebpush" version = "2.3.0" # PYTHON_VER requires-python = ">= 3.10" -license = { text = "MPL-2.0" } +license = "MPL-2.0" authors = [{ name = "JR Conlin", email = "src+webpusher@jrconlin.com" }] description = "WebPush publication library" readme = "README.md" From 5dfd4a24cf3a69afb89356e74fb3af30f19bf6a5 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:15:03 -0700 Subject: [PATCH 10/14] f fix conditional --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a343ff3..8930079 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -66,7 +66,7 @@ jobs: pypi-publish: # Remember, matching strings need to be in single quotes. - if: ${{ github.event_name == 'push' && ( github.ref_name == 'main' || startsWith(github.ref, 'refs/tags/')) }} + if: ${{ github.event_name == 'push' && ( contains(fromJSON('["main", "ga"]), github.ref_name ) || startsWith(github.ref, 'refs/tags/')) }} runs-on: ubuntu-latest needs: - release-build From f079a886d5729379476941a46c362dd6dfd96b20 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:16:59 -0700 Subject: [PATCH 11/14] f fix conditional --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8930079..f1f3fd6 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -66,7 +66,7 @@ jobs: pypi-publish: # Remember, matching strings need to be in single quotes. - if: ${{ github.event_name == 'push' && ( contains(fromJSON('["main", "ga"]), github.ref_name ) || startsWith(github.ref, 'refs/tags/')) }} + if: ${{ github.event_name == 'push' && ( contains(fromJSON('["main", "ga"]'), github.ref_name ) || startsWith(github.ref, 'refs/tags/') ) }} runs-on: ubuntu-latest needs: - release-build From 3b62b8eb7ca14e1a7446d31f82bfb5a75c2c04d0 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Tue, 28 Jul 2026 14:24:40 -0700 Subject: [PATCH 12/14] f add real pub --- .github/workflows/python-package.yml | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f1f3fd6..386b9a2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -94,7 +94,7 @@ jobs: - name: validate build run: | ls -aFl dist/ - # - name: Publish release distributions to PyPI - # uses: pypa/gh-action-pypi-publish@release/v1 - # with: - # packages-dir: dist/ + - name: Publish release distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ diff --git a/pyproject.toml b/pyproject.toml index 680bb96..3bba4ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pywebpush" -version = "2.3.0" +version = "2.4.0-beta.1" # PYTHON_VER requires-python = ">= 3.10" license = "MPL-2.0" From 642a2fa55db962cc7f2786eddaa255bf59712d47 Mon Sep 17 00:00:00 2001 From: jrconlin Date: Fri, 31 Jul 2026 10:37:11 -0700 Subject: [PATCH 13/14] f remove test `ga` --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 386b9a2..4606529 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -66,7 +66,7 @@ jobs: pypi-publish: # Remember, matching strings need to be in single quotes. - if: ${{ github.event_name == 'push' && ( contains(fromJSON('["main", "ga"]'), github.ref_name ) || startsWith(github.ref, 'refs/tags/') ) }} + if: ${{ github.event_name == 'push' && ( github.ref_name == 'main' || startsWith(github.ref, 'refs/tags/') ) }} runs-on: ubuntu-latest needs: - release-build From d77bee896a77c797fdea19cfc726f481cfbe89bc Mon Sep 17 00:00:00 2001 From: jrconlin Date: Mon, 3 Aug 2026 15:44:41 -0700 Subject: [PATCH 14/14] f exclude pycache, include requirements --- MANIFEST.in | 2 ++ pyproject.toml | 11 +++++++++-- requirements.txt | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 99f45a9..5b0b94b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,5 @@ include *.txt include setup.* include LICENSE recursive-include pywebpush *.py +global-exclude */__pycache__/* +global-exclude *.pyc diff --git a/pyproject.toml b/pyproject.toml index 3bba4ce..3041210 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pywebpush" -version = "2.4.0-beta.1" +version = "2.4.0" # PYTHON_VER requires-python = ">= 3.10" license = "MPL-2.0" @@ -23,7 +23,14 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", ] -dynamic = ["dependencies"] +dependencies = [ + "aiohttp", + "cryptography>=47.0.0", + "http-ece>=1.1.0", + "requests>=2.21.0", + "py-vapid>=1.7.0", +] + [project.urls] Homepage = "https://github.com/web-push-libs/pywebpush" diff --git a/requirements.txt b/requirements.txt index eedfe85..d0cf97d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +# NOTE: Requirements are now in pyproject.toml aiohttp cryptography>=2.6.1 http-ece>=1.1.0