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