././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8343036 isodate-0.7.0/0000755000076600000240000000000014701114064012554 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1728354355.817984 isodate-0.7.0/.github/0000755000076600000240000000000014701114064014114 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8234704 isodate-0.7.0/.github/workflows/0000755000076600000240000000000014701114064016151 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/.github/workflows/lint.yml0000644000076600000240000000041314701112300017627 0ustar00uqgweisstaffname: Lint on: [push, pull_request, workflow_dispatch] jobs: lint: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - uses: pre-commit/action@v3.0.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/.github/workflows/test.yml0000644000076600000240000000215614701112300017646 0ustar00uqgweisstaffname: Test on: [push, pull_request, workflow_dispatch] env: FORCE_COLOR: 1 jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ["pypy-3.9", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, macos-latest, windows-latest] include: # Include new variables for Codecov - { codecov-flag: GHA_Ubuntu, os: ubuntu-latest } - { codecov-flag: GHA_macOS, os: macos-latest } - { codecov-flag: GHA_Windows, os: windows-latest } steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install -U pip wheel tox - name: Tox tests run: | tox -e py - name: Upload coverage uses: codecov/codecov-action@v4 with: flags: ${{ matrix.codecov-flag }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/.gitignore0000644000076600000240000000025314701112300014533 0ustar00uqgweisstaff*.pyc isodate.egg-info build/ .installed.cfg .pydevproject .settings .tox bin develop-eggs dist parts .coverage coverage.xml htmlcov src/isodate/version.py .eggs .vscode/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/.pre-commit-config.yaml0000644000076600000240000000112114701112300017017 0ustar00uqgweisstaffrepos: - repo: https://github.com/asottile/pyupgrade rev: v3.17.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: - id: flake8 args: - "--max-line-length=88" - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-merge-conflict - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353051.0 isodate-0.7.0/CHANGES.txt0000644000076600000240000000537514701111433014374 0ustar00uqgweisstaff CHANGES ======= 0.7.1 (unreleased) ------------------ - no changes yet 0.7.0 (2024-10-08) ------------------ - drop end of life python versions - Don't match garbage characters at the end of parsed strings #16 (Gabriel de Perthuis) Potentially breaking changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fractional seconds are cut off to microseconds (always round down) - Allow control over return type of parse_duration #64 (Felix Claessen) 0.6.1 (2021-12-13) ------------------ - support python 3.10 (Hugo van Kemenade) - last version to support py 2.7 0.6.0 (2017-10-13) ------------------ - support incomplete month date (Fabien Loffredo) - rely on duck typing when doing duration maths - support ':' as separator in fractional time zones (usrenmae) 0.5.4 (2015-08-06) ------------------ - Fix parsing of Periods (Fabien Bochu) - Make Duration objects hashable (Geoffrey Fairchild) - Add multiplication to duration (Reinoud Elhorst) 0.5.1 (2014-11-07) ------------------ - fixed pickling of Duration objects - raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) 0.5.0 (2014-02-23) ------------------ - ISO8601Error are subclasses of ValueError now (Michael Hrivnak) - improve compatibility across various python variants and versions - raise exceptions when using fractional years and months in date maths with durations - renamed method todatetime on Duraction objects to totimedelta 0.4.9 (2012-10-30) ------------------ - support pickling FixedOffset instances - make sure parsed fractional seconds are in microseconds - add leading zeros when formattig microseconds (Jarom Loveridge) 0.4.8 (2012-05-04) ------------------ - fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 and 3.2) 0.4.7 (2012-01-26) ------------------ - fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) 0.4.6 (2012-01-06) ------------------ - added Python 3 compatibility via 2to3 0.4.5 (2012-01-06) ------------------ - made setuptools dependency optional 0.4.4 (2011-04-16) ------------------ - Fixed formatting of microseconds for datetime objects 0.4.3 (2010-10-29) ------------------ - Fixed problem with %P formatting and fractions (supplied by David Brooks) 0.4.2 (2010-10-28) ------------------ - Implemented unary - for Duration (supplied by David Brooks) - Output fractional seconds with '%P' format. (partly supplied by David Brooks) 0.4.1 (2010-10-13) ------------------ - fixed bug in comparison between timedelta and Duration. - fixed precision problem with microseconds (reported by Tommi Virtanen) 0.4.0 (2009-02-09) ------------------ - added method to parse ISO 8601 time zone strings - added methods to create ISO 8601 conforming strings 0.3.0 (2009-1-05) ------------------ - Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639348100.0 isodate-0.7.0/LICENSE0000644000076600000240000000311114155473604013570 0ustar00uqgweisstaffCopyright (c) 2021, Hugo van Kemenade and contributors Copyright (c) 2009-2018, Gerhard Weis and contributors Copyright (c) 2009, Gerhard Weis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8339095 isodate-0.7.0/PKG-INFO0000644000076600000240000002667014701114064013664 0ustar00uqgweisstaffMetadata-Version: 2.1 Name: isodate Version: 0.7.0 Summary: An ISO 8601 date/time/duration parser and formatter Author: Gerhard Weis License: Copyright (c) 2021, Hugo van Kemenade and contributors Copyright (c) 2009-2018, Gerhard Weis and contributors Copyright (c) 2009, Gerhard Weis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: Homepage, https://github.com/gweis/isodate/ Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst License-File: LICENSE ISO 8601 date/time parser ========================= .. image:: https://travis-ci.org/gweis/isodate.svg?branch=master :target: https://travis-ci.org/gweis/isodate :alt: Travis-CI .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master :target: https://coveralls.io/r/gweis/isodate?branch=master :alt: Coveralls .. image:: https://img.shields.io/pypi/v/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: License This module implements ISO 8601 date, time and duration parsing. The implementation follows ISO8601:2004 standard, and implements only date/time representations mentioned in the standard. If something is not mentioned there, then it is treated as non existent, and not as an allowed option. For instance, ISO8601:2004 never mentions 2 digit years. So, it is not intended by this module to support 2 digit years. (while it may still be valid as ISO date, because it is not explicitly forbidden.) Another example is, when no time zone information is given for a time, then it should be interpreted as local time, and not UTC. As this module maps ISO 8601 dates/times to standard Python data types, like *date*, *time*, *datetime* and *timedelta*, it is not possible to convert all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are not allowed by the Python *date* and *datetime* classes. Additionally fractional seconds are limited to microseconds. That means if the parser finds for instance nanoseconds it will round it down to microseconds. Documentation ------------- The following parsing methods are available. * parse_time: parses an ISO 8601 time string into a *time* object * parse_date: parses an ISO 8601 date string into a *date* object * parse_datetime: parses an ISO 8601 date-time string into a *datetime* object * parse_duration: parses an ISO 8601 duration string into a *timedelta* or *Duration* object. * parse_tzinfo: parses the time zone info part of an ISO 8601 string into a *tzinfo* object. As ISO 8601 allows to define durations in years and months, and *timedelta* does not handle years and months, this module provides a *Duration* class, which can be used almost like a *timedelta* object (with some limitations). However, a *Duration* object can be converted into a *timedelta* object. There are also ISO formatting methods for all supported data types. Each *xxx_isoformat* method accepts a format parameter. The default format is always the ISO 8601 expanded format. This is the same format used by *datetime.isoformat*: * time_isoformat: Intended to create ISO time strings with default format *hh:mm:ssZ*. * date_isoformat: Intended to create ISO date strings with default format *yyyy-mm-dd*. * datetime_isoformat: Intended to create ISO date-time strings with default format *yyyy-mm-ddThh:mm:ssZ*. * duration_isoformat: Intended to create ISO duration strings with default format *PnnYnnMnnDTnnHnnMnnS*. * tz_isoformat: Intended to create ISO time zone strings with default format *hh:mm*. * strftime: A re-implementation mostly compatible with Python's *strftime*, but supports only those format strings, which can also be used for dates prior 1900. This method also understands how to format *datetime* and *Duration* instances. Installation ------------ This module can easily be installed with Python standard installation methods. Use *pip install isodate*. Limitations ----------- * The parser accepts several date/time representation which should be invalid according to ISO 8601 standard. 1. for date and time together, this parser accepts a mixture of basic and extended format. e.g. the date could be in basic format, while the time is accepted in extended format. It also allows short dates and times in date-time strings. 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of 1901-01-01. 3. negative *Duration* and *timedelta* value are not fully supported yet. Further information ------------------- The doc strings and unit tests should provide rather detailed information about the methods and their limitations. The source release provides a *setup.py* script, which can be used to run the unit tests included. Source code is available at ``_. CHANGES ======= 0.7.1 (unreleased) ------------------ - no changes yet 0.7.0 (2024-10-08) ------------------ - drop end of life python versions - Don't match garbage characters at the end of parsed strings #16 (Gabriel de Perthuis) Potentially breaking changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fractional seconds are cut off to microseconds (always round down) - Allow control over return type of parse_duration #64 (Felix Claessen) 0.6.1 (2021-12-13) ------------------ - support python 3.10 (Hugo van Kemenade) - last version to support py 2.7 0.6.0 (2017-10-13) ------------------ - support incomplete month date (Fabien Loffredo) - rely on duck typing when doing duration maths - support ':' as separator in fractional time zones (usrenmae) 0.5.4 (2015-08-06) ------------------ - Fix parsing of Periods (Fabien Bochu) - Make Duration objects hashable (Geoffrey Fairchild) - Add multiplication to duration (Reinoud Elhorst) 0.5.1 (2014-11-07) ------------------ - fixed pickling of Duration objects - raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) 0.5.0 (2014-02-23) ------------------ - ISO8601Error are subclasses of ValueError now (Michael Hrivnak) - improve compatibility across various python variants and versions - raise exceptions when using fractional years and months in date maths with durations - renamed method todatetime on Duraction objects to totimedelta 0.4.9 (2012-10-30) ------------------ - support pickling FixedOffset instances - make sure parsed fractional seconds are in microseconds - add leading zeros when formattig microseconds (Jarom Loveridge) 0.4.8 (2012-05-04) ------------------ - fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 and 3.2) 0.4.7 (2012-01-26) ------------------ - fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) 0.4.6 (2012-01-06) ------------------ - added Python 3 compatibility via 2to3 0.4.5 (2012-01-06) ------------------ - made setuptools dependency optional 0.4.4 (2011-04-16) ------------------ - Fixed formatting of microseconds for datetime objects 0.4.3 (2010-10-29) ------------------ - Fixed problem with %P formatting and fractions (supplied by David Brooks) 0.4.2 (2010-10-28) ------------------ - Implemented unary - for Duration (supplied by David Brooks) - Output fractional seconds with '%P' format. (partly supplied by David Brooks) 0.4.1 (2010-10-13) ------------------ - fixed bug in comparison between timedelta and Duration. - fixed precision problem with microseconds (reported by Tommi Virtanen) 0.4.0 (2009-02-09) ------------------ - added method to parse ISO 8601 time zone strings - added methods to create ISO 8601 conforming strings 0.3.0 (2009-1-05) ------------------ - Initial release TODOs ===== This to do list contains some thoughts and ideas about missing features, and parts to think about, whether to implement them or not. This list is probably not complete. Missing features: ----------------- * time formatting does not allow to create fractional representations. * parser for ISO intervals. * currently microseconds are always padded to a length of 6 characters. trailing 0s should be optional Documentation: -------------- * parse_datetime: - complete documentation to show what this function allows, but ISO forbids. and vice verse. - support other separators between date and time than 'T' * parse_date: - yeardigits should be always greater than 4 - dates before 0001-01-01 are not supported * parse_duration: - alternative formats are not fully supported due to parse_date restrictions - standard duration format is fully supported but not very restrictive. * Duration: - support fractional years and month in calculations - implement w3c order relation? (``_) - refactor to have duration mathematics only at one place. - localize __str__ method (does timedelta do this?) - when is a Duration negative? - normalize Durations. months [00-12] and years ]-inf,+inf[ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353497.0 isodate-0.7.0/README.rst0000644000076600000240000001106314701112331014237 0ustar00uqgweisstaff ISO 8601 date/time parser ========================= .. image:: https://travis-ci.org/gweis/isodate.svg?branch=master :target: https://travis-ci.org/gweis/isodate :alt: Travis-CI .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master :target: https://coveralls.io/r/gweis/isodate?branch=master :alt: Coveralls .. image:: https://img.shields.io/pypi/v/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: License This module implements ISO 8601 date, time and duration parsing. The implementation follows ISO8601:2004 standard, and implements only date/time representations mentioned in the standard. If something is not mentioned there, then it is treated as non existent, and not as an allowed option. For instance, ISO8601:2004 never mentions 2 digit years. So, it is not intended by this module to support 2 digit years. (while it may still be valid as ISO date, because it is not explicitly forbidden.) Another example is, when no time zone information is given for a time, then it should be interpreted as local time, and not UTC. As this module maps ISO 8601 dates/times to standard Python data types, like *date*, *time*, *datetime* and *timedelta*, it is not possible to convert all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are not allowed by the Python *date* and *datetime* classes. Additionally fractional seconds are limited to microseconds. That means if the parser finds for instance nanoseconds it will round it down to microseconds. Documentation ------------- The following parsing methods are available. * parse_time: parses an ISO 8601 time string into a *time* object * parse_date: parses an ISO 8601 date string into a *date* object * parse_datetime: parses an ISO 8601 date-time string into a *datetime* object * parse_duration: parses an ISO 8601 duration string into a *timedelta* or *Duration* object. * parse_tzinfo: parses the time zone info part of an ISO 8601 string into a *tzinfo* object. As ISO 8601 allows to define durations in years and months, and *timedelta* does not handle years and months, this module provides a *Duration* class, which can be used almost like a *timedelta* object (with some limitations). However, a *Duration* object can be converted into a *timedelta* object. There are also ISO formatting methods for all supported data types. Each *xxx_isoformat* method accepts a format parameter. The default format is always the ISO 8601 expanded format. This is the same format used by *datetime.isoformat*: * time_isoformat: Intended to create ISO time strings with default format *hh:mm:ssZ*. * date_isoformat: Intended to create ISO date strings with default format *yyyy-mm-dd*. * datetime_isoformat: Intended to create ISO date-time strings with default format *yyyy-mm-ddThh:mm:ssZ*. * duration_isoformat: Intended to create ISO duration strings with default format *PnnYnnMnnDTnnHnnMnnS*. * tz_isoformat: Intended to create ISO time zone strings with default format *hh:mm*. * strftime: A re-implementation mostly compatible with Python's *strftime*, but supports only those format strings, which can also be used for dates prior 1900. This method also understands how to format *datetime* and *Duration* instances. Installation ------------ This module can easily be installed with Python standard installation methods. Use *pip install isodate*. Limitations ----------- * The parser accepts several date/time representation which should be invalid according to ISO 8601 standard. 1. for date and time together, this parser accepts a mixture of basic and extended format. e.g. the date could be in basic format, while the time is accepted in extended format. It also allows short dates and times in date-time strings. 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of 1901-01-01. 3. negative *Duration* and *timedelta* value are not fully supported yet. Further information ------------------- The doc strings and unit tests should provide rather detailed information about the methods and their limitations. The source release provides a *setup.py* script, which can be used to run the unit tests included. Source code is available at ``_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728009874.0 isodate-0.7.0/TODO.txt0000644000076600000240000000254314677653222014106 0ustar00uqgweisstaff TODOs ===== This to do list contains some thoughts and ideas about missing features, and parts to think about, whether to implement them or not. This list is probably not complete. Missing features: ----------------- * time formatting does not allow to create fractional representations. * parser for ISO intervals. * currently microseconds are always padded to a length of 6 characters. trailing 0s should be optional Documentation: -------------- * parse_datetime: - complete documentation to show what this function allows, but ISO forbids. and vice verse. - support other separators between date and time than 'T' * parse_date: - yeardigits should be always greater than 4 - dates before 0001-01-01 are not supported * parse_duration: - alternative formats are not fully supported due to parse_date restrictions - standard duration format is fully supported but not very restrictive. * Duration: - support fractional years and month in calculations - implement w3c order relation? (``_) - refactor to have duration mathematics only at one place. - localize __str__ method (does timedelta do this?) - when is a Duration negative? - normalize Durations. months [00-12] and years ]-inf,+inf[ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354279.0 isodate-0.7.0/pyproject.toml0000644000076600000240000000327314701113747015504 0ustar00uqgweisstaff[project] name = "isodate" description = "An ISO 8601 date/time/duration parser and formatter" authors = [{name="Gerhard Weis"}] # keywords = license = {file = "LICENSE"} # license = {text="BSD-3-Clause"} classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet", "Topic :: Software Development :: Libraries :: Python Modules", ] # requires-python = ">3.7" dynamic = ["version", "readme"] [project.urls] Homepage = "https://github.com/gweis/isodate/" [tool.setuptools.dynamic] readme = {file=["README.rst", "CHANGES.txt", "TODO.txt"], content-type="text/x-rst"} [build-system] requires = ["setuptools", "setuptools_scm[toml]"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/isodate/version.py" fallback_version = "0.0.0.dev0" [tool.pytest.ini_options] testpaths = ["tests"] filterwarnings = [ # treat all warnings as errors "error", # ignore: # e.g.: # ignore:jsonschema.RefResolver is deprecated as of v4.18.0 ] junit_family = "xunit2" [tool.coverage.run] source_pkgs = ["isodate"] omit = ["tests/"] [tool.black] line-length = 88 [tool.isort] profile = "black" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8343523 isodate-0.7.0/setup.cfg0000644000076600000240000000004614701114064014375 0ustar00uqgweisstaff[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8185713 isodate-0.7.0/src/0000755000076600000240000000000014701114064013343 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8289063 isodate-0.7.0/src/isodate/0000755000076600000240000000000014701114064014773 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/__init__.py0000644000076600000240000000453514701112300017102 0ustar00uqgweisstaff""" Import all essential functions and constants to re-export them here for easy access. This module contains also various pre-defined ISO 8601 format strings. """ from isodate.duration import Duration from isodate.isodates import date_isoformat, parse_date from isodate.isodatetime import datetime_isoformat, parse_datetime from isodate.isoduration import duration_isoformat, parse_duration from isodate.isoerror import ISO8601Error from isodate.isostrf import ( D_ALT_BAS, D_ALT_BAS_ORD, D_ALT_EXT, D_ALT_EXT_ORD, D_DEFAULT, D_WEEK, DATE_BAS_COMPLETE, DATE_BAS_MONTH, DATE_BAS_ORD_COMPLETE, DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE, DATE_CENTURY, DATE_EXT_COMPLETE, DATE_EXT_MONTH, DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE, DATE_YEAR, DT_BAS_COMPLETE, DT_BAS_ORD_COMPLETE, DT_BAS_WEEK_COMPLETE, DT_EXT_COMPLETE, DT_EXT_ORD_COMPLETE, DT_EXT_WEEK_COMPLETE, TIME_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_HOUR, TZ_BAS, TZ_EXT, TZ_HOUR, strftime, ) from isodate.isotime import parse_time, time_isoformat from isodate.isotzinfo import parse_tzinfo, tz_isoformat from isodate.tzinfo import LOCAL, UTC, FixedOffset from isodate.version import version as __version__ __all__ = [ "parse_date", "date_isoformat", "parse_time", "time_isoformat", "parse_datetime", "datetime_isoformat", "parse_duration", "duration_isoformat", "ISO8601Error", "parse_tzinfo", "tz_isoformat", "UTC", "FixedOffset", "LOCAL", "Duration", "strftime", "DATE_BAS_COMPLETE", "DATE_BAS_ORD_COMPLETE", "DATE_BAS_WEEK", "DATE_BAS_WEEK_COMPLETE", "DATE_CENTURY", "DATE_EXT_COMPLETE", "DATE_EXT_ORD_COMPLETE", "DATE_EXT_WEEK", "DATE_EXT_WEEK_COMPLETE", "DATE_YEAR", "DATE_BAS_MONTH", "DATE_EXT_MONTH", "TIME_BAS_COMPLETE", "TIME_BAS_MINUTE", "TIME_EXT_COMPLETE", "TIME_EXT_MINUTE", "TIME_HOUR", "TZ_BAS", "TZ_EXT", "TZ_HOUR", "DT_BAS_COMPLETE", "DT_EXT_COMPLETE", "DT_BAS_ORD_COMPLETE", "DT_EXT_ORD_COMPLETE", "DT_BAS_WEEK_COMPLETE", "DT_EXT_WEEK_COMPLETE", "D_DEFAULT", "D_WEEK", "D_ALT_EXT", "D_ALT_BAS", "D_ALT_BAS_ORD", "D_ALT_EXT_ORD", "__version__", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/duration.py0000644000076600000240000002540014701112300017162 0ustar00uqgweisstaff""" This module defines a Duration class. The class Duration allows to define durations in years and months and can be used as limited replacement for timedelta objects. """ from datetime import timedelta from decimal import ROUND_FLOOR, Decimal def fquotmod(val, low, high): """ A divmod function with boundaries. """ # assumes that all the maths is done with Decimals. # divmod for Decimal uses truncate instead of floor as builtin # divmod, so we have to do it manually here. a, b = val - low, high - low div = (a / b).to_integral(ROUND_FLOOR) mod = a - div * b # if we were not using Decimal, it would look like this. # div, mod = divmod(val - low, high - low) mod += low return int(div), mod def max_days_in_month(year, month): """ Determines the number of days of a specific month in a specific year. """ if month in (1, 3, 5, 7, 8, 10, 12): return 31 if month in (4, 6, 9, 11): return 30 if ((year % 400) == 0) or ((year % 100) != 0) and ((year % 4) == 0): return 29 return 28 class Duration: """ A class which represents a duration. The difference to datetime.timedelta is, that this class handles also differences given in years and months. A Duration treats differences given in year, months separately from all other components. A Duration can be used almost like any timedelta object, however there are some restrictions: * It is not really possible to compare Durations, because it is unclear, whether a duration of 1 year is bigger than 365 days or not. * Equality is only tested between the two (year, month vs. timedelta) basic components. A Duration can also be converted into a datetime object, but this requires a start date or an end date. The algorithm to add a duration to a date is defined at http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes """ def __init__( self, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0, months=0, years=0, ): """ Initialise this Duration instance with the given parameters. """ if not isinstance(months, Decimal): months = Decimal(str(months)) if not isinstance(years, Decimal): years = Decimal(str(years)) self.months = months self.years = years self.tdelta = timedelta( days, seconds, microseconds, milliseconds, minutes, hours, weeks ) def __getstate__(self): return self.__dict__ def __setstate__(self, state): self.__dict__.update(state) def __getattr__(self, name): """ Provide direct access to attributes of included timedelta instance. """ return getattr(self.tdelta, name) def __str__(self): """ Return a string representation of this duration similar to timedelta. """ params = [] if self.years: params.append("%d years" % self.years) if self.months: fmt = "%d months" if self.months <= 1: fmt = "%d month" params.append(fmt % self.months) params.append(str(self.tdelta)) return ", ".join(params) def __repr__(self): """ Return a string suitable for repr(x) calls. """ return "%s.%s(%d, %d, %d, years=%d, months=%d)" % ( self.__class__.__module__, self.__class__.__name__, self.tdelta.days, self.tdelta.seconds, self.tdelta.microseconds, self.years, self.months, ) def __hash__(self): """ Return a hash of this instance so that it can be used in, for example, dicts and sets. """ return hash((self.tdelta, self.months, self.years)) def __neg__(self): """ A simple unary minus. Returns a new Duration instance with all it's negated. """ negduration = Duration(years=-self.years, months=-self.months) negduration.tdelta = -self.tdelta return negduration def __add__(self, other): """ Durations can be added with Duration, timedelta, date and datetime objects. """ if isinstance(other, Duration): newduration = Duration( years=self.years + other.years, months=self.months + other.months ) newduration.tdelta = self.tdelta + other.tdelta return newduration try: # try anything that looks like a date or datetime # 'other' has attributes year, month, day # and relies on 'timedelta + other' being implemented if not (float(self.years).is_integer() and float(self.months).is_integer()): raise ValueError( "fractional years or months not supported" " for date calculations" ) newmonth = other.month + self.months carry, newmonth = fquotmod(newmonth, 1, 13) newyear = other.year + self.years + carry maxdays = max_days_in_month(newyear, newmonth) if other.day > maxdays: newday = maxdays else: newday = other.day newdt = other.replace( year=int(newyear), month=int(newmonth), day=int(newday) ) # does a timedelta + date/datetime return self.tdelta + newdt except AttributeError: # other probably was not a date/datetime compatible object pass try: # try if other is a timedelta # relies on timedelta + timedelta supported newduration = Duration(years=self.years, months=self.months) newduration.tdelta = self.tdelta + other return newduration except AttributeError: # ignore ... other probably was not a timedelta compatible object pass # we have tried everything .... return a NotImplemented return NotImplemented __radd__ = __add__ def __mul__(self, other): if isinstance(other, int): newduration = Duration(years=self.years * other, months=self.months * other) newduration.tdelta = self.tdelta * other return newduration return NotImplemented __rmul__ = __mul__ def __sub__(self, other): """ It is possible to subtract Duration and timedelta objects from Duration objects. """ if isinstance(other, Duration): newduration = Duration( years=self.years - other.years, months=self.months - other.months ) newduration.tdelta = self.tdelta - other.tdelta return newduration try: # do maths with our timedelta object .... newduration = Duration(years=self.years, months=self.months) newduration.tdelta = self.tdelta - other return newduration except TypeError: # looks like timedelta - other is not implemented pass return NotImplemented def __rsub__(self, other): """ It is possible to subtract Duration objects from date, datetime and timedelta objects. TODO: there is some weird behaviour in date - timedelta ... if timedelta has seconds or microseconds set, then date - timedelta != date + (-timedelta) for now we follow this behaviour to avoid surprises when mixing timedeltas with Durations, but in case this ever changes in the stdlib we can just do: return -self + other instead of all the current code """ if isinstance(other, timedelta): tmpdur = Duration() tmpdur.tdelta = other return tmpdur - self try: # check if other behaves like a date/datetime object # does it have year, month, day and replace? if not (float(self.years).is_integer() and float(self.months).is_integer()): raise ValueError( "fractional years or months not supported" " for date calculations" ) newmonth = other.month - self.months carry, newmonth = fquotmod(newmonth, 1, 13) newyear = other.year - self.years + carry maxdays = max_days_in_month(newyear, newmonth) if other.day > maxdays: newday = maxdays else: newday = other.day newdt = other.replace( year=int(newyear), month=int(newmonth), day=int(newday) ) return newdt - self.tdelta except AttributeError: # other probably was not compatible with data/datetime pass return NotImplemented def __eq__(self, other): """ If the years, month part and the timedelta part are both equal, then the two Durations are considered equal. """ if isinstance(other, Duration): if (self.years * 12 + self.months) == ( other.years * 12 + other.months ) and self.tdelta == other.tdelta: return True return False # check if other con be compared against timedelta object # will raise an AssertionError when optimisation is off if self.years == 0 and self.months == 0: return self.tdelta == other return False def __ne__(self, other): """ If the years, month part or the timedelta part is not equal, then the two Durations are considered not equal. """ if isinstance(other, Duration): if (self.years * 12 + self.months) != ( other.years * 12 + other.months ) or self.tdelta != other.tdelta: return True return False # check if other can be compared against timedelta object # will raise an AssertionError when optimisation is off if self.years == 0 and self.months == 0: return self.tdelta != other return True def totimedelta(self, start=None, end=None): """ Convert this duration into a timedelta object. This method requires a start datetime or end datetimem, but raises an exception if both are given. """ if start is None and end is None: raise ValueError("start or end required") if start is not None and end is not None: raise ValueError("only start or end allowed") if start is not None: return (start + self) - start return end - (end - self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isodates.py0000644000076600000240000002052114701112300017147 0ustar00uqgweisstaff""" This modules provides a method to parse an ISO 8601:2004 date string to a python datetime.date instance. It supports all basic, extended and expanded formats as described in the ISO standard. The only limitations it has, are given by the Python datetime.date implementation, which does not support dates before 0001-01-01. """ import re from datetime import date, timedelta from isodate.isoerror import ISO8601Error from isodate.isostrf import DATE_EXT_COMPLETE, strftime DATE_REGEX_CACHE = {} # A dictionary to cache pre-compiled regular expressions. # A set of regular expressions is identified, by number of year digits allowed # and whether a plus/minus sign is required or not. (This option is changeable # only for 4 digit years). def build_date_regexps(yeardigits=4, expanded=False): """ Compile set of regular expressions to parse ISO dates. The expressions will be created only if they are not already in REGEX_CACHE. It is necessary to fix the number of year digits, else it is not possible to automatically distinguish between various ISO date formats. ISO 8601 allows more than 4 digit years, on prior agreement, but then a +/- sign is required (expanded format). To support +/- sign for 4 digit years, the expanded parameter needs to be set to True. """ if yeardigits != 4: expanded = True if (yeardigits, expanded) not in DATE_REGEX_CACHE: cache_entry = [] # ISO 8601 expanded DATE formats allow an arbitrary number of year # digits with a leading +/- sign. if expanded: sign = 1 else: sign = 0 def add_re(regex_text): cache_entry.append(re.compile(r"\A" + regex_text + r"\Z")) # 1. complete dates: # YYYY-MM-DD or +- YYYYYY-MM-DD... extended date format add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"-(?P[0-9]{2})-(?P[0-9]{2})" % (sign, yeardigits) ) # YYYYMMDD or +- YYYYYYMMDD... basic date format add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"(?P[0-9]{2})(?P[0-9]{2})" % (sign, yeardigits) ) # 2. complete week dates: # YYYY-Www-D or +-YYYYYY-Www-D ... extended week date add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"-W(?P[0-9]{2})-(?P[0-9]{1})" % (sign, yeardigits) ) # YYYYWwwD or +-YYYYYYWwwD ... basic week date add_re( r"(?P[+-]){%d}(?P[0-9]{%d})W" r"(?P[0-9]{2})(?P[0-9]{1})" % (sign, yeardigits) ) # 3. ordinal dates: # YYYY-DDD or +-YYYYYY-DDD ... extended format add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"-(?P[0-9]{3})" % (sign, yeardigits) ) # YYYYDDD or +-YYYYYYDDD ... basic format add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"(?P[0-9]{3})" % (sign, yeardigits) ) # 4. week dates: # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date # 4. week dates: # YYYY-Www or +-YYYYYY-Www ... extended reduced accuracy week date add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"-W(?P[0-9]{2})" % (sign, yeardigits) ) # YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date add_re( r"(?P[+-]){%d}(?P[0-9]{%d})W" r"(?P[0-9]{2})" % (sign, yeardigits) ) # 5. month dates: # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month # 5. month dates: # YYY-MM or +-YYYYYY-MM ... reduced accuracy specific month add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"-(?P[0-9]{2})" % (sign, yeardigits) ) # YYYMM or +-YYYYYYMM ... basic incomplete month date format add_re( r"(?P[+-]){%d}(?P[0-9]{%d})" r"(?P[0-9]{2})" % (sign, yeardigits) ) # 6. year dates: # YYYY or +-YYYYYY ... reduced accuracy specific year add_re(r"(?P[+-]){%d}(?P[0-9]{%d})" % (sign, yeardigits)) # 7. century dates: # YY or +-YYYY ... reduced accuracy specific century add_re(r"(?P[+-]){%d}" r"(?P[0-9]{%d})" % (sign, yeardigits - 2)) DATE_REGEX_CACHE[(yeardigits, expanded)] = cache_entry return DATE_REGEX_CACHE[(yeardigits, expanded)] def parse_date(datestring, yeardigits=4, expanded=False, defaultmonth=1, defaultday=1): """ Parse an ISO 8601 date string into a datetime.date object. As the datetime.date implementation is limited to dates starting from 0001-01-01, negative dates (BC) and year 0 can not be parsed by this method. For incomplete dates, this method chooses the first day for it. For instance if only a century is given, this method returns the 1st of January in year 1 of this century. supported formats: (expanded formats are shown with 6 digits for year) YYYYMMDD +-YYYYYYMMDD basic complete date YYYY-MM-DD +-YYYYYY-MM-DD extended complete date YYYYWwwD +-YYYYYYWwwD basic complete week date YYYY-Www-D +-YYYYYY-Www-D extended complete week date YYYYDDD +-YYYYYYDDD basic ordinal date YYYY-DDD +-YYYYYY-DDD extended ordinal date YYYYWww +-YYYYYYWww basic incomplete week date YYYY-Www +-YYYYYY-Www extended incomplete week date YYYMM +-YYYYYYMM basic incomplete month date YYY-MM +-YYYYYY-MM incomplete month date YYYY +-YYYYYY incomplete year date YY +-YYYY incomplete century date @param datestring: the ISO date string to parse @param yeardigits: how many digits are used to represent a year @param expanded: if True then +/- signs are allowed. This parameter is forced to True, if yeardigits != 4 @return: a datetime.date instance represented by datestring @raise ISO8601Error: if this function can not parse the datestring @raise ValueError: if datestring can not be represented by datetime.date """ if yeardigits != 4: expanded = True isodates = build_date_regexps(yeardigits, expanded) for pattern in isodates: match = pattern.match(datestring) if match: groups = match.groupdict() # sign, century, year, month, week, day, # FIXME: negative dates not possible with python standard types sign = (groups["sign"] == "-" and -1) or 1 if "century" in groups: return date( sign * (int(groups["century"]) * 100 + 1), defaultmonth, defaultday ) if "month" not in groups: # weekdate or ordinal date ret = date(sign * int(groups["year"]), 1, 1) if "week" in groups: isotuple = ret.isocalendar() if "day" in groups: days = int(groups["day"] or 1) else: days = 1 # if first week in year, do weeks-1 return ret + timedelta( weeks=int(groups["week"]) - (((isotuple[1] == 1) and 1) or 0), days=-isotuple[2] + days, ) elif "day" in groups: # ordinal date return ret + timedelta(days=int(groups["day"]) - 1) else: # year date return ret.replace(month=defaultmonth, day=defaultday) # year-, month-, or complete date if "day" not in groups or groups["day"] is None: day = defaultday else: day = int(groups["day"]) return date( sign * int(groups["year"]), int(groups["month"]) or defaultmonth, day ) raise ISO8601Error("Unrecognised ISO 8601 date format: %r" % datestring) def date_isoformat(tdate, format=DATE_EXT_COMPLETE, yeardigits=4): """ Format date strings. This method is just a wrapper around isodate.isostrf.strftime and uses Date-Extended-Complete as default format. """ return strftime(tdate, format, yeardigits) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isodatetime.py0000644000076600000240000000253514701112300017650 0ustar00uqgweisstaff""" This module defines a method to parse an ISO 8601:2004 date time string. For this job it uses the parse_date and parse_time methods defined in date and time module. """ from datetime import datetime from isodate.isodates import parse_date from isodate.isoerror import ISO8601Error from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT, strftime from isodate.isotime import parse_time def parse_datetime(datetimestring): """ Parses ISO 8601 date-times into datetime.datetime objects. This function uses parse_date and parse_time to do the job, so it allows more combinations of date and time representations, than the actual ISO 8601:2004 standard allows. """ try: datestring, timestring = datetimestring.split("T") except ValueError: raise ISO8601Error( "ISO 8601 time designator 'T' missing. Unable to" " parse datetime string %r" % datetimestring ) tmpdate = parse_date(datestring) tmptime = parse_time(timestring) return datetime.combine(tmpdate, tmptime) def datetime_isoformat( tdt, format=DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + TZ_EXT ): """ Format datetime strings. This method is just a wrapper around isodate.isostrf.strftime and uses Extended-Complete as default format. """ return strftime(tdt, format) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isoduration.py0000644000076600000240000001245714701112300017705 0ustar00uqgweisstaff""" This module provides an ISO 8601:2004 duration parser. It also provides a wrapper to strftime. This wrapper makes it easier to format timedelta or Duration instances as ISO conforming strings. """ import re from datetime import timedelta from decimal import Decimal from isodate.duration import Duration from isodate.isodatetime import parse_datetime from isodate.isoerror import ISO8601Error from isodate.isostrf import D_DEFAULT, strftime ISO8601_PERIOD_REGEX = re.compile( r"^(?P[+-])?" r"P(?!\b)" r"(?P[0-9]+([,.][0-9]+)?Y)?" r"(?P[0-9]+([,.][0-9]+)?M)?" r"(?P[0-9]+([,.][0-9]+)?W)?" r"(?P[0-9]+([,.][0-9]+)?D)?" r"((?PT)(?P[0-9]+([,.][0-9]+)?H)?" r"(?P[0-9]+([,.][0-9]+)?M)?" r"(?P[0-9]+([,.][0-9]+)?S)?)?$" ) # regular expression to parse ISO duration strings. def parse_duration(datestring, as_timedelta_if_possible=True): """ Parses an ISO 8601 durations into datetime.timedelta or Duration objects. If the ISO date string does not contain years or months, a timedelta instance is returned, else a Duration instance is returned. The following duration formats are supported: -PnnW duration in weeks -PnnYnnMnnDTnnHnnMnnS complete duration specification -PYYYYMMDDThhmmss basic alternative complete date format -PYYYY-MM-DDThh:mm:ss extended alternative complete date format -PYYYYDDDThhmmss basic alternative ordinal date format -PYYYY-DDDThh:mm:ss extended alternative ordinal date format The '-' is optional. Limitations: ISO standard defines some restrictions about where to use fractional numbers and which component and format combinations are allowed. This parser implementation ignores all those restrictions and returns something when it is able to find all necessary components. In detail: it does not check, whether only the last component has fractions. it allows weeks specified with all other combinations The alternative format does not support durations with years, months or days set to 0. """ if not isinstance(datestring, str): raise TypeError("Expecting a string %r" % datestring) match = ISO8601_PERIOD_REGEX.match(datestring) if not match: # try alternative format: if datestring.startswith("P"): durdt = parse_datetime(datestring[1:]) if as_timedelta_if_possible and durdt.year == 0 and durdt.month == 0: # FIXME: currently not possible in alternative format # create timedelta ret = timedelta( days=durdt.day, seconds=durdt.second, microseconds=durdt.microsecond, minutes=durdt.minute, hours=durdt.hour, ) else: # create Duration ret = Duration( days=durdt.day, seconds=durdt.second, microseconds=durdt.microsecond, minutes=durdt.minute, hours=durdt.hour, months=durdt.month, years=durdt.year, ) return ret raise ISO8601Error("Unable to parse duration string %r" % datestring) groups = match.groupdict() for key, val in groups.items(): if key not in ("separator", "sign"): if val is None: groups[key] = "0n" # print groups[key] if key in ("years", "months"): groups[key] = Decimal(groups[key][:-1].replace(",", ".")) else: # these values are passed into a timedelta object, # which works with floats. groups[key] = float(groups[key][:-1].replace(",", ".")) if as_timedelta_if_possible and groups["years"] == 0 and groups["months"] == 0: ret = timedelta( days=groups["days"], hours=groups["hours"], minutes=groups["minutes"], seconds=groups["seconds"], weeks=groups["weeks"], ) if groups["sign"] == "-": ret = timedelta(0) - ret else: ret = Duration( years=groups["years"], months=groups["months"], days=groups["days"], hours=groups["hours"], minutes=groups["minutes"], seconds=groups["seconds"], weeks=groups["weeks"], ) if groups["sign"] == "-": ret = Duration(0) - ret return ret def duration_isoformat(tduration, format=D_DEFAULT): """ Format duration strings. This method is just a wrapper around isodate.isostrf.strftime and uses P%P (D_DEFAULT) as default format. """ # TODO: implement better decision for negative Durations. # should be done in Duration class in consistent way with timedelta. if ( isinstance(tduration, Duration) and ( tduration.years < 0 or tduration.months < 0 or tduration.tdelta < timedelta(0) ) ) or (isinstance(tduration, timedelta) and (tduration < timedelta(0))): ret = "-" else: ret = "" ret += strftime(tduration, format) return ret ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1639348364.0 isodate-0.7.0/src/isodate/isoerror.py0000644000076600000240000000025014155474214017217 0ustar00uqgweisstaff""" This module defines all exception classes in the whole package. """ class ISO8601Error(ValueError): """Raised when the given ISO string can not be parsed.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isostrf.py0000644000076600000240000001557714701112300017044 0ustar00uqgweisstaff""" This module provides an alternative strftime method. The strftime method in this module allows only a subset of Python's strftime format codes, plus a few additional. It supports the full range of date values possible with standard Python date/time objects. Furthermore there are several pr-defined format strings in this module to make ease producing of ISO 8601 conforming strings. """ import re from datetime import date, timedelta from isodate.duration import Duration from isodate.isotzinfo import tz_isoformat # Date specific format strings DATE_BAS_COMPLETE = "%Y%m%d" DATE_EXT_COMPLETE = "%Y-%m-%d" DATE_BAS_WEEK_COMPLETE = "%YW%W%w" DATE_EXT_WEEK_COMPLETE = "%Y-W%W-%w" DATE_BAS_ORD_COMPLETE = "%Y%j" DATE_EXT_ORD_COMPLETE = "%Y-%j" DATE_BAS_WEEK = "%YW%W" DATE_EXT_WEEK = "%Y-W%W" DATE_BAS_MONTH = "%Y%m" DATE_EXT_MONTH = "%Y-%m" DATE_YEAR = "%Y" DATE_CENTURY = "%C" # Time specific format strings TIME_BAS_COMPLETE = "%H%M%S" TIME_EXT_COMPLETE = "%H:%M:%S" TIME_BAS_MINUTE = "%H%M" TIME_EXT_MINUTE = "%H:%M" TIME_HOUR = "%H" # Time zone formats TZ_BAS = "%z" TZ_EXT = "%Z" TZ_HOUR = "%h" # DateTime formats DT_EXT_COMPLETE = DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + TZ_EXT DT_BAS_COMPLETE = DATE_BAS_COMPLETE + "T" + TIME_BAS_COMPLETE + TZ_BAS DT_EXT_ORD_COMPLETE = DATE_EXT_ORD_COMPLETE + "T" + TIME_EXT_COMPLETE + TZ_EXT DT_BAS_ORD_COMPLETE = DATE_BAS_ORD_COMPLETE + "T" + TIME_BAS_COMPLETE + TZ_BAS DT_EXT_WEEK_COMPLETE = DATE_EXT_WEEK_COMPLETE + "T" + TIME_EXT_COMPLETE + TZ_EXT DT_BAS_WEEK_COMPLETE = DATE_BAS_WEEK_COMPLETE + "T" + TIME_BAS_COMPLETE + TZ_BAS # Duration formts D_DEFAULT = "P%P" D_WEEK = "P%p" D_ALT_EXT = "P" + DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE D_ALT_BAS = "P" + DATE_BAS_COMPLETE + "T" + TIME_BAS_COMPLETE D_ALT_EXT_ORD = "P" + DATE_EXT_ORD_COMPLETE + "T" + TIME_EXT_COMPLETE D_ALT_BAS_ORD = "P" + DATE_BAS_ORD_COMPLETE + "T" + TIME_BAS_COMPLETE STRF_DT_MAP = { "%d": lambda tdt, yds: "%02d" % tdt.day, "%f": lambda tdt, yds: "%06d" % tdt.microsecond, "%H": lambda tdt, yds: "%02d" % tdt.hour, "%j": lambda tdt, yds: "%03d" % (tdt.toordinal() - date(tdt.year, 1, 1).toordinal() + 1), "%m": lambda tdt, yds: "%02d" % tdt.month, "%M": lambda tdt, yds: "%02d" % tdt.minute, "%S": lambda tdt, yds: "%02d" % tdt.second, "%w": lambda tdt, yds: "%1d" % tdt.isoweekday(), "%W": lambda tdt, yds: "%02d" % tdt.isocalendar()[1], "%Y": lambda tdt, yds: (((yds != 4) and "+") or "") + (("%%0%dd" % yds) % tdt.year), "%C": lambda tdt, yds: (((yds != 4) and "+") or "") + (("%%0%dd" % (yds - 2)) % (tdt.year / 100)), "%h": lambda tdt, yds: tz_isoformat(tdt, "%h"), "%Z": lambda tdt, yds: tz_isoformat(tdt, "%Z"), "%z": lambda tdt, yds: tz_isoformat(tdt, "%z"), "%%": lambda tdt, yds: "%", } STRF_D_MAP = { "%d": lambda tdt, yds: "%02d" % tdt.days, "%f": lambda tdt, yds: "%06d" % tdt.microseconds, "%H": lambda tdt, yds: "%02d" % (tdt.seconds / 60 / 60), "%m": lambda tdt, yds: "%02d" % tdt.months, "%M": lambda tdt, yds: "%02d" % ((tdt.seconds / 60) % 60), "%S": lambda tdt, yds: "%02d" % (tdt.seconds % 60), "%W": lambda tdt, yds: "%02d" % (abs(tdt.days / 7)), "%Y": lambda tdt, yds: (((yds != 4) and "+") or "") + (("%%0%dd" % yds) % tdt.years), "%C": lambda tdt, yds: (((yds != 4) and "+") or "") + (("%%0%dd" % (yds - 2)) % (tdt.years / 100)), "%%": lambda tdt, yds: "%", } def _strfduration(tdt, format, yeardigits=4): """ this is the work method for timedelta and Duration instances. see strftime for more details. """ def repl(match): """ lookup format command and return corresponding replacement. """ if match.group(0) in STRF_D_MAP: return STRF_D_MAP[match.group(0)](tdt, yeardigits) elif match.group(0) == "%P": ret = [] if isinstance(tdt, Duration): if tdt.years: ret.append("%sY" % abs(tdt.years)) if tdt.months: ret.append("%sM" % abs(tdt.months)) usecs = abs( (tdt.days * 24 * 60 * 60 + tdt.seconds) * 1000000 + tdt.microseconds ) seconds, usecs = divmod(usecs, 1000000) minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if days: ret.append("%sD" % days) if hours or minutes or seconds or usecs: ret.append("T") if hours: ret.append("%sH" % hours) if minutes: ret.append("%sM" % minutes) if seconds or usecs: if usecs: ret.append(("%d.%06d" % (seconds, usecs)).rstrip("0")) else: ret.append("%d" % seconds) ret.append("S") # at least one component has to be there. return ret and "".join(ret) or "0D" elif match.group(0) == "%p": return str(abs(tdt.days // 7)) + "W" return match.group(0) return re.sub("%d|%f|%H|%m|%M|%S|%W|%Y|%C|%%|%P|%p", repl, format) def _strfdt(tdt, format, yeardigits=4): """ this is the work method for time and date instances. see strftime for more details. """ def repl(match): """ lookup format command and return corresponding replacement. """ if match.group(0) in STRF_DT_MAP: return STRF_DT_MAP[match.group(0)](tdt, yeardigits) return match.group(0) return re.sub("%d|%f|%H|%j|%m|%M|%S|%w|%W|%Y|%C|%z|%Z|%h|%%", repl, format) def strftime(tdt, format, yeardigits=4): """Directive Meaning Notes %d Day of the month as a decimal number [01,31]. %f Microsecond as a decimal number [0,999999], zero-padded on the left (1) %H Hour (24-hour clock) as a decimal number [00,23]. %j Day of the year as a decimal number [001,366]. %m Month as a decimal number [01,12]. %M Minute as a decimal number [00,59]. %S Second as a decimal number [00,61]. (3) %w Weekday as a decimal number [0(Monday),6]. %W Week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0. (4) %Y Year with century as a decimal number. [0000,9999] %C Century as a decimal number. [00,99] %z UTC offset in the form +HHMM or -HHMM (empty string if the object is naive). (5) %Z Time zone name (empty string if the object is naive). %P ISO8601 duration format. %p ISO8601 duration format in weeks. %% A literal '%' character. """ if isinstance(tdt, (timedelta, Duration)): return _strfduration(tdt, format, yeardigits) return _strfdt(tdt, format, yeardigits) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isotime.py0000644000076600000240000001265014701112300017011 0ustar00uqgweisstaff""" This modules provides a method to parse an ISO 8601:2004 time string to a Python datetime.time instance. It supports all basic and extended formats including time zone specifications as described in the ISO standard. """ import re from datetime import time from decimal import ROUND_FLOOR, Decimal from isodate.isoerror import ISO8601Error from isodate.isostrf import TIME_EXT_COMPLETE, TZ_EXT, strftime from isodate.isotzinfo import TZ_REGEX, build_tzinfo TIME_REGEX_CACHE = [] # used to cache regular expressions to parse ISO time strings. def build_time_regexps(): """ Build regular expressions to parse ISO time string. The regular expressions are compiled and stored in TIME_REGEX_CACHE for later reuse. """ if not TIME_REGEX_CACHE: # ISO 8601 time representations allow decimal fractions on least # significant time component. Command and Full Stop are both valid # fraction separators. # The letter 'T' is allowed as time designator in front of a time # expression. # Immediately after a time expression, a time zone definition is # allowed. # a TZ may be missing (local time), be a 'Z' for UTC or a string of # +-hh:mm where the ':mm' part can be skipped. # TZ information patterns: # '' # Z # +-hh:mm # +-hhmm # +-hh => # isotzinfo.TZ_REGEX def add_re(regex_text): TIME_REGEX_CACHE.append(re.compile(r"\A" + regex_text + TZ_REGEX + r"\Z")) # 1. complete time: # hh:mm:ss.ss ... extended format add_re( r"T?(?P[0-9]{2}):" r"(?P[0-9]{2}):" r"(?P[0-9]{2}" r"([,.][0-9]+)?)" ) # hhmmss.ss ... basic format add_re( r"T?(?P[0-9]{2})" r"(?P[0-9]{2})" r"(?P[0-9]{2}" r"([,.][0-9]+)?)" ) # 2. reduced accuracy: # hh:mm.mm ... extended format add_re(r"T?(?P[0-9]{2}):" r"(?P[0-9]{2}" r"([,.][0-9]+)?)") # hhmm.mm ... basic format add_re(r"T?(?P[0-9]{2})" r"(?P[0-9]{2}" r"([,.][0-9]+)?)") # hh.hh ... basic format add_re(r"T?(?P[0-9]{2}" r"([,.][0-9]+)?)") return TIME_REGEX_CACHE def parse_time(timestring): """ Parses ISO 8601 times into datetime.time objects. Following ISO 8601 formats are supported: (as decimal separator a ',' or a '.' is allowed) hhmmss.ssTZD basic complete time hh:mm:ss.ssTZD extended complete time hhmm.mmTZD basic reduced accuracy time hh:mm.mmTZD extended reduced accuracy time hh.hhTZD basic reduced accuracy time TZD is the time zone designator which can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours """ isotimes = build_time_regexps() for pattern in isotimes: match = pattern.match(timestring) if match: groups = match.groupdict() for key, value in groups.items(): if value is not None: groups[key] = value.replace(",", ".") tzinfo = build_tzinfo( groups["tzname"], groups["tzsign"], int(groups["tzhour"] or 0), int(groups["tzmin"] or 0), ) if "second" in groups: second = Decimal(groups["second"]).quantize( Decimal(".000001"), rounding=ROUND_FLOOR ) microsecond = (second - int(second)) * int(1e6) # int(...) ... no rounding # to_integral() ... rounding return time( int(groups["hour"]), int(groups["minute"]), int(second), int(microsecond.to_integral()), tzinfo, ) if "minute" in groups: minute = Decimal(groups["minute"]) second = Decimal((minute - int(minute)) * 60).quantize( Decimal(".000001"), rounding=ROUND_FLOOR ) microsecond = (second - int(second)) * int(1e6) return time( int(groups["hour"]), int(minute), int(second), int(microsecond.to_integral()), tzinfo, ) else: microsecond, second, minute = 0, 0, 0 hour = Decimal(groups["hour"]) minute = (hour - int(hour)) * 60 second = (minute - int(minute)) * 60 microsecond = (second - int(second)) * int(1e6) return time( int(hour), int(minute), int(second), int(microsecond.to_integral()), tzinfo, ) raise ISO8601Error("Unrecognised ISO 8601 time format: %r" % timestring) def time_isoformat(ttime, format=TIME_EXT_COMPLETE + TZ_EXT): """ Format time strings. This method is just a wrapper around isodate.isostrf.strftime and uses Time-Extended-Complete with extended time zone as default format. """ return strftime(ttime, format) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/isotzinfo.py0000644000076600000240000000516014701112300017362 0ustar00uqgweisstaff""" This module provides an ISO 8601:2004 time zone info parser. It offers a function to parse the time zone offset as specified by ISO 8601. """ import re from isodate.isoerror import ISO8601Error from isodate.tzinfo import UTC, ZERO, FixedOffset TZ_REGEX = ( r"(?P(Z|(?P[+-])" r"(?P[0-9]{2})(:?(?P[0-9]{2}))?)?)" ) TZ_RE = re.compile(TZ_REGEX) def build_tzinfo(tzname, tzsign="+", tzhour=0, tzmin=0): """ create a tzinfo instance according to given parameters. tzname: 'Z' ... return UTC '' | None ... return None other ... return FixedOffset """ if tzname is None or tzname == "": return None if tzname == "Z": return UTC tzsign = ((tzsign == "-") and -1) or 1 return FixedOffset(tzsign * tzhour, tzsign * tzmin, tzname) def parse_tzinfo(tzstring): """ Parses ISO 8601 time zone designators to tzinfo objects. A time zone designator can be in the following format: no designator indicates local time zone Z UTC +-hhmm basic hours and minutes +-hh:mm extended hours and minutes +-hh hours """ match = TZ_RE.match(tzstring) if match: groups = match.groupdict() return build_tzinfo( groups["tzname"], groups["tzsign"], int(groups["tzhour"] or 0), int(groups["tzmin"] or 0), ) raise ISO8601Error("%s not a valid time zone info" % tzstring) def tz_isoformat(dt, format="%Z"): """ return time zone offset ISO 8601 formatted. The various ISO formats can be chosen with the format parameter. if tzinfo is None returns '' if tzinfo is UTC returns 'Z' else the offset is rendered to the given format. format: %h ... +-HH %z ... +-HHMM %Z ... +-HH:MM """ tzinfo = dt.tzinfo if (tzinfo is None) or (tzinfo.utcoffset(dt) is None): return "" if tzinfo.utcoffset(dt) == ZERO and tzinfo.dst(dt) == ZERO: return "Z" tdelta = tzinfo.utcoffset(dt) seconds = tdelta.days * 24 * 60 * 60 + tdelta.seconds sign = ((seconds < 0) and "-") or "+" seconds = abs(seconds) minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) if hours > 99: raise OverflowError("can not handle differences > 99 hours") if format == "%Z": return "%s%02d:%02d" % (sign, hours, minutes) elif format == "%z": return "%s%02d%02d" % (sign, hours, minutes) elif format == "%h": return "%s%02d" % (sign, hours) raise ValueError('unknown format string "%s"' % format) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/src/isodate/tzinfo.py0000644000076600000240000000731014701112300016646 0ustar00uqgweisstaff""" This module provides some datetime.tzinfo implementations. All those classes are taken from the Python documentation. """ import time from datetime import timedelta, tzinfo ZERO = timedelta(0) # constant for zero time offset. class Utc(tzinfo): """UTC Universal time coordinated time zone. """ def utcoffset(self, dt): """ Return offset from UTC in minutes east of UTC, which is ZERO for UTC. """ return ZERO def tzname(self, dt): """ Return the time zone name corresponding to the datetime object dt, as a string. """ return "UTC" def dst(self, dt): """ Return the daylight saving time (DST) adjustment, in minutes east of UTC. """ return ZERO def __reduce__(self): """ When unpickling a Utc object, return the default instance below, UTC. """ return _Utc, () UTC = Utc() # the default instance for UTC. def _Utc(): """ Helper function for unpickling a Utc object. """ return UTC class FixedOffset(tzinfo): """ A class building tzinfo objects for fixed-offset time zones. Note that FixedOffset(0, 0, "UTC") or FixedOffset() is a different way to build a UTC tzinfo object. """ def __init__(self, offset_hours=0, offset_minutes=0, name="UTC"): """ Initialise an instance with time offset and name. The time offset should be positive for time zones east of UTC and negate for time zones west of UTC. """ self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) self.__name = name def utcoffset(self, dt): """ Return offset from UTC in minutes of UTC. """ return self.__offset def tzname(self, dt): """ Return the time zone name corresponding to the datetime object dt, as a string. """ return self.__name def dst(self, dt): """ Return the daylight saving time (DST) adjustment, in minutes east of UTC. """ return ZERO def __repr__(self): """ Return nicely formatted repr string. """ return "" % self.__name STDOFFSET = timedelta(seconds=-time.timezone) # locale time zone offset # calculate local daylight saving offset if any. if time.daylight: DSTOFFSET = timedelta(seconds=-time.altzone) else: DSTOFFSET = STDOFFSET DSTDIFF = DSTOFFSET - STDOFFSET # difference between local time zone and local DST time zone class LocalTimezone(tzinfo): """ A class capturing the platform's idea of local time. """ def utcoffset(self, dt): """ Return offset from UTC in minutes of UTC. """ if self._isdst(dt): return DSTOFFSET else: return STDOFFSET def dst(self, dt): """ Return daylight saving offset. """ if self._isdst(dt): return DSTDIFF else: return ZERO def tzname(self, dt): """ Return the time zone name corresponding to the datetime object dt, as a string. """ return time.tzname[self._isdst(dt)] def _isdst(self, dt): """ Returns true if DST is active for given datetime object dt. """ tt = ( dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1, ) stamp = time.mktime(tt) tt = time.localtime(stamp) return tt.tm_isdst > 0 # the default instance for local time zone. LOCAL = LocalTimezone() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354355.0 isodate-0.7.0/src/isodate/version.py0000644000076600000240000000063314701114063017033 0ustar00uqgweisstaff# file generated by setuptools_scm # don't change, don't track in version control TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple, Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '0.7.0' __version_tuple__ = version_tuple = (0, 7, 0) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8335066 isodate-0.7.0/src/isodate.egg-info/0000755000076600000240000000000014701114064016465 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354355.0 isodate-0.7.0/src/isodate.egg-info/PKG-INFO0000644000076600000240000002667014701114063017574 0ustar00uqgweisstaffMetadata-Version: 2.1 Name: isodate Version: 0.7.0 Summary: An ISO 8601 date/time/duration parser and formatter Author: Gerhard Weis License: Copyright (c) 2021, Hugo van Kemenade and contributors Copyright (c) 2009-2018, Gerhard Weis and contributors Copyright (c) 2009, Gerhard Weis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Project-URL: Homepage, https://github.com/gweis/isodate/ Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Internet Classifier: Topic :: Software Development :: Libraries :: Python Modules Description-Content-Type: text/x-rst License-File: LICENSE ISO 8601 date/time parser ========================= .. image:: https://travis-ci.org/gweis/isodate.svg?branch=master :target: https://travis-ci.org/gweis/isodate :alt: Travis-CI .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master :target: https://coveralls.io/r/gweis/isodate?branch=master :alt: Coveralls .. image:: https://img.shields.io/pypi/v/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: Latest Version .. image:: https://img.shields.io/pypi/l/isodate.svg :target: https://pypi.python.org/pypi/isodate/ :alt: License This module implements ISO 8601 date, time and duration parsing. The implementation follows ISO8601:2004 standard, and implements only date/time representations mentioned in the standard. If something is not mentioned there, then it is treated as non existent, and not as an allowed option. For instance, ISO8601:2004 never mentions 2 digit years. So, it is not intended by this module to support 2 digit years. (while it may still be valid as ISO date, because it is not explicitly forbidden.) Another example is, when no time zone information is given for a time, then it should be interpreted as local time, and not UTC. As this module maps ISO 8601 dates/times to standard Python data types, like *date*, *time*, *datetime* and *timedelta*, it is not possible to convert all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are not allowed by the Python *date* and *datetime* classes. Additionally fractional seconds are limited to microseconds. That means if the parser finds for instance nanoseconds it will round it down to microseconds. Documentation ------------- The following parsing methods are available. * parse_time: parses an ISO 8601 time string into a *time* object * parse_date: parses an ISO 8601 date string into a *date* object * parse_datetime: parses an ISO 8601 date-time string into a *datetime* object * parse_duration: parses an ISO 8601 duration string into a *timedelta* or *Duration* object. * parse_tzinfo: parses the time zone info part of an ISO 8601 string into a *tzinfo* object. As ISO 8601 allows to define durations in years and months, and *timedelta* does not handle years and months, this module provides a *Duration* class, which can be used almost like a *timedelta* object (with some limitations). However, a *Duration* object can be converted into a *timedelta* object. There are also ISO formatting methods for all supported data types. Each *xxx_isoformat* method accepts a format parameter. The default format is always the ISO 8601 expanded format. This is the same format used by *datetime.isoformat*: * time_isoformat: Intended to create ISO time strings with default format *hh:mm:ssZ*. * date_isoformat: Intended to create ISO date strings with default format *yyyy-mm-dd*. * datetime_isoformat: Intended to create ISO date-time strings with default format *yyyy-mm-ddThh:mm:ssZ*. * duration_isoformat: Intended to create ISO duration strings with default format *PnnYnnMnnDTnnHnnMnnS*. * tz_isoformat: Intended to create ISO time zone strings with default format *hh:mm*. * strftime: A re-implementation mostly compatible with Python's *strftime*, but supports only those format strings, which can also be used for dates prior 1900. This method also understands how to format *datetime* and *Duration* instances. Installation ------------ This module can easily be installed with Python standard installation methods. Use *pip install isodate*. Limitations ----------- * The parser accepts several date/time representation which should be invalid according to ISO 8601 standard. 1. for date and time together, this parser accepts a mixture of basic and extended format. e.g. the date could be in basic format, while the time is accepted in extended format. It also allows short dates and times in date-time strings. 2. For incomplete dates, the first day is chosen. e.g. 19th century results in a date of 1901-01-01. 3. negative *Duration* and *timedelta* value are not fully supported yet. Further information ------------------- The doc strings and unit tests should provide rather detailed information about the methods and their limitations. The source release provides a *setup.py* script, which can be used to run the unit tests included. Source code is available at ``_. CHANGES ======= 0.7.1 (unreleased) ------------------ - no changes yet 0.7.0 (2024-10-08) ------------------ - drop end of life python versions - Don't match garbage characters at the end of parsed strings #16 (Gabriel de Perthuis) Potentially breaking changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Fractional seconds are cut off to microseconds (always round down) - Allow control over return type of parse_duration #64 (Felix Claessen) 0.6.1 (2021-12-13) ------------------ - support python 3.10 (Hugo van Kemenade) - last version to support py 2.7 0.6.0 (2017-10-13) ------------------ - support incomplete month date (Fabien Loffredo) - rely on duck typing when doing duration maths - support ':' as separator in fractional time zones (usrenmae) 0.5.4 (2015-08-06) ------------------ - Fix parsing of Periods (Fabien Bochu) - Make Duration objects hashable (Geoffrey Fairchild) - Add multiplication to duration (Reinoud Elhorst) 0.5.1 (2014-11-07) ------------------ - fixed pickling of Duration objects - raise ISO8601Error when there is no 'T' separator in datetime strings (Adrian Coveney) 0.5.0 (2014-02-23) ------------------ - ISO8601Error are subclasses of ValueError now (Michael Hrivnak) - improve compatibility across various python variants and versions - raise exceptions when using fractional years and months in date maths with durations - renamed method todatetime on Duraction objects to totimedelta 0.4.9 (2012-10-30) ------------------ - support pickling FixedOffset instances - make sure parsed fractional seconds are in microseconds - add leading zeros when formattig microseconds (Jarom Loveridge) 0.4.8 (2012-05-04) ------------------ - fixed incompatibility of unittests with python 2.5 and 2.6 (runs fine on 2.7 and 3.2) 0.4.7 (2012-01-26) ------------------ - fixed tzinfo formatting (never pass None into tzinfo.utcoffset()) 0.4.6 (2012-01-06) ------------------ - added Python 3 compatibility via 2to3 0.4.5 (2012-01-06) ------------------ - made setuptools dependency optional 0.4.4 (2011-04-16) ------------------ - Fixed formatting of microseconds for datetime objects 0.4.3 (2010-10-29) ------------------ - Fixed problem with %P formatting and fractions (supplied by David Brooks) 0.4.2 (2010-10-28) ------------------ - Implemented unary - for Duration (supplied by David Brooks) - Output fractional seconds with '%P' format. (partly supplied by David Brooks) 0.4.1 (2010-10-13) ------------------ - fixed bug in comparison between timedelta and Duration. - fixed precision problem with microseconds (reported by Tommi Virtanen) 0.4.0 (2009-02-09) ------------------ - added method to parse ISO 8601 time zone strings - added methods to create ISO 8601 conforming strings 0.3.0 (2009-1-05) ------------------ - Initial release TODOs ===== This to do list contains some thoughts and ideas about missing features, and parts to think about, whether to implement them or not. This list is probably not complete. Missing features: ----------------- * time formatting does not allow to create fractional representations. * parser for ISO intervals. * currently microseconds are always padded to a length of 6 characters. trailing 0s should be optional Documentation: -------------- * parse_datetime: - complete documentation to show what this function allows, but ISO forbids. and vice verse. - support other separators between date and time than 'T' * parse_date: - yeardigits should be always greater than 4 - dates before 0001-01-01 are not supported * parse_duration: - alternative formats are not fully supported due to parse_date restrictions - standard duration format is fully supported but not very restrictive. * Duration: - support fractional years and month in calculations - implement w3c order relation? (``_) - refactor to have duration mathematics only at one place. - localize __str__ method (does timedelta do this?) - when is a Duration negative? - normalize Durations. months [00-12] and years ]-inf,+inf[ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354355.0 isodate-0.7.0/src/isodate.egg-info/SOURCES.txt0000644000076600000240000000125114701114063020347 0ustar00uqgweisstaff.gitignore .pre-commit-config.yaml CHANGES.txt LICENSE README.rst TODO.txt pyproject.toml tox.ini .github/workflows/lint.yml .github/workflows/test.yml src/isodate/__init__.py src/isodate/duration.py src/isodate/isodates.py src/isodate/isodatetime.py src/isodate/isoduration.py src/isodate/isoerror.py src/isodate/isostrf.py src/isodate/isotime.py src/isodate/isotzinfo.py src/isodate/tzinfo.py src/isodate/version.py src/isodate.egg-info/PKG-INFO src/isodate.egg-info/SOURCES.txt src/isodate.egg-info/dependency_links.txt src/isodate.egg-info/top_level.txt tests/test_date.py tests/test_datetime.py tests/test_duration.py tests/test_pickle.py tests/test_strf.py tests/test_time.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354355.0 isodate-0.7.0/src/isodate.egg-info/dependency_links.txt0000644000076600000240000000000114701114063022532 0ustar00uqgweisstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728354355.0 isodate-0.7.0/src/isodate.egg-info/top_level.txt0000644000076600000240000000001014701114063021205 0ustar00uqgweisstaffisodate ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1728354355.8328652 isodate-0.7.0/tests/0000755000076600000240000000000014701114064013716 5ustar00uqgweisstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_date.py0000644000076600000240000000575214701112300016244 0ustar00uqgweisstaff""" Test cases for the isodate module. """ from datetime import date import pytest from isodate import ( DATE_BAS_COMPLETE, DATE_BAS_MONTH, DATE_BAS_ORD_COMPLETE, DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE, DATE_CENTURY, DATE_EXT_COMPLETE, DATE_EXT_MONTH, DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE, DATE_YEAR, ISO8601Error, date_isoformat, parse_date, ) # the following list contains tuples of ISO date strings and the expected # result from the parse_date method. A result of None means an ISO8601Error # is expected. The test cases are grouped into dates with 4 digit years # and 6 digit years. TEST_CASES = { # yeardigits = 4 (4, "19", date(1901, 1, 1), DATE_CENTURY), (4, "1985", date(1985, 1, 1), DATE_YEAR), (4, "1985-04", date(1985, 4, 1), DATE_EXT_MONTH), (4, "198504", date(1985, 4, 1), DATE_BAS_MONTH), (4, "1985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), (4, "19850412", date(1985, 4, 12), DATE_BAS_COMPLETE), (4, "1985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), (4, "1985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), (4, "1985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), (4, "1985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), (4, "1985W15", date(1985, 4, 8), DATE_BAS_WEEK), (4, "1985-W15", date(1985, 4, 8), DATE_EXT_WEEK), (4, "1989-W15", date(1989, 4, 10), DATE_EXT_WEEK), (4, "1989-W15-5", date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE), (4, "1-W1-1", None, DATE_BAS_WEEK_COMPLETE), # yeardigits = 6 (6, "+0019", date(1901, 1, 1), DATE_CENTURY), (6, "+001985", date(1985, 1, 1), DATE_YEAR), (6, "+001985-04", date(1985, 4, 1), DATE_EXT_MONTH), (6, "+001985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), (6, "+0019850412", date(1985, 4, 12), DATE_BAS_COMPLETE), (6, "+001985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), (6, "+001985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), (6, "+001985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), (6, "+001985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), (6, "+001985W15", date(1985, 4, 8), DATE_BAS_WEEK), (6, "+001985-W15", date(1985, 4, 8), DATE_EXT_WEEK), } @pytest.mark.parametrize("yeardigits,datestring,expected,_", TEST_CASES) def test_parse(yeardigits, datestring, expected, _): if expected is None: with pytest.raises(ISO8601Error): parse_date(datestring, yeardigits) else: result = parse_date(datestring, yeardigits) assert result == expected @pytest.mark.parametrize("yeardigits, datestring, expected, format", TEST_CASES) def test_format(yeardigits, datestring, expected, format): """ Take date object and create ISO string from it. This is the reverse test to test_parse. """ if expected is None: with pytest.raises(AttributeError): date_isoformat(expected, format, yeardigits) else: assert date_isoformat(expected, format, yeardigits) == datestring ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_datetime.py0000644000076600000240000001100314701112300017105 0ustar00uqgweisstaff""" Test cases for the isodatetime module. """ from datetime import datetime import pytest from isodate import ( DATE_BAS_COMPLETE, DATE_BAS_ORD_COMPLETE, DATE_BAS_WEEK_COMPLETE, DATE_EXT_COMPLETE, DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK_COMPLETE, TIME_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_EXT_COMPLETE, TIME_EXT_MINUTE, TZ_BAS, TZ_EXT, TZ_HOUR, UTC, FixedOffset, ISO8601Error, datetime_isoformat, parse_datetime, ) # the following list contains tuples of ISO datetime strings and the expected # result from the parse_datetime method. A result of None means an ISO8601Error # is expected. TEST_CASES = [ ( "19850412T1015", datetime(1985, 4, 12, 10, 15), DATE_BAS_COMPLETE + "T" + TIME_BAS_MINUTE, "19850412T1015", ), ( "1985-04-12T10:15", datetime(1985, 4, 12, 10, 15), DATE_EXT_COMPLETE + "T" + TIME_EXT_MINUTE, "1985-04-12T10:15", ), ( "1985102T1015Z", datetime(1985, 4, 12, 10, 15, tzinfo=UTC), DATE_BAS_ORD_COMPLETE + "T" + TIME_BAS_MINUTE + TZ_BAS, "1985102T1015Z", ), ( "1985-102T10:15Z", datetime(1985, 4, 12, 10, 15, tzinfo=UTC), DATE_EXT_ORD_COMPLETE + "T" + TIME_EXT_MINUTE + TZ_EXT, "1985-102T10:15Z", ), ( "1985W155T1015+0400", datetime(1985, 4, 12, 10, 15, tzinfo=FixedOffset(4, 0, "+0400")), DATE_BAS_WEEK_COMPLETE + "T" + TIME_BAS_MINUTE + TZ_BAS, "1985W155T1015+0400", ), ( "1985-W15-5T10:15+04", datetime( 1985, 4, 12, 10, 15, tzinfo=FixedOffset(4, 0, "+0400"), ), DATE_EXT_WEEK_COMPLETE + "T" + TIME_EXT_MINUTE + TZ_HOUR, "1985-W15-5T10:15+04", ), ( "1985-W15-5T10:15-0430", datetime( 1985, 4, 12, 10, 15, tzinfo=FixedOffset(-4, -30, "-0430"), ), DATE_EXT_WEEK_COMPLETE + "T" + TIME_EXT_MINUTE + TZ_BAS, "1985-W15-5T10:15-0430", ), ( "1985-W15-5T10:15+04:45", datetime( 1985, 4, 12, 10, 15, tzinfo=FixedOffset(4, 45, "+04:45"), ), DATE_EXT_WEEK_COMPLETE + "T" + TIME_EXT_MINUTE + TZ_EXT, "1985-W15-5T10:15+04:45", ), ( "20110410T101225.123000Z", datetime(2011, 4, 10, 10, 12, 25, 123000, tzinfo=UTC), DATE_BAS_COMPLETE + "T" + TIME_BAS_COMPLETE + ".%f" + TZ_BAS, "20110410T101225.123000Z", ), ( "2012-10-12T08:29:46.069178Z", datetime(2012, 10, 12, 8, 29, 46, 69178, tzinfo=UTC), DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + ".%f" + TZ_BAS, "2012-10-12T08:29:46.069178Z", ), ( "2012-10-12T08:29:46.691780Z", datetime(2012, 10, 12, 8, 29, 46, 691780, tzinfo=UTC), DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + ".%f" + TZ_BAS, "2012-10-12T08:29:46.691780Z", ), ( "2012-10-30T08:55:22.1234567Z", datetime(2012, 10, 30, 8, 55, 22, 123456, tzinfo=UTC), DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + ".%f" + TZ_BAS, "2012-10-30T08:55:22.123456Z", ), ( "2012-10-30T08:55:22.1234561Z", datetime(2012, 10, 30, 8, 55, 22, 123456, tzinfo=UTC), DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + ".%f" + TZ_BAS, "2012-10-30T08:55:22.123456Z", ), ( "2014-08-18 14:55:22.123456Z", None, DATE_EXT_COMPLETE + "T" + TIME_EXT_COMPLETE + ".%f" + TZ_BAS, "2014-08-18T14:55:22.123456Z", ), ] @pytest.mark.parametrize("datetimestring, expected, format, output", TEST_CASES) def test_parse(datetimestring, expected, format, output): """ Parse an ISO datetime string and compare it to the expected value. """ if expected is None: with pytest.raises(ISO8601Error): parse_datetime(datetimestring) else: assert parse_datetime(datetimestring) == expected @pytest.mark.parametrize("datetimestring, expected, format, output", TEST_CASES) def test_format(datetimestring, expected, format, output): """ Take datetime object and create ISO string from it. This is the reverse test to test_parse. """ if expected is None: with pytest.raises(AttributeError): datetime_isoformat(expected, format) else: assert datetime_isoformat(expected, format) == output ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_duration.py0000644000076600000240000003753614701112300017161 0ustar00uqgweisstaff"""Test cases for the isoduration module.""" from datetime import date, datetime, timedelta import pytest from isodate import ( D_ALT_EXT, D_DEFAULT, D_WEEK, Duration, ISO8601Error, duration_isoformat, parse_duration, ) # the following list contains tuples of ISO duration strings and the expected # result from the parse_duration method. A result of None means an ISO8601Error # is expected. PARSE_TEST_CASES = ( ("P18Y9M4DT11H9M8S", Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_DEFAULT, None), ("P2W", timedelta(weeks=2), D_WEEK, None), ("P3Y6M4DT12H30M5S", Duration(4, 5, 0, 0, 30, 12, 0, 6, 3), D_DEFAULT, None), ("P23DT23H", timedelta(hours=23, days=23), D_DEFAULT, None), ("P4Y", Duration(years=4), D_DEFAULT, None), ("P1M", Duration(months=1), D_DEFAULT, None), ("PT1M", timedelta(minutes=1), D_DEFAULT, None), ("P0.5Y", Duration(years=0.5), D_DEFAULT, None), ("PT36H", timedelta(hours=36), D_DEFAULT, "P1DT12H"), ("P1DT12H", timedelta(days=1, hours=12), D_DEFAULT, None), ("+P11D", timedelta(days=11), D_DEFAULT, "P11D"), ("-P2W", timedelta(weeks=-2), D_WEEK, None), ("-P2.2W", timedelta(weeks=-2.2), D_DEFAULT, "-P15DT9H36M"), ("P1DT2H3M4S", timedelta(days=1, hours=2, minutes=3, seconds=4), D_DEFAULT, None), ("P1DT2H3M", timedelta(days=1, hours=2, minutes=3), D_DEFAULT, None), ("P1DT2H", timedelta(days=1, hours=2), D_DEFAULT, None), ("PT2H", timedelta(hours=2), D_DEFAULT, None), ("PT2.3H", timedelta(hours=2.3), D_DEFAULT, "PT2H18M"), ("PT2H3M4S", timedelta(hours=2, minutes=3, seconds=4), D_DEFAULT, None), ("PT3M4S", timedelta(minutes=3, seconds=4), D_DEFAULT, None), ("PT22S", timedelta(seconds=22), D_DEFAULT, None), ("PT22.22S", timedelta(seconds=22.22), "PT%S.%fS", "PT22.220000S"), ("-P2Y", Duration(years=-2), D_DEFAULT, None), ("-P3Y6M4DT12H30M5S", Duration(-4, -5, 0, 0, -30, -12, 0, -6, -3), D_DEFAULT, None), ( "-P1DT2H3M4S", timedelta(days=-1, hours=-2, minutes=-3, seconds=-4), D_DEFAULT, None, ), # alternative format ("P0018-09-04T11:09:08", Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_ALT_EXT, None), # 'PT000022.22', timedelta(seconds=22.22), ) # d1 d2 '+', '-', '>' # A list of test cases to test addition and subtraction between datetime and # Duration objects. # each tuple contains 2 duration strings, and a result string for addition and # one for subtraction. The last value says, if the first duration is greater # than the second. MATH_TEST_CASES = ( ( "P5Y7M1DT9H45M16.72S", "PT27M24.68S", "P5Y7M1DT10H12M41.4S", "P5Y7M1DT9H17M52.04S", None, ), ("PT28M12.73S", "PT56M29.92S", "PT1H24M42.65S", "-PT28M17.19S", False), ( "P3Y7M23DT5H25M0.33S", "PT1H1.95S", "P3Y7M23DT6H25M2.28S", "P3Y7M23DT4H24M58.38S", None, ), ( "PT1H1.95S", "P3Y7M23DT5H25M0.33S", "P3Y7M23DT6H25M2.28S", "-P3Y7M23DT4H24M58.38S", None, ), ("P1332DT55M0.33S", "PT1H1.95S", "P1332DT1H55M2.28S", "P1331DT23H54M58.38S", True), ( "PT1H1.95S", "P1332DT55M0.33S", "P1332DT1H55M2.28S", "-P1331DT23H54M58.38S", False, ), ) # A list of test cases to test addition and subtraction of date/datetime # and Duration objects. They are tested against the results of an # equal long timedelta duration. DATE_TEST_CASES = ( ( date(2008, 2, 29), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), ( date(2008, 1, 31), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), ( datetime(2008, 2, 29), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), ( datetime(2008, 1, 31), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), ( datetime(2008, 4, 21), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), ( datetime(2008, 5, 5), timedelta(days=10, hours=12, minutes=20), Duration(days=10, hours=12, minutes=20), ), (datetime(2000, 1, 1), timedelta(hours=-33), Duration(hours=-33)), ( datetime(2008, 5, 5), Duration(years=1, months=1, days=10, hours=12, minutes=20), Duration(months=13, days=10, hours=12, minutes=20), ), ( datetime(2000, 3, 30), Duration(years=1, months=1, days=10, hours=12, minutes=20), Duration(months=13, days=10, hours=12, minutes=20), ), ) # A list of test cases of addition of date/datetime and Duration. The results # are compared against a given expected result. DATE_CALC_TEST_CASES = ( (date(2000, 2, 1), Duration(years=1, months=1), date(2001, 3, 1)), (date(2000, 2, 29), Duration(years=1, months=1), date(2001, 3, 29)), (date(2000, 2, 29), Duration(years=1), date(2001, 2, 28)), (date(1996, 2, 29), Duration(years=4), date(2000, 2, 29)), (date(2096, 2, 29), Duration(years=4), date(2100, 2, 28)), (date(2000, 2, 1), Duration(years=-1, months=-1), date(1999, 1, 1)), (date(2000, 2, 29), Duration(years=-1, months=-1), date(1999, 1, 29)), (date(2000, 2, 1), Duration(years=1, months=1, days=1), date(2001, 3, 2)), (date(2000, 2, 29), Duration(years=1, months=1, days=1), date(2001, 3, 30)), (date(2000, 2, 29), Duration(years=1, days=1), date(2001, 3, 1)), (date(1996, 2, 29), Duration(years=4, days=1), date(2000, 3, 1)), (date(2096, 2, 29), Duration(years=4, days=1), date(2100, 3, 1)), (date(2000, 2, 1), Duration(years=-1, months=-1, days=-1), date(1998, 12, 31)), (date(2000, 2, 29), Duration(years=-1, months=-1, days=-1), date(1999, 1, 28)), (date(2001, 4, 1), Duration(years=-1, months=-1, days=-1), date(2000, 2, 29)), (date(2000, 4, 1), Duration(years=-1, months=-1, days=-1), date(1999, 2, 28)), ( Duration(years=1, months=2), Duration(years=0, months=0, days=1), Duration(years=1, months=2, days=1), ), (Duration(years=-1, months=-1, days=-1), date(2000, 4, 1), date(1999, 2, 28)), (Duration(years=1, months=1, weeks=5), date(2000, 1, 30), date(2001, 4, 4)), (parse_duration("P1Y1M5W"), date(2000, 1, 30), date(2001, 4, 4)), (parse_duration("P0.5Y"), date(2000, 1, 30), None), ( Duration(years=1, months=1, hours=3), datetime(2000, 1, 30, 12, 15, 00), datetime(2001, 2, 28, 15, 15, 00), ), ( parse_duration("P1Y1MT3H"), datetime(2000, 1, 30, 12, 15, 00), datetime(2001, 2, 28, 15, 15, 00), ), ( Duration(years=1, months=2), timedelta(days=1), Duration(years=1, months=2, days=1), ), ( timedelta(days=1), Duration(years=1, months=2), Duration(years=1, months=2, days=1), ), (datetime(2008, 1, 1, 0, 2), Duration(months=1), datetime(2008, 2, 1, 0, 2)), ( datetime.strptime("200802", "%Y%M"), parse_duration("P1M"), datetime(2008, 2, 1, 0, 2), ), (datetime(2008, 2, 1), Duration(months=1), datetime(2008, 3, 1)), (datetime.strptime("200802", "%Y%m"), parse_duration("P1M"), datetime(2008, 3, 1)), # (date(2000, 1, 1), # Duration(years=1.5), # date(2001, 6, 1)), # (date(2000, 1, 1), # Duration(years=1, months=1.5), # date(2001, 2, 14)), ) # A list of test cases of multiplications of durations # are compared against a given expected result. DATE_MUL_TEST_CASES = ( (Duration(years=1, months=1), 3, Duration(years=3, months=3)), (Duration(years=1, months=1), -3, Duration(years=-3, months=-3)), (3, Duration(years=1, months=1), Duration(years=3, months=3)), (-3, Duration(years=1, months=1), Duration(years=-3, months=-3)), (5, Duration(years=2, minutes=40), Duration(years=10, hours=3, minutes=20)), (-5, Duration(years=2, minutes=40), Duration(years=-10, hours=-3, minutes=-20)), (7, Duration(years=1, months=2, weeks=40), Duration(years=8, months=2, weeks=280)), ) def test_associative(): """Adding 2 durations to a date is not associative.""" days1 = Duration(days=1) months1 = Duration(months=1) start = date(2000, 3, 30) res1 = start + days1 + months1 res2 = start + months1 + days1 assert res1 != res2 def test_typeerror(): """Test if TypError is raised with certain parameters.""" with pytest.raises(TypeError): parse_duration(date(2000, 1, 1)) with pytest.raises(TypeError): Duration(years=1) - date(2000, 1, 1) with pytest.raises(TypeError): "raise exc" - Duration(years=1) with pytest.raises(TypeError): Duration(years=1, months=1, weeks=5) + "raise exception" with pytest.raises(TypeError): "raise exception" + Duration(years=1, months=1, weeks=5) with pytest.raises(TypeError): Duration(years=1, months=1, weeks=5) * "raise exception" with pytest.raises(TypeError): "raise exception" * Duration(years=1, months=1, weeks=5) with pytest.raises(TypeError): Duration(years=1, months=1, weeks=5) * 3.14 with pytest.raises(TypeError): 3.14 * Duration(years=1, months=1, weeks=5) def test_parseerror(): """Test for unparseable duration string.""" with pytest.raises(ISO8601Error): parse_duration("T10:10:10") def test_repr(): """Test __repr__ and __str__ for Duration objects.""" dur = Duration(10, 10, years=10, months=10) assert "10 years, 10 months, 10 days, 0:00:10" == str(dur) assert "isodate.duration.Duration(10, 10, 0," " years=10, months=10)" == repr(dur) dur = Duration(months=0) assert "0:00:00" == str(dur) dur = Duration(months=1) assert "1 month, 0:00:00" == str(dur) def test_hash(): """Test __hash__ for Duration objects.""" dur1 = Duration(10, 10, years=10, months=10) dur2 = Duration(9, 9, years=9, months=9) dur3 = Duration(10, 10, years=10, months=10) assert hash(dur1) != hash(dur2) assert id(dur1) != id(dur2) assert hash(dur1) == hash(dur3) assert id(dur1) != id(dur3) durSet = set() durSet.add(dur1) durSet.add(dur2) durSet.add(dur3) assert len(durSet) == 2 def test_neg(): """Test __neg__ for Duration objects.""" assert -Duration(0) == Duration(0) assert -Duration(years=1, months=1) == Duration(years=-1, months=-1) assert -Duration(years=1, months=1) == Duration(months=-13) assert -Duration(years=1) != timedelta(days=-365) assert -timedelta(days=365) != Duration(years=-1) # FIXME: this test fails in python 3... it seems like python3 # treats a == b the same b == a # assert -timedelta(days=10) != -Duration(days=10) def test_format(): """Test various other strftime combinations.""" assert duration_isoformat(Duration(0)) == "P0D" assert duration_isoformat(-Duration(0)) == "P0D" assert duration_isoformat(Duration(seconds=10)) == "PT10S" assert duration_isoformat(Duration(years=-1, months=-1)) == "-P1Y1M" assert duration_isoformat(-Duration(years=1, months=1)) == "-P1Y1M" assert duration_isoformat(-Duration(years=-1, months=-1)) == "P1Y1M" assert duration_isoformat(-Duration(years=-1, months=-1)) == "P1Y1M" dur = Duration(years=3, months=7, days=23, hours=5, minutes=25, milliseconds=330) assert duration_isoformat(dur) == "P3Y7M23DT5H25M0.33S" assert duration_isoformat(-dur) == "-P3Y7M23DT5H25M0.33S" def test_equal(): """Test __eq__ and __ne__ methods.""" assert Duration(years=1, months=1) == Duration(years=1, months=1) assert Duration(years=1, months=1) == Duration(months=13) assert Duration(years=1, months=2) != Duration(years=1, months=1) assert Duration(years=1, months=1) != Duration(months=14) assert Duration(years=1) != timedelta(days=365) assert (Duration(years=1, months=1) != Duration(years=1, months=1)) is False assert (Duration(years=1, months=1) != Duration(months=13)) is False assert (Duration(years=1, months=2) != Duration(years=1, months=1)) is True assert (Duration(years=1, months=1) != Duration(months=14)) is True assert (Duration(years=1) != timedelta(days=365)) is True assert Duration(days=1) == timedelta(days=1) # FIXME: this test fails in python 3... it seems like python3 # treats a != b the same b != a # assert timedelta(days=1) != Duration(days=1) def test_totimedelta(): """Test conversion form Duration to timedelta.""" dur = Duration(years=1, months=2, days=10) assert dur.totimedelta(datetime(1998, 2, 25)) == timedelta(434) # leap year has one day more in february assert dur.totimedelta(datetime(2000, 2, 25)) == timedelta(435) dur = Duration(months=2) # march is longer than february, but april is shorter than # march (cause only one day difference compared to 2) assert dur.totimedelta(datetime(2000, 2, 25)) == timedelta(60) assert dur.totimedelta(datetime(2001, 2, 25)) == timedelta(59) assert dur.totimedelta(datetime(2001, 3, 25)) == timedelta(61) @pytest.mark.parametrize( "durationstring, expectation, format, altstr", PARSE_TEST_CASES, ) def test_parse(durationstring, expectation, format, altstr): """Parse an ISO duration string and compare it to the expected value.""" result = parse_duration(durationstring) assert result == expectation @pytest.mark.parametrize( "durationstring, expectation, format, altstr", PARSE_TEST_CASES, ) def test_format_parse(durationstring, expectation, format, altstr): """Take duration/timedelta object and create ISO string from it. This is the reverse test to test_parse. """ if altstr: assert duration_isoformat(expectation, format) == altstr else: # if durationstring == '-P2W': # import pdb; pdb.set_trace() assert duration_isoformat(expectation, format) == durationstring @pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) def test_add(dur1, dur2, resadd, ressub, resge): dur1 = parse_duration(dur1) dur2 = parse_duration(dur2) resadd = parse_duration(resadd) assert dur1 + dur2 == resadd @pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) def test_sub(dur1, dur2, resadd, ressub, resge): """ Test operator - (__sub__, __rsub__) """ dur1 = parse_duration(dur1) dur2 = parse_duration(dur2) ressub = parse_duration(ressub) assert dur1 - dur2 == ressub @pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) def test_ge(dur1, dur2, resadd, ressub, resge): """Test operator > and <.""" dur1 = parse_duration(dur1) dur2 = parse_duration(dur2) def dogetest(): """Test greater than.""" return dur1 > dur2 def doletest(): """Test less than.""" return dur1 < dur2 if resge is None: with pytest.raises(TypeError): dogetest() with pytest.raises(TypeError): doletest() else: assert dogetest() is resge assert doletest() is not resge @pytest.mark.parametrize("start, tdelta, duration", DATE_TEST_CASES) def test_add_date(start, tdelta, duration): assert start + tdelta == start + duration @pytest.mark.parametrize("start, tdelta, duration", DATE_TEST_CASES) def test_sub_date(start, tdelta, duration): assert start - tdelta == start - duration @pytest.mark.parametrize("start, duration, expectation", DATE_CALC_TEST_CASES) def test_calc_date(start, duration, expectation): """Test operator +.""" if expectation is None: with pytest.raises(ValueError): start + duration else: assert start + duration == expectation @pytest.mark.parametrize("operand1, operand2, expectation", DATE_MUL_TEST_CASES) def test_mul_date(operand1, operand2, expectation): """Test operator *.""" assert operand1 * operand2 == expectation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_pickle.py0000644000076600000240000000203514701112300016565 0ustar00uqgweisstaffimport pickle import isodate def test_pickle_datetime(): """Parse an ISO datetime string and compare it to the expected value.""" dti = isodate.parse_datetime("2012-10-26T09:33+00:00") for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): pikl = pickle.dumps(dti, proto) assert dti == pickle.loads(pikl), "pickle proto %d failed" % proto def test_pickle_duration(): """Pickle / unpickle duration objects.""" from isodate.duration import Duration dur = Duration() failed = [] for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): try: pikl = pickle.dumps(dur, proto) if dur != pickle.loads(pikl): raise Exception("not equal") except Exception as e: failed.append("pickle proto %d failed (%s)" % (proto, repr(e))) assert len(failed) == 0, "pickle protos failed: %s" % str(failed) def test_pickle_utc(): """isodate.UTC objects remain the same after pickling.""" assert isodate.UTC is pickle.loads(pickle.dumps(isodate.UTC)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_strf.py0000644000076600000240000000422314701112300016275 0ustar00uqgweisstaff"""Test cases for the isodate module.""" import time from datetime import datetime, timedelta import pytest from isodate import DT_EXT_COMPLETE, LOCAL, strftime, tzinfo TEST_CASES = ( ( datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE, "2012-12-25T13:30:00+10:00", ), # DST ON ( datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), DT_EXT_COMPLETE, "1999-12-25T13:30:00+11:00", ), # microseconds ( datetime(2012, 10, 12, 8, 29, 46, 69178), "%Y-%m-%dT%H:%M:%S.%f", "2012-10-12T08:29:46.069178", ), ( datetime(2012, 10, 12, 8, 29, 46, 691780), "%Y-%m-%dT%H:%M:%S.%f", "2012-10-12T08:29:46.691780", ), ) @pytest.fixture def tz_patch(monkeypatch): # local time zone mock function localtime_orig = time.localtime def localtime_mock(secs): """Mock time to fixed date. Mock time.localtime so that it always returns a time_struct with tm_dst=1 """ tt = localtime_orig(secs) # before 2000 everything is dst, after 2000 no dst. if tt.tm_year < 2000: dst = 1 else: dst = 0 tt = ( tt.tm_year, tt.tm_mon, tt.tm_mday, tt.tm_hour, tt.tm_min, tt.tm_sec, tt.tm_wday, tt.tm_yday, dst, ) return time.struct_time(tt) monkeypatch.setattr(time, "localtime", localtime_mock) # assume LOC = +10:00 monkeypatch.setattr(tzinfo, "STDOFFSET", timedelta(seconds=36000)) # assume DST = +11:00 monkeypatch.setattr(tzinfo, "DSTOFFSET", timedelta(seconds=39600)) monkeypatch.setattr(tzinfo, "DSTDIFF", tzinfo.DSTOFFSET - tzinfo.STDOFFSET) @pytest.mark.parametrize("dt, format, expectation", TEST_CASES) def test_format(tz_patch, dt, format, expectation): """Take date object and create ISO string from it. This is the reverse test to test_parse. """ if expectation is None: with pytest.raises(AttributeError): strftime(dt, format) else: assert strftime(dt, format) == expectation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tests/test_time.py0000644000076600000240000000744514701112300016266 0ustar00uqgweisstaff""" Test cases for the isotime module. """ from datetime import time import pytest from isodate import ( TIME_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_HOUR, TZ_BAS, TZ_EXT, TZ_HOUR, UTC, FixedOffset, ISO8601Error, parse_time, time_isoformat, ) # the following list contains tuples of ISO time strings and the expected # result from the parse_time method. A result of None means an ISO8601Error # is expected. TEST_CASES = [ ("232050", time(23, 20, 50), TIME_BAS_COMPLETE + TZ_BAS), ("23:20:50", time(23, 20, 50), TIME_EXT_COMPLETE + TZ_EXT), ("2320", time(23, 20), TIME_BAS_MINUTE), ("23:20", time(23, 20), TIME_EXT_MINUTE), ("23", time(23), TIME_HOUR), ("232050,5", time(23, 20, 50, 500000), None), ("23:20:50.5", time(23, 20, 50, 500000), None), # test precision ("15:33:42.123456", time(15, 33, 42, 123456), None), ("15:33:42.1234564", time(15, 33, 42, 123456), None), ("15:33:42.1234557", time(15, 33, 42, 123455), None), ( "10:59:59.9999999Z", time(10, 59, 59, 999999, tzinfo=UTC), None, ), # TIME_EXT_COMPLETE + TZ_EXT), ("2320,8", time(23, 20, 48), None), ("23:20,8", time(23, 20, 48), None), ("23,3", time(23, 18), None), ("232030Z", time(23, 20, 30, tzinfo=UTC), TIME_BAS_COMPLETE + TZ_BAS), ("2320Z", time(23, 20, tzinfo=UTC), TIME_BAS_MINUTE + TZ_BAS), ("23Z", time(23, tzinfo=UTC), TIME_HOUR + TZ_BAS), ("23:20:30Z", time(23, 20, 30, tzinfo=UTC), TIME_EXT_COMPLETE + TZ_EXT), ("23:20Z", time(23, 20, tzinfo=UTC), TIME_EXT_MINUTE + TZ_EXT), ( "152746+0100", time(15, 27, 46, tzinfo=FixedOffset(1, 0, "+0100")), TIME_BAS_COMPLETE + TZ_BAS, ), ( "152746-0500", time(15, 27, 46, tzinfo=FixedOffset(-5, 0, "-0500")), TIME_BAS_COMPLETE + TZ_BAS, ), ( "152746+01", time(15, 27, 46, tzinfo=FixedOffset(1, 0, "+01:00")), TIME_BAS_COMPLETE + TZ_HOUR, ), ( "152746-05", time(15, 27, 46, tzinfo=FixedOffset(-5, -0, "-05:00")), TIME_BAS_COMPLETE + TZ_HOUR, ), ( "15:27:46+01:00", time(15, 27, 46, tzinfo=FixedOffset(1, 0, "+01:00")), TIME_EXT_COMPLETE + TZ_EXT, ), ( "15:27:46-05:00", time(15, 27, 46, tzinfo=FixedOffset(-5, -0, "-05:00")), TIME_EXT_COMPLETE + TZ_EXT, ), ( "15:27:46+01", time(15, 27, 46, tzinfo=FixedOffset(1, 0, "+01:00")), TIME_EXT_COMPLETE + TZ_HOUR, ), ( "15:27:46-05", time(15, 27, 46, tzinfo=FixedOffset(-5, -0, "-05:00")), TIME_EXT_COMPLETE + TZ_HOUR, ), ( "15:27:46-05:30", time(15, 27, 46, tzinfo=FixedOffset(-5, -30, "-05:30")), TIME_EXT_COMPLETE + TZ_EXT, ), ( "15:27:46-0545", time(15, 27, 46, tzinfo=FixedOffset(-5, -45, "-0545")), TIME_EXT_COMPLETE + TZ_BAS, ), ("1:17:30", None, TIME_EXT_COMPLETE), ] @pytest.mark.parametrize("timestring, expectation, format", TEST_CASES) def test_parse(timestring, expectation, format): """ Parse an ISO time string and compare it to the expected value. """ if expectation is None: with pytest.raises(ISO8601Error): parse_time(timestring) else: assert parse_time(timestring) == expectation @pytest.mark.parametrize("timestring, expectation, format", TEST_CASES) def test_format(timestring, expectation, format): """ Take time object and create ISO string from it. This is the reverse test to test_parse. """ if expectation is None: with pytest.raises(AttributeError): time_isoformat(expectation, format) elif format is not None: assert time_isoformat(expectation, format) == timestring ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728353472.0 isodate-0.7.0/tox.ini0000644000076600000240000000060114701112300014053 0ustar00uqgweisstaff[tox] isolated_build = True envlist = lint py{39, 310, 311, 312, py39} [testenv] deps = pytest pytest-cov setenv = PYTHONWARNINGS = default commands = pytest # {envpython} setup.py clean --all # pytest --cov=isodate --cov-report=xml [testenv:lint] deps = pre-commit commands = pre-commit run --all-files skip_install = true passenv = PRE_COMMIT_COLOR