[Git][debian-gis-team/cftime][master] 4 commits: New upstream version 1.6.6

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Sep 18 13:10:09 BST 2026



Bas Couwenberg pushed to branch master at Debian GIS Project / cftime


Commits:
1712bfca by Bas Couwenberg at 2026-09-18T05:03:19-07:00
New upstream version 1.6.6
- - - - -
3de01c5a by Bas Couwenberg at 2026-09-18T05:03:19-07:00
Update upstream source from tag 'upstream/1.6.6'

Update to upstream version '1.6.6'
with Debian dir 9a0a5ea264d7ac712c65ebfbb4c18e5c16b9323c
- - - - -
cfc94efa by Bas Couwenberg at 2026-09-18T05:03:40-07:00
New upstream release.

- - - - -
2169d617 by Bas Couwenberg at 2026-09-18T05:05:18-07:00
Set distribution to unstable.

- - - - -


13 changed files:

- .github/dependabot.yml
- .github/workflows/cibuildwheel.yml
- .github/workflows/deploy-docs.yml
- .github/workflows/tests_conda.yml
- .github/workflows/tests_latest.yml
- Changelog
- README.md
- debian/changelog
- pyproject.toml
- setup.py
- src/cftime/__init__.py
- src/cftime/_cftime.pyx
- test/test_cftime.py


Changes:

=====================================
.github/dependabot.yml
=====================================
@@ -11,3 +11,5 @@ updates:
           - "*"  # Group all Actions updates into a single larger pull request
     schedule:
       interval: weekly
+    cooldown:
+      default-days: 7


=====================================
.github/workflows/cibuildwheel.yml
=====================================
@@ -4,9 +4,9 @@ on:
   pull_request:
     branches:
       - master
-  push:
-    tags:
-      - "v*"
+  release:
+    types:
+      - published
 
 jobs:
   build_bdist:
@@ -17,114 +17,94 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - os: ubuntu-22.04
+          - os: ubuntu-24.04
             arch: x86_64
-          - os: ubuntu-22.04
+          - os: ubuntu-24.04-arm
             arch: aarch64
           - os: windows-2022
             arch: AMD64
           - os: macos-14
             arch: arm64
-          - os: macos-13
+          - os: macos-15-intel
             arch: x86_64
+          - os: windows-11-arm
+            arch: ARM64
 
     steps:
-    - uses: actions/checkout at v5
+    - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
         fetch-depth: 0
-
-    # For aarch64 support
-    # https://cibuildwheel.pypa.io/en/stable/faq/#emulation
-    - uses: docker/setup-qemu-action at v3
-      with:
-        platforms: all
-      if: runner.os == 'Linux' && matrix.arch == 'aarch64'
+        persist-credentials: false
 
     - name: Build just oldest and newest on PRs, all on tags
+      if: ${{ github.event_name == 'pull_request' }}
       shell: bash
-      # - Always omit musllinux_aarch64 because it's slow and niche
-      # - Always skip free-threading wheels for now
       # - On PPs, omit musllinux for speed
-      # - On PRs, run just oldest and newest Python versions
+      # - On PRs, run just oldest and newest Python versions (3.11 is the oldest abi3 target)
       run: |
-        if [[ "${{ github.event_name }}" == "pull_request" ]]; then
-          CIBW_SKIP="cp311-* cp312-* cp313-* cp314t-* *musllinux*"
-        else
-          CIBW_SKIP="cp314t-* *musllinux_aarch64"
-        fi
+        CIBW_SKIP="cp310-win_arm64 *musllinux*"
         echo "CIBW_SKIP=$CIBW_SKIP" >> $GITHUB_ENV
         echo "Setting CIBW_SKIP=$CIBW_SKIP"
 
+        CIBW_TEST_SKIP="cp312-* cp313-*"
+        echo "CIBW_TEST_SKIP=$CIBW_TEST_SKIP" >> $GITHUB_ENV
+        echo "Setting CIBW_TEST_SKIP=$CIBW_TEST_SKIP"
+
     - name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels"
-      uses: pypa/cibuildwheel at v3.2.1
+      uses: pypa/cibuildwheel at 1828c10ab37f080699c7b81cea34097c684a7074  # v4.2.0
       env:
         CIBW_SKIP: ${{ env.CIBW_SKIP }}
