From 22c9ae89a91da1106a381fee270edfc218c23d04 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 08:18:24 -0800 Subject: [PATCH 1/9] Dual-license under Apache-2.0 OR MIT Match the Go geoipupdate project's licensing. Rename LICENSE to LICENSE-APACHE, add LICENSE-MIT, and update pyproject.toml and README.md accordingly. Co-Authored-By: Claude Opus 4.6 --- LICENSE => LICENSE-APACHE | 0 LICENSE-MIT | 17 +++++++++++++++++ README.md | 7 +++++-- pyproject.toml | 4 ++-- 4 files changed, 24 insertions(+), 4 deletions(-) rename LICENSE => LICENSE-APACHE (100%) create mode 100644 LICENSE-MIT diff --git a/LICENSE b/LICENSE-APACHE similarity index 100% rename from LICENSE rename to LICENSE-APACHE diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..f85e365 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 57ac338..8bed279 100644 --- a/README.md +++ b/README.md @@ -193,9 +193,12 @@ twice per week. Here's an example cron entry: - Python 3.11+ - A MaxMind account with a license key -## License +## Copyright and License -Apache-2.0 +This software is Copyright (c) 2025 - 2026 by MaxMind, Inc. + +This is free software, licensed under the [Apache License, Version +2.0](LICENSE-APACHE) or the [MIT License](LICENSE-MIT), at your option. ## Links diff --git a/pyproject.toml b/pyproject.toml index e083ef6..306622f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ dependencies = [ ] requires-python = ">=3.11" readme = "README.md" -license = "Apache-2.0" -license-files = ["LICENSE"] +license = "Apache-2.0 OR MIT" +license-files = ["LICENSE-*"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", From eac3e193956721ac5bb415d4e69e7bfb5776b337 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 08:29:12 -0800 Subject: [PATCH 2/9] Add mise configuration for tooling management Configures mise to provide uv with strict lockfile enforcement (locked = true) and SHA-256 checksums for all platforms. Python deps remain managed by uv, not mise. Co-Authored-By: Claude Opus 4.6 --- mise.lock | 8 ++++++++ mise.toml | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 mise.lock create mode 100644 mise.toml diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..64baedd --- /dev/null +++ b/mise.lock @@ -0,0 +1,8 @@ +[[tools."aqua:astral-sh/uv"]] +version = "0.10.0" +backend = "aqua:astral-sh/uv" +"platforms.linux-arm64" = { checksum = "sha256:edf1adb1d183730302f87eef9b71bc4e47b4b8058832c3393b0fbcd86f270510", url = "https://github.com/astral-sh/uv/releases/download/0.10.0/uv-aarch64-unknown-linux-musl.tar.gz"} +"platforms.linux-x64" = { checksum = "sha256:312d37f31b6f2c3bfc65668ba0efea9f1f9eaf7bc3209fe1a109e5cf861b95fa", url = "https://github.com/astral-sh/uv/releases/download/0.10.0/uv-x86_64-unknown-linux-musl.tar.gz"} +"platforms.macos-arm64" = { checksum = "sha256:82d4b99dc6ea686695b5ee142ceba03dd3e3eda2b414e94215ab7bce94972fbb", url = "https://github.com/astral-sh/uv/releases/download/0.10.0/uv-aarch64-apple-darwin.tar.gz"} +"platforms.macos-x64" = { checksum = "sha256:664aed584c276f8d79cdc3b7685cd48f5d64657bd6840b06b4b2b0db731b9c99", url = "https://github.com/astral-sh/uv/releases/download/0.10.0/uv-x86_64-apple-darwin.tar.gz"} +"platforms.windows-x64" = { checksum = "sha256:4037b444541f695cd2eb93188a9346de3e334af562381411deade0a31c7bf898", url = "https://github.com/astral-sh/uv/releases/download/0.10.0/uv-x86_64-pc-windows-msvc.zip"} diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..b2959e3 --- /dev/null +++ b/mise.toml @@ -0,0 +1,21 @@ +[settings] +# Require lockfile URLs to be present during installation +lockfile = true +locked = true + +# Disable backends that use community-maintained bash scripts (supply chain risk) +# See: https://github.com/jdx/mise/discussions/4054 +disable_backends = [ + "asdf", # Community bash plugins - supply chain attack vector + "vfox", # Same bash plugin model as asdf - supply chain risk +] + +[tools] +"aqua:astral-sh/uv" = "latest" + +[hooks] +enter = "mise install --quiet" + +[[watch_files]] +patterns = ["mise.toml", "mise.lock"] +run = "mise install --quiet" From e146c85174b35d8d57024de6f780d0412f774c84 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 08:43:20 -0800 Subject: [PATCH 3/9] Add GitHub Actions workflows and Dependabot config Add CI workflows matching GeoIP2-python conventions: - test.yml: matrix across Python 3.11-3.14 and 4 OS targets - release.yml: build sdist/wheel, publish to PyPI on release - codeql-analysis.yml: CodeQL security scanning - zizmor.yml: GitHub Actions workflow security analysis - dependabot.yml: automated updates for uv deps and Actions Co-Authored-By: Claude Opus 4.6 --- .github/dependabot.yml | 17 ++++++++++ .github/workflows/codeql-analysis.yml | 38 +++++++++++++++++++++ .github/workflows/release.yml | 49 +++++++++++++++++++++++++++ .github/workflows/test.yml | 38 +++++++++++++++++++++ .github/workflows/zizmor.yml | 23 +++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b552ae4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: uv + directory: / + schedule: + interval: daily + time: '14:00' + open-pull-requests-limit: 10 + cooldown: + default-days: 7 + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + time: '14:00' + cooldown: + default-days: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..ceb1e69 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,38 @@ +name: "Code scanning - action" + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + schedule: + - cron: '0 11 * * 2' + +permissions: {} + +jobs: + CodeQL-Build: + + runs-on: ubuntu-latest + + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 2 + persist-credentials: false + + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + + - name: Autobuild + uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d3899ab --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +name: Build and upload to PyPI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + release: + types: + - published + +permissions: {} + +jobs: + build: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # 7.2.1 + + - name: Build + run: uv build + + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + path: | + dist/*.tar.gz + dist/*.whl + + upload_pypi: + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # 1.13.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ed35e71 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Python tests + +on: + push: + pull_request: + schedule: + - cron: '3 15 * * SUN' + +permissions: {} + +jobs: + test: + name: test with ${{ matrix.env }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + env: [3.11, 3.12, 3.13, 3.14] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Install the latest version of uv + uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # 7.2.1 + - name: Install tox + run: uv tool install --python-preference only-managed --python 3.13 tox --with tox-uv --with tox-gh + - name: Install Python + if: matrix.env != '3.13' + run: uv python install --python-preference only-managed ${{ matrix.env }} + - name: Setup test suite + run: tox run -vv --notest --skip-missing-interpreters false + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.env }} + - name: Run test suite + run: tox run --skip-pkg-install + env: + TOX_GH_MAJOR_MINOR: ${{ matrix.env }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..47d318a --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,23 @@ +name: GitHub Actions Security Analysis with zizmor + +on: + push: + branches: ["main"] + pull_request: + branches: ["**"] + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0 From 14ae43926aaaca0dd647ec070aea2a34735b21ff Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 08:49:32 -0800 Subject: [PATCH 4/9] Prepare pyproject.toml for release workflow Set version to 0.1.0, update HISTORY.rst to match, and use __version__ import in version tests to prevent breakage on future version bumps. Co-Authored-By: Claude Opus 4.6 --- HISTORY.rst | 2 +- pyproject.toml | 4 ++-- src/pygeoipupdate/__init__.py | 4 +++- tests/test_cli.py | 5 +++-- uv.lock | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9dfb7ea..83cd985 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ History ------- -1.0.0 (2024-12-05) +0.1.0 (2026-02-10) ++++++++++++++++++ * Initial release diff --git a/pyproject.toml b/pyproject.toml index 306622f..51087d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pygeoipupdate" -version = "1.0.0" +version = "0.1.0" description = "MaxMind GeoIP database updater" authors = [ {name = "Gregory Oschwald", email = "goschwald@maxmind.com"}, @@ -51,7 +51,7 @@ lint = [ ] [build-system] -requires = ["uv_build>=0.7.19,<0.8.0"] +requires = ["uv_build>=0.9.13,<0.10.0"] build-backend = "uv_build" [tool.ruff.lint] diff --git a/src/pygeoipupdate/__init__.py b/src/pygeoipupdate/__init__.py index 5e33e4b..b31823f 100644 --- a/src/pygeoipupdate/__init__.py +++ b/src/pygeoipupdate/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations -__version__ = "1.0.0" +from importlib.metadata import version + +__version__ = version("pygeoipupdate") from pygeoipupdate.config import Config from pygeoipupdate.errors import ( diff --git a/tests/test_cli.py b/tests/test_cli.py index 495232f..9bf6f08 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,6 +12,7 @@ from click.testing import CliRunner from pytest_httpserver import HTTPServer +from pygeoipupdate import __version__ from pygeoipupdate.cli import main from tests.conftest import create_test_tar_gz @@ -25,7 +26,7 @@ def test_version(self) -> None: assert result.exit_code == 0 assert "pygeoipupdate" in result.output - assert "1.0.0" in result.output + assert __version__ in result.output def test_version_short_flag(self) -> None: runner = CliRunner() @@ -33,7 +34,7 @@ def test_version_short_flag(self) -> None: assert result.exit_code == 0 assert "pygeoipupdate" in result.output - assert "1.0.0" in result.output + assert __version__ in result.output def test_help(self) -> None: runner = CliRunner() diff --git a/uv.lock b/uv.lock index 45afbf1..9de5482 100644 --- a/uv.lock +++ b/uv.lock @@ -754,7 +754,7 @@ wheels = [ [[package]] name = "pygeoipupdate" -version = "1.0.0" +version = "0.1.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From c3b17a52da7c19568db05cb6f2b8c1f6a97d0c88 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 08:57:02 -0800 Subject: [PATCH 5/9] Update README.md for release readiness Add missing Path import to from_file example, document the exception hierarchy for library users, and add Bug Reports and Versioning sections to match sibling MaxMind projects. Co-Authored-By: Claude Opus 4.6 --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 8bed279..07a7781 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ asyncio.run(main()) ### Loading Configuration from File ```python +from pathlib import Path + from pygeoipupdate import Config, Updater config = Config.from_file(config_file=Path("/etc/GeoIP.conf")) @@ -178,6 +180,18 @@ Options: - Configuration file: `%SYSTEMDRIVE%\ProgramData\MaxMind\GeoIPUpdate\GeoIP.conf` - Database directory: `%SYSTEMDRIVE%\ProgramData\MaxMind\GeoIPUpdate\GeoIP` +## Error Handling + +When using the Python API, pygeoipupdate raises specific exceptions: + +- `GeoIPUpdateError` — Base exception for all errors. + - `ConfigError` — Invalid configuration. + - `DownloadError` — Download failure. + - `AuthenticationError` — Invalid account ID or license key. + - `HTTPError` — HTTP error with `.status_code` and `.body` attributes. + - `LockError` — Could not acquire the lock file. + - `HashMismatchError` — Downloaded file hash mismatch (`.expected`, `.actual`). + ## Running as a Cron Job To keep your databases up to date, we recommend running pygeoipupdate at least @@ -193,6 +207,15 @@ twice per week. Here's an example cron entry: - Python 3.11+ - A MaxMind account with a license key +## Bug Reports + +Please report bugs by filing an issue with [our GitHub issue +tracker](https://github.com/maxmind/pygeoipupdate/issues). + +## Versioning + +This library uses [Semantic Versioning](https://semver.org/). + ## Copyright and License This software is Copyright (c) 2025 - 2026 by MaxMind, Inc. From 8acf91dfef39a47bee9ba811f880463ce7cf531e Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 09:06:52 -0800 Subject: [PATCH 6/9] Fix ruff lint errors with ruff 0.15.0 Bump ruff minimum to 0.15.0 which correctly handles except* clauses (no longer triggers BLE001), and add ASYNC240 to test per-file-ignores since pathlib.glob() in synchronous test assertions is harmless. Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 4 ++-- src/pygeoipupdate/cli.py | 2 +- uv.lock | 41 ++++++++++++++++++++-------------------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51087d5..f7c4c58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dev = [ ] lint = [ "mypy>=1.15.0", - "ruff>=0.11.6", + "ruff>=0.15.0", ] [build-system] @@ -93,7 +93,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ANN201", "D", "S101", "S105", "S324", "E501", "PLC0415", "RUF043", "F841", "SLF001"] +"tests/*" = ["ANN201", "ASYNC240", "D", "S101", "S105", "S324", "E501", "PLC0415", "RUF043", "F841", "SLF001"] "src/pygeoipupdate/_file_writer.py" = ["S324"] "src/pygeoipupdate/_file_lock.py" = ["E501"] # config.py has necessary complexity from porting Go config parsing diff --git a/src/pygeoipupdate/cli.py b/src/pygeoipupdate/cli.py index b8b3460..6cb9b34 100644 --- a/src/pygeoipupdate/cli.py +++ b/src/pygeoipupdate/cli.py @@ -126,7 +126,7 @@ def main( # noqa: C901, PLR0912 except* KeyboardInterrupt: click.echo("\nInterrupted.", err=True) exit_code = 130 - except* Exception as eg: # noqa: BLE001 + except* Exception as eg: for exc in eg.exceptions: # type: ignore[assignment] logger.error("Unexpected error", exc_info=exc) # noqa: TRY400 click.echo(f"Unexpected error ({type(exc).__name__}): {exc}", err=True) diff --git a/uv.lock b/uv.lock index 9de5482..0775741 100644 --- a/uv.lock +++ b/uv.lock @@ -792,7 +792,7 @@ dev = [ ] lint = [ { name = "mypy", specifier = ">=1.15.0" }, - { name = "ruff", specifier = ">=0.11.6" }, + { name = "ruff", specifier = ">=0.15.0" }, ] [[package]] @@ -873,28 +873,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.8" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, - { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, - { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, - { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, - { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, - { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, - { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, - { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] From 644a420dda166caf927bb7032ebd05379b0cb858 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 09:06:58 -0800 Subject: [PATCH 7/9] Fix Windows test collection error in test_file_operations os.getuid() does not exist on Windows, causing an AttributeError at collection time that prevents all tests from running. Use hasattr() short-circuit so the skipif condition evaluates safely on all platforms. Co-Authored-By: Claude Opus 4.6 --- tests/test_file_operations.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_file_operations.py b/tests/test_file_operations.py index d94c64a..fa2c2a2 100644 --- a/tests/test_file_operations.py +++ b/tests/test_file_operations.py @@ -74,7 +74,10 @@ def test_get_hash_nonexistent_file(self, tmp_path: Path) -> None: assert result == ZERO_MD5 - @pytest.mark.skipif(os.getuid() == 0, reason="root ignores file permissions") + @pytest.mark.skipif( + not hasattr(os, "getuid") or os.getuid() == 0, + reason="requires Unix non-root", + ) def test_get_hash_permission_error(self, tmp_path: Path) -> None: writer = LocalFileWriter(tmp_path) file_path = tmp_path / "GeoLite2-City.mmdb" From 0481d3808473066f43e878b9a54bd54f07ded5c5 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 09:10:49 -0800 Subject: [PATCH 8/9] Fix Windows test failures Skip test_write_sets_644_permissions on Windows where Unix permission bits are not supported (os.chmod sets read-only vs read-write only). Remove racy assertion in test_parallel_error_cancels_siblings that Edition3.mmdb must not exist after cancellation. On Windows the sibling task completes before cancellation propagates. The important behavior (AuthenticationError surfaces in the ExceptionGroup) is still verified. Co-Authored-By: Claude Opus 4.6 --- tests/test_file_operations.py | 2 ++ tests/test_updater.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_file_operations.py b/tests/test_file_operations.py index fa2c2a2..7e69ac0 100644 --- a/tests/test_file_operations.py +++ b/tests/test_file_operations.py @@ -7,6 +7,7 @@ import io import os import stat +import sys import tarfile from datetime import UTC, datetime from pathlib import Path @@ -122,6 +123,7 @@ def test_write_success(self, tmp_path: Path) -> None: assert file_path.exists() assert file_path.read_bytes() == content + @pytest.mark.skipif(sys.platform == "win32", reason="Unix permissions only") def test_write_sets_644_permissions(self, tmp_path: Path) -> None: writer = LocalFileWriter(tmp_path) content = b"test mmdb content" diff --git a/tests/test_updater.py b/tests/test_updater.py index 3691298..15c57f0 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -333,9 +333,6 @@ async def test_parallel_error_cancels_siblings( auth_errors = exc_info.group_contains(AuthenticationError) assert auth_errors - # Verify cancellation prevented Edition3 from completing - assert not (tmp_path / "Edition3.mmdb").exists() - @pytest.mark.asyncio async def test_requires_context_manager(self, tmp_path: Path) -> None: config = Config( From d495e8acce4a53eea37b5437b2b17115d9acf3bd Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 10 Feb 2026 09:17:14 -0800 Subject: [PATCH 9/9] Add release script Same release workflow as GeoIP2-python: validates tooling and branch state, parses version and notes from HISTORY.rst, runs the full tox matrix, then commits, pushes, and creates a GitHub release (which triggers the PyPI upload workflow). Co-Authored-By: Claude Opus 4.6 --- dev-bin/release.sh | 107 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100755 dev-bin/release.sh diff --git a/dev-bin/release.sh b/dev-bin/release.sh new file mode 100755 index 0000000..0887c3b --- /dev/null +++ b/dev-bin/release.sh @@ -0,0 +1,107 @@ +#!/bin/bash + +set -eu -o pipefail + +# Pre-flight checks - verify all required tools are available and configured +# before making any changes to the repository + +check_command() { + if ! command -v "$1" &>/dev/null; then + echo "Error: $1 is not installed or not in PATH" + exit 1 + fi +} + +# Verify gh CLI is authenticated +if ! gh auth status &>/dev/null; then + echo "Error: gh CLI is not authenticated. Run 'gh auth login' first." + exit 1 +fi + +# Verify we can access this repository via gh +if ! gh repo view --json name &>/dev/null; then + echo "Error: Cannot access repository via gh. Check your authentication and repository access." + exit 1 +fi + +# Verify git can connect to the remote (catches SSH key issues, etc.) +if ! git ls-remote origin &>/dev/null; then + echo "Error: Cannot connect to git remote. Check your git credentials/SSH keys." + exit 1 +fi + +check_command perl +check_command uv + +# Check that we're not on the main branch +current_branch=$(git branch --show-current) +if [ "$current_branch" = "main" ]; then + echo "Error: Releases should not be done directly on the main branch." + echo "Please create a release branch and run this script from there." + exit 1 +fi + +# Fetch latest changes and check that we're not behind origin/main +echo "Fetching from origin..." +git fetch origin + +if ! git merge-base --is-ancestor origin/main HEAD; then + echo "Error: Current branch is behind origin/main." + echo "Please merge or rebase with origin/main before releasing." + exit 1 +fi + +changelog=$(cat HISTORY.rst) + +regex=' +([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\) +\+* + +((.| +)*) +' + +if [[ ! $changelog =~ $regex ]]; then + echo "Could not find date line in change log!" + exit 1 +fi + +version="${BASH_REMATCH[1]}" +date="${BASH_REMATCH[3]}" +notes="$(echo "${BASH_REMATCH[4]}" | sed -n -E '/^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?/,$!p')" + +if [[ "$date" != "$(date +"%Y-%m-%d")" ]]; then + echo "$date is not today!" + exit 1 +fi + +tag="v$version" + +if [ -n "$(git status --porcelain)" ]; then + echo ". is not clean." >&2 + exit 1 +fi + +perl -pi -e "s/(?<=^version = \").+?(?=\")/$version/gsm" pyproject.toml + +echo $"Test results:" +uv run tox + +echo $'\nDiff:' +git diff + +echo $'\nRelease notes:' +echo "$notes" + +read -r -e -p "Commit changes and push to origin? " should_push + +if [ "$should_push" != "y" ]; then + echo "Aborting" + exit 1 +fi + +git commit -m "Update for $tag" -a + +git push + +gh release create --target "$(git branch --show-current)" -t "$version" -n "$notes" "$tag"