Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pyRFC3339 for 
openSUSE:Factory checked in at 2026-04-09 16:09:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyRFC3339 (Old)
 and      /work/SRC/openSUSE:Factory/.python-pyRFC3339.new.21863 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pyRFC3339"

Thu Apr  9 16:09:27 2026 rev:9 rq:1345297 version:2.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pyRFC3339/python-pyRFC3339.changes        
2025-05-02 15:00:48.137487122 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-pyRFC3339.new.21863/python-pyRFC3339.changes 
    2026-04-09 16:22:20.472582800 +0200
@@ -1,0 +2,12 @@
+Wed Apr  8 21:56:34 UTC 2026 - Dirk MΓΌller <[email protected]>
+
+- update to 2.1.0:
+  * Greatly simplify timestamp parsing and generation code,
+    leveraging improvements in native Python capabilities in the
+    past decade-and-a-half. See :commit:`53c2d15` for further
+    details.
+  * Simplify GitHub Actions workflow. Add zizmor to audit
+    workflows.
+  * Modernize packaging and documentation configuration.
+
+-------------------------------------------------------------------

Old:
----
  pyRFC3339-2.0.1.tar.gz

New:
----
  pyRFC3339-2.1.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pyRFC3339.spec ++++++
--- /var/tmp/diff_new_pack.RFtADp/_old  2026-04-09 16:22:22.284657153 +0200
+++ /var/tmp/diff_new_pack.RFtADp/_new  2026-04-09 16:22:22.300657810 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-pyRFC3339
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-pyRFC3339
-Version:        2.0.1
+Version:        2.1.0
 Release:        0
 Summary:        Generate and parse RFC 3339 timestamps
 License:        MIT
@@ -27,7 +27,8 @@
 Source:         
https://github.com/kurtraschke/pyRFC3339/archive/refs/tags/v%{version}.tar.gz#/pyRFC3339-%{version}.tar.gz
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module pytest}
-BuildRequires:  %{python_module setuptools}
+BuildRequires:  %{python_module setuptools >= 64}
+BuildRequires:  %{python_module setuptools_scm >= 8}
 BuildRequires:  %{python_module wheel}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
@@ -43,6 +44,7 @@
 %autosetup -p1 -n pyRFC3339-%{version}
 
 %build
+export SETUPTOOLS_SCM_PRETEND_VERSION=%{version}
 %pyproject_wheel
 
 %install

++++++ pyRFC3339-2.0.1.tar.gz -> pyRFC3339-2.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.github/dependabot.yml 
new/pyRFC3339-2.1.0/.github/dependabot.yml
--- old/pyRFC3339-2.0.1/.github/dependabot.yml  2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/.github/dependabot.yml  2025-08-23 18:22:25.000000000 
+0200
@@ -9,3 +9,9 @@
     directory: "/" # Location of package manifests
     schedule:
       interval: "weekly"