+        CIBW_TEST_SKIP: ${{ env.CIBW_TEST_SKIP }}
         CIBW_ARCHS: ${{ matrix.arch }}
-        CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
-        CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014
-        # Emulated testing is slow, so trust that the Python 3.14 test is good enough on aarch64
-        # (takes about 5 minutes per wheel to build, and 5 minutes to test)
-        CIBW_TEST_SKIP: "cp3{10,11,12,13}-*_aarch64"
-        CIBW_TEST_GROUPS: dev
-        CIBW_TEST_COMMAND: >
-          python -c "import cftime; print(f'cftime v{cftime.__version__}')" &&
-          python -m pytest -vv {package}/test
 
-    - uses: actions/upload-artifact at v4
+    - uses: actions/upload-artifact at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
       with:
         name: pypi-artifacts-${{ matrix.os }}-${{ matrix.arch }}
         path: ${{ github.workspace }}/wheelhouse/*.whl
-
+    permissions:
+      actions: write
 
   build_sdist:
     name: Build source distribution
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     steps:
-      - uses: actions/checkout at v5
+      - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
         with:
           fetch-depth: 0
+          persist-credentials: false
 
       - name: Build sdist
         run: >
           pip install build
           && python -m build --sdist . --outdir dist
 
-      - uses: actions/upload-artifact at v4
+      - uses: actions/upload-artifact at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
         with:
           name: pypi-artifacts
           path: ${{ github.workspace }}/dist/*.tar.gz
 
-  show-artifacts:
-    needs: [build_bdist, build_sdist]
-    name: "Show artifacts"
-    runs-on: ubuntu-22.04
-    steps:
-    - uses: actions/download-artifact at v5
-      with:
-        pattern: pypi-artifacts*
-        path: ${{ github.workspace }}/dist
-        merge-multiple: true
-
-    - shell: bash
-      run: |
-        ls -l ${{ github.workspace }}/dist
-
+    permissions:
+      actions: write
 
   publish-artifacts-pypi:
     needs: [build_bdist, build_sdist]
     name: "Publish to PyPI"
-    runs-on: ubuntu-22.04
+    runs-on: ubuntu-24.04
     # upload to PyPI for every tag starting with 'v'
     if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
     steps:
-    - uses: actions/download-artifact at v5
+    - uses: actions/download-artifact at 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8
       with:
         pattern: pypi-artifacts*
         path: ${{ github.workspace }}/dist
         merge-multiple: true
 
-    - uses: pypa/gh-action-pypi-publish at release/v1
+    # Move to Trusted Publishing
+    - uses: pypa/gh-action-pypi-publish at dc37677b2e1c63e2034f94d8a5b11f265b73ba33  # v1.14.2
       with:
-        user: __token__
-        password: ${{ secrets.PYPI_PASSWORD }}
         print_hash: true
+    environment:
+      name: release
+      url: https://pypi.org/p/cftime
+    permissions:
+      id-token: write  # IMPORTANT: this permission is mandatory for trusted publishing


=====================================
.github/workflows/deploy-docs.yml
=====================================
@@ -1,22 +1,32 @@
 name: Build and Deploy docs
 
 on:
+  pull_request:
   push:
     branches:
       - master
+  release:
+    types:
+      - published
+
+# Deny all permissions by default
+permissions: {}
 
 jobs:
   build-docs:
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
 
     steps:
     - name: checkout
-      uses: actions/checkout at v5
+      uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
         fetch-depth: 0
+        persist-credentials: false
 
     - name: Setup Mamba
-      uses: mamba-org/setup-micromamba at v2
+      uses: mamba-org/setup-micromamba at f457c30a868e4760d3a6fcea5f25dc655b8edf39 # v3.2.1
       with:
         environment-name: TEST
         create-args: >-
@@ -43,7 +53,8 @@ jobs:
         popd
 
     - name: Deploy
-      uses: peaceiris/actions-gh-pages at v4
+      if: success() && github.event_name == 'release'
+      uses: peaceiris/actions-gh-pages at 84c30a85c19949d7eee79c4ff27748b70285e453  # v4.1.0
       with:
         github_token: ${{ secrets.GITHUB_TOKEN }}
         publish_dir: docs/_build/html


=====================================
.github/workflows/tests_conda.yml
=====================================
@@ -5,6 +5,9 @@ on:
   push:
     branches: [master]
 
+# Deny all permissions by default
+permissions: {}
+
 jobs:
   run:
     runs-on: ${{ matrix.os }}
@@ -25,10 +28,12 @@ jobs:
             experimental: true
 
     steps:
-    - uses: actions/checkout at v5
+    - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
+      with:
+        persist-credentials: false
 
     - name: Setup micromamba Env
-      uses: mamba-org/setup-micromamba at v2
+      uses: mamba-org/setup-micromamba at f457c30a868e4760d3a6fcea5f25dc655b8edf39 # v3.2.1
       with:
         environment-name: TEST
         create-args: >-


=====================================
.github/workflows/tests_latest.yml
=====================================
@@ -1,18 +1,27 @@
 name: Build and test with development python
-on: [push, pull_request]
+
+on:
+  pull_request:
+  push:
+
+# Deny all permissions by default
+permissions: {}
+
 jobs:
   build-linux:
     name: Python (${{ matrix.python-version }})
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.14.0b.1"]
+        python-version: ["3.14"]
     steps:
 
-    - uses: actions/checkout at v5
+    - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
+      with:
+        persist-credentials: false
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python at v6
+      uses: actions/setup-python at 5fda3b95a4ea91299a34e894583c3862153e4b97  # v7.0.0
       with:
         python-version: ${{ matrix.python-version }}
 


=====================================
Changelog
=====================================
@@ -1,3 +1,8 @@
+version 1.6.6 (release tag v1.6.6rel)
+=====================================
+* added new CF calendar "tai", which is the same as proleptic_gregorian but is only
+  valid for dates after 1958-01-01.
+
 version 1.6.5 (release tag v1.6.5rel)
 =====================================
 * python 3.14 wheels, 3.8/3.9 support dropped.


=====================================
README.md
=====================================
@@ -1,10 +1,9 @@
 # cftime
 Time-handling functionality from netcdf4-python
 
-[![Build status](https://github.com/Unidata/cftime/workflows/cftime%20test/badge.svg)](https://github.com/Unidata/cftime/actions)
-[![PyPI package](https://badge.fury.io/py/cftime.svg)](http://python.org/pypi/cftime)
-[![Coverage Status](https://coveralls.io/repos/github/Unidata/cftime/badge.svg?branch=master)](https://coveralls.io/github/Unidata/cftime?branch=master)
-[![Tag Status](https://img.shields.io/github/tag/UniData/cftime.svg)](https://github.com/Unidata/cftime/tags)
+[![CodeQL](https://github.com/Unidata/cftime/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/Unidata/cftime/actions/workflows/github-code-scanning/codeql)
+[![PyPI package](https://img.shields.io/pypi/v/cftime.svg)](http://python.org/pypi/cftime)
+[![Anaconda-Server Badge](https://anaconda.org/conda-forge/cftime/badges/version.svg)](https://anaconda.org/conda-forge/cftime)
 [![Release Status](https://img.shields.io/github/release/UniData/cftime.svg)](https://github.com/Unidata/cftime/releases)
 [![Commits Status](https://img.shields.io/github/commits-since/UniData/cftime/latest.svg)](https://github.com/UniData/cftime/commits/master)
 [![DOI](https://zenodo.org/badge/73107250.svg)](https://zenodo.org/badge/latestdoi/73107250)
@@ -12,6 +11,8 @@ Time-handling functionality from netcdf4-python
 ## News
 For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
 
+9/15/2026:  Version 1.6.6 release. Limited API compliant wheels (should work with python 3.15). New cftime calendar "tai".
+
 10/13/2025:  Version 1.6.5 release.  Minor bugfixes/optimizations, wheels for python 3.14, no more wheels for 3.8/3.9.
 
 6/7/2024:  Version 1.6.4 release.  Wheels for muslinux and aarch64, numpy 2.0 compatibility. 


=====================================
debian/changelog
=====================================
@@ -1,11 +1,12 @@
-cftime (1.6.5-2) UNRELEASED; urgency=medium
+cftime (1.6.6-1) unstable; urgency=medium
 
+  * New upstream release.
   * Update Upstream-Contact email address.
   * Use test-build-validate-cleanup instead of test-build-twice.
   * Drop Priority: optional, default since dpkg 1.22.13.
   * Bump Standards-Version to 4.7.4, changes: priority.
 
- -- Bas Couwenberg <sebastic at debian.org>  Tue, 14 Oct 2025 05:39:22 +0200
+ -- Bas Couwenberg <sebastic at debian.org>  Fri, 18 Sep 2026 05:05:09 -0700
 
 cftime (1.6.5-1) unstable; urgency=medium
 


=====================================
pyproject.toml
=====================================
@@ -1,6 +1,6 @@
 [project]
 name = "cftime"
-version = "1.6.5"
+version = "1.6.6"
 description = "Time-handling functionality from netcdf4-python"
 readme = "README.md"
 authors = [
@@ -20,10 +20,12 @@ classifiers = [
     'Programming Language :: Python :: 3.12',
     'Programming Language :: Python :: 3.13',
     'Programming Language :: Python :: 3.14',
+    'Programming Language :: Python :: 3.15',
     'Topic :: Scientific/Engineering',
 ]
 dependencies = [
-    "numpy>=1.21.2",
+    "numpy>=2.3.0; platform_system == 'Windows' and platform_machine == 'ARM64'",
+    "numpy>=1.21.2; platform_system != 'Windows' or platform_machine != 'ARM64'",
 ]
 requires-python = ">=3.10"
 
@@ -35,7 +37,6 @@ dev = [
     "cython>=0.29.20",
     "pytest",
     "sphinx",
-    "twine",
 ]
 
 [build-system]
@@ -60,6 +61,11 @@ doctest_optionflags = [
     "NORMALIZE_WHITESPACE",
     "ELLIPSIS",
 ]
+filterwarnings = [
+    "error",
+    "ignore::UserWarning",
+    "ignore::RuntimeWarning",
+]
 
 [tool.coverage.run]
 source = ["src"]
@@ -100,3 +106,27 @@ ignore = [
     "test",
     "test/*",
 ]
+
+[tool.cibuildwheel]
+build-verbosity = 1
+skip = [
+    "*musllinux_aarch64*",
+    # Skipped due to unavailability of Python binary and NumPy for Win-ARM64
+    "cp310-win_arm64",
+]
+
+test-skip = [
+    # Emulated testing is slow, so trust that the Python 3.14 test is good enough on aarch64
+    # (takes about 5 minutes per wheel to build, and 5 minutes to test)
+    "cp3{10,11,12,13}-*_aarch64",
+]
+test-groups = "dev"
+test-command = [
+    '''python -c "import cftime; print(f'cftime v{cftime.__version__}')"''',
+    "python -m pytest -vv {package}/test"
+]
+#manylinux-x86_64-image = "manylinux2014"
+#manylinux-aarch64-image = "manylinux2014"
+manylinux-x86_64-image = "manylinux_2_28"
+manylinux-aarch64-image = "manylinux_2_28"
+environment = { CFTIME_LIMITED_API = "1" }


=====================================
setup.py
=====================================
@@ -1,5 +1,6 @@
 import os
 import sys
+import sysconfig
 import numpy
 
 from Cython.Build import cythonize
@@ -29,6 +30,22 @@ NAME = 'cftime'
 CFTIME_DIR = os.path.join(SRCDIR, NAME)
 CYTHON_FNAME = os.path.join(CFTIME_DIR, '_{}.pyx'.format(NAME))
 
+USE_PY_LIMITED_API = (
+    # require opt-in (builds are specialized by default)
+    os.getenv('CFTIME_LIMITED_API', '0') == '1'
+    # Cython + numpy + limited API de facto requires Python >=3.11
+    and sys.version_info >= (3, 11)
+    # as of Python 3.14t, free-threaded builds don't support the limited API
+    and not sysconfig.get_config_var("Py_GIL_DISABLED")
+)
+ABI3_TARGET_VERSION = "".join(str(_) for _ in sys.version_info[:2])
+ABI3_TARGET_HEX = hex(sys.hexversion & 0xFFFF00F0)
+
+if USE_PY_LIMITED_API:
+    DEFINE_MACROS  += [(("Py_LIMITED_API", ABI3_TARGET_HEX))]
+    SETUP_OPTIONS = {"bdist_wheel": {"py_limited_api": f"cp{ABI3_TARGET_VERSION}"}}
+else:
+    SETUP_OPTIONS = {}
 
 class CleanCython(Command):
     description = 'Purge artifacts built by Cython'
@@ -60,6 +77,7 @@ if ((FLAG_COVERAGE in sys.argv or os.environ.get('CYTHON_COVERAGE', None))
     }
     DEFINE_MACROS += [('CYTHON_TRACE', '1'),
                      ('CYTHON_TRACE_NOGIL', '1')]
+
     if FLAG_COVERAGE in sys.argv:
         sys.argv.remove(FLAG_COVERAGE)
     print('enable: "linetrace" Cython compiler directive')
@@ -71,7 +89,8 @@ else:
     extension = Extension('{}._{}'.format(NAME, NAME),
                           sources=[os.path.relpath(CYTHON_FNAME, BASEDIR)],
                           define_macros=DEFINE_MACROS,
-                          include_dirs=[numpy.get_include(),])
+                          include_dirs=[numpy.get_include()],
+                          py_limited_api=USE_PY_LIMITED_API)
 
     ext_modules = cythonize(
         extension,
@@ -82,4 +101,5 @@ else:
 setup(
     cmdclass={'clean_cython': CleanCython},
     ext_modules=ext_modules,
+    options=SETUP_OPTIONS,
 )


=====================================
src/cftime/__init__.py
=====================================
@@ -7,7 +7,7 @@ from ._cftime import (microsec_units, millisec_units,
 from ._cftime import CFWarning
 # these will be removed in a future release
 from ._cftime import (DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
-                     Datetime360Day, DatetimeJulian, 
+                     Datetime360Day, DatetimeJulian, DatetimeTAI,
                      DatetimeGregorian, DatetimeProlepticGregorian)
 
 


=====================================
src/cftime/_cftime.pyx
=====================================
@@ -30,7 +30,7 @@ _units = microsec_units+millisec_units+sec_units+min_units+hr_units+day_units
 # '366_day'=='all_leap','365_day'=='noleap')
 # see http://cfconventions.org/cf-conventions/cf-conventions#calendar
 # for definitions.
-_calendars = ['standard', 'gregorian', 'proleptic_gregorian',
+_calendars = ['standard', 'gregorian', 'proleptic_gregorian', 'tai',
               'noleap', 'julian', 'all_leap', '365_day', '366_day', '360_day']
 _idealized_calendars= ['all_leap','noleap','366_day','365_day','360_day']
 # Following are number of days per month
@@ -83,7 +83,7 @@ def _datesplit(timestr):
 
     return units.lower(), remainder
 
-def _dateparse(timestr,calendar,has_year_zero=None):
+def _dateparse(timestr, calendar, has_year_zero=None):
     """parse a string of the form time-units since yyyy-mm-dd hh:mm:ss,
     return a datetime instance"""
     # same as version in cftime, but returns a timezone naive
@@ -109,6 +109,9 @@ def _dateparse(timestr,calendar,has_year_zero=None):
     # parse the date string.
     year, month, day, hour, minute, second, microsecond, utc_offset =\
         _parse_date( isostring.strip() )
+    if calendar == 'tai':
+        if year < 1958 or utc_offset:
+            raise ValueError('TAI calendar must have a reference date of 1958-01-01T00:00:00 or later (with no utc offset)')
     if year == 0 and not has_year_zero and calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
         msg='zero not allowed as a reference year when has_year_zero=False'
         raise ValueError(msg)
@@ -157,7 +160,7 @@ def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False):
     **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
     `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
-    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is `None` which means the calendar associated with the first
     input datetime instance will be used.
@@ -206,6 +209,11 @@ def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False):
         mask = dates.mask
         ismasked = True
 
+    # Convert numpy.datetime64 to a list to convert them to datetime instances.
+    if hasattr(dates, "dtype"):
+        if np.issubdtype(dates.dtype, np.datetime64):
+            dates = dates.tolist()
+
     # are all input dates 'real' python datetime objects?
     dates = np.asanyarray(dates) # convert to numpy array
     shape = dates.shape # save shape of input
@@ -373,6 +381,7 @@ UNIT_CONVERSION_FACTORS = {
 
 DATE_TYPES = {
      "proleptic_gregorian": DatetimeProlepticGregorian,
+     "tai": DatetimeTAI,
      "standard": DatetimeGregorian,
      "noleap": DatetimeNoLeap,
      "365_day": DatetimeNoLeap,
@@ -537,7 +546,7 @@ def num2date(
     **calendar**: describes the calendar used in the time calculations.
     All the values currently defined in the
     `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
-    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
 
@@ -652,7 +661,7 @@ def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None)
     **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
     `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
-    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+    Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is `None` which means the calendar associated with the first
     input datetime instance will be used.
@@ -864,7 +873,7 @@ def _date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None
     order.
 
     **calendar**: Describes the calendar used in the time calculation.
-    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
     Default is 'standard', which is a mixed Julian/Gregorian calendar
     If `calendar` is None, its value is given by `nctime.calendar` or
@@ -911,7 +920,7 @@ def time2index(times, nctime, calendar=None, select='exact'):
     order.
 
     **calendar**: Describes the calendar used in the time calculation.
-    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+    Valid calendars 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
     Default is `standard`, which is a mixed Julian/Gregorian calendar
     If `calendar` is None, its value is given by `nctime.calendar` or
@@ -1072,7 +1081,7 @@ for cftime.datetime instances using
 
 All the calendars currently defined in the
 `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
-Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian'
+Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
 'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
 Default is 'standard', which is a mixed Julian/Gregorian calendar.
 'standard' and 'gregorian' are synonyms, as are 'all_leap'/'366_day'
@@ -1152,6 +1161,12 @@ The default format of the string produced by strftime is controlled by self.form
         # and has a year zero, so for now this is the default in cftime.
         if calendar in ['julian','gregorian','standard'] and year <= 0:
             warnings.warn(cfwarnmsg,category=CFWarning)
+        # raise exception if date before 1958-01-01 requested for tai calendar.
+        if calendar == 'tai':
+            if year < 1958:
+                raise ValueError('dates before 1958-01-01 not allowed in TAI calendar')
+            if has_year_zero:
+                raise ValueError('year zero not allowed in TAI calendar')
         # raise exception if year zero requested but has_year_zero set
         # to False (issue #248).
         if year == 0 and has_year_zero==False:
@@ -1185,7 +1200,7 @@ The default format of the string produced by strftime is controlled by self.form
             self.calendar = calendar
             self.datetime_compatible = False
             assert_valid_date(self, is_leap_julian, False, has_year_zero=has_year_zero)
-        elif calendar == 'proleptic_gregorian':
+        elif calendar == 'proleptic_gregorian' or calendar == 'tai':
             self.calendar = calendar
             self.datetime_compatible = True
             assert_valid_date(self, is_leap_proleptic_gregorian, False, has_year_zero=has_year_zero)
@@ -1275,7 +1290,7 @@ The default format of the string produced by strftime is controlled by self.form
             if getattr(pydatetime, 'tzinfo',None) is not None:
                  pydatetime = pydatetime.replace(tzinfo=None) - pydatetime.utcoffset()
             compatible_date =\
-            calendar == 'proleptic_gregorian' or \
+            calendar == 'proleptic_gregorian' or calendar == 'tai' or \
             (calendar in ['gregorian','standard'] and (pydatetime.year > 1582 or \
              (pydatetime.year == 1582 and pydatetime.month > 10) or \
              (pydatetime.year == 1582 and pydatetime.month == 10 and pydatetime.day > 15)))
@@ -1480,7 +1495,7 @@ The default format of the string produced by strftime is controlled by self.form
                units = 'days since -4712-1-1-12'
             else:
                units = 'days since -4713-1-1-12'
-        elif calendar == 'proleptic_gregorian':
+        elif calendar == 'proleptic_gregorian' or calendar == 'tai':
             if has_year_zero:
                 units = 'days since -4713-11-24-12'
             else:
@@ -1566,6 +1581,9 @@ The default format of the string produced by strftime is controlled by self.form
         elif calendar == 'proleptic_gregorian':
             #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
             return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=has_year_zero)
+        elif calendar == 'tai':
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
+            return DatetimeTAI(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=has_year_zero)
         else:
             return NotImplemented
 
@@ -1626,6 +1644,10 @@ datetime object."""
                     #return self.__class__(*add_timedelta(self, -other,
                     #    is_leap_proleptic_gregorian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
                     return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=self.has_year_zero)
+                elif self.calendar == 'tai':
+                    #return self.__class__(*add_timedelta(self, -other,
+                    #    is_leap_proleptic_gregorian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
+                    return DatetimeTAI(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=self.has_year_zero)
                 else:
                     return NotImplemented
             else:
@@ -1692,7 +1714,7 @@ cdef _strftime(datetime dt, fmt):
     year = year + ((2000 - year) // 28) * 28
     timetuple = dt.timetuple()
     s1 = time.strftime(fmt1, (year,) + timetuple[1:])
-    twodigityear = 'y' in fmt1
+    twodigityear = '%y' in fmt1
     if twodigityear:
         sites1 = _findall(s1, str(year)[-2:])
     else:
@@ -1944,7 +1966,7 @@ cdef _is_leap(int year, calendar, has_year_zero=None):
         tyear = year + 1
     else:
         tyear = year
-    if calendar == 'proleptic_gregorian' or (calendar == 'standard' and year > 1581):
+    if calendar == 'proleptic_gregorian' or calendar == 'tai' or (calendar == 'standard' and year > 1581):
         if tyear % 4: # not divisible by 4
             leap = False
         elif tyear % 100: # not divisible by 100
@@ -2040,7 +2062,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     elif calendar == '366_day':
         return year*366 + _cumdayspermonth_leap[month-1] + day - 1
 
-    # handle standard, julian, proleptic_gregorian calendars.
+    # handle standard, julian, tai, proleptic_gregorian calendars.
     if year == 0 and not has_year_zero:
         raise ValueError('year zero does not exist in the %s calendar' %\
                 calendar)
@@ -2066,7 +2088,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     jday_greg -= 31739
     if calendar == 'julian':
         return jday_jul
-    elif calendar == 'proleptic_gregorian':
+    elif calendar == 'proleptic_gregorian' or calendar == 'tai':
         return jday_greg
     elif calendar in ['standard','gregorian']:
         # check for invalid days in mixed calendar (there are 10 missing)
@@ -2080,7 +2102,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
             else:
                 return jday_greg
 
-# legacy calendar specific sub-classes (will be removed in a future release).
+# legacy calendar specific sub-classes (may be removed in a future release).
 
 @cython.embedsignature(True)
 cdef class DatetimeNoLeap(datetime):
@@ -2141,3 +2163,13 @@ but allows for dates that don't exist in the proleptic gregorian calendar.
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='proleptic_gregorian'
         super().__init__( *args, **kwargs)
+
+ at cython.embedsignature(True)
+cdef class DatetimeTAI(datetime):
+    """
+Phony datetime object which mimics the python datetime object,
+but allows for dates that don't exist in the proleptic gregorian calendar.
+    """
+    def __init__(self, *args, **kwargs):
+        kwargs['calendar']='tai'
+        super().__init__( *args, **kwargs)


=====================================
test/test_cftime.py
=====================================
@@ -3,7 +3,7 @@ from cftime import datetime as datetimex
 from cftime import real_datetime
 from cftime import (Datetime360Day, DatetimeAllLeap,
                     DatetimeGregorian, DatetimeJulian, DatetimeNoLeap,
-                    DatetimeProlepticGregorian, _parse_date,
+                    DatetimeProlepticGregorian, DatetimeTAI, _parse_date,
                     date2index, date2num, num2date,  UNIT_CONVERSION_FACTORS)
 import copy
 import unittest
@@ -204,6 +204,9 @@ class cftimeTestCase(unittest.TestCase):
         self.assertTrue(d.strftime(dateformat) == d2.strftime(dateformat))
         # make sure two digit years work in strftime (issue #362)
         self.assertTrue(d.strftime(dateformat2) == d2.strftime(dateformat2))
+        # issue #401 (check for %y not y)
+        t = cftime.DatetimeGregorian(2026,4,14,0)
+        self.assertTrue(t.strftime('year=%Y') == 'year=2026')
         # check day of year.
         ndayr = d.timetuple()[7]
         self.assertTrue(ndayr == 125)
@@ -2170,6 +2173,22 @@ def test_date2num_missing_data():
     assert out is np.ma.masked
 
 
+def test_date2num_numpy_datetime64():
+    # Array
+    array = np.array([
+        np.datetime64(123, "s"),
+        np.datetime64(124, "s"),
+        np.datetime64(125, "s")
+    ])
+    out = date2num(array, units="seconds since 1970-01-01T00:00:00", calendar="gregorian")
+    assert ((out == np.array([123, 124, 125]))).all()
+
+    # Scalar
+    array = np.datetime64(123, "s")
+    out = date2num(array, units="seconds since 1970-01-01T00:00:00", calendar="gregorian")
+    assert out == np.int64(123)
+
+
 def test_num2date_preserves_shape():
     # The optimized num2date algorithm operates on a flattened array.  This
     # check ensures that the original shape of the times is restored in the
@@ -2248,6 +2267,77 @@ def test_num2date_precision():
             assert np.ma.is_masked(date2[0])
             assert date[1] == date2[1]
 
+# NOTE: using pytest style tests -- these won't run without pytest
+#       but it looks like you're using pytest, so this is cleaner and easier
+
+#       There's really no reason to put these in a class, but it does organize things
+class Test_tai:
+    """
+    tests specific to the tai calendar
+    """
+    def test_dateparse_valid(self):
+        """
+        It should raise for an epoch before 1958
+        """
+        # This is directly testing the _dateparse function
+        # Which is a "proper" unit test, but also testing an internal function.
+        timestring = "seconds since 1958-01-01T00:00:00"
+        basedate = cftime._dateparse(timestring, 'tai')
+
+        print(repr(basedate))
+
+        assert basedate == cftime.datetime(1958, 1, 1, 0, 0, 0, 0, calendar='tai', has_year_zero=False)
+
+    def test_dateparse_before_1958(self):
+        """
+        It should raise for an epoch before 1958
+        """
+        # This is directly testing the _dateparse function
+        # Which is a "proper" unit test, but also testing an internal function.
+        timestring = "seconds since 1957-01-01T00:00:00"
+        with pytest.raises(ValueError):
+            basedate = cftime._dateparse(timestring, 'tai')
+            # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+            print(basedate)
+
+    def test_dateparse_with_offset(self):
+        """
+        It should raise if there's an offset
+        """
+        # This is directly testing the _dateparse function
+        # Which is a "proper" unit test, but also testing an internal function.
+        timestring = "seconds since 1965-01-01T00:00:00+08:00"
+        with pytest.raises(ValueError):
+            basedate = cftime._dateparse(timestring, 'tai')
+            # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+            print(basedate)
+
+    def test_year_zero(self):
+        """
+        tai does not have a year zero -- not sure this is worth testing, but for full coverage.
+        """
+        # This is directly testing the _dateparse function
+        # Which is a "proper" unit test, but also testing an internal function.
+        timestring = "seconds since 1965-01-01T00:00:00"
+        with pytest.raises(ValueError):
+            basedate = cftime._dateparse(timestring, 'tai', has_year_zero=True)
+            # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+            print(basedate)
+
+    def test_creation_valid(self):
+        dt = DatetimeTAI(2025, 1, 9, 14, 18)
+
+        assert dt.calendar == 'tai'
+        assert dt.has_year_zero is False
+
+        print(repr(dt))
+
+    def test_creation_before_1958(self):
+        with pytest.raises(ValueError):
+            dt = DatetimeTAI(1957, 1, 9, 14, 18)
+            print(repr(dt))
+
+
 
 if __name__ == '__main__':
     unittest.main()



View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/2111e831a4c3df831d7a3a3142498e9e8fecec37...2169d617487bf503374c32372751ec9305f415a9

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/2111e831a4c3df831d7a3a3142498e9e8fecec37...2169d617487bf503374c32372751ec9305f415a9
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260918/9f86d2c4/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list