From 51311932167a4b6b646020f00e4648e9e21a194e Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 16 Jan 2026 09:03:34 -0500 Subject: [PATCH 01/29] Preliminary sphinx-pyodide integration --- docs/conf.py | 10 ++++++---- docs/index.rst | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a55e3ae..2e1e361 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,9 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys + +sys.path.insert(0, os.path.abspath(".")) import undate @@ -23,7 +24,7 @@ author = "DHtech Community" # The full version, including alpha/beta/rc tags -release = undate.__version__ +release = undate.__version__ # type: ignore[attr-defined] master_doc = "index" @@ -37,6 +38,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "myst_parser", + "sphinx_pyodide", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/index.rst b/docs/index.rst index 4f302f9..9ad10ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ undate documentation :caption: Contents: readme + pyodide undate/index CONTRIBUTING DEVELOPER_NOTES From 67436234168abd987a1e8946e00c7c246713c500 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 7 May 2026 17:30:06 -0400 Subject: [PATCH 02/29] Set next release version to 0.8 --- CHANGELOG.md | 3 +++ src/undate/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1635a90..778fd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## 0.8 + +- ## 0.7 diff --git a/src/undate/__init__.py b/src/undate/__init__.py index 06082fe..5b91527 100644 --- a/src/undate/__init__.py +++ b/src/undate/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.7.0" +__version__ = "0.8.0.dev0" # this sort order is important to avoid circular imports From a250380f59673626dd205d94de418f7f7dd2e627 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 21:30:25 +0000 Subject: [PATCH 03/29] Bump the actions group with 3 updates Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `pypa/gh-action-pypi-publish` from 1.4.2 to 1.14.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/27b31702a0e7fc50959f5ad993c78deac1bdfc29...cef221092ed1bacb1cc03d23a2d87d1d172e277b) Updates `codecov/codecov-action` from 5 to 6 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check.yml | 2 +- .github/workflows/python-publish.yml | 4 ++-- .github/workflows/unit_tests.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e46401c..98c7662 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,7 +19,7 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v8.1.0 diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 2a008cf..0e2ed52 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: @@ -35,7 +35,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 806b68f..23c1471 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -34,7 +34,7 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 # use github python action instead of uv to take advantage of caching - name: Set up Python ${{ matrix.python }} @@ -58,7 +58,7 @@ jobs: if: ${{ matrix.python == env.COV_PYTHON_VERSION }} - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: ${{ matrix.python == env.COV_PYTHON_VERSION }} From 961b1582b28151838c4ef521d99826d53870e830 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 12:07:15 -0400 Subject: [PATCH 04/29] Update github workflows & precommit to use pinned hashes instead of tags --- .github/workflows/check.yml | 6 +++--- .github/workflows/python-publish.yml | 4 ++-- .github/workflows/unit_tests.yml | 6 +++--- .pre-commit-config.yaml | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 98c7662..04b21e3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,10 +19,10 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: "3.12" enable-cache: true @@ -32,7 +32,7 @@ jobs: run: uv sync --group check - name: Run Ruff linter - uses: astral-sh/ruff-action@v3 + uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 with: args: "check --output-format=github" diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 0e2ed52..0b2390e 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -23,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 23c1471..9214284 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -34,11 +34,11 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # use github python action instead of uv to take advantage of caching - name: Set up Python ${{ matrix.python }} - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ matrix.python }} cache: 'pip' @@ -58,7 +58,7 @@ jobs: if: ${{ matrix.python == env.COV_PYTHON_VERSION }} - name: Upload coverage to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: ${{ matrix.python == env.COV_PYTHON_VERSION }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 873c6fd..e00d252 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,13 +2,13 @@ files: \.py repos: # ruff for linting and formatting python - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.12 + rev: 0c7b6c989466a93942def1f84baf36ddfcd60c83 # frozen: v0.15.14 hooks: - id: ruff-check args: [ --fix, --show-fixes, --exit-non-zero-on-fix ] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -22,18 +22,18 @@ repos: args: [--pytest-test-first] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v2.0.0 + rev: d2823d321df3af8f878f7ee3414dc94d037145b9 # frozen: v2.1.0 hooks: - id: mypy additional_dependencies: [numpy] # yamlfmt for formatting YAML files - repo: https://github.com/google/yamlfmt - rev: v0.21.0 + rev: b5ca1890231d5e1e5181fef75a1be609d1e25029 # frozen: v0.21.0 hooks: - id: yamlfmt # Codespell for spell checking - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 hooks: - id: codespell additional_dependencies: @@ -41,6 +41,6 @@ repos: exclude_types: ["css", "html", "javascript", "json"] # Validate GitHub Actions workflow files - repo: https://github.com/mpalmer/action-validator - rev: v0.9.0 + rev: 76a805bbfcba3506d6cdb4bba1810ab504e0d72b # frozen: v0.9.0 hooks: - id: action-validator From e15710e273df25404fdae146c295431aa1f026a9 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 12:27:25 -0400 Subject: [PATCH 05/29] Configure to not persist permissions after checkout --- .github/workflows/check.yml | 3 ++- .github/workflows/python-publish.yml | 35 ++++++++++++++-------------- .github/workflows/unit_tests.yml | 12 +++++----- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 04b21e3..94d0525 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,7 +20,8 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - + with: + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 0b2390e..7364816 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -19,23 +19,24 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 9214284..ef56dff 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -13,8 +13,8 @@ on: branches: - "**" paths: - - 'undate/**' - - 'tests/**' + - "undate/**" + - "tests/**" pull_request: branches: - "**" @@ -35,14 +35,15 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - + with: + persist-credentials: false # use github python action instead of uv to take advantage of caching - name: Set up Python ${{ matrix.python }} uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ matrix.python }} - cache: 'pip' - cache-dependency-path: '**/pyproject.toml' + cache: "pip" + cache-dependency-path: "**/pyproject.toml" - name: Install package with check dependencies run: uv sync --group test @@ -62,4 +63,3 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: ${{ matrix.python == env.COV_PYTHON_VERSION }} - From 29ea5b22ad5c7fde6d810473498c694051b6288d Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 12:35:55 -0400 Subject: [PATCH 06/29] Update pre-commit hooks and copyright year in docs --- .pre-commit-config.yaml | 15 ++++++++------- docs/conf.py | 13 ++++++++----- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e00d252..f0529d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,13 +2,13 @@ files: \.py repos: # ruff for linting and formatting python - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 0c7b6c989466a93942def1f84baf36ddfcd60c83 # frozen: v0.15.14 + rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 hooks: - id: ruff-check - args: [ --fix, --show-fixes, --exit-non-zero-on-fix ] + args: [--fix, --show-fixes, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -22,18 +22,19 @@ repos: args: [--pytest-test-first] - repo: https://github.com/pre-commit/mirrors-mypy - rev: d2823d321df3af8f878f7ee3414dc94d037145b9 # frozen: v2.1.0 + rev: d2823d321df3af8f878f7ee3414dc94d037145b9 # frozen: v2.1.0 hooks: - id: mypy additional_dependencies: [numpy] + # yamlfmt for formatting YAML files - repo: https://github.com/google/yamlfmt - rev: b5ca1890231d5e1e5181fef75a1be609d1e25029 # frozen: v0.21.0 + rev: b5ca1890231d5e1e5181fef75a1be609d1e25029 # frozen: v0.21.0 hooks: - id: yamlfmt # Codespell for spell checking - repo: https://github.com/codespell-project/codespell - rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 hooks: - id: codespell additional_dependencies: @@ -41,6 +42,6 @@ repos: exclude_types: ["css", "html", "javascript", "json"] # Validate GitHub Actions workflow files - repo: https://github.com/mpalmer/action-validator - rev: 76a805bbfcba3506d6cdb4bba1810ab504e0d72b # frozen: v0.9.0 + rev: 76a805bbfcba3506d6cdb4bba1810ab504e0d72b # frozen: v0.9.0 hooks: - id: action-validator diff --git a/docs/conf.py b/docs/conf.py index a55e3ae..cc7f688 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,20 +10,22 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path().resolve())) import undate # -- Project information ----------------------------------------------------- project = "undate" -copyright = "2024, DHtech" +copyright = "2026, DHtech" author = "DHtech Community" # The full version, including alpha/beta/rc tags -release = undate.__version__ +release = undate.__version__ # type: ignore[attr-defined] master_doc = "index" @@ -37,6 +39,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "myst_parser", + "sphinx_pyodide", ] # Add any paths that contain templates here, relative to this directory. From 363293a50209ff039b7899a59645dfa1c4abc993 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 15:42:26 -0400 Subject: [PATCH 07/29] Switch docs theme to furo, add light/dark mode logo Assisted-by: OpenCode:BigPickle (glm-5.1) --- docs/_static/custom.css | 5 ++++ docs/_static/undate_logo.svg | 14 +++++++++++ docs/_static/undate_logo_dark.svg | 15 +++++++++++ docs/conf.py | 42 +++++++++++++++++-------------- pyproject.toml | 9 +++++-- 5 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 docs/_static/undate_logo.svg create mode 100644 docs/_static/undate_logo_dark.svg diff --git a/docs/_static/custom.css b/docs/_static/custom.css index aa35e82..6ba85cf 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,4 +1,9 @@ +.sidebar-logo { + width: 200px; + height: auto; +} + div.sphinxsidebar .powered_by a { text-decoration: none; border-bottom: none; diff --git a/docs/_static/undate_logo.svg b/docs/_static/undate_logo.svg new file mode 100644 index 0000000..52bba2e --- /dev/null +++ b/docs/_static/undate_logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/_static/undate_logo_dark.svg b/docs/_static/undate_logo_dark.svg new file mode 100644 index 0000000..bc907b7 --- /dev/null +++ b/docs/_static/undate_logo_dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/conf.py b/docs/conf.py index 2e1e361..4044f4e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,10 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os import sys +from pathlib import Path -sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, str(Path().resolve())) import undate @@ -59,30 +59,34 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "alabaster" +html_theme = "furo" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_css_files = ["custom.css"] html_theme_options = { - "logo": "undate_logo.png", - "logo_name": False, - "github_user": "dh-tech", - "github_repo": "undate-python", - "github_button": False, - "github_banner": True, -} - -html_sidebars = { - "**": [ - "about.html", - "navigation.html", - "localtoc.html", - "searchbox.html", - "sidebar_dhtech.html", - ], + "light_logo": "undate_logo.svg", + "dark_logo": "undate_logo_dark.svg", + "source_repository": "https://github.com/dh-tech/undate-python", + "source_branch": "main", + "source_directory": "docs/", + "light_css_variables": { + "color-brand-primary": "#6670bb", + "color-brand-content": "#6670bb", + "color-background-primary": "#f0f1f8", + "color-background-secondary": "#e5e7f2", + "sidebar-logo-width": "200px", + }, + "dark_css_variables": { + "color-brand-primary": "#9198d0", + "color-brand-content": "#9198d0", + "color-background-primary": "#1d202f", + "color-background-secondary": "#151825", + "sidebar-logo-width": "200px", + }, } # turn on relative links; make sure both github and sphinx links work diff --git a/pyproject.toml b/pyproject.toml index 09c8ca5..02aae48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ classifiers = [ ] [dependency-groups] -docs = ["sphinx>=7.0.0", "alabaster", "myst-parser", "myst-parser[linkify]"] +docs = ["sphinx>=7.0.0", "furo", "myst-parser", "myst-parser[linkify]"] test = ["pytest>=9", "pytest-ordering", "pytest-cov"] notebooks = ["jupyterlab", "pandas", "treon", "altair"] check = [ { include-group = "docs" }, {include-group = "notebooks"}, "mypy", "ruff"] @@ -60,9 +60,11 @@ dev = [ "twine", "wheel", "build", + "sphinx-autobuild>=2024.10.3", + "sphinx-pyodide", { include-group = "test" }, { include-group = "check" }, - { include-group = "docs" } + { include-group = "docs" }, ] [project.urls] @@ -122,3 +124,6 @@ ignore = [] [tool.ruff.lint.per-file-ignores] # for test files, don't require docstrings or return type annotations "tests/**.py" = ["D", "ANN", "RUF"] + +[tool.uv.sources] +sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } From bc91b1d7fb0bfe5ea37ac21913fee90aacbcfe08 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 15:43:17 -0400 Subject: [PATCH 08/29] Remove sphinx_pyodide ext since not yet available --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index cc7f688..ff5d7d3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,7 +39,6 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "myst_parser", - "sphinx_pyodide", ] # Add any paths that contain templates here, relative to this directory. From 76af22ed6b6f18d66b92c86deaa6c0f8a090364c Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:29:12 -0400 Subject: [PATCH 09/29] Add live examples based on the readme --- docs/conf.py | 3 ++ docs/example_usage.rst | 103 +++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 4 +- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 docs/example_usage.rst diff --git a/docs/conf.py b/docs/conf.py index 4044f4e..43c7c81 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,6 +41,9 @@ "sphinx_pyodide", ] +# build static output for noscript fallback +pyodide_build_output = True + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/example_usage.rst b/docs/example_usage.rst new file mode 100644 index 0000000..f1900fd --- /dev/null +++ b/docs/example_usage.rst @@ -0,0 +1,103 @@ + +Example Usage +-------------- + +Often humanities and cultural data include imprecise or uncertain temporal information. We want to store that information but also work with it in a structured way, not just treat it as text for display. Different projects may need to work with or convert between different date formats or even different calendars. + +An ``undate.Undate`` is analogous to python’s builtin ``datetime.date`` object, but with support for varying degrees of precision and unknown information. You can initialize an ``Undate`` with either strings or numbers for whichever parts of the date are known or partially known. An ``Undate`` can take an optional label. + +.. note:: + These are examples are written to run live in the browser with `Pyodide `_ and `sphinx-pyodide `_. + +.. pyodide:: + :packages: ./wheels/PyMeeus-0.5.12-py3-none-any.whl,./wheels/undate-0.8.0.dev0-py3-none-any.whl + + from undate import __version__ + + print(f"Running undate v{__version__}") + + +.. pyodide:: + from undate import Undate + + november7 = Undate(2000, 11, 7) + november = Undate(2000, 11) + year2k = Undate(2000) + november7_some_year = Undate(month=11, day=7) + + partially_known_year = Undate("19XX") + partially_known_month = Undate(2022, "1X") + + easter1916 = Undate(1916, 4, 23, label="Easter 1916") + +You can convert an ``Undate`` to string using a date formatter (current default is ISO8601): + + +.. pyodide:: + print([str(d) for d in [november7, november, year2k, november7_some_year]]) + + +If enough information is known, an ``Undate`` object can report on its duration: + +.. pyodide:: + december = Undate(2000, 12) + feb_leapyear = Undate(2024, 2) + feb_regularyear = Undate(2023, 2) + + example_dates = [ + november7, november, december, year2k, + november7_some_year, feb_regularyear, feb_leapyear + ] + for d in example_dates: + print(f"{d!s:<10} duration in days: {d.duration().days:>2}") + +If enough of the date is known and the precision supports it, you can +check if one date falls within another date: + +.. pyodide:: + november7 = Undate(2000, 11, 7) + november2000 = Undate(2000, 11) + year2k = Undate(2000) + ad100 = Undate(100) + november7 in november + + yes_no = {True: "✅", False: "❌"} + for range in [november2000, year2k, ad100]: + print(f"{november7!s:>10} within {range!s:<10}? {yes_no[november7 in range]}") + if november2000 != range: # don't test against itself + print(f"{november2000!s:>10} within {range!s:<10}? {yes_no[november2000 in range]}") + + +For dates that are imprecise or partially known, ``undate`` calculates +earliest and latest possible dates for comparison purposes so you can +sort dates ... + +.. pyodide:: + november7_2020 = Undate(2020, 11, 7) + november_2001 = Undate(2001, 11) + year2k = Undate(2000) + ad100 = Undate(100) + for date in sorted([november7_2020, november_2001, year2k, ad100]): + # print the date in ISO/EDTF format along with the python representation + print(f"{date!s:>10} : {repr(date)}") + + +You can also compare with equals, greater than, and less than. You +can also compare with python ``datetime.date`` objects. + +.. pyodide:: + from datetime import date + + jan2001 = date(2001, 1, 1) + + print(f"{november7_2020!s:>10} before {november_2001!s:<10} ? {yes_no[november7_2020 > november_2001]}") + print(f"{year2k!s:>10} before {ad100!s:<10} ? {yes_no[year2k > ad100]}") + print(f"{year2k!s:>10} after {jan2001} ? {yes_no[year2k > jan2001]}") + + +When dates cannot be compared due to ambiguity or precision, comparison +methods raise a ``NotImplementedError``. + +.. pyodide:: + november_2020 = Undate(2020, 11) + november7_2020 > november_2020 diff --git a/docs/index.rst b/docs/index.rst index 9ad10ca..b1679eb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ undate documentation .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: readme @@ -16,6 +16,8 @@ undate documentation CONTRIBUTORS LICENSE +.. include:: example_usage.rst + Indices and tables ================== From fe0dc8e3a0ea56c42e9328e3f682132ae0d844a8 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:42:10 -0400 Subject: [PATCH 10/29] Convert remaining examples from readme to live pyodide examples Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 72 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index f1900fd..1a9742f 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -101,3 +101,75 @@ methods raise a ``NotImplementedError``. .. pyodide:: november_2020 = Undate(2020, 11) november7_2020 > november_2020 + +An ``UndateInterval`` is a date range between two ``Undate`` objects. +Intervals can be open-ended, allow for optional labels, and can +calculate duration if enough information is known. ``UndateIntervals`` +are inclusive (i.e., a closed interval), and include both the earliest +and latest date as part of the range. + +.. pyodide:: + from undate import UndateInterval + + century19 = UndateInterval(Undate(1801), Undate(1900), label="19th century") + century20 = UndateInterval(Undate(1901), Undate(2000), label="20th century") + before2000 = UndateInterval(latest=Undate(2000)) # before 2000 + after1900 = UndateInterval(Undate(1900)) # after 1900 + + for interval in [century19, century20, before2000, after1900]: + print(repr(interval)) + +Intervals can calculate duration if enough information is known: + +.. pyodide:: + for interval in [century19, century20]: + print(f"{interval.label}: {interval.duration().days} days") + print(f"January 2000: {UndateInterval(Undate(2000, 1, 1), Undate(2000, 1, 31)).duration().days} days") + +You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a +date string with a specific converter, and you can also output an +``Undate`` object in those formats. Currently available converters +are "ISO8601" and "EDTF" and supported calendars. + +.. pyodide:: + print(repr(Undate.parse("2002", "ISO8601"))) + print(repr(Undate.parse("2002-05", "EDTF"))) + print(repr(Undate.parse("--05-03", "ISO8601"))) + print(Undate.parse("--05-03", "ISO8601").format("EDTF")) + print(repr(Undate.parse("1800/1900", format="EDTF"))) + + +Calendars +~~~~~~~~~ + +All ``Undate`` objects are calendar aware, and date converters include +support for parsing and working with dates from other calendars. The +Gregorian calendar is used by default; currently ``undate`` supports the +Islamic Hijri calendar and the Hebrew Anno Mundi calendar. + +Dates are stored with the year, month, day and appropriate precision for +the original calendar; internally, earliest and latest dates are +calculated in Gregorian / Proleptic Gregorian calendar for standardized +comparison across dates from different calendars. + +.. pyodide:: + tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew") + rajab495 = Undate.parse("Rajab 495", "Islamic") + y2k = Undate.parse("2001", "EDTF") + + for d in [tammuz4816, rajab495, y2k]: + print(repr(d)) + +.. pyodide:: + print("Earliest Gregorian equivalent:") + for d in [rajab495, tammuz4816, y2k]: + print(f" {str(d):<30} earliest: {d.earliest}") + + print("\nPrecision:") + for d in [rajab495, tammuz4816, y2k]: + print(f" {str(d):<30} precision: {d.precision}") + +.. pyodide:: + print("Sorted by Gregorian date:") + for d in sorted([rajab495, tammuz4816, y2k]): + print(f" {repr(d)}") From f4fc9d66fac6f8509dde1647b9d6dfa140f9c405 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 17:56:19 -0400 Subject: [PATCH 11/29] Revise examples ported from readme --- docs/example_usage.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 1a9742f..4ae0390 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -99,6 +99,7 @@ When dates cannot be compared due to ambiguity or precision, comparison methods raise a ``NotImplementedError``. .. pyodide:: + :show-errors: november_2020 = Undate(2020, 11) november7_2020 > november_2020 @@ -117,14 +118,18 @@ and latest date as part of the range. after1900 = UndateInterval(Undate(1900)) # after 1900 for interval in [century19, century20, before2000, after1900]: - print(repr(interval)) + print(f"{repr(interval)}\n{interval}\n") Intervals can calculate duration if enough information is known: .. pyodide:: - for interval in [century19, century20]: - print(f"{interval.label}: {interval.duration().days} days") - print(f"January 2000: {UndateInterval(Undate(2000, 1, 1), Undate(2000, 1, 31)).duration().days} days") + jan2000_interval = UndateInterval( + Undate(2000, 1, 1), + Undate(2000, 1, 31), + label="January 2000" + ) + for interval in [century19, century20, jan2000_interval]: + print(f"{interval.label}: {interval.duration().days:,} days") You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a date string with a specific converter, and you can also output an @@ -158,16 +163,16 @@ comparison across dates from different calendars. y2k = Undate.parse("2001", "EDTF") for d in [tammuz4816, rajab495, y2k]: - print(repr(d)) + print(f"{d!s:<10} {repr(d)}") .. pyodide:: print("Earliest Gregorian equivalent:") for d in [rajab495, tammuz4816, y2k]: - print(f" {str(d):<30} earliest: {d.earliest}") + print(f" {str(d):<20} earliest: {d.earliest}") print("\nPrecision:") for d in [rajab495, tammuz4816, y2k]: - print(f" {str(d):<30} precision: {d.precision}") + print(f" {str(d):<20} precision: {d.precision}") .. pyodide:: print("Sorted by Gregorian date:") From f3300e6fd23e1a6c32e63056cccd7a11cd7ab4fd Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 18:05:14 -0400 Subject: [PATCH 12/29] Clean up examples and highlight new features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed output consistency — all calls use repr(), with the format conversion separated into a clear comment - Added "Parsing and Formatting" section heading (same ~~~~~ level as "Calendars") - Added Gregorian examples — French and Kinyarwanda to illustrate the multilingual support - Added holidays examples — Epiphany (fixed), Easter and Ash Wednesday (movable feasts) Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 4ae0390..c31e986 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -100,6 +100,7 @@ methods raise a ``NotImplementedError``. .. pyodide:: :show-errors: + november_2020 = Undate(2020, 11) november7_2020 > november_2020 @@ -131,17 +132,37 @@ Intervals can calculate duration if enough information is known: for interval in [century19, century20, jan2000_interval]: print(f"{interval.label}: {interval.duration().days:,} days") +Parsing and Formatting +~~~~~~~~~~~~~~~~~~~~~~ + You can initialize ``Undate`` or ``UndateInterval`` objects by parsing a -date string with a specific converter, and you can also output an -``Undate`` object in those formats. Currently available converters -are "ISO8601" and "EDTF" and supported calendars. +date string with a named converter, and output an ``Undate`` in a +different format than it was parsed. The ``"ISO8601"`` and ``"EDTF"`` +converters handle the most common structured formats: .. pyodide:: print(repr(Undate.parse("2002", "ISO8601"))) print(repr(Undate.parse("2002-05", "EDTF"))) print(repr(Undate.parse("--05-03", "ISO8601"))) + print(repr(Undate.parse("1800/1900", "EDTF"))) + + # convert between formats print(Undate.parse("--05-03", "ISO8601").format("EDTF")) - print(repr(Undate.parse("1800/1900", format="EDTF"))) + +The ``"Gregorian"`` converter parses dates with full or abbreviated month +names across multiple languages: + +.. pyodide:: + print(repr(Undate.parse("avril 1362", "Gregorian"))) # French + print(repr(Undate.parse("2022 Ugushyingo 26", "Gregorian"))) # Kinyarwanda + +The ``"holidays"`` converter parses Christian liturgical dates, including +movable feasts: + +.. pyodide:: + print(repr(Undate.parse("Epiphany 1942", "holidays"))) + print(repr(Undate.parse("Easter 1942", "holidays"))) + print(repr(Undate.parse("Ash Wednesday 1942", "holidays"))) Calendars From 59b5f409f9762fad38a505477be07ae89616c6c8 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 18:27:24 -0400 Subject: [PATCH 13/29] Add editable examples & make them nicer to play with --- docs/example_usage.rst | 47 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index c31e986..fa2de58 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -102,7 +102,10 @@ methods raise a ``NotImplementedError``. :show-errors: november_2020 = Undate(2020, 11) - november7_2020 > november_2020 + try: + november7_2020 > november_2020 + except NotImplementedError as err: + print(err) An ``UndateInterval`` is a date range between two ``Undate`` objects. Intervals can be open-ended, allow for optional labels, and can @@ -153,16 +156,35 @@ The ``"Gregorian"`` converter parses dates with full or abbreviated month names across multiple languages: .. pyodide:: - print(repr(Undate.parse("avril 1362", "Gregorian"))) # French - print(repr(Undate.parse("2022 Ugushyingo 26", "Gregorian"))) # Kinyarwanda + :editable: + :show-errors: + + dates = [ + "7 November 2000", + "Nov 2000", + "avril 1362", + "2022 Ugushyingo 26", + ] + for date in dates: + print(repr(Undate.parse(date, "Gregorian"))) + The ``"holidays"`` converter parses Christian liturgical dates, including movable feasts: .. pyodide:: - print(repr(Undate.parse("Epiphany 1942", "holidays"))) - print(repr(Undate.parse("Easter 1942", "holidays"))) - print(repr(Undate.parse("Ash Wednesday 1942", "holidays"))) + :editable: + :show-errors: + + holidays = [ + "Epiphany 1942", + "Easter 1942", + "Ash Wednesday 1942", + ] + undate_holidays = [Undate.parse(hol, "holidays") for hol in holidays] + + for holidate in undate_holidays: + print(f"{holidate.label}: {holidate!s} (earliest: {holidate.earliest}, latest: {holidate.latest})") Calendars @@ -199,3 +221,16 @@ comparison across dates from different calendars. print("Sorted by Gregorian date:") for d in sorted([rajab495, tammuz4816, y2k]): print(f" {repr(d)}") + +Try it yourself +~~~~~~~~~~~~~~~ + +.. pyodide:: + :editable: + :show-errors: + + # try your own dates here + from undate import Undate, UndateInterval + + mydate = Undate(2025, 6) + print(mydate) From d597eacf8293411dc453d0cc719a0b85d5599c38 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 19:23:30 -0400 Subject: [PATCH 14/29] Restructure docs to provide brief overview; move examples to new page Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/example_usage.rst | 4 --- docs/index.rst | 82 ++++++++++++++++++++++++++++++++++++++---- docs/readme.md | 4 --- 3 files changed, 75 insertions(+), 15 deletions(-) delete mode 100644 docs/readme.md diff --git a/docs/example_usage.rst b/docs/example_usage.rst index fa2de58..8db176b 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -1,4 +1,3 @@ - Example Usage -------------- @@ -6,9 +5,6 @@ Often humanities and cultural data include imprecise or uncertain temporal infor An ``undate.Undate`` is analogous to python’s builtin ``datetime.date`` object, but with support for varying degrees of precision and unknown information. You can initialize an ``Undate`` with either strings or numbers for whichever parts of the date are known or partially known. An ``Undate`` can take an optional label. -.. note:: - These are examples are written to run live in the browser with `Pyodide `_ and `sphinx-pyodide `_. - .. pyodide:: :packages: ./wheels/PyMeeus-0.5.12-py3-none-any.whl,./wheels/undate-0.8.0.dev0-py3-none-any.whl diff --git a/docs/index.rst b/docs/index.rst index b1679eb..4341493 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,91 @@ -undate documentation -==================== +undate +====== -**undate** is a python library for working with uncertain or partially known dates. +.. image:: _static/undate_logo.png + :alt: undate +**undate** is a Python library for working with uncertain or partially known dates. + +.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.11068867.svg + :target: https://doi.org/10.5281/zenodo.11068867 +.. image:: https://img.shields.io/badge/License-Apache_2.0-blue.svg + :target: https://opensource.org/licenses/Apache-2.0 +.. image:: https://readthedocs.org/projects/undate-python/badge/?version=latest + :target: https://undate-python.readthedocs.io/en/latest/?badge=latest +.. image:: https://github.com/dh-tech/undate-python/actions/workflows/unit_tests.yml/badge.svg + :target: https://github.com/dh-tech/undate-python/actions/workflows/unit_tests.yml +.. image:: https://codecov.io/gh/dh-tech/undate-python/branch/main/graph/badge.svg?token=GE7HZE8C9D + :target: https://codecov.io/gh/dh-tech/undate-python + +.. note:: + This is beta software; it is still in development and not fully feature complete. + If you use it, please let us know and share your feedback. + +``undate`` supports parsing, formatting, and reasoning with dates in varying +precision and calendars. Dates with different precision and from different original +calendars can be used together. Supported formats include: + +- portions of EDTF (Extended Date Time Format) +- ISO8601 +- parsing and calendar conversion for dates in Hebrew Anno Mundi and Islamic Hijri calendars +- Gregorian dates with full or abbreviated month names in any order for multiple languages + (English, Spanish, French, German, Kinyarwanda, Ganda, Tigrinya) +- Christian liturgical dates (fixed holidays and movable feasts) + +For unambiguous dates, there is an experimental omnibus parser which combines all available parsers. + +For more about the origin and goals of ``undate``, read our 2025 software paper: + + Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. + "`Undate: Humanistic Dates for Computation `_." + *Computational Humanities Research*, August 5, 2025. + +Quick Start +----------- + +An ``Undate`` is analogous to Python's ``datetime.date``, but with support for +varying precision and unknown information: + +.. code-block:: python + + from undate import Undate, UndateInterval + + november = Undate(2000, 11) # year and month known + year2k = Undate(2000) # year only + partially_known = Undate("19XX") # partially unknown year + + print([str(d) for d in [november, year2k, partially_known]]) + # ['2000-11', '2000', '19XX'] + +Dates can be parsed from multiple formats and calendars, sorted, and compared +even when precision differs. See :doc:`example_usage` for interactive examples. + +Installation +------------ + +Install the latest published version from PyPI: + +.. code-block:: console + + pip install undate + +To install a development version or specific branch: + +.. code-block:: console + + pip install git+https://github.com/dh-tech/undate-python@develop#egg=undate .. toctree:: :maxdepth: 1 :caption: Contents: - readme - pyodide + example_usage undate/index CONTRIBUTING DEVELOPER_NOTES CONTRIBUTORS LICENSE -.. include:: example_usage.rst - Indices and tables ================== diff --git a/docs/readme.md b/docs/readme.md deleted file mode 100644 index 4ec94a5..0000000 --- a/docs/readme.md +++ /dev/null @@ -1,4 +0,0 @@ -```{include} ../README.md -:relative-docs: / -:relative-images: -``` From fdd26f103059ce58405dd709da6d1a540861fda5 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 20:00:31 -0400 Subject: [PATCH 15/29] Add sphinx-pyodide to dependencies --- pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 02aae48..4c72439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,13 @@ classifiers = [ ] [dependency-groups] -docs = ["sphinx>=7.0.0", "furo", "myst-parser", "myst-parser[linkify]"] +docs = [ + "sphinx>=7.0.0", + "furo", + "myst-parser", + "myst-parser[linkify]", + "sphinx-pyodide", +] test = ["pytest>=9", "pytest-ordering", "pytest-cov"] notebooks = ["jupyterlab", "pandas", "treon", "altair"] check = [ { include-group = "docs" }, {include-group = "notebooks"}, "mypy", "ruff"] From 04bc5a9d0f5fa337d344707e46b050b55c692fec Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 2 Jul 2026 20:05:37 -0400 Subject: [PATCH 16/29] Remove sphinx-pyodide from dev deps --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4c72439..69a4d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ classifiers = [ [dependency-groups] docs = [ - "sphinx>=7.0.0", + "sphinx>=8.0.0", "furo", "myst-parser", "myst-parser[linkify]", @@ -67,7 +67,6 @@ dev = [ "wheel", "build", "sphinx-autobuild>=2024.10.3", - "sphinx-pyodide", { include-group = "test" }, { include-group = "check" }, { include-group = "docs" }, From 885605f55f2ab57f27264acb343aba653b3f05de Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:43:07 -0400 Subject: [PATCH 17/29] Fix py dep caching discrepancy - everything uv Assisted-by: ClaudeCode:claude-sonnet-4-6 --- .github/workflows/check.yml | 2 +- .github/workflows/unit_tests.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 94d0525..c7047ca 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,7 +27,7 @@ jobs: with: python-version: "3.12" enable-cache: true - cache-dependency-glob: "pyproject.toml" + cache-dependency-glob: "uv.lock" - name: Install package with check dependencies run: uv sync --group check diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index ef56dff..66bdd49 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -42,8 +42,8 @@ jobs: uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ matrix.python }} - cache: "pip" - cache-dependency-path: "**/pyproject.toml" + enable-cache: true + cache-dependency-glob: "uv.lock" - name: Install package with check dependencies run: uv sync --group test From 92450b041ce458cbce67269cb3a692cf0b2d47c6 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:46:26 -0400 Subject: [PATCH 18/29] Switch publish workflow to uv as well Assisted-by: ClaudeCode:claude-sonnet-4-6 --- .github/workflows/python-publish.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 7364816..7583831 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -25,16 +25,12 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - python-version: "3.x" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build + python-version: "3.12" - name: Build package - run: python -m build + run: uv build - name: Publish package uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b with: From 4a78125873fc33663632ce0af561a5aaba21ee4e Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 10:56:15 -0400 Subject: [PATCH 19/29] Comment out local path used for sphinx-pyodide dev --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69a4d82..23fb665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,5 +130,6 @@ ignore = [] # for test files, don't require docstrings or return type annotations "tests/**.py" = ["D", "ANN", "RUF"] -[tool.uv.sources] -sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } +# uncomment for local development of sphinx-pyodide +#[tool.uv.sources] +#sphinx-pyodide = { path = "../sphinx-pyodide", editable = true } From 7e2f703443f2a81dc30fbab99234b7de755d24b0 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Sat, 4 Jul 2026 11:03:02 -0400 Subject: [PATCH 20/29] Use light/dark mode logo for docs index Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/index.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 4341493..294c40d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,15 @@ undate ====== -.. image:: _static/undate_logo.png +.. image:: _static/undate_logo.svg :alt: undate + :width: 350px + :class: only-light + +.. image:: _static/undate_logo_dark.svg + :alt: undate + :width: 350px + :class: only-dark **undate** is a Python library for working with uncertain or partially known dates. From d2d942ed6970b308d8cb511646d0434f55b62d76 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Tue, 7 Jul 2026 10:50:18 -0400 Subject: [PATCH 21/29] Fix before/after to match the actual comparison --- docs/example_usage.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 8db176b..0b3f850 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -87,8 +87,8 @@ can also compare with python ``datetime.date`` objects. jan2001 = date(2001, 1, 1) print(f"{november7_2020!s:>10} before {november_2001!s:<10} ? {yes_no[november7_2020 > november_2001]}") - print(f"{year2k!s:>10} before {ad100!s:<10} ? {yes_no[year2k > ad100]}") - print(f"{year2k!s:>10} after {jan2001} ? {yes_no[year2k > jan2001]}") + print(f"{year2k!s:>10} after {ad100!s:<10} ? {yes_no[year2k > ad100]}") + print(f"{year2k!s:>10} after {jan2001} ? {yes_no[year2k > jan2001]}") When dates cannot be compared due to ambiguity or precision, comparison From 4cb5a6539246210fcd51e7712ebbd549b0c02124 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Wed, 8 Jul 2026 18:35:30 -0400 Subject: [PATCH 22/29] Consolidate and shorten readme to avoid redundancy with docs Assisted-by: ClaudeCode:claude-sonnet-4-6 --- README.md | 203 ++++-------------------------------------------------- 1 file changed, 14 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 6eacf95..28fa2db 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,17 @@ Currently `undate` supports parsing, formatting, and reasoning with dates in varying precision and calendars; dates with different precision and from different original calendars can be used together. Supported formats include: -- portions of EDTF (Extended Date Time Format) -- ISO8601 -- parsing and calendar conversion for dates in Hebrew Anno Mundi and Islamic Hijri calendars -- Gregorian dates with full or abbreviated month names in any order for multiple languages (English, Spanish, French, German, Kinyarwanda, Ganda, Tigrinya) -- Christian liturgical dates (fixed holidays and movable feasts) +- portions of **EDTF** (Extended Date Time Format) +- **ISO8601** +- parsing and calendar conversion for dates in **Hebrew Anno Mundi** and **Islamic Hijri** calendars +- **Gregorian** dates with full or abbreviated month names in any order for multiple languages (English, Spanish, French, German, Kinyarwanda, Ganda, Tigrinya) +- **Christian liturgical** dates (fixed holidays and movable feasts) For unambiguous dates, there is an experimental omnibus parser which combines all available dates (bare years are currently assumed to be Gregorian calendar). For more about the origin and goals of `undate`, read our 2025 software paper: -> Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. “[Undate: Humanistic Dates for Computation](https://doi.org/10.1017/chr.2025.10006).” _Computational Humanities Research_, August 5, 2025. - +> Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. "[Undate: Humanistic Dates for Computation](https://doi.org/10.1017/chr.2025.10006)." _Computational Humanities Research_, August 5, 2025. --- @@ -31,8 +30,6 @@ For more about the origin and goals of `undate`, read our 2025 software paper: [![codecov](https://codecov.io/gh/dh-tech/undate-python/branch/main/graph/badge.svg?token=GE7HZE8C9D)](https://codecov.io/gh/dh-tech/undate-python) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -Project documentation is [available on ReadTheDocs](https://undate-python.readthedocs.io/en/latest/). - [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](CONTRIBUTORS.md) @@ -54,198 +51,26 @@ Use the `@name` notation to specify the branch or tag; e.g., to install developm pip install git+https://github.com/dh-tech/undate-python@develop#egg=undate ``` -## Example Usage +## Quick Start -Often humanities and cultural data include imprecise or uncertain -temporal information. We want to store that information but also work -with it in a structured way, not just treat it as text for display. -Different projects may need to work with or convert between different -date formats or even different calendars. - -An `undate.Undate` is analogous to python’s builtin `datetime.date` -object, but with support for varying degrees of precision and unknown -information. You can initialize an `Undate` with either strings or -numbers for whichever parts of the date are known or partially known. -An `Undate` can take an optional label. +An `Undate` is analogous to python’s `datetime.date` but supports varying degrees of precision and unknown information. Initialize with strings or numbers for whichever parts of the date are known: ```python -from undate import Undate +from undate import Undate, UndateInterval november7 = Undate(2000, 11, 7) november = Undate(2000, 11) year2k = Undate(2000) -november7_some_year = Undate(month=11, day=7) - partially_known_year = Undate("19XX") -partially_known_month = Undate(2022, "1X") - -easter1916 = Undate(1916, 4, 23, label="Easter 1916") -``` - -You can convert an `Undate` to string using a date formatter (current default is ISO8601): - -```python ->>> [str(d) for d in [november7, november, year2k, november7_some_year]] -['2000-11-07', '2000-11', '2000', '--11-07'] -``` - -If enough information is known, an `Undate` object can report on its duration: - -```python ->>> december = Undate(2000, 12) ->>> feb_leapyear = Undate(2024, 2) ->>> feb_regularyear = Undate(2023, 2) ->>> for d in [november7, november, december, year2k, november7_some_year, feb_regularyear, feb_leapyear]: -... print(f"{d} - duration in days: {d.duration().days}") -... -2000-11-07 - duration in days: 1 -2000-11 - duration in days: 30 -2000-12 - duration in days: 31 -2000 - duration in days: 366 ---11-07 - duration in days: 1 -2023-02 - duration in days: 28 -2024-02 - duration in days: 29 -``` - -If enough of the date is known and the precision supports it, you can -check if one date falls within another date: - -```python ->>> november7 = Undate(2000, 11, 7) ->>> november2000 = Undate(2000, 11) ->>> year2k = Undate(2000) ->>> ad100 = Undate(100) ->>> november7 in november -True ->>> november2000 in year2k -True ->>> november7 in year2k -True ->>> november2000 in ad100 -False ->>> november7 in ad100 -False -``` -For dates that are imprecise or partially known, `undate` calculates -earliest and latest possible dates for comparison purposes so you can -sort dates and compare with equals, greater than, and less than. You -can also compare with python `datetime.date` objects. - -```python ->>> november7_2020 = Undate(2020, 11, 7) ->>> november_2001 = Undate(2001, 11) ->>> year2k = Undate(2000) ->>> ad100 = Undate(100) ->>> sorted([november7_2020, november_2001, year2k, ad100]) -[undate.Undate(year=100, calendar="Gregorian"), undate.Undate(year=2000, calendar="Gregorian"), undate.Undate(year=2001, month=11, calendar="Gregorian"), undate.Undate(year=2020, month=11, day=7, calendar="Gregorian")] ->>> november7_2020 > november_2001 -True ->>> year2k < ad100 -False ->>> from datetime import date ->>> year2k > date(2001, 1, 1) -False -``` - -When dates cannot be compared due to ambiguity or precision, comparison -methods raise a `NotImplementedError`. - -```python ->>> november_2020 = Undate(2020, 11) ->>> november7_2020 > november_2020 -Traceback (most recent call last): - File "", line 1, in - File "/Users/rkoeser/workarea/github/undate-python/src/undate/undate.py", line 262, in __gt__ - return not (self < other or self == other) - File "/Users/rkoeser/workarea/github/undate-python/src/undate/undate.py", line 245, in __lt__ - raise NotImplementedError( -NotImplementedError: Can't compare when one date falls within the other -``` +[str(d) for d in [november7, november, year2k, partially_known_year]] +# [‘2000-11-07’, ‘2000-11’, ‘2000’, ‘19XX’] -An `UndateInterval` is a date range between two `Undate` objects. -Intervals can be open-ended, allow for optional labels, and can -calculate duration if enough information is known. `UndateIntervals` -are inclusive (i.e., a closed interval), and include both the earliest -and latest date as part of the range. - -```python ->>> from undate import UndateInterval ->>> UndateInterval(Undate(1900), Undate(2000)) -undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian")) ->>> UndateInterval(Undate(1801), Undate(1900), label="19th century") -undate.UndateInterval(earliest=undate.Undate(year=1801, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian"), label="19th century") ->>> UndateInterval(Undate(1801), Undate(1900), label="19th century").duration().days -36524 ->>> UndateInterval(Undate(1901), Undate(2000), label="20th century") -undate.UndateInterval(earliest=undate.Undate(year=1901, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian"), label="20th century") ->>> UndateInterval(latest=Undate(2000)) # before 2000 -undate.UndateInterval(latest=undate.Undate(year=2000, calendar="Gregorian")) ->>> UndateInterval(Undate(1900)) # after 1900 -undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian")) ->>> UndateInterval(Undate(1900), Undate(2000), label="19th century").duration().days -36890 ->>> UndateInterval(Undate(2000, 1, 1), Undate(2000, 1,31)).duration().days -31 +Undate.parse("Rajab 495", "Islamic") +# undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") ``` -You can initialize `Undate` or `UndateInterval` objects by parsing a -date string with a specific converter, and you can also output an -`Undate` object in those formats. Currently available converters -are "ISO8601" and "EDTF" and supported calendars. - -```python ->>> from undate import Undate ->>> Undate.parse("2002", "ISO8601") -undate.Undate(year=2002, calendar="Gregorian") ->>> Undate.parse("2002-05", "EDTF") -undate.Undate(year=2002, month=5, calendar="Gregorian") ->>> Undate.parse("--05-03", "ISO8601") -undate.Undate(month=5, day=3, calendar="Gregorian") ->>> Undate.parse("--05-03", "ISO8601").format("EDTF") -'XXXX-05-03' ->>> Undate.parse("1800/1900", format="EDTF") -undate.UndateInterval(earliest=undate.Undate(year=1800, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian")) -``` - -### Calendars - -All `Undate` objects are calendar aware, and date converters include -support for parsing and working with dates from other calendars. The -Gregorian calendar is used by default; currently `undate` supports the -Islamic Hijri calendar and the Hebrew Anno Mundi calendar based on -calendar conversion logic implemented in the -[convertdate](https://convertdate.readthedocs.io/en/latest/) package. - -Dates are stored with the year, month, day and appropriate precision for -the original calendar; internally, earliest and latest dates are -calculated in Gregorian / Proleptic Gregorian calendar for standardized -comparison across dates from different calendars. - -```python ->>> from undate import Undate ->>> tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew") ->>> tammuz4816 -undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew") ->>> rajab495 = Undate.parse("Rajab 495", "Islamic") ->>> rajab495 -undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") ->>> y2k = Undate.parse("2001", "EDTF") ->>> y2k -undate.Undate(year=2001, calendar="Gregorian") ->>> [str(d.earliest) for d in [rajab495, tammuz4816, y2k]] -['1102-04-28', '1056-07-17', '2001-01-01'] ->>> [str(d.precision) for d in [rajab495, tammuz4816, y2k]] -['MONTH', 'DAY', 'YEAR'] ->>> sorted([rajab495, tammuz4816, y2k]) -[undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew"), undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic"), undate.Undate(year=2001, calendar="Gregorian")] -``` - ---- - -For more examples, refer to the code notebooks included in the -[examples](https://github.com/dh-tech/undate-python/tree/main/examples/) -directory in this repository. +For full examples including duration, comparison, intervals, parsing, and calendar support, see the [interactive documentation](https://undate-python.readthedocs.io/en/latest/). ## Documentation From 8bb3cc034e6967180a04aef78b380841e29c352b Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:00:35 -0400 Subject: [PATCH 23/29] Update pre-commit hooks with precommit auto-update --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0529d7..475d234 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ files: \.py repos: # ruff for linting and formatting python - repo: https://github.com/astral-sh/ruff-pre-commit - rev: c59bba8fb259db0fec2bbb77ad8ba51ea7341b56 # frozen: v0.15.20 + rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0 hooks: - id: ruff-check args: [--fix, --show-fixes, --exit-non-zero-on-fix] @@ -22,7 +22,7 @@ repos: args: [--pytest-test-first] - repo: https://github.com/pre-commit/mirrors-mypy - rev: d2823d321df3af8f878f7ee3414dc94d037145b9 # frozen: v2.1.0 + rev: 41e691678310dfd3833f7ab4e180ddb014310356 # frozen: v2.3.0 hooks: - id: mypy additional_dependencies: [numpy] @@ -34,7 +34,7 @@ repos: - id: yamlfmt # Codespell for spell checking - repo: https://github.com/codespell-project/codespell - rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 + rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3 hooks: - id: codespell additional_dependencies: From 393ec92fd75e4b62b0a4682f09af6b09fccce127 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:09:13 -0400 Subject: [PATCH 24/29] Update github action versions --- .github/workflows/check.yml | 6 +++--- .github/workflows/python-publish.yml | 6 +++--- .github/workflows/unit_tests.yml | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c7047ca..f301c44 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,11 +19,11 @@ jobs: working-directory: . steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: python-version: "3.12" enable-cache: true @@ -33,7 +33,7 @@ jobs: run: uv sync --group check - name: Run Ruff linter - uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0 + uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89b # v4.1.0 with: args: "check --output-format=github" diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 7583831..5aebffd 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -22,17 +22,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: python-version: "3.12" - name: Build package run: uv build - name: Publish package - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 66bdd49..3b1df3c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -34,12 +34,11 @@ jobs: working-directory: . steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # use github python action instead of uv to take advantage of caching - name: Set up Python ${{ matrix.python }} - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 with: python-version: ${{ matrix.python }} enable-cache: true @@ -59,7 +58,7 @@ jobs: if: ${{ matrix.python == env.COV_PYTHON_VERSION }} - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} if: ${{ matrix.python == env.COV_PYTHON_VERSION }} From 5b7b97a51643385e37e5b8651ff18ddd085f1db0 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:21:59 -0400 Subject: [PATCH 25/29] Update with fixes flagged by latest version of ruff --- src/undate/converters/base.py | 6 ++-- .../calendars/gregorian/converter.py | 2 +- .../converters/calendars/hebrew/converter.py | 2 +- .../converters/calendars/islamic/converter.py | 4 +-- src/undate/date.py | 4 +-- src/undate/interval.py | 4 +-- src/undate/undate.py | 2 +- tests/test_undate.py | 36 +++++++++---------- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/undate/converters/base.py b/src/undate/converters/base.py index 3845311..044f31a 100644 --- a/src/undate/converters/base.py +++ b/src/undate/converters/base.py @@ -158,9 +158,9 @@ class BaseCalendarConverter(BaseDateConverter): LEAP_YEAR: int # minimum year for this calendar, if there is one - MIN_YEAR: None | int = None + MIN_YEAR: int | None = None # maximum year for this calendar, if there is one - MAX_YEAR: None | int = None + MAX_YEAR: int | None = None def min_month(self) -> int: """Smallest numeric month for this calendar.""" @@ -195,7 +195,7 @@ def days_in_year(self, year: int) -> int: # add 1 because the difference doesn't include the end point return (year_end - year_start).days + 1 - def representative_years(self, years: None | list[int] = None) -> list[int]: + def representative_years(self, years: list[int] | None = None) -> list[int]: """Returns a list of representative years within the specified list. Result should include one for each type of variant year for this calendar (e.g., leap year and non-leap year). If no years are specified, diff --git a/src/undate/converters/calendars/gregorian/converter.py b/src/undate/converters/calendars/gregorian/converter.py index 31bf05d..8effedf 100644 --- a/src/undate/converters/calendars/gregorian/converter.py +++ b/src/undate/converters/calendars/gregorian/converter.py @@ -53,7 +53,7 @@ def max_day(self, year: int, month: int) -> int: return max_day - def representative_years(self, years: None | list[int] = None) -> list[int]: + def representative_years(self, years: list[int] | None = None) -> list[int]: """Takes a list of years and returns a subset with one leap year and one non-leap year. If no years are specified, returns a known leap year and non-leap year. """ diff --git a/src/undate/converters/calendars/hebrew/converter.py b/src/undate/converters/calendars/hebrew/converter.py index 901c8a3..ac46e7d 100644 --- a/src/undate/converters/calendars/hebrew/converter.py +++ b/src/undate/converters/calendars/hebrew/converter.py @@ -59,7 +59,7 @@ def days_in_year(self, year: int) -> int: """the number of days in the specified year for this calendar""" return int(hebrew.year_days(year)) - def representative_years(self, years: None | list[int] = None) -> list[int]: + def representative_years(self, years: list[int] | None = None) -> list[int]: """Takes a list of years and returns a subset with all possible variations in number of days. If no years are specified, returns ... """ diff --git a/src/undate/converters/calendars/islamic/converter.py b/src/undate/converters/calendars/islamic/converter.py index f0962fc..c33c2b7 100644 --- a/src/undate/converters/calendars/islamic/converter.py +++ b/src/undate/converters/calendars/islamic/converter.py @@ -25,7 +25,7 @@ class IslamicDateConverter(BaseCalendarConverter): LEAP_YEAR: int = 1458 # minimum year for islamic calendar is 1 AH, does not go negative - MIN_YEAR: None | int = 1 + MIN_YEAR: int | None = 1 # convertdate gives a month 34 for numpy max year 2.5^16, so scale it back a bit MAX_YEAR = int(2.5e12) @@ -44,7 +44,7 @@ def max_month(self, year: int) -> int: """maximum numeric month for this calendar""" return 12 - def representative_years(self, years: None | list[int] = None) -> list[int]: + def representative_years(self, years: list[int] | None = None) -> list[int]: """Takes a list of years and returns a subset with one leap year and one non-leap year. If no years are specified, returns a known leap year and non-leap year. """ diff --git a/src/undate/date.py b/src/undate/date.py index 9ef3da3..706a7fe 100644 --- a/src/undate/date.py +++ b/src/undate/date.py @@ -1,5 +1,5 @@ import operator -from collections.abc import Iterable +from collections.abc import Iterator from dataclasses import dataclass, replace from enum import IntEnum @@ -49,7 +49,7 @@ def __post_init__(self): f"Lower value ({self.lower}) must be less than upper ({self.upper})" ) - def __iter__(self) -> Iterable: + def __iter__(self) -> Iterator: # yield all integers in range from lower to upper, inclusive yield from range(self.lower, self.upper + 1) diff --git a/src/undate/interval.py b/src/undate/interval.py index 774cc89..8a88274 100644 --- a/src/undate/interval.py +++ b/src/undate/interval.py @@ -145,9 +145,9 @@ def __contains__(self, other: object) -> bool: try: other = Undate.to_undate(other) other_latest = other_earliest = other - except TypeError: + except TypeError as err: # if conversion fails, then we don't support comparison - raise + raise TypeError(f"Comparison not supported with {other}") from err # if either bound of the current interval is None, # then it is an open interval and we don't need to check the other value. diff --git a/src/undate/undate.py b/src/undate/undate.py index 7128084..30f5dc2 100644 --- a/src/undate/undate.py +++ b/src/undate/undate.py @@ -40,7 +40,7 @@ def get_converter(calendar) -> BaseCalendarConverter: except KeyError as err: raise ValueError(f"Unknown calendar '{calendar}'") from err if not issubclass(converter_cls, BaseCalendarConverter): - raise ValueError( + raise TypeError( f"Requested converter '{calendar.value.title()}' is not a CalendarConverter" ) return converter_cls() diff --git a/tests/test_undate.py b/tests/test_undate.py index 3d65667..7725af9 100644 --- a/tests/test_undate.py +++ b/tests/test_undate.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +import datetime from enum import auto from unittest import mock @@ -180,11 +180,11 @@ def test_invalid_date(self): Undate(1990, 22) def test_to_undate(self): - undate_from_date = Undate.to_undate(date(2001, 3, 5)) + undate_from_date = Undate.to_undate(datetime.date(2001, 3, 5)) assert isinstance(undate_from_date, Undate) assert undate_from_date == Undate(2001, 3, 5) - now = datetime.now() + now = datetime.datetime.now(tz=datetime.timezone.utc) undate_from_dt = Undate.to_undate(now) assert isinstance(undate_from_dt, Undate) assert undate_from_dt == Undate(now.year, now.month, now.day) @@ -266,13 +266,13 @@ def test_eq(self): def test_eq_datetime_date(self): # support comparisons with datetime objects for full day-precision - assert Undate(2022, 10, 1) == date(2022, 10, 1) - assert Undate(2022, 10, 1) != date(2022, 10, 2) - assert Undate(1980, 10, 1) != date(2022, 10, 1) + assert Undate(2022, 10, 1) == datetime.date(2022, 10, 1) + assert Undate(2022, 10, 1) != datetime.date(2022, 10, 2) + assert Undate(1980, 10, 1) != datetime.date(2022, 10, 1) # other date precisions are not equal - assert Undate(2022) != date(2022, 10, 1) - assert Undate(2022, 10) != date(2022, 10, 1) + assert Undate(2022) != datetime.date(2022, 10, 1) + assert Undate(2022, 10) != datetime.date(2022, 10, 1) def test_not_eq(self): assert Undate(2022) != Undate(2023) @@ -301,8 +301,8 @@ def test_not_eq(self): (Undate("19XX"), Undate("20XX")), (Undate(1900, "0X"), Undate(1900, "1X")), # compare with datetime.date objects - (Undate("19XX"), date(2020, 1, 1)), - (Undate(1991, 1), date(1992, 3, 4)), + (Undate("19XX"), datetime.date(2020, 1, 1)), + (Undate(1991, 1), datetime.date(1992, 3, 4)), ] @pytest.mark.parametrize("earlier,later", testdata_lt_gt) @@ -318,7 +318,7 @@ def test_lt(self, earlier, later): (Undate(1991, 1), Undate(1991, 1)), (Undate(1492, 5, 3), Undate(1492, 5, 3)), # compare with datetime.date also - (Undate(1492, 5, 3), date(1492, 5, 3)), + (Undate(1492, 5, 3), datetime.date(1492, 5, 3)), ] ) @@ -327,8 +327,8 @@ def test_lt_when_eq(self): assert not Undate(1900) > Undate(1900) assert not Undate(1900) < Undate(1900) # same for datetime.date - assert not Undate(1903, 1, 5) < date(1903, 1, 5) - assert not Undate(1903, 1, 5) > date(1903, 1, 5) + assert not Undate(1903, 1, 5) < datetime.date(1903, 1, 5) + assert not Undate(1903, 1, 5) > datetime.date(1903, 1, 5) @pytest.mark.parametrize("earlier,later", testdata_lte_gte) def test_lte(self, earlier, later): @@ -376,8 +376,8 @@ def test_lt_notimplemented(self): (Undate(2022, 12, 31), Undate(2022)), (Undate(2022, 6, 15), Undate(2022, 6)), # support contains with datetime.date - (date(2022, 6, 1), Undate(2022)), - (date(2022, 6, 1), Undate(2022, 6)), + (datetime.date(2022, 6, 1), Undate(2022)), + (datetime.date(2022, 6, 1), Undate(2022, 6)), ] @pytest.mark.parametrize("date1,date2", testdata_contains) @@ -393,8 +393,8 @@ def test_contains(self, date1, date2): (Undate(1980), Undate(2020, 6)), (Undate(1980, 6), Undate(2020, 6)), # support contains with datetime.date - (date(1980, 6, 1), Undate(2022)), - (date(3001, 6, 1), Undate(2022, 6)), + (datetime.date(1980, 6, 1), Undate(2022)), + (datetime.date(3001, 6, 1), Undate(2022, 6)), # partially known dates that are similar but same precision, # so one does not contain the other (Undate("199X"), Undate("19XX")), @@ -684,6 +684,6 @@ class DummyFormatter(BaseDateConverter): # NOTE: this fails because get_converter converts the enum to title case... # can't be tested with any of the existing non-calendar converters with pytest.raises( - ValueError, match="Requested converter 'Dummy' is not a CalendarConverter" + TypeError, match="Requested converter 'Dummy' is not a CalendarConverter" ): Calendar.get_converter(BogusCalendar.DUMMY) From e16b522916511a53162ae7d515b7b53d0a9f5789 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:34:25 -0400 Subject: [PATCH 26/29] More fixes from ruff --- docs/conf.py | 2 +- examples/pgp_dates.ipynb | 7 +++---- scripts/generate_gregorian_grammar.py | 0 src/undate/converters/base.py | 2 +- src/undate/converters/calendars/hebrew/converter.py | 2 +- src/undate/converters/calendars/islamic/converter.py | 2 +- tests/test_date.py | 2 +- tests/test_interval.py | 4 ++-- 8 files changed, 10 insertions(+), 11 deletions(-) mode change 100644 => 100755 scripts/generate_gregorian_grammar.py diff --git a/docs/conf.py b/docs/conf.py index 0f16646..29957ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path().resolve())) +sys.path.insert(0, str(Path.cwd())) import undate diff --git a/examples/pgp_dates.ipynb b/examples/pgp_dates.ipynb index 65ece5e..f3819aa 100644 --- a/examples/pgp_dates.ipynb +++ b/examples/pgp_dates.ipynb @@ -1065,7 +1065,7 @@ " \"night, \",\n", " ]\n", " for mod in modifiers:\n", - " value = re.sub(mod, \"\", value, flags=re.I)\n", + " value = re.sub(mod, \"\", value, flags=re.IGNORECASE)\n", "\n", " # there are a handful of misspelled wednesdays...\n", " value = value.replace(\"Wedensday\", \"Wednesday\")\n", @@ -1073,10 +1073,9 @@ " value = value.replace(\"Thrusday\", \"Thursday\")\n", "\n", " # three Hebrew calendar dates include text \"AM\" at end; at least one AH date\n", - " if value.endswith(\" AM\") or value.endswith(\" AH\"):\n", + " if value.endswith((\" AM\", \" AH\")):\n", " value = value[:-3]\n", - " if value.endswith(\".\"): # strip off trailing period\n", - " value = value[:-1]\n", + " value = value.removesuffix(\".\")\n", "\n", " # about 62 have ordinals; strip them out\n", " value = remove_ordinals(value)\n", diff --git a/scripts/generate_gregorian_grammar.py b/scripts/generate_gregorian_grammar.py old mode 100644 new mode 100755 diff --git a/src/undate/converters/base.py b/src/undate/converters/base.py index 044f31a..33c8308 100644 --- a/src/undate/converters/base.py +++ b/src/undate/converters/base.py @@ -121,7 +121,7 @@ def available_converters(cls) -> dict[str, type["BaseDateConverter"]]: """ Dictionary of available converters keyed on name. """ - return {c.name: c for c in cls.subclasses()} # type: ignore + return {c.name: c for c in cls.subclasses()} @classmethod def subclasses(cls) -> set[type["BaseDateConverter"]]: diff --git a/src/undate/converters/calendars/hebrew/converter.py b/src/undate/converters/calendars/hebrew/converter.py index ac46e7d..5735311 100644 --- a/src/undate/converters/calendars/hebrew/converter.py +++ b/src/undate/converters/calendars/hebrew/converter.py @@ -1,4 +1,4 @@ -from convertdate import hebrew # type: ignore +from convertdate import hebrew from lark.exceptions import UnexpectedInput from undate import Undate, UndateInterval diff --git a/src/undate/converters/calendars/islamic/converter.py b/src/undate/converters/calendars/islamic/converter.py index c33c2b7..ad77e6c 100644 --- a/src/undate/converters/calendars/islamic/converter.py +++ b/src/undate/converters/calendars/islamic/converter.py @@ -1,4 +1,4 @@ -from convertdate import islamic # type: ignore +from convertdate import islamic from lark.exceptions import UnexpectedInput from undate import Undate, UndateInterval diff --git a/tests/test_date.py b/tests/test_date.py index e0eb5d7..3406d08 100644 --- a/tests/test_date.py +++ b/tests/test_date.py @@ -252,7 +252,7 @@ def test_eq(self): # not equal to equivalent undelta range assert udelt1 != udelt2 # equal to self - assert udelt1 is udelt1 + assert udelt1 is udelt1 # noqa: PLR0124 def test_lt(self): week_or_tenday = UnDelta(7, 10) diff --git a/tests/test_interval.py b/tests/test_interval.py index 828833c..9c97464 100644 --- a/tests/test_interval.py +++ b/tests/test_interval.py @@ -210,7 +210,7 @@ def test_contains(self): decade1990s = UndateInterval(Undate(1990), Undate(1999)) # an interval DOES contain itself for interval in [century11th, century20th, decade1990s]: - assert interval in interval + assert interval in interval # noqa: PLR0124 # checking if an interval is within another interval assert decade1990s in century20th @@ -252,4 +252,4 @@ def test_contains(self): assert decade1990s in whenever # NOTE: an interval contains itself or an equivalent interval, # but that may not make sense for open intervals... - assert whenever in whenever + assert whenever in whenever # noqa: PLR0124 From cbe41cc2086e3c5bfa44a9d49c4a50e0123fa6c4 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:41:50 -0400 Subject: [PATCH 27/29] Updates flagged by coderabbit --- .github/workflows/unit_tests.yml | 4 +++- README.md | 20 +++++++++----------- docs/example_usage.rst | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 3b1df3c..df80b81 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -13,8 +13,10 @@ on: branches: - "**" paths: - - "undate/**" + - "src/undate/**" - "tests/**" + - "pyproject.toml" + - "uv.lock" pull_request: branches: - "**" diff --git a/README.md b/README.md index 28fa2db..e35c2d0 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ > [!NOTE] > This is beta software; it is still in development and not fully feature complete. If you use it, please let us know and share your feedback. - Currently `undate` supports parsing, formatting, and reasoning with dates in varying precision and calendars; dates with different precision and from different original calendars can be used together. Supported formats include: + - portions of **EDTF** (Extended Date Time Format) - **ISO8601** - parsing and calendar conversion for dates in **Hebrew Anno Mundi** and **Islamic Hijri** calendars @@ -63,10 +63,10 @@ november = Undate(2000, 11) year2k = Undate(2000) partially_known_year = Undate("19XX") -[str(d) for d in [november7, november, year2k, partially_known_year]] +print([str(d) for d in [november7, november, year2k, partially_known_year]]) # [‘2000-11-07’, ‘2000-11’, ‘2000’, ‘19XX’] -Undate.parse("Rajab 495", "Islamic") +print(repr(Undate.parse("Rajab 495", "Islamic"))) # undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") ``` @@ -80,23 +80,21 @@ For instructions on setting up for local development, see [Developer Notes](DEVE See [Contributors](CONTRIBUTORS.md) for more detailed information about contributors. - ## Publications & Presentations -* Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. “[Undate: Humanistic Dates for Computation](https://doi.org/10.1017/chr.2025.10006).” Software paper published in _Computational Humanities Research_, August 5, 2025. -* Rebecca Sutton Koeser. “[Undate: Computing with Uncertain and Partially-Unknown Dates](https://doi.org/10.5281/zenodo.17253974).” Computational notebook presented at USRSE’25. October 6, 2025. -* Rebecca Sutton Koeser. “[Undate in Action](https://rlskoeser.github.io/undate-in-action/).” Presentation at [Digital Humanities Tech Symposium](https://dh-tech.github.io/2025/06/04/digital-humanities-tech-symposium-agenda/), DH2025. July 14, 2025. -* Rebecca Sutton Koeser. 2025. “[Undate in Action](https://doi.org/10.63744/SFtXXpIE4ERh).” In Digital Humanities Tech Symposium 2025—Anthology of Computers and the Humanities, edited by Julia Damerow and Rebecca Sutton Koeser, vol. 2. -* Rebecca Sutton Koeser. [Join me for a DHTech hackathon? It’s an un-date!](https://dh-tech.github.io/blog/2023/02/09/hackathon-undate/) DHTech, February 9, 2023. +- Rebecca Sutton Koeser, Julia Damerow, Robert Casties, and Cole Crawford. “[Undate: Humanistic Dates for Computation](https://doi.org/10.1017/chr.2025.10006).” Software paper published in _Computational Humanities Research_, August 5, 2025. +- Rebecca Sutton Koeser. “[Undate: Computing with Uncertain and Partially-Unknown Dates](https://doi.org/10.5281/zenodo.17253974).” Computational notebook presented at USRSE’25. October 6, 2025. +- Rebecca Sutton Koeser. “[Undate in Action](https://rlskoeser.github.io/undate-in-action/).” Presentation at [Digital Humanities Tech Symposium](https://dh-tech.github.io/2025/06/04/digital-humanities-tech-symposium-agenda/), DH2025. July 14, 2025. +- Rebecca Sutton Koeser. 2025. “[Undate in Action](https://doi.org/10.63744/SFtXXpIE4ERh).” In Digital Humanities Tech Symposium 2025—Anthology of Computers and the Humanities, edited by Julia Damerow and Rebecca Sutton Koeser, vol. 2. +- Rebecca Sutton Koeser. [Join me for a DHTech hackathon? It’s an un-date!](https://dh-tech.github.io/blog/2023/02/09/hackathon-undate/) DHTech, February 9, 2023. ## Related Projects - ISO8601 date support and dates with unknown years, including duration, adapted from [Shakespeare and Company Project](https://shakespeareandco.princeton.edu/) ([codebase](https://github.com/Princeton-CDH/mep-django)) -- Parsing and calendar conversion for Hebrew Anno Mundi and Islamic Hijri calendars adapted from [Princeton Geniza Project (PGP)](https://geniza.princeton.edu/) ([codebase](https://github.com/Princeton-CDH/geniza/)); improved and verified with data and logic from the [Islamic Scientific Manuscripts Initiative (ISMI)](https://ismi.mpiwg-berlin.mpg.de/) +- Parsing and calendar conversion for Hebrew Anno Mundi and Islamic Hijri calendars adapted from [Princeton Geniza Project (PGP)](https://geniza.princeton.edu/) ([codebase](https://github.com/Princeton-CDH/geniza/)); improved and verified with data and logic from the [Islamic Scientific Manuscripts Initiative (ISMI)](https://ismi.mpiwg-berlin.mpg.de/) - Parsing for dates in African languages inspired by work on and partially checked against data from [MasakhaNER](https://github.com/masakhane-io/masakhane-ner) - Parsing and calendar conversion for Christian liturgical holidays adapted from work on [Hale/Eliot Letters project](https://cdh.princeton.edu/projects/haleeliot-letters/) - ## License This software is licensed under the [Apache 2.0 License](LICENSE.md). diff --git a/docs/example_usage.rst b/docs/example_usage.rst index 0b3f850..c6c501b 100644 --- a/docs/example_usage.rst +++ b/docs/example_usage.rst @@ -86,7 +86,7 @@ can also compare with python ``datetime.date`` objects. jan2001 = date(2001, 1, 1) - print(f"{november7_2020!s:>10} before {november_2001!s:<10} ? {yes_no[november7_2020 > november_2001]}") + print(f"{november7_2020!s:>10} before {november_2001!s:<10} ? {yes_no[november7_2020 < november_2001]}") print(f"{year2k!s:>10} after {ad100!s:<10} ? {yes_no[year2k > ad100]}") print(f"{year2k!s:>10} after {jan2001} ? {yes_no[year2k > jan2001]}") @@ -114,8 +114,8 @@ and latest date as part of the range. century19 = UndateInterval(Undate(1801), Undate(1900), label="19th century") century20 = UndateInterval(Undate(1901), Undate(2000), label="20th century") - before2000 = UndateInterval(latest=Undate(2000)) # before 2000 - after1900 = UndateInterval(Undate(1900)) # after 1900 + before2000 = UndateInterval(latest=Undate(1999)) # before 2000 + after1900 = UndateInterval(Undate(1901)) # after 1900 for interval in [century19, century20, before2000, after1900]: print(f"{repr(interval)}\n{interval}\n") @@ -199,23 +199,23 @@ comparison across dates from different calendars. .. pyodide:: tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew") rajab495 = Undate.parse("Rajab 495", "Islamic") - y2k = Undate.parse("2001", "EDTF") + y2k1 = Undate.parse("2001", "EDTF") - for d in [tammuz4816, rajab495, y2k]: + for d in [tammuz4816, rajab495, y2k1]: print(f"{d!s:<10} {repr(d)}") .. pyodide:: print("Earliest Gregorian equivalent:") - for d in [rajab495, tammuz4816, y2k]: + for d in [rajab495, tammuz4816, y2k1]: print(f" {str(d):<20} earliest: {d.earliest}") print("\nPrecision:") - for d in [rajab495, tammuz4816, y2k]: + for d in [rajab495, tammuz4816, y2k1]: print(f" {str(d):<20} precision: {d.precision}") .. pyodide:: print("Sorted by Gregorian date:") - for d in sorted([rajab495, tammuz4816, y2k]): + for d in sorted([rajab495, tammuz4816, y2k1]): print(f" {repr(d)}") Try it yourself From 3bd0a3b85e08c9fd8cce2df62d9e535369705ae4 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:44:22 -0400 Subject: [PATCH 28/29] Add mypy flag to work around missing types --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f301c44..7945749 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -49,7 +49,7 @@ jobs: # check types with mypy - name: Check types in python src directory; install needed types - run: uv run mypy --install-types --non-interactive src + run: uv run mypy --install-types --non-interactive src --follow-untyped-imports # use treon to make sure that example notebooks run - name: Check jupyter notebooks with treon From 0ee5d8c5394073ef674388c114bccc2a85dda1c1 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 30 Jul 2026 10:47:40 -0400 Subject: [PATCH 29/29] Try testing against py3.14 --- .github/workflows/unit_tests.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index df80b81..d0a1c31 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.10", "3.11", "3.12", "3.13"] + python: ["3.10", "3.11", "3.12", "3.13", "3.14"] defaults: run: working-directory: . diff --git a/pyproject.toml b/pyproject.toml index 23fb665..3a05fa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: OS Independent",