+  - package-ecosystem: "pip" # See documentation for possible values
+    directories:
+      - "/docs"
+      - "/"
+    schedule:
+      interval: "weekly"
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.github/workflows/pypi-publish.yml 
new/pyRFC3339-2.1.0/.github/workflows/pypi-publish.yml
--- old/pyRFC3339-2.0.1/.github/workflows/pypi-publish.yml      2024-11-04 
02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/.github/workflows/pypi-publish.yml      1970-01-01 
01:00:00.000000000 +0100
@@ -1,117 +0,0 @@
-name: Publish Python 🐍 distribution πŸ“¦ to PyPI and TestPyPI
-
-on: push
-
-jobs:
-  build:
-    name: Build distribution πŸ“¦
-    runs-on: ubuntu-latest
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.x"
-      - name: Install pypa/build
-        run: >-
-          python3 -m
-          pip install
-          build
-          --user
-      - name: Build a binary wheel and a source tarball
-        run: python3 -m build
-      - name: Store the distribution packages
-        uses: actions/upload-artifact@v4
-        with:
-          name: python-package-distributions
-          path: dist/
-
-  publish-to-pypi:
-    name: >-
-      Publish Python 🐍 distribution πŸ“¦ to PyPI
-    if: startsWith(github.ref, 'refs/tags/')  # only publish to PyPI on tag 
pushes
-    needs:
-      - build
-    runs-on: ubuntu-latest
-    environment:
-      name: pypi
-      url: https://pypi.org/p/pyRFC3339
-    permissions:
-      id-token: write  # IMPORTANT: mandatory for trusted publishing
-
-    steps:
-      - name: Download all the dists
-        uses: actions/download-artifact@v4
-        with:
-          name: python-package-distributions
-          path: dist/
-      - name: Publish distribution πŸ“¦ to PyPI
-        uses: pypa/gh-action-pypi-publish@release/v1
-
-  github-release:
-    name: >-
-      Sign the Python 🐍 distribution πŸ“¦ with Sigstore
-      and upload them to GitHub Release
-    needs:
-      - publish-to-pypi
-    runs-on: ubuntu-latest
-
-    permissions:
-      contents: write  # IMPORTANT: mandatory for making GitHub Releases
-      id-token: write  # IMPORTANT: mandatory for sigstore
-
-    steps:
-      - name: Download all the dists
-        uses: actions/download-artifact@v4
-        with:
-          name: python-package-distributions
-          path: dist/
-      - name: Sign the dists with Sigstore
-        uses: sigstore/[email protected]
-        with:
-          inputs: >-
-            ./dist/*.tar.gz
-            ./dist/*.whl
-      - name: Create GitHub Release
-        env:
-          GITHUB_TOKEN: ${{ github.token }}
-        run: >-
-          gh release create
-          '${{ github.ref_name }}'
-          --repo '${{ github.repository }}'
-          --notes ""
-      - name: Upload artifact signatures to GitHub Release
-        env:
-          GITHUB_TOKEN: ${{ github.token }}
-        # Upload to GitHub Release using the `gh` CLI.
-        # `dist/` contains the built packages, and the
-        # sigstore-produced signatures and certificates.
-        run: >-
-          gh release upload
-          '${{ github.ref_name }}' dist/**
-          --repo '${{ github.repository }}'
-
-  publish-to-testpypi:
-    name: Publish Python 🐍 distribution πŸ“¦ to TestPyPI
-    needs:
-      - build
-    runs-on: ubuntu-latest
-
-    environment:
-      name: testpypi
-      url: https://test.pypi.org/p/pyRFC3339
-
-    permissions:
-      id-token: write  # IMPORTANT: mandatory for trusted publishing
-
-    steps:
-      - name: Download all the dists
-        uses: actions/download-artifact@v4
-        with:
-          name: python-package-distributions
-          path: dist/
-      - name: Publish distribution πŸ“¦ to TestPyPI
-        uses: pypa/gh-action-pypi-publish@release/v1
-        with:
-          repository-url: https://test.pypi.org/legacy/
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.github/workflows/python.yml 
new/pyRFC3339-2.1.0/.github/workflows/python.yml
--- old/pyRFC3339-2.0.1/.github/workflows/python.yml    1970-01-01 
01:00:00.000000000 +0100
+++ new/pyRFC3339-2.1.0/.github/workflows/python.yml    2025-08-23 
18:22:25.000000000 +0200
@@ -0,0 +1,238 @@
+name: Test, build, and publish Python distribution to PyPI and TestPyPI
+
+on:
+  push:
+  pull_request:
+
+permissions:
+  contents: read
+
+concurrency:
+  group: check-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Test with ${{ matrix.env }} on ${{ matrix.os }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        env:
+          - "3.9"
+          - "3.10"
+          - "3.11"
+          - "3.12"
+          - "3.13"
+        os:
+          - ubuntu-latest
+          - macos-latest
+          - windows-latest
+    steps:
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 
v5.0.0
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Install the latest version of uv
+        uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # 
v6.6.0
+        with:
+          enable-cache: true
+          cache-dependency-glob: "pyproject.toml"
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Add .local/bin to Windows PATH
+        if: runner.os == 'Windows'
+        shell: bash
+        run: echo "$USERPROFILE/.local/bin" >> $GITHUB_PATH
+
+      - 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 }}
+
+  build-docs:
+    name: Build docs
+    needs:
+      - test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 
v5.0.0
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Install the latest version of uv
+        uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # 
v6.6.0
+        with:
+          enable-cache: true
+          cache-dependency-glob: "pyproject.toml"
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Install tox
+        run: uv tool install --python-preference only-managed --python 3.13 
tox --with tox-uv
+
+      - name: Run test suite
+        run: tox run -e docs
+
+      - name: Upload static files as artifact
+        id: deployment
+        uses: 
actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
+        with:
+          path: docs/build/html/
+
+  deploy-docs:
+    name: Deploy docs to GitHub Pages
+    if: github.ref == 'refs/heads/main'
+    needs:
+      - build-docs
+
+    permissions:
+      pages: write
+      id-token: write
+
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+
+    runs-on: ubuntu-latest
+    steps:
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # 
v4.0.5
+
+  build:
+    name: Build distribution
+    needs:
+      - test
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 
v5.0.0
+        with:
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Set up Python
+        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 
v5.6.0
+        with:
+          python-version: "3.x"
+
+      - name: Install pypa/build
+        run: python3 -m pip install build --user
+
+      - name: Build a binary wheel and a source tarball
+        env:
+          SETUPTOOLS_SCM_OVERRIDES_FOR_PYRFC3339: ${{ startsWith(github.ref, 
'refs/tags/v') && '{}' || '{local_scheme = "no-local-version"}' }}
+        run: python3 -m build
+
+      - name: Store the distribution packages
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 
# v4.6.2
+        with:
+          name: python-package-distributions
+          path: dist/
+
+  publish-to-pypi:
+    name: Publish Python distribution to PyPI
+    if: startsWith(github.ref, 'refs/tags/v')  # only publish to PyPI on 
pushes of tags whose names start with 'v'
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/pyRFC3339
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+
+    steps:
+      - name: Download all the dists
+        uses: 
actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
+        with:
+          name: python-package-distributions
+          path: dist/
+
+      - name: Publish distribution to PyPI
+        uses: 
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
+
+  github-release:
+    name: Sign the Python distribution with Sigstore and upload them to GitHub 
Release
+    needs:
+      - publish-to-pypi
+    runs-on: ubuntu-latest
+
+    permissions:
+      contents: write  # IMPORTANT: mandatory for making GitHub Releases
+      id-token: write  # IMPORTANT: mandatory for sigstore
+
+    steps:
+      - name: Download all the dists
+        uses: 
actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
+        with:
+          name: python-package-distributions
+          path: dist/
+
+      - name: Sign the dists with Sigstore
+        uses: 
sigstore/gh-action-sigstore-python@f7ad0af51a5648d09a20d00370f0a91c3bdf8f84 # 
v3.0.1
+        with:
+          inputs: >-
+            ./dist/*.tar.gz
+            ./dist/*.whl
+
+      - name: Create GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        run: >-
+          gh release create
+          '${GITHUB_REF_NAME}'
+          --repo '${{ github.repository }}'
+          --notes ""
+
+      - name: Upload artifact signatures to GitHub Release
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+        # Upload to GitHub Release using the `gh` CLI.
+        # `dist/` contains the built packages, and the
+        # sigstore-produced signatures and certificates.
+        run: >-
+          gh release upload
+          '${GITHUB_REF_NAME}' dist/**
+          --repo '${{ github.repository }}'
+
+  publish-to-testpypi:
+    name: Publish Python distribution to TestPyPI
+    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 
'refs/tags/v')  # only publish to Test PyPI on pushes to main, or pushes of 
tags whose names start with 'v'
+    needs:
+      - build
+    runs-on: ubuntu-latest
+
+    environment:
+      name: testpypi
+      url: https://test.pypi.org/p/pyRFC3339
+
+    permissions:
+      id-token: write  # IMPORTANT: mandatory for trusted publishing
+
+    steps:
+      - name: Download all the dists
+        uses: 
actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
+        with:
+          name: python-package-distributions
+          path: dist/
+
+      - name: Publish distribution to TestPyPI
+        uses: 
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
+        with:
+          repository-url: https://test.pypi.org/legacy/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.github/workflows/test-python.yml 
new/pyRFC3339-2.1.0/.github/workflows/test-python.yml
--- old/pyRFC3339-2.0.1/.github/workflows/test-python.yml       2024-11-04 
02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/.github/workflows/test-python.yml       1970-01-01 
01:00:00.000000000 +0100
@@ -1,25 +0,0 @@
-name: Test Python with tox
-
-on:
-  - push
-  - pull_request
-
-jobs:
-  build:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
-
-    steps:
-      - uses: actions/checkout@v4
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-      - name: Install dependencies
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install tox tox-gh-actions
-      - name: Test with tox
-        run: tox
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.github/workflows/zizmor.yml 
new/pyRFC3339-2.1.0/.github/workflows/zizmor.yml
--- old/pyRFC3339-2.0.1/.github/workflows/zizmor.yml    1970-01-01 
01:00:00.000000000 +0100
+++ new/pyRFC3339-2.1.0/.github/workflows/zizmor.yml    2025-08-23 
18:22:25.000000000 +0200
@@ -0,0 +1,24 @@
+name: GitHub Actions Security Analysis with zizmor
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    branches: ["**"]
+
+permissions: {}
+
+jobs:
+  zizmor:
+    name: Run zizmor
+    runs-on: ubuntu-latest
+    permissions:
+      security-events: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 
v4.2.2
+        with:
+          persist-credentials: false
+
+      - name: Run zizmor
+        uses: 
zizmorcore/zizmor-action@f52a838cfabf134edcbaa7c8b3677dde20045018 # v0.1.1
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.idea/misc.xml 
new/pyRFC3339-2.1.0/.idea/misc.xml
--- old/pyRFC3339-2.0.1/.idea/misc.xml  2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/.idea/misc.xml  2025-08-23 18:22:25.000000000 +0200
@@ -1,10 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="Black">
-    <option name="sdkName" value="Python 3.12 (pyrfc3339)" />
+    <option name="executionMode" value="BINARY" />
+    <option name="pathToExecutable" value="/opt/homebrew/bin/black" />
+    <option name="sdkName" value="Python 3.13" />
   </component>
   <component name="JavaScriptSettings">
     <option name="languageLevel" value="ES6" />
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_9" 
project-jdk-name="Python 3.12 (pyrfc3339)" project-jdk-type="Python SDK" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_9" 
project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
 </project>
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/.readthedocs.yml 
new/pyRFC3339-2.1.0/.readthedocs.yml
--- old/pyRFC3339-2.0.1/.readthedocs.yml        2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/.readthedocs.yml        2025-08-23 18:22:25.000000000 
+0200
@@ -10,9 +10,9 @@
   configuration: docs/source/conf.py
 
 build:
-  os: ubuntu-22.04
+  os: ubuntu-24.04
   tools:
-    python: "3.12"
+    python: "3.13"
 
 # Build documentation with MkDocs
 #mkdocs:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/CHANGES.rst 
new/pyRFC3339-2.1.0/CHANGES.rst
--- old/pyRFC3339-2.0.1/CHANGES.rst     2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/CHANGES.rst     2025-08-23 18:22:25.000000000 +0200
@@ -1,6 +1,16 @@
 Changelog
 =========
 
+2.1.0 (2025-08-23)
+------------------
+
+(The Git branch used for development was renamed from ``master`` to ``main``)
+
+- Greatly simplify timestamp parsing and generation code, leveraging 
improvements in native Python
+  capabilities in the past decade-and-a-half. See :commit:`53c2d15` for 
further details.
+- Simplify GitHub Actions workflow. Add `zizmor <http://zizmor.sh/>`_ to audit 
workflows.
+- Modernize packaging and documentation configuration.
+
 2.0.1 (2024-11-03)
 ------------------
 
@@ -11,9 +21,9 @@
 
 (not released to PyPI)
 
-- Migrate tests from `nose` to `unittest` and `pytest` (:issue:`16`)
+- Migrate tests from ``nose`` to ``unittest`` and ``pytest`` (:issue:`16`)
 - Replace :mod:`pytz` dependency with :attr:`datetime.timezone.utc` and 
:mod:`zoneinfo` (:issue:`15`)
-- Reformat codebase with `black` and `isort`
+- Reformat codebase with ``black`` and ``isort``
 - Configure GitHub Actions; remove Travis CI configuration file
 
 1.1 (2018-06-10)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/LICENSE.txt 
new/pyRFC3339-2.1.0/LICENSE.txt
--- old/pyRFC3339-2.0.1/LICENSE.txt     2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/LICENSE.txt     2025-08-23 18:22:25.000000000 +0200
@@ -1,4 +1,4 @@
-Copyright (c) 2024 Kurt Raschke
+Copyright (c) 2025 Contributors to the pyRFC3339 project
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/MANIFEST.in 
new/pyRFC3339-2.1.0/MANIFEST.in
--- old/pyRFC3339-2.0.1/MANIFEST.in     2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/MANIFEST.in     2025-08-23 18:22:25.000000000 +0200
@@ -1 +1,4 @@
-include LICENSE.txt
+prune pyrfc3339/tests
+prune .idea
+prune .github
+exclude .readthedocs.yml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/README.rst 
new/pyRFC3339-2.1.0/README.rst
--- old/pyRFC3339-2.0.1/README.rst      2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/README.rst      2025-08-23 18:22:25.000000000 +0200
@@ -1,8 +1,8 @@
 Description
 ===========
 
-.. image:: 
https://github.com/kurtraschke/pyRFC3339/actions/workflows/test-python.yml/badge.svg
-    :target: 
https://github.com/kurtraschke/pyRFC3339/actions/workflows/test-python.yml
+.. image:: 
https://github.com/kurtraschke/pyRFC3339/actions/workflows/python.yml/badge.svg
+    :target: 
https://github.com/kurtraschke/pyRFC3339/actions/workflows/python.yml
     :alt: Build Status
 
 .. image:: https://readthedocs.org/projects/pyrfc3339/badge/?version=latest
@@ -18,24 +18,27 @@
 >>> parse('2009-01-01T10:01:02Z')
 datetime.datetime(2009, 1, 1, 10, 1, 2, tzinfo=datetime.timezone.utc)
 >>> parse('2009-01-01T14:01:02-04:00')
-datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000), 
'<UTC-04:00>'))
+datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)))
 
 Installation
 ============
 
-To install the latest version from `PyPI <https://pypi.python.org/pypi>`_:
+To install the latest version from `PyPI <https://pypi.org/>`_:
 
 ``$ pip install pyRFC3339``
 
 To install the latest development version:
 
-``$ pip install 
https://github.com/kurtraschke/pyRFC3339/tarball/master#egg=pyRFC3339-dev``
+``$ pip install 
https://github.com/kurtraschke/pyRFC3339/tarball/main#egg=pyRFC3339-dev``
+
+Tests as well as enforcement of code style, formatting, and type safety are 
run with `tox <https://tox.wiki/>`_:
+
+``$ tox``
 
 To build the documentation with Sphinx:
 
-#. ``$ pip install -r docs/requirements.txt``
-#. ``$ sphinx-build -M html docs/source/ docs/build``
+``$ tox -e docs``
 
 The documentation is also available online at:
 
-``https://pyrfc3339.readthedocs.io/``
+https://pyrfc3339.readthedocs.io/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/docs/requirements.txt 
new/pyRFC3339-2.1.0/docs/requirements.txt
--- old/pyRFC3339-2.0.1/docs/requirements.txt   2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/docs/requirements.txt   2025-08-23 18:22:25.000000000 
+0200
@@ -1,3 +1,3 @@
-Sphinx==8.1.3
-sphinx-issues==5.0.0
-sphinx-rtd-theme==3.0.1
\ No newline at end of file
+Sphinx==8.2.3
+sphinx-issues==5.0.1
+sphinx-rtd-theme==3.0.2
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/docs/source/conf.py 
new/pyRFC3339-2.1.0/docs/source/conf.py
--- old/pyRFC3339-2.0.1/docs/source/conf.py     2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/docs/source/conf.py     2025-08-23 18:22:25.000000000 
+0200
@@ -12,6 +12,8 @@
 # serve to show the default.
 
 import sys, os
+from importlib.metadata import version as get_version
+
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -28,7 +30,7 @@
 templates_path = ['_templates']
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = {'.rst': 'restructuredtext'}
 
 # The encoding of source files.
 #source_encoding = 'utf-8'
@@ -38,16 +40,15 @@
 
 # General information about the project.
 project = u'pyRFC3339'
-copyright = u'2024, Kurt Raschke'
+copyright = u'2025 Contributors to the pyRFC3339 project'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
-# The short X.Y version.
-version = '2.0.1'
-# The full version, including alpha/beta/rc tags.
-release = '2.0.1'
+
+release: str = get_version("pyrfc3339")
+version: str = ".".join(release.split('.')[:2])
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -197,4 +198,10 @@
 # Example configuration for intersphinx: refer to the Python standard library.
 intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
 
-issues_github_path = 'kurtraschke/pyrfc3339'
\ No newline at end of file
+issues_github_path = 'kurtraschke/pyrfc3339'
+
+rst_prolog = """
+.. role:: python(code)
+    :language: python
+    :class: highlight
+"""
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/docs/source/index.rst 
new/pyRFC3339-2.1.0/docs/source/index.rst
--- old/pyRFC3339-2.0.1/docs/source/index.rst   2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/docs/source/index.rst   2025-08-23 18:22:25.000000000 
+0200
@@ -12,5 +12,4 @@
    intro
    changes
    doc
-
-
+   license
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/docs/source/license.rst 
new/pyRFC3339-2.1.0/docs/source/license.rst
--- old/pyRFC3339-2.0.1/docs/source/license.rst 1970-01-01 01:00:00.000000000 
+0100
+++ new/pyRFC3339-2.1.0/docs/source/license.rst 2025-08-23 18:22:25.000000000 
+0200
@@ -0,0 +1,4 @@
+License
+=======
+
+.. include:: ../../LICENSE.txt
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyproject.toml 
new/pyRFC3339-2.1.0/pyproject.toml
--- old/pyRFC3339-2.0.1/pyproject.toml  2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/pyproject.toml  2025-08-23 18:22:25.000000000 +0200
@@ -1,3 +1,99 @@
 [build-system]
-requires = ["setuptools"]
+requires = ["setuptools>=64", "setuptools_scm>=8"]
 build-backend = "setuptools.build_meta"
+
+[project]
+name = "pyRFC3339"
+dynamic = ["version"]
+requires-python = ">= 3.9"
+authors = [
+    { name = "Kurt Raschke", email = "[email protected]" }
+]
+description = "Generate and parse RFC 3339 timestamps"
+readme = { file = "README.rst", content-type = "text/x-rst" }
+keywords = ["rfc-3339", "timestamp", "iso-8601", "datetime"]
+license = "MIT"
+license-files = ["LICENSE.txt"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Topic :: Internet"
+]
+
+[project.urls]
+Homepage = "https://github.com/kurtraschke/pyrfc3339";
+Documentation = "https://pyrfc3339.readthedocs.io/";
+Repository = "https://github.com/kurtraschke/pyRFC3339.git";
+"Bug Tracker" = "https://github.com/kurtraschke/pyRFC3339/issues";
+Changelog = "https://github.com/kurtraschke/pyRFC3339/blob/main/CHANGES.rst";
+
+[tool.setuptools]
+packages = ["pyrfc3339"]
+
+[tool.setuptools_scm]
+
+[tool.mypy]
+strict = true
+
+[tool.flake8]
+max-line-length = 132
+
+[tool.isort]
+profile = "black"
+
+[tool.coverage.run]
+omit = ["pyrfc3339/tests/*"]
+
+[tool.tox]
+requires = ["tox>=4"]
+env_list = ["sort", "format", "style", "type", "3.9", "3.10", "3.11", "3.12", 
"3.13"]
+skip_missing_interpreters = true
+
+[tool.tox.gh.python]
+"3.13" = ["sort", "format", "style", "type", "3.13"]
+"3.12" = ["3.12"]
+"3.11" = ["3.11"]
+"3.10" = ["3.10"]
+"3.9" = ["3.9"]
+
+[tool.tox.env_run_base]
+deps = ["pytest", "pytest-subtests", "pytest-cov", 
"tzdata;platform_system==\"Windows\""]
+commands = [["pytest",
+    "--doctest-glob=docs/source/*.rst",
+    "--doctest-glob=README.rst",
+    "--doctest-modules",
+    "--doctest-continue-on-failure",
+    "--cov=pyrfc3339",
+    "."]]
+
+[tool.tox.env.type]
+skip_install = true
+deps = ["mypy"]
+commands = [["mypy", "pyrfc3339"]]
+
+[tool.tox.env.sort]
+skip_install = true
+deps = ["isort"]
+commands = [["isort", "--check", "--diff", "pyrfc3339"]]
+
+[tool.tox.env.format]
+skip_install = true
+deps = ["black"]
+commands = [["black", "--check", "--diff", "pyrfc3339"]]
+
+[tool.tox.env.style]
+skip_install = true
+deps = ["flake8", "flake8-pyproject"]
+commands = [["flake8", "pyrfc3339"]]
+
+[tool.tox.env.docs]
+deps = ["-r docs/requirements.txt"]
+commands = [["sphinx-build", "-M", "html", "docs/source", "docs/build"]]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyrfc3339/__init__.py 
new/pyRFC3339-2.1.0/pyrfc3339/__init__.py
--- old/pyRFC3339-2.0.1/pyrfc3339/__init__.py   2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/pyrfc3339/__init__.py   2025-08-23 18:22:25.000000000 
+0200
@@ -9,11 +9,18 @@
 >>> parse('2009-01-01T10:01:02Z')
 datetime.datetime(2009, 1, 1, 10, 1, 2, tzinfo=datetime.timezone.utc)
 >>> parse('2009-01-01T14:01:02-04:00')
-datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000), 
'<UTC-04:00>'))
+datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)))
 
 """
 
-from pyrfc3339.generator import generate
-from pyrfc3339.parser import parse
+from importlib.metadata import PackageNotFoundError, version
+
+from .generator import generate
+from .parser import parse
+
+try:
+    __version__ = version("pyrfc3339")
+except PackageNotFoundError:
+    pass
 
 __all__ = ["generate", "parse"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyrfc3339/generator.py 
new/pyRFC3339-2.1.0/pyrfc3339/generator.py
--- old/pyRFC3339-2.0.1/pyrfc3339/generator.py  2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/pyrfc3339/generator.py  2025-08-23 18:22:25.000000000 
+0200
@@ -1,21 +1,23 @@
-import datetime
+import re
+from datetime import datetime, timezone
 
-from pyrfc3339.utils import format_timezone
 
-
-def generate(dt, utc=True, accept_naive=False, microseconds=False):
+def generate(
+    dt: datetime,
+    utc: bool = True,
+    accept_naive: bool = False,
+    microseconds: bool = False,
+) -> str:
     """
-    Generate an :RFC:`3339`-formatted timestamp from a
-    :class:`datetime.datetime`.
+    Generate an :RFC:`3339`-formatted timestamp from a 
:class:`datetime.datetime`.
 
     >>> from datetime import datetime, timezone
     >>> from zoneinfo import ZoneInfo
     >>> generate(datetime(2009, 1, 1, 12, 59, 59, 0, timezone.utc))
     '2009-01-01T12:59:59Z'
 
-    The timestamp will use UTC unless `utc=False` is specified, in which case
-    it will use the timezone from the :class:`datetime.datetime`'s
-    :attr:`tzinfo` parameter.
+    The timestamp be normalized to UTC unless :python:`utc=False` is 
specified, in which case
+    it will use the timezone from the :class:`~datetime.datetime`'s 
:attr:`~datetime.datetime.tzinfo` attribute.
 
     >>> eastern = ZoneInfo('US/Eastern')
     >>> dt = datetime(2009, 1, 1, 12, 59, 59, tzinfo=eastern)
@@ -24,7 +26,7 @@
     >>> generate(dt, utc=False)
     '2009-01-01T12:59:59-05:00'
 
-    Unless `accept_naive=True` is specified, the `datetime` must not be naive.
+    Unless :python:`accept_naive=True` is specified, the 
:class:`~datetime.datetime` must not be naive.
 
     >>> generate(datetime(2009, 1, 1, 12, 59, 59, 0))
     Traceback (most recent call last):
@@ -34,21 +36,33 @@
     >>> generate(datetime(2009, 1, 1, 12, 59, 59, 0), accept_naive=True)
     '2009-01-01T12:59:59Z'
 
-    If `accept_naive=True` is specified, the `datetime` is assumed to be UTC.
-    Attempting to generate a local timestamp from a naive datetime will result
-    in an error.
+    If, however, :python:`accept_naive=True` is specified, the 
:class:`~datetime.datetime` is assumed to represent a UTC time.
+    Attempting to generate a local timestamp from a naive datetime will result 
in an error.
 
     >>> generate(datetime(2009, 1, 1, 12, 59, 59, 0), accept_naive=True, 
utc=False)
     Traceback (most recent call last):
     ...
     ValueError: cannot generate a local timestamp from a naive datetime
 
+    :param datetime.datetime dt: the :class:`~datetime.datetime` for which to 
generate an :RFC:`3339` timestamp.
+    :param bool utc: :const:`True` to normalize the supplied 
:class:`datetime.datetime` to UTC; :const:`False` otherwise.
+                     Defaults to :const:`True`.
+    :param bool accept_naive: :const:`True` if :func:`generate()` should 
accept a 'naive' datetime
+                              (that is, one without timezone information) and 
treat it as a UTC timestamp;
+                              :const:`False` otherwise. Defaults to 
:const:`False`.
+    :param bool microseconds: :const:`True` to generate a timestamp which 
includes fractional seconds, if present;
+                              :const:`False` otherwise. Defaults to 
:const:`False`.
+                              Note that fractional seconds are *truncated*,
+                              not rounded when :obj:`microseconds` is 
:const:`False`.
+    :return: the supplied :class:`~datetime.datetime` instance represented as 
an :RFC:`3339` timestamp
+    :rtype: str
+
     """
 
     if dt.tzinfo is None:
-        if accept_naive is True:
-            if utc is True:
-                dt = dt.replace(tzinfo=datetime.timezone.utc)
+        if accept_naive:
+            if utc:
+                dt = dt.replace(tzinfo=timezone.utc)
             else:
                 raise ValueError(
                     "cannot generate a local timestamp from a naive datetime"
@@ -56,15 +70,12 @@
         else:
             raise ValueError("naive datetime and accept_naive is False")
 
-    if utc is True:
-        dt = dt.astimezone(datetime.timezone.utc)
+    if utc:
+        dt = dt.astimezone(timezone.utc)
+
+    timestamp = dt.isoformat(timespec="microseconds" if microseconds else 
"seconds")
 
-    timestamp = dt.strftime("%Y-%m-%dT%H:%M:%S")
-    if microseconds is True:
-        timestamp += dt.strftime(".%f")
-    if dt.tzinfo is datetime.timezone.utc:
-        timestamp += "Z"
-    else:
-        timestamp += format_timezone(dt.tzinfo.utcoffset(dt).total_seconds())
+    if dt.tzinfo == timezone.utc:
+        timestamp = re.sub(r"\+00:00$", "Z", timestamp)
 
     return timestamp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyrfc3339/parser.py 
new/pyRFC3339-2.1.0/pyrfc3339/parser.py
--- old/pyRFC3339-2.0.1/pyrfc3339/parser.py     2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/pyrfc3339/parser.py     2025-08-23 18:22:25.000000000 
+0200
@@ -1,94 +1,82 @@
 import re
-from datetime import datetime, timedelta, timezone
+import sys
+from datetime import datetime, timezone
 
-from pyrfc3339.utils import format_timezone
+from .utils import datetime_utcoffset
 
 
-def parse(timestamp, utc=False, produce_naive=False):
+def parse(timestamp: str, utc: bool = False, produce_naive: bool = False) -> 
datetime:
     """
-    Parse an :RFC:`3339`-formatted timestamp and return a
-    :class:`datetime.datetime`.
+    Parse an :RFC:`3339`-formatted timestamp and return a 
:class:`datetime.datetime`.
 
-    If the timestamp is presented in UTC, then the `tzinfo` parameter of the
+    If the timestamp is presented in UTC, then the 
:attr:`~datetime.datetime.tzinfo` attribute of the
     returned `datetime` will be set to :attr:`datetime.timezone.utc`.
 
     >>> parse('2009-01-01T10:01:02Z')
     datetime.datetime(2009, 1, 1, 10, 1, 2, tzinfo=datetime.timezone.utc)
 
     Otherwise, a :class:`datetime.timezone` instance is created with the 
appropriate offset, and
-    the `tzinfo` parameter of the returned `datetime` is set to that value.
+    the :attr:`~datetime.datetime.tzinfo` attribute of the returned 
:class:`~datetime.datetime` is set to that value.
 
     >>> parse('2009-01-01T14:01:02-04:00')
-    datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000), 
'<UTC-04:00>'))
+    datetime.datetime(2009, 1, 1, 14, 1, 2, 
tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000)))
 
-    However, if `parse()`  is called with `utc=True`, then the returned
-    `datetime` will be normalized to UTC (and its tzinfo parameter set to
-    `datetime.timezone.utc`), regardless of the input timezone.
+    However, if :meth:`parse()`  is called with :python:`utc=True`, then the 
returned
+    :class:`~datetime.datetime` will be normalized to UTC (and its 
:attr:`~datetime.datetime.tzinfo` attribute set to
+    :attr:`~datetime.timezone.utc`), regardless of the input timezone.
 
     >>> parse('2009-01-01T06:01:02-04:00', utc=True)
     datetime.datetime(2009, 1, 1, 10, 1, 2, tzinfo=datetime.timezone.utc)
 
-    The input is strictly required to conform to :RFC:`3339`, and appropriate
-    exceptions are thrown for invalid input.
+    As parsing is delegated to :meth:`datetime.datetime.fromisoformat()`, 
certain
+    timestamps which do not strictly adhere to :RFC:`3339` are nonetheless 
accepted.
 
     >>> parse('2009-01-01T06:01:02')
-    Traceback (most recent call last):
-    ...
-    ValueError: timestamp does not conform to RFC 3339
+    datetime.datetime(2009, 1, 1, 6, 1, 2)
+
+    Exceptions will, however, be thrown for blatantly invalid input:
 
     >>> parse('2009-01-01T25:01:02Z')
     Traceback (most recent call last):
     ...
     ValueError: hour must be in 0..23
 
-    """
+    :param str timestamp: the :RFC:`3339` timestamp to be parsed
+    :param bool utc: :const:`True` to normalize the timestamp to UTC; 
:const:`False` otherwise. Defaults to :const:`False`.
+    :param bool produce_naive: :const:`True` if the produced 
:class:`~datetime.datetime` instance should
+                               not have a timezone attached (that is, be 
'naive'); :const:`False` otherwise.
+                               Defaults to :const:`False`.
+    :return: the parsed timestamp
+    :rtype: datetime.datetime
 
-    parse_re = re.compile(
-        
r"""^(?:(?:(?P<date_fullyear>[0-9]{4})\-(?P<date_month>[0-9]{2})\-(?P<date_mday>[0-9]{2}))T(?:(?:(?P<time_hour>[0-9]{2})\:(?P<time_minute>[0-9]{2})\:(?P<time_second>[0-9]{2})(?P<time_secfrac>(?:\.[0-9]{1,}))?)(?P<time_offset>(?:Z|(?P<time_numoffset>(?P<time_houroffset>(?:\+|\-)[0-9]{2})\:(?P<time_minuteoffset>[0-9]{2}))))))$""",
-        re.I | re.X,
-    )
-
-    match = parse_re.match(timestamp)
-
-    if match is not None:
-        if match.group("time_offset") in ["Z", "z", "+00:00", "-00:00"]:
-            if produce_naive is True:
-                tzinfo = None
-            else:
-                tzinfo = timezone.utc
-        else:
-            if produce_naive is True:
-                raise ValueError(
-                    "cannot produce a naive datetime from a local timestamp"
-                )
-            else:
-                tz_hours = int(match.group("time_houroffset"))
-                tz_minutes = int(match.group("time_minuteoffset"))
-                if tz_hours < 0:
-                    tz_minutes *= -1
-                td = timedelta(hours=tz_hours, minutes=tz_minutes)
-                tzinfo = timezone(td, 
f"<UTC{format_timezone(td.total_seconds())}>")
-
-        secfrac = match.group("time_secfrac")
-        if secfrac is None:
-            microsecond = 0
-        else:
-            microsecond = int(round(float(secfrac) * 1000000))
+    """
 
-        dt_out = datetime(
-            year=int(match.group("date_fullyear")),
-            month=int(match.group("date_month")),
-            day=int(match.group("date_mday")),
-            hour=int(match.group("time_hour")),
-            minute=int(match.group("time_minute")),
-            second=int(match.group("time_second")),
-            microsecond=microsecond,
-            tzinfo=tzinfo,
+    # Python does not recognize "Z" as an alias for "+00:00", so we perform the
+    # substitution here.
+    timestamp = re.sub("Z$", "+00:00", timestamp, flags=re.IGNORECASE)
+
+    # Python releases prior to 3.11 only support three or six digits of 
fractional
+    # seconds. RFC 3339 is more lenient, so pad to six digits and truncate any
+    # excessive digits.
+    # This can be removed in October 2026, once Python 3.10 and earlier
+    # have been retired.
+    # noinspection PyUnreachableCode
+    if sys.version_info < (3, 11):
+        timestamp = re.sub(
+            r"(\.)([0-9]+)(?=[+\-][0-9]{2}:[0-9]{2}$)",
+            lambda match: match.group(1) + match.group(2).ljust(6, "0")[:6],
+            timestamp,
         )
 
-        if utc:
-            dt_out = dt_out.astimezone(timezone.utc)
+    dt_out = datetime.fromisoformat(timestamp)
+
+    if utc:
+        dt_out = dt_out.astimezone(timezone.utc)
+
+    if produce_naive:
+        if datetime_utcoffset(dt_out) == 0:
+            dt_out = dt_out.replace(tzinfo=None)
+        else:
+            raise ValueError("cannot produce a naive datetime from a local 
timestamp")
 
-        return dt_out
-    else:
-        raise ValueError("timestamp does not conform to RFC 3339")
+    return dt_out
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyrfc3339/tests/test_all.py 
new/pyRFC3339-2.1.0/pyrfc3339/tests/test_all.py
--- old/pyRFC3339-2.0.1/pyrfc3339/tests/test_all.py     2024-11-04 
02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/pyrfc3339/tests/test_all.py     2025-08-23 
18:22:25.000000000 +0200
@@ -13,12 +13,12 @@
 
 class TestCore(unittest.TestCase):
     """
-    This test suite contains tests to address cases not tested in the doctests,
+    This test case contains tests to address cases not tested in the doctests,
     as well as additional tests for end-to-end verification.
 
     """
 
-    def test_zero_offset(self):
+    def test_zero_offset(self) -> None:
         """
         Both +00:00 and -00:00 are equivalent to the offset 'Z' (UTC).
 
@@ -31,7 +31,7 @@
         dt = parse(timestamp)
         self.assertEqual(dt.tzinfo, timezone.utc)
 
-    def test_parse_microseconds(self):
+    def test_parse_microseconds(self) -> None:
         """
         Test parsing timestamps with microseconds.
 
@@ -40,7 +40,24 @@
         dt = parse(timestamp)
         self.assertEqual(dt.microsecond, 250000)
 
-    def test_generate_microseconds(self):
+        timestamp = "2009-01-01T10:02:03.2543Z"
+        dt = parse(timestamp)
+        self.assertEqual(dt.microsecond, 254300)
+
+    def test_excessive_precision(self) -> None:
+        """
+        Test that timestamps with more than six fractional digits
+        are correctly parsed and that the excessive precision is truncated.
+
+        For Python versions before 3.11, we are responsible for performing
+        the truncation.
+
+        """
+        timestamp = "2009-01-01T10:02:03.2500009Z"
+        dt = parse(timestamp)
+        self.assertEqual(dt.microsecond, 250000)
+
+    def test_generate_microseconds(self) -> None:
         """
         Test generating timestamps with microseconds.
 
@@ -49,7 +66,7 @@
         timestamp = generate(dt, microseconds=True)
         self.assertEqual(timestamp, "2009-01-01T10:02:03.500000Z")
 
-    def test_mixed_case(self):
+    def test_mixed_case(self) -> None:
         """
         Timestamps may use either 'T' or 't' and either 'Z' or 'z'
         according to :RFC:`3339`.
@@ -60,7 +77,23 @@
 
         self.assertEqual(dt1, dt2)
 
-    def test_parse_naive_utc(self):
+    def test_z(self) -> None:
+        """
+        Timestamps which are explicitly in UTC should end in 'Z', while
+        those in other zones which happen to have an offset of '+00:00'
+        should retain that offset.
+
+        """
+
+        dt1 = datetime(2024, 11, 8, 19, 17, 11, tzinfo=ZoneInfo("US/Eastern"))
+        ts1 = generate(dt1, utc=True)
+        self.assertRegex(ts1, r"Z$")
+
+        dt2 = datetime(1863, 1, 10, 6, 0, tzinfo=ZoneInfo("Europe/London"))
+        ts2 = generate(dt2, utc=False)
+        self.assertRegex(ts2, r"\+00:00$")
+
+    def test_parse_naive_utc(self) -> None:
         """
         Test parsing a UTC timestamp to a naive datetime.
 
@@ -68,7 +101,7 @@
         dt1 = parse("2009-01-01T10:01:02Z", produce_naive=True)
         self.assertEqual(dt1.tzinfo, None)
 
-    def test_parse_naive_local(self):
+    def test_parse_naive_local(self) -> None:
         """
         Test that parsing a local timestamp to a naive datetime fails.
 
@@ -76,7 +109,7 @@
         with self.assertRaises(ValueError):
             parse("2009-01-01T10:01:02-04:00", produce_naive=True)
 
-    def test_generate_utc_parse_utc(self):
+    def test_generate_utc_parse_utc(self) -> None:
         """
         Generate a UTC timestamp and parse it into a UTC datetime.
 
@@ -86,7 +119,7 @@
         dt2 = parse(generate(dt1, microseconds=True))
         self.assertEqual(dt1, dt2)
 
-    def test_generate_local_parse_local(self):
+    def test_generate_local_parse_local(self) -> None:
         """
         Generate a local timestamp and parse it into a local datetime.
 
@@ -96,7 +129,7 @@
         dt2 = parse(generate(dt1, utc=False, microseconds=True), utc=False)
         self.assertEqual(dt1, dt2)
 
-    def test_generate_local_parse_utc(self):
+    def test_generate_local_parse_utc(self) -> None:
         """
         Generate a local timestamp and parse it into a UTC datetime.
 
@@ -107,26 +140,31 @@
         self.assertEqual(dt1, dt2)
 
     @unittest.skip("fails due to python/cpython#120713")
-    def test_three_digit_year(self):
+    def test_three_digit_year(self) -> None:
         dt = datetime(999, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
         self.assertEqual(generate(dt), "0999-01-01T00:00:00Z")
 
 
 class TestExhaustiveRoundtrip(unittest.TestCase):
     """
-    This test suite exhaustively tests parsing and generation by generating
-    a local RFC 3339 timestamp for every timezone supported by `zoneinfo`,
-    and parsing that timestamp into a local datetime and a UTC datetime.
+    This test case exhaustively tests parsing and generation by generating
+    a local RFC 3339 timestamp for every timezone supported by :mod:`zoneinfo`,
+    parsing that timestamp into a local datetime and a UTC datetime
+    and asserting that those represent the same instant.
+
     """
 
-    def test_local_roundtrip(self):
+    def setUp(self) -> None:
+        self.available_timezones = zoneinfo.available_timezones()
+
+    def test_local_roundtrip(self) -> None:
         """
         Generates a local datetime using the given timezone,
         produces a local timestamp from the datetime, parses the timestamp
         to a local datetime, and verifies that the two datetimes are equal.
 
         """
-        for tz_name in zoneinfo.available_timezones():
+        for tz_name in self.available_timezones:
             with self.subTest(tz=tz_name):
                 tzinfo = ZoneInfo(tz_name)
                 dt1 = datetime.now(tzinfo)
@@ -134,14 +172,14 @@
                 dt2 = parse(timestamp, utc=False)
                 self.assertEqual(dt1, dt2)
 
-    def test_utc_roundtrip(self):
+    def test_utc_roundtrip(self) -> None:
         """
         Generates a local datetime using the given timezone,
         produces a local timestamp from the datetime, parses the timestamp
         to a UTC datetime, and verifies that the two datetimes are equal.
 
         """
-        for tz_name in zoneinfo.available_timezones():
+        for tz_name in self.available_timezones:
             with self.subTest(tz=tz_name):
                 tzinfo = ZoneInfo(tz_name)
                 dt1 = datetime.now(tzinfo)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/pyrfc3339/utils.py 
new/pyRFC3339-2.1.0/pyrfc3339/utils.py
--- old/pyRFC3339-2.0.1/pyrfc3339/utils.py      2024-11-04 02:40:49.000000000 
+0100
+++ new/pyRFC3339-2.1.0/pyrfc3339/utils.py      2025-08-23 18:22:25.000000000 
+0200
@@ -1,22 +1,34 @@
-def format_timezone(utcoffset):
+from datetime import datetime
+
+
+def datetime_utcoffset(dt: datetime) -> float:
     """
-    Return a string representing the timezone offset.
-    Remaining seconds are rounded to the nearest minute.
+    Return the UTC offset for an aware :class:`datetime.datetime` in seconds.
+
+    >>> from datetime import datetime
+    >>> from zoneinfo import ZoneInfo
+    >>> z = ZoneInfo('US/Eastern')
+    >>> dt = datetime(2024, 11, 5, 19, 7, 6, tzinfo=z)
+    >>> datetime_utcoffset(dt)
+    -18000.0
 
-    >>> format_timezone(3600)
-    '+01:00'
-    >>> format_timezone(5400)
-    '+01:30'
-    >>> format_timezone(-28800)
-    '-08:00'
+    >>> dt = datetime(2024, 11, 5, 19, 7, 6)
+    >>> datetime_utcoffset(dt)
+    Traceback (most recent call last):
+    ...
+    AssertionError
+
+    :param datetime.datetime dt: a :class:`~datetime.datetime` instance; must 
be aware (that is, have a timezone attached)
+    :return: the UTC offset of the supplied :class:`~datetime.datetime` in 
seconds
+    :rtype: float
 
     """
 
-    hours, seconds = divmod(abs(utcoffset), 3600)
-    minutes = round(float(seconds) / 60)
+    assert dt.tzinfo is not None
+
+    tz = dt.tzinfo
+    offset = tz.utcoffset(dt)
+
+    assert offset is not None
 
-    if utcoffset >= 0:
-        sign = "+"
-    else:
-        sign = "-"
-    return "{0}{1:02d}:{2:02d}".format(sign, int(hours), int(minutes))
+    return offset.total_seconds()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/setup.py new/pyRFC3339-2.1.0/setup.py
--- old/pyRFC3339-2.0.1/setup.py        2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/setup.py        1970-01-01 01:00:00.000000000 +0100
@@ -1,26 +0,0 @@
-from setuptools import setup
-
-with open("README.rst", "r") as readme:
-    long_description = readme.read()
-
-setup(
-    name = "pyRFC3339",
-    version = "2.0.1",
-    author = "Kurt Raschke",
-    author_email = "[email protected]",
-    url = "https://github.com/kurtraschke/pyRFC3339";,
-    description = "Generate and parse RFC 3339 timestamps",
-    long_description = long_description,
-    keywords = "rfc 3339 timestamp",
-    license = "MIT",
-    classifiers = [
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3",
-        "Topic :: Internet"
-        ],
-
-    packages = ['pyrfc3339']
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pyRFC3339-2.0.1/tox.ini new/pyRFC3339-2.1.0/tox.ini
--- old/pyRFC3339-2.0.1/tox.ini 2024-11-04 02:40:49.000000000 +0100
+++ new/pyRFC3339-2.1.0/tox.ini 1970-01-01 01:00:00.000000000 +0100
@@ -1,20 +0,0 @@
-[tox]
-requires = tox>=4
-env_list = 3.{9,10,11,12,13}
-skip_missing_interpreters = true
-
-[gh-actions]
-python =
-    3.9: 3.9
-    3.10: 3.10
-    3.11: 3.11
-    3.12: 3.12
-    3.13: 3.13
-
-[testenv]
-deps =
-   pytest
-   pytest-subtests
-   pytest-cov
-commands =
-    pytest --doctest-glob="docs/source/*.rst" --doctest-glob="README.rst" 
--doctest-modules --doctest-continue-on-failure --cov=pyrfc3339

Reply via email to