python-isoduration-20.11.0+git20211126.ae0bd61/0000755000175000017500000000000014600361651016345 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/.github/0000755000175000017500000000000014150264231017701 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/.github/workflows/0000755000175000017500000000000014150264231021736 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/.github/workflows/semgrep.yml0000644000175000017500000000063114150264231024123 0ustar jdgjdgname: Semgrep on: workflow_dispatch: pull_request: push: branches: [master] jobs: semgrep: name: Scan runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: returntocorp/semgrep-action@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} publishDeployment: 123 python-isoduration-20.11.0+git20211126.ae0bd61/.github/workflows/test.yml0000644000175000017500000000131514150264231023440 0ustar jdgjdgname: Test on: workflow_dispatch: pull_request: push: branches: [master] jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install Tox run: pip install tox - name: Lint run: tox -e linting - name: Test run: tox -e py - name: Upload test coverage uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} python-isoduration-20.11.0+git20211126.ae0bd61/.github/workflows/codeql-analysis.yml0000644000175000017500000000206714150264231025556 0ustar jdgjdgname: "CodeQL" on: workflow_dispatch: pull_request: push: branches: [master] schedule: - cron: '0 14 * * 2' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: ['python'] steps: - name: Checkout repository uses: actions/checkout@v2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 python-isoduration-20.11.0+git20211126.ae0bd61/.github/workflows/publish.yml0000644000175000017500000000261214150264231024130 0ustar jdgjdgname: Publish on: push: tags: - '*' jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install Tox run: pip install tox - name: Build and Publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: tox -e publish - name: Get version number id: get_version run: echo ::set-output name=version::${GITHUB_REF#refs/tags/} - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Version ${{ steps.get_version.outputs.version }} body: Please refer to the CHANGELOG file for a detailed change log. draft: true prerelease: false - name: Upload Release Asset id: upload_release_asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: dist/isoduration-${{ steps.get_version.outputs.version }}.tar.gz asset_name: isoduration-${{ steps.get_version.outputs.version }}.tar.gz asset_content_type: application/gzip python-isoduration-20.11.0+git20211126.ae0bd61/README.md0000644000175000017500000001124214150264231017620 0ustar jdgjdg# isoduration: Operations with ISO 8601 durations. [![PyPI Package](https://img.shields.io/pypi/v/isoduration?style=flat-square)](https://pypi.org/project/isoduration/) ## What is this. ISO 8601 is most commonly known as a way to exchange datetimes in textual format. A lesser known aspect of the standard is the representation of durations. They have a shape similar to this: ``` P3Y6M4DT12H30M5S ``` This string represents a duration of 3 years, 6 months, 4 days, 12 hours, 30 minutes, and 5 seconds. The state of the art of ISO 8601 duration handling in Python is more or less limited to what's offered by [`isodate`](https://pypi.org/project/isodate/). What we are trying to achieve here is to address the shortcomings of `isodate` (as described in their own [_Limitations_](https://github.com/gweis/isodate/#limitations) section), and a few of our own annoyances with their interface, such as the lack of uniformity in their handling of types, and the use of regular expressions for parsing. ## How to use it. This package revolves around the [`Duration`](src/isoduration/types.py) type. Given a ISO duration string we can produce such a type by using the `parse_duration()` function: ```py >>> from isoduration import parse_duration >>> duration = parse_duration("P3Y6M4DT12H30M5S") >>> duration.date DateDuration(years=Decimal('3'), months=Decimal('6'), days=Decimal('4'), weeks=Decimal('0')) >>> duration.time TimeDuration(hours=Decimal('12'), minutes=Decimal('30'), seconds=Decimal('5')) ``` The `date` and `time` portions of the parsed duration are just regular [dataclasses](https://docs.python.org/3/library/dataclasses.html), so their members can be accessed in a non-surprising way. Besides just parsing them, a number of additional operations are available: - Durations can be compared and negated: ```py >>> parse_duration("P3Y4D") == parse_duration("P3Y4DT0H") True >>> -parse_duration("P3Y4D") Duration(DateDuration(years=Decimal('-3'), months=Decimal('0'), days=Decimal('-4'), weeks=Decimal('0')), TimeDuration(hours=Decimal('0'), minutes=Decimal('0'), seconds=Decimal('0'))) ``` - Durations can be added to, or subtracted from, Python datetimes: ```py >>> from datetime import datetime >>> datetime(2020, 3, 15) + parse_duration("P2Y") datetime.datetime(2022, 3, 15, 0, 0) >>> datetime(2020, 3, 15) - parse_duration("P33Y1M4D") datetime.datetime(1987, 2, 11, 0, 0) ``` - Durations are hashable, so they can be used as dictionary keys or as part of sets. - Durations can be formatted back to a ISO 8601-compliant duration string: ```py >>> from isoduration import parse_duration, format_duration >>> format_duration(parse_duration("P11YT2H")) 'P11YT2H' >>> str(parse_duration("P11YT2H")) 'P11YT2H' ``` ## How to improve it. These steps, in this order, should land you in a development environment: ```sh git clone git@github.com:bolsote/isoduration.git cd isoduration/ python -m venv ve . ve/bin/activate pip install -U pip pip install -e . pip install -r requirements/dev.txt ``` Adapt to your own likings and/or needs. Testing is driven by [tox](https://tox.readthedocs.io). The output of `tox -l` and a careful read of [tox.ini](tox.ini) should get you there. ## FAQs. ### How come `P1Y != P365D`? Some years have 366 days. If it's not always the same, then it's not the same. ### Why do you create your own types, instead of somewhat shoehorning a `timedelta`? `timedelta` cannot represent certain durations, such as those involving years or months. Since it cannot represent all possible durations without dangerous arithmetic, then it must not be the right type. ### Why don't you use regular expressions to parse duration strings? [Regular expressions should only be used to parse regular languages.](https://stackoverflow.com/a/1732454) ### Why is parsing the inverse of formatting, but the converse is not true? Because this wonderful representation is not unique. ### Why do you support ``? Probably because the standard made me to. ### Why do you not support ``? Probably because the standard doesn't allow me to. ### Why is it not possible to subtract a datetime from a duration? I'm confused. ### Why should I use this over some other thing? You shouldn't do what people on the Internet tell you to do. ### Why are ISO standards so strange? Yes. ## References. - [XML Schema Part 2: Datatypes, Appendix D](https://www.w3.org/TR/xmlschema-2/#isoformats): This excitingly named document contains more details about ISO 8601 than any human should be allowed to understand. - [`isodate`](https://pypi.org/project/isodate/): The original implementation of ISO durations in Python. Worth a look. But ours is cooler. python-isoduration-20.11.0+git20211126.ae0bd61/src/0000755000175000017500000000000014150264231017130 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/0000755000175000017500000000000014150264231021470 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/0000755000175000017500000000000014150264231022764 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/exceptions.py0000644000175000017500000000130614150264231025517 0ustar jdgjdg""" Exception +- ValueError | +- DurationParsingException | | +- EmptyDuration | | +- IncorrectDesignator | | +- NoTime | | +- UnknownToken | | +- UnparseableValue | | +- InvalidFractional +- KeyError +- OutOfDesignators """ class DurationParsingException(ValueError): ... class EmptyDuration(DurationParsingException): ... class IncorrectDesignator(DurationParsingException): ... class NoTime(DurationParsingException): ... class UnknownToken(DurationParsingException): ... class UnparseableValue(DurationParsingException): ... class InvalidFractional(DurationParsingException): ... class OutOfDesignators(KeyError): ... python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/validation.py0000644000175000017500000000115214150264231025467 0ustar jdgjdgfrom isoduration.parser.exceptions import InvalidFractional from isoduration.parser.util import is_integer from isoduration.types import Duration def validate_fractional(duration: Duration) -> None: fractional_allowed = True for _, value in reversed(duration): if fractional_allowed: if not value.is_zero(): # Fractional values are only allowed in the lowest order # non-zero component. fractional_allowed = False elif not is_integer(value): raise InvalidFractional("Only the lowest order component can be fractional") python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/util.py0000644000175000017500000000164514150264231024321 0ustar jdgjdgimport decimal import re from typing import Dict from isoduration.constants import PERIOD_PREFIX, TIME_PREFIX, WEEK_PREFIX from isoduration.parser.exceptions import OutOfDesignators def is_period(ch: str) -> bool: return ch == PERIOD_PREFIX def is_time(ch: str) -> bool: return ch == TIME_PREFIX def is_week(ch: str) -> bool: return ch == WEEK_PREFIX def is_number(ch: str) -> bool: return bool(re.match(r"[+\-0-9.,eE]", ch)) def is_letter(ch: str) -> bool: return ch.isalpha() and ch.lower() != "e" def parse_designator(designators: Dict[str, str], target: str) -> str: while True: try: key, value = designators.popitem(last=False) # type: ignore except KeyError as exc: raise OutOfDesignators from exc if key == target: return value def is_integer(number: decimal.Decimal) -> bool: return number == number.to_integral_value() python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/__init__.py0000644000175000017500000000156214150264231025101 0ustar jdgjdgfrom isoduration.parser.exceptions import EmptyDuration from isoduration.parser.parsing import parse_date_duration from isoduration.parser.util import is_period from isoduration.parser.validation import validate_fractional from isoduration.types import Duration def parse_duration(duration_str: str) -> Duration: if len(duration_str) < 2: raise EmptyDuration("No duration information provided") beginning = 1 first = duration_str[beginning - 1] sign = +1 if first == "+": beginning += 1 if first == "-": sign = -1 beginning += 1 prefix = duration_str[beginning - 1] duration = duration_str[beginning:] if not is_period(prefix): raise EmptyDuration("No prefix provided") parsed_duration = parse_date_duration(duration, sign) validate_fractional(parsed_duration) return parsed_duration python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/parser/parsing.py0000644000175000017500000000767414150264231025017 0ustar jdgjdgfrom collections import OrderedDict from decimal import Decimal, InvalidOperation import arrow from isoduration.parser.exceptions import ( IncorrectDesignator, NoTime, OutOfDesignators, UnknownToken, UnparseableValue, ) from isoduration.parser.util import ( is_letter, is_number, is_time, is_week, parse_designator, ) from isoduration.types import DateDuration, Duration, TimeDuration def parse_datetime_duration(duration_str: str, sign: int) -> Duration: try: duration: arrow.Arrow = arrow.get(duration_str) except (arrow.ParserError, ValueError): raise UnparseableValue(f"Value could not be parsed as datetime: {duration_str}") return Duration( DateDuration( years=Decimal(sign * duration.year), months=Decimal(sign * duration.month), days=Decimal(sign * duration.day), ), TimeDuration( hours=Decimal(sign * duration.hour), minutes=Decimal(sign * duration.minute), seconds=Decimal(sign * duration.second), ), ) def parse_date_duration(date_str: str, sign: int) -> Duration: date_designators = OrderedDict( (("Y", "years"), ("M", "months"), ("D", "days"), ("W", "weeks")) ) duration = DateDuration() tmp_value = "" for idx, ch in enumerate(date_str): if is_time(ch): if tmp_value != "" and tmp_value == date_str[:idx]: # PYYYY-MM-DDThh:mm:ss # PYYYYMMDDThhmmss return parse_datetime_duration(date_str, sign) time_idx = idx + 1 time_str = date_str[time_idx:] if time_str == "": raise NoTime("Wanted time, no time provided") return Duration(duration, parse_time_duration(time_str, sign)) if is_letter(ch): try: key = parse_designator(date_designators, ch) value = sign * Decimal(tmp_value) except OutOfDesignators as exc: raise IncorrectDesignator( f"Wrong date designator, or designator in the wrong order: {ch}" ) from exc except InvalidOperation as exc: raise UnparseableValue( f"Value could not be parsed as decimal: {tmp_value}" ) from exc if is_week(ch) and duration != DateDuration(): raise IncorrectDesignator( "Week is incompatible with any other date designator" ) setattr(duration, key, value) tmp_value = "" continue if is_number(ch): if ch == ",": tmp_value += "." else: tmp_value += ch continue raise UnknownToken(f"Token not recognizable: {ch}") return Duration(duration, TimeDuration()) def parse_time_duration(time_str: str, sign: int) -> TimeDuration: time_designators = OrderedDict((("H", "hours"), ("M", "minutes"), ("S", "seconds"))) duration = TimeDuration() tmp_value = "" for ch in time_str: if is_letter(ch): try: key = parse_designator(time_designators, ch) value = sign * Decimal(tmp_value) except OutOfDesignators as exc: raise IncorrectDesignator( f"Wrong time designator, or designator in the wrong order: {ch}" ) from exc except InvalidOperation as exc: raise UnparseableValue( f"Value could not be parsed as decimal: {tmp_value}" ) from exc setattr(duration, key, value) tmp_value = "" continue if is_number(ch): if ch == ",": tmp_value += "." else: tmp_value += ch continue raise UnknownToken(f"Token not recognizable: {ch}") return duration python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/operations/0000755000175000017500000000000014150264231023653 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/operations/util.py0000644000175000017500000000230614150264231025203 0ustar jdgjdgfrom decimal import ROUND_FLOOR, Decimal def quot2(dividend: Decimal, divisor: Decimal) -> Decimal: return (dividend / divisor).to_integral_value(rounding=ROUND_FLOOR) def mod2(dividend: Decimal, divisor: Decimal) -> Decimal: return dividend - quot2(dividend, divisor) * divisor def quot3(value: Decimal, low: Decimal, high: Decimal) -> Decimal: dividend = value - low divisor = high - low return (dividend / divisor).to_integral_value(rounding=ROUND_FLOOR) def mod3(value: Decimal, low: Decimal, high: Decimal) -> Decimal: dividend = value - low divisor = high - low return mod2(dividend, divisor) + low def max_day_in_month(year: Decimal, month: Decimal) -> Decimal: norm_month = int(mod3(month, Decimal(1), Decimal(13))) norm_year = year + quot3(month, Decimal(1), Decimal(13)) if norm_month in (1, 3, 5, 7, 8, 10, 12): return Decimal(31) if norm_month in (4, 6, 9, 11): return Decimal(30) is_leap_year = ( mod2(norm_year, Decimal(400)) == 0 or mod2(norm_year, Decimal(100)) != 0 and mod2(norm_year, Decimal(4)) == 0 ) if norm_month == 2 and is_leap_year: return Decimal(29) return Decimal(28) python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/operations/__init__.py0000644000175000017500000000535414150264231025773 0ustar jdgjdgfrom __future__ import annotations from datetime import datetime from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal from typing import TYPE_CHECKING from isoduration.operations.util import max_day_in_month, mod2, mod3, quot2, quot3 if TYPE_CHECKING: # pragma: no cover from isoduration.types import Duration def add(start: datetime, duration: Duration) -> datetime: """ https://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes """ # Months. temp = Decimal(start.month) + duration.date.months end_month = mod3(temp, Decimal(1), Decimal(13)) carry = quot3(temp, Decimal(1), Decimal(13)) # Years. end_year = Decimal(start.year) + duration.date.years + carry # Zone. end_tzinfo = start.tzinfo # Microseconds. seconds = duration.time.seconds.to_integral_value(rounding=ROUND_DOWN) microseconds = (duration.time.seconds - seconds) * Decimal("1e6") temp = Decimal(start.microsecond) + microseconds end_microsecond = mod2(temp, Decimal("1e6")) carry = quot2(temp, Decimal("1e6")) # Seconds. temp = Decimal(start.second) + seconds + carry end_second = mod2(temp, Decimal("60")) carry = quot2(temp, Decimal("60")) # Minutes. temp = Decimal(start.minute) + duration.time.minutes + carry end_minute = mod2(temp, Decimal("60")) carry = quot2(temp, Decimal("60")) # Hours. temp = Decimal(start.hour) + duration.time.hours + carry end_hour = mod2(temp, Decimal("24")) carry = quot2(temp, Decimal("24")) # Days. end_max_day_in_month = max_day_in_month(end_year, end_month) if start.day > end_max_day_in_month: temp = end_max_day_in_month else: temp = Decimal(start.day) end_day = temp + duration.date.days + (7 * duration.date.weeks) + carry while True: if end_day < 1: end_day += max_day_in_month(end_year, end_month - 1) carry = Decimal(-1) elif end_day > max_day_in_month(end_year, end_month): end_day -= max_day_in_month(end_year, end_month) carry = Decimal(1) else: break temp = end_month + carry end_month = mod3(temp, Decimal(1), Decimal(13)) end_year = end_year + quot3(temp, Decimal(1), Decimal(13)) return datetime( year=int(end_year.to_integral_value(ROUND_HALF_UP)), month=int(end_month.to_integral_value(ROUND_HALF_UP)), day=int(end_day.to_integral_value(ROUND_HALF_UP)), hour=int(end_hour.to_integral_value(ROUND_HALF_UP)), minute=int(end_minute.to_integral_value(ROUND_HALF_UP)), second=int(end_second.to_integral_value(ROUND_HALF_UP)), microsecond=int(end_microsecond.to_integral_value(ROUND_HALF_UP)), tzinfo=end_tzinfo, ) python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/constants.py0000644000175000017500000000007014150264231024053 0ustar jdgjdgPERIOD_PREFIX = "P" TIME_PREFIX = "T" WEEK_PREFIX = "W" python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/types.py0000644000175000017500000000612714150264231023214 0ustar jdgjdgfrom __future__ import annotations from dataclasses import dataclass from datetime import datetime from decimal import Decimal from typing import Iterator, Tuple from isoduration.formatter import format_duration from isoduration.operations import add @dataclass class DateDuration: years: Decimal = Decimal(0) months: Decimal = Decimal(0) days: Decimal = Decimal(0) weeks: Decimal = Decimal(0) def __neg__(self) -> DateDuration: return DateDuration( years=-self.years, months=-self.months, days=-self.days, weeks=-self.weeks, ) @dataclass class TimeDuration: hours: Decimal = Decimal(0) minutes: Decimal = Decimal(0) seconds: Decimal = Decimal(0) def __neg__(self) -> TimeDuration: return TimeDuration( hours=-self.hours, minutes=-self.minutes, seconds=-self.seconds, ) class Duration: def __init__(self, date_duration: DateDuration, time_duration: TimeDuration): self.date = date_duration self.time = time_duration def __repr__(self) -> str: return f"{self.__class__.__name__}({self.date}, {self.time})" def __str__(self) -> str: return format_duration(self) def __hash__(self) -> int: return hash( ( self.date.years, self.date.months, self.date.days, self.date.weeks, self.time.hours, self.time.minutes, self.time.seconds, ) ) def __eq__(self, other: object) -> bool: if isinstance(other, Duration): return self.date == other.date and self.time == other.time raise NotImplementedError def __iter__(self) -> Iterator[Tuple[str, Decimal]]: time_duration = self.time time_order = ("hours", "minutes", "seconds") date_duration = self.date date_order = ("years", "months", "days", "weeks") for element in date_order: yield element, getattr(date_duration, element) for element in time_order: yield element, getattr(time_duration, element) def __reversed__(self) -> Iterator[Tuple[str, Decimal]]: time_duration = self.time time_order = ("seconds", "minutes", "hours") date_duration = self.date date_order = ("weeks", "days", "months", "years") for element in time_order: yield element, getattr(time_duration, element) for element in date_order: yield element, getattr(date_duration, element) def __neg__(self) -> Duration: return Duration(-self.date, -self.time) def __add__(self, other: datetime) -> datetime: if isinstance(other, datetime): return add(other, self) raise NotImplementedError __radd__ = __add__ def __sub__(self, other: object) -> None: raise NotImplementedError def __rsub__(self, other: datetime) -> datetime: if isinstance(other, datetime): return -self + other raise NotImplementedError python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/__init__.py0000644000175000017500000000055314150264231023604 0ustar jdgjdgfrom isoduration.formatter import format_duration from isoduration.formatter.exceptions import DurationFormattingException from isoduration.parser import parse_duration from isoduration.parser.exceptions import DurationParsingException __all__ = ( "format_duration", "parse_duration", "DurationParsingException", "DurationFormattingException", ) python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/formatter/0000755000175000017500000000000014150264231023473 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/formatter/exceptions.py0000644000175000017500000000017614150264231026232 0ustar jdgjdg""" Exception +- ValueError | +- DurationFormattingException """ class DurationFormattingException(ValueError): ... python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/formatter/checking.py0000644000175000017500000000274714150264231025632 0ustar jdgjdgfrom __future__ import annotations from typing import TYPE_CHECKING from isoduration.formatter.exceptions import DurationFormattingException if TYPE_CHECKING: # pragma: no cover from isoduration.types import DateDuration, Duration def check_global_sign(duration: Duration) -> int: is_date_zero = ( duration.date.years == 0 and duration.date.months == 0 and duration.date.days == 0 and duration.date.weeks == 0 ) is_time_zero = ( duration.time.hours == 0 and duration.time.minutes == 0 and duration.time.seconds == 0 ) is_date_negative = ( duration.date.years <= 0 and duration.date.months <= 0 and duration.date.days <= 0 and duration.date.weeks <= 0 ) is_time_negative = ( duration.time.hours <= 0 and duration.time.minutes <= 0 and duration.time.seconds <= 0 ) if not is_date_zero and not is_time_zero: if is_date_negative and is_time_negative: return -1 elif not is_date_zero: if is_date_negative: return -1 elif not is_time_zero: if is_time_negative: return -1 return +1 def validate_date_duration(date_duration: DateDuration) -> None: if date_duration.weeks: if date_duration.years or date_duration.months or date_duration.days: raise DurationFormattingException( "Weeks are incompatible with other date designators" ) python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/formatter/__init__.py0000644000175000017500000000141214150264231025602 0ustar jdgjdgfrom __future__ import annotations from typing import TYPE_CHECKING from isoduration.constants import PERIOD_PREFIX from isoduration.formatter.checking import check_global_sign from isoduration.formatter.formatting import format_date, format_time if TYPE_CHECKING: # pragma: no cover from isoduration.types import Duration def format_duration(duration: Duration) -> str: global_sign = check_global_sign(duration) date_duration_str = format_date(duration.date, global_sign) time_duration_str = format_time(duration.time, global_sign) duration_str = f"{date_duration_str}{time_duration_str}" sign_str = "-" if global_sign < 0 else "" if duration_str == PERIOD_PREFIX: return f"{PERIOD_PREFIX}0D" return f"{sign_str}{duration_str}" python-isoduration-20.11.0+git20211126.ae0bd61/src/isoduration/formatter/formatting.py0000644000175000017500000000263014150264231026220 0ustar jdgjdgfrom __future__ import annotations from typing import TYPE_CHECKING from isoduration.constants import PERIOD_PREFIX, TIME_PREFIX from isoduration.formatter.checking import validate_date_duration if TYPE_CHECKING: # pragma: no cover from isoduration.types import DateDuration, TimeDuration def format_date(date_duration: DateDuration, global_sign: int) -> str: validate_date_duration(date_duration) date_duration_str = PERIOD_PREFIX if date_duration.weeks != 0: date_duration_str += f"{(date_duration.weeks * global_sign):g}W" if date_duration.years != 0: date_duration_str += f"{(date_duration.years * global_sign):g}Y" if date_duration.months != 0: date_duration_str += f"{(date_duration.months * global_sign):g}M" if date_duration.days != 0: date_duration_str += f"{(date_duration.days * global_sign):g}D" return date_duration_str def format_time(time_duration: TimeDuration, global_sign: int) -> str: time_duration_str = TIME_PREFIX if time_duration.hours != 0: time_duration_str += f"{(time_duration.hours * global_sign):g}H" if time_duration.minutes != 0: time_duration_str += f"{(time_duration.minutes * global_sign):g}M" if time_duration.seconds != 0: time_duration_str += f"{(time_duration.seconds * global_sign):g}S" if time_duration_str == TIME_PREFIX: return "" return time_duration_str python-isoduration-20.11.0+git20211126.ae0bd61/tox.ini0000644000175000017500000000301014150264231017646 0ustar jdgjdg[tox] envlist = {py37,py38,py39,py310} linting publish [testenv] deps = coverage hypothesis isodate pytest pytest-benchmark commands = coverage run -m pytest {posargs} coverage report coverage xml [testenv:linting] deps = isodate bandit black dlint flake8 flake8-bugbear isort<5 mypy pylint commands = flake8 src/isoduration tests pylint src/isoduration mypy --strict --no-error-summary src/isoduration black -q --check src/isoduration tests isort -rc -c src/isoduration bandit -qr src/isoduration [testenv:publish] skip_install = true passenv = TWINE_REPOSITORY_URL TWINE_USERNAME TWINE_PASSWORD deps = wheel setuptools twine commands = python setup.py sdist bdist_wheel clean --all - twine upload --non-interactive dist/* [pytest] addopts = -ra testpaths = tests [coverage:run] branch = true source = isoduration [coverage:paths] source = src/isoduration .tox/*/lib/python*/site-packages/isoduration [coverage:report] exclude_lines = pragma: no cover def __repr__ if self.debug: if settings.DEBUG raise AssertionError raise NotImplementedError if 0: if __name__ == .__main__.: fail_under = 98 precision = 2 show_missing = true [flake8] max-line-length = 88 max-complexity = 15 select = C,E,F,W,B,B950 ignore = E203, E501, W503 [isort] line_length = 88 known_first_party = isoduration # Vertical Hanging Indent. multi_line_output = 3 include_trailing_comma = true use_parentheses = true python-isoduration-20.11.0+git20211126.ae0bd61/setup.py0000644000175000017500000000262514150264231020060 0ustar jdgjdgfrom setuptools import setup, find_packages with open("VERSION", "r") as f: version = f.read().strip() with open("README.md", "r") as f: long_description = f.read() setup( name="isoduration", version=version, author="Víctor Muñoz", author_email="victorm@marshland.es", description="Operations with ISO 8601 durations", url="https://github.com/bolsote/isoduration", package_dir={"": "src"}, packages=find_packages(where="src"), install_requires=["arrow>=0.15.0"], python_requires=">=3.7", zip_safe=False, long_description=long_description, long_description_content_type="text/markdown", project_urls={ "Repository": "https://github.com/bolsote/isoduration", "Bug Reports": "https://github.com/bolsote/isoduration/issues", "Changelog": "https://github.com/bolsote/isoduration/blob/master/CHANGELOG", }, keywords=[ "datetime", "date", "time", "duration", "duration-parsing", "duration-string", "iso8601", "iso8601-duration", ], classifiers=[ "Programming Language :: Python :: 3", "Operating System :: OS Independent", "License :: OSI Approved :: ISC License (ISCL)", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", ], ) python-isoduration-20.11.0+git20211126.ae0bd61/.pylintrc0000644000175000017500000000044114150264231020205 0ustar jdgjdg[MASTER] ignore=.git ve jobs=4 persistent=yes suggestion-mode=yes [MESSAGES CONTROL] disable=missing-docstring, raise-missing-from [REPORTS] output-format=text reports=no score=no [BASIC] good-names=ch, k, l, m, n, _ [FORMAT] max-line-length=88 [MISCELLANEOUS] notes=FIXME, XXX, TODO python-isoduration-20.11.0+git20211126.ae0bd61/tests/0000755000175000017500000000000014150264231017503 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/tests/test_duration.py0000644000175000017500000002767214150264231022757 0ustar jdgjdgfrom datetime import datetime from decimal import Decimal import pytest from isoduration.parser import parse_duration from isoduration.types import DateDuration, Duration, TimeDuration def test_representations(): duration = Duration(DateDuration(weeks=3), TimeDuration(hours=2, seconds=59)) date_repr = ( "DateDuration(years=Decimal('0'), months=Decimal('0'), " "days=Decimal('0'), weeks=3)" ) time_repr = "TimeDuration(hours=2, minutes=Decimal('0'), seconds=59)" duration_repr = f"Duration({date_repr}, {time_repr})" duration_str = "P3WT2H59S" assert repr(duration) == duration_repr assert str(duration) == duration_str def test_is_hashable(): durations = { Duration(DateDuration(weeks=7), TimeDuration()), Duration(DateDuration(weeks=11), TimeDuration()), Duration(DateDuration(weeks=7), TimeDuration()), } assert len(durations) == 2 def test_iteration(): duration = Duration(DateDuration(weeks=3), TimeDuration(hours=2, seconds=59)) assert list(duration) == [ ("years", Decimal("0")), ("months", Decimal("0")), ("days", Decimal("0")), ("weeks", Decimal("3")), ("hours", Decimal("2")), ("minutes", Decimal("0")), ("seconds", Decimal("59")), ] def test_reverse_iteration(): duration = Duration(DateDuration(weeks=3), TimeDuration(hours=2, seconds=59)) assert list(reversed(duration)) == [ ("seconds", Decimal("59")), ("minutes", Decimal("0")), ("hours", Decimal("2")), ("weeks", Decimal("3")), ("days", Decimal("0")), ("months", Decimal("0")), ("years", Decimal("0")), ] @pytest.mark.parametrize( "duration1, duration2", ( ( Duration(DateDuration(days=1), TimeDuration()), Duration(DateDuration(), TimeDuration(hours=24)), ), ( Duration(DateDuration(weeks=3), TimeDuration()), Duration(DateDuration(days=21), TimeDuration()), ), ( Duration(DateDuration(days=3), TimeDuration(hours=25)), Duration(DateDuration(days=4), TimeDuration(hours=1)), ), ), ) def test_distinguishes_representations(duration1, duration2): base = datetime(2000, 6, 1) assert base + duration1 == base + duration2 assert duration1 != duration2 @pytest.mark.parametrize( "original, negated", ( (DateDuration(), DateDuration()), (DateDuration(years=7), DateDuration(years=-7)), (DateDuration(months=3), DateDuration(months=-3)), (DateDuration(days=11), DateDuration(days=-11)), (DateDuration(weeks=42), DateDuration(weeks=-42)), (DateDuration(weeks=-42), DateDuration(weeks=42)), (DateDuration(years=3, months=2), DateDuration(years=-3, months=-2)), (DateDuration(years=-3, months=2), DateDuration(years=3, months=-2)), (DateDuration(years=3, months=-2), DateDuration(years=-3, months=2)), ( DateDuration(years=1, months=2, days=3), DateDuration(years=-1, months=-2, days=-3), ), ( DateDuration(years=1, months=-2, days=3), DateDuration(years=-1, months=2, days=-3), ), (DateDuration(years=7, weeks=11), DateDuration(years=-7, weeks=-11)), (DateDuration(years=-7, weeks=-11), DateDuration(years=7, weeks=11)), ), ) def test_negate_date_duration(original, negated): assert -original == negated @pytest.mark.parametrize( "original, negated", ( (TimeDuration(), TimeDuration()), (TimeDuration(hours=11), TimeDuration(hours=-11)), (TimeDuration(hours=-11), TimeDuration(hours=11)), (TimeDuration(minutes=3), TimeDuration(minutes=-3)), (TimeDuration(minutes=-3), TimeDuration(minutes=3)), (TimeDuration(seconds=9), TimeDuration(seconds=-9)), (TimeDuration(seconds=-9), TimeDuration(seconds=9)), (TimeDuration(hours=11, minutes=2), TimeDuration(hours=-11, minutes=-2)), (TimeDuration(hours=-11, minutes=2), TimeDuration(hours=11, minutes=-2)), ( TimeDuration(hours=3, minutes=2, seconds=1), TimeDuration(hours=-3, minutes=-2, seconds=-1), ), ( TimeDuration(hours=-3, minutes=2, seconds=-1), TimeDuration(hours=3, minutes=-2, seconds=1), ), ), ) def test_negate_time_duration(original, negated): assert -original == negated @pytest.mark.parametrize( "original, negated", ( ( Duration(DateDuration(), TimeDuration()), Duration(DateDuration(), TimeDuration()), ), ( Duration(DateDuration(years=11, days=3), TimeDuration(hours=8)), Duration(DateDuration(years=-11, days=-3), TimeDuration(hours=-8)), ), ( Duration(DateDuration(years=8, weeks=-2), TimeDuration()), Duration(DateDuration(years=-8, weeks=2), TimeDuration()), ), ( Duration(DateDuration(), TimeDuration(hours=9, seconds=11)), Duration(DateDuration(), TimeDuration(hours=-9, seconds=-11)), ), ( Duration( DateDuration(years=6, months=11, days=3), TimeDuration(hours=11, minutes=2), ), Duration( DateDuration(years=-6, months=-11, days=-3), TimeDuration(hours=-11, minutes=-2), ), ), ( Duration( DateDuration(years=6, months=-11, days=-3), TimeDuration(hours=11, minutes=-2), ), Duration( DateDuration(years=-6, months=11, days=3), TimeDuration(hours=-11, minutes=2), ), ), ), ) def test_negate_duration(original, negated): assert -original == negated @pytest.mark.parametrize( "start, duration, end", ( (datetime(2000, 1, 12), "PT33H", datetime(2000, 1, 13, 9)), (datetime(2000, 1, 12), "P4W", datetime(2000, 2, 9)), (datetime(2000, 2, 1), "P1Y1M", datetime(2001, 3, 1)), (datetime(2000, 2, 1), "-P1Y1M", datetime(1999, 1, 1)), (datetime(2000, 2, 1), "P1Y1M1D", datetime(2001, 3, 2)), (datetime(2000, 2, 1), "-P1Y1M1D", datetime(1998, 12, 31)), (datetime(2000, 2, 29), "P1Y", datetime(2001, 2, 28)), (datetime(1996, 2, 29), "P4Y", datetime(2000, 2, 29)), (datetime(2096, 2, 29), "P4Y", datetime(2100, 2, 28)), (datetime(2000, 2, 29), "P370D", datetime(2001, 3, 5)), (datetime(2000, 2, 29), "P1Y1D", datetime(2001, 3, 1)), (datetime(1996, 2, 29), "P4Y1D", datetime(2000, 3, 1)), (datetime(2096, 2, 29), "P4Y1D", datetime(2100, 3, 1)), (datetime(2000, 2, 29), "P1Y1M", datetime(2001, 3, 29)), (datetime(2000, 2, 29), "-P1Y1M", datetime(1999, 1, 29)), (datetime(2000, 2, 29), "P1Y1M1D", datetime(2001, 3, 30)), (datetime(2000, 2, 29), "-P1Y1M1D", datetime(1999, 1, 28)), (datetime(2000, 1, 1), "-P3M", datetime(1999, 10, 1)), (datetime(2000, 4, 1), "-P1Y1M1D", datetime(1999, 2, 28)), (datetime(2001, 4, 1), "-P1Y1M1D", datetime(2000, 2, 29)), (datetime(1987, 2, 11), "P33Y6M11D", datetime(2020, 8, 22)), (datetime(2011, 10, 8, 23), "PT1H", datetime(2011, 10, 9)), (datetime(2011, 10, 8, 23), "PT90M", datetime(2011, 10, 9, 0, 30)), (datetime(2011, 10, 8, 23), "PT26H", datetime(2011, 10, 10, 1)), ( datetime(2000, 1, 12), "P1Y3M5DT3.301S", datetime(2001, 4, 17, 0, 0, 3, 301000), ), ( datetime(2000, 1, 12, 0, 0, 0, 700000), "P1Y3M5DT3.301S", datetime(2001, 4, 17, 0, 0, 4, 1000), ), ( datetime(2000, 1, 12, 12, 13, 14), "P1Y3M5DT7H10M3.3S", datetime(2001, 4, 17, 19, 23, 17, 300000), ), ( datetime(2000, 1, 12, 12, 13, 14, 700000), "P1Y3M5DT7H10M3.3S", datetime(2001, 4, 17, 19, 23, 18), ), ( datetime(2000, 1, 12, 12, 13, 14, 699999), "P1Y3M5DT7H10M3.3S", datetime(2001, 4, 17, 19, 23, 17, 999999), ), ( datetime(2000, 1, 12, 12, 13, 14, 700001), "P1Y3M5DT7H10M3.3S", datetime(2001, 4, 17, 19, 23, 18, 1), ), ( datetime(2000, 1, 12, 12, 13, 14, 629000), "P1Y3M5DT7H10M3.371S", datetime(2001, 4, 17, 19, 23, 18), ), ( datetime(2000, 1, 12, 12, 13, 14), "P1Y3M5DT7H10M3.371001S", datetime(2001, 4, 17, 19, 23, 17, 371001), ), ( datetime(2000, 1, 12, 12, 13, 14, 803142), "P1Y3M5DT7H10M3.992103S", datetime(2001, 4, 17, 19, 23, 18, 795245), ), ( datetime(2000, 1, 12), "-P1Y3M5DT3.301S", datetime(1998, 10, 6, 23, 59, 56, 699000), ), ( datetime(2000, 1, 12, 12, 13, 14, 700000), "P1Y3M5DT-7H10M-3.3S", datetime(2001, 4, 17, 5, 23, 11, 400000), ), ( datetime(2000, 1, 12, 12, 13, 14, 699999), "P1Y3M5DT7H10M-3.7S", datetime(2001, 4, 17, 19, 23, 10, 999999), ), ( datetime(2000, 1, 12, 12, 13, 14, 700001), "P-1Y3M5DT7H10M-3.7S", datetime(1999, 4, 17, 19, 23, 11, 1), ), ( datetime(2000, 1, 12, 12, 13, 14, 629000), "P1Y3M5DT7H10M-3.629S", datetime(2001, 4, 17, 19, 23, 11), ), ( datetime(2000, 1, 12, 12, 13, 14), "P1Y3M5DT7H10M-3.371001S", datetime(2001, 4, 17, 19, 23, 10, 628999), ), ( datetime(2000, 3, 12, 12, 13, 14, 803142), "-P1Y2M5DT7H10M3.992103S", datetime(1999, 1, 7, 5, 3, 10, 811039), ), (datetime(2014, 10, 6, 1, 30, 3), "P2WT-3H-3S", datetime(2014, 10, 19, 22, 30)), ), ) def test_add_datetime_duration(start, duration, end): assert start + parse_duration(duration) == end assert parse_duration(duration) + start == end @pytest.mark.parametrize( "start, duration, end", ( (datetime(2000, 1, 1), "P3M", datetime(1999, 10, 1)), (datetime(2000, 1, 1), "P6W", datetime(1999, 11, 20)), (datetime(2000, 2, 1), "P1Y1M", datetime(1999, 1, 1)), (datetime(2000, 2, 1), "P1Y1M1D", datetime(1998, 12, 31)), (datetime(2000, 2, 29), "P1Y1M", datetime(1999, 1, 29)), (datetime(2000, 2, 29), "P1Y1M1D", datetime(1999, 1, 28)), (datetime(2001, 4, 1), "P1Y1M1D", datetime(2000, 2, 29)), (datetime(2000, 4, 1), "P1Y1M1D", datetime(1999, 2, 28)), (datetime(2011, 10, 9), "PT1H", datetime(2011, 10, 8, 23)), (datetime(2014, 10, 20, 1, 30), "P2WT2H", datetime(2014, 10, 5, 23, 30)), ( datetime(2014, 10, 20, 1, 30, 2, 200000), "P2WT2H2.2S", datetime(2014, 10, 5, 23, 30), ), ( datetime(2014, 10, 20, 1, 30, 2, 800000), "P2WT2H2.2S", datetime(2014, 10, 5, 23, 30, 0, 600000), ), ( datetime(2014, 10, 20, 1, 30, 2), "P2WT2H2.2S", datetime(2014, 10, 5, 23, 29, 59, 800000), ), ( datetime(2014, 10, 20, 1, 30, 2, 104000), "P2WT2H2.204S", datetime(2014, 10, 5, 23, 29, 59, 900000), ), ), ) def test_sub_datetime_duration(start, duration, end): assert start - parse_duration(duration) == end def test_non_commutativity(): """ https://www.w3.org/TR/xmlschema-2/#adding-durations-to-instants-commutativity-associativity """ start = datetime(2000, 3, 30) duration1 = parse_duration("P1D") duration2 = parse_duration("P1M") assert start + duration1 + duration2 == datetime(2000, 4, 30) assert start + duration2 + duration1 == datetime(2000, 5, 1) python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_parsing_util.py0000644000175000017500000000165614150264231023624 0ustar jdgjdgimport decimal import pytest from isoduration.parser.util import is_integer @pytest.mark.parametrize( "number, expected", ( (decimal.Decimal(0), True), (decimal.Decimal(0.0), True), (decimal.Decimal(0.00001), False), (decimal.Decimal(0.0000000000000001), False), (decimal.Decimal(1), True), (decimal.Decimal(1.0), True), (decimal.Decimal(1.0000000001), False), (decimal.Decimal(100), True), (decimal.Decimal(100.0), True), (decimal.Decimal(100.27), False), (decimal.Decimal(100.99999999999999), False), (decimal.Decimal(-384), True), (decimal.Decimal(-384.0), True), (decimal.Decimal(-384.47231), False), (decimal.Decimal(384), True), (decimal.Decimal(384.0), True), (decimal.Decimal(384.27236), False), ), ) def test_is_integer(number, expected): assert is_integer(number) == expected python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_operations_util.py0000644000175000017500000000520314150264231024334 0ustar jdgjdgfrom decimal import Decimal import pytest from isoduration.operations import max_day_in_month, mod2, mod3, quot2, quot3 @pytest.mark.parametrize( "op1, op2, result", ( ("-1", "3", "-1"), ("0", "3", "0"), ("1", "3", "0"), ("2", "3", "0"), ("3", "3", "1"), ("3.123", "3", "1"), ), ) def test_f_quotient_2(op1, op2, result): assert quot2(Decimal(op1), Decimal(op2)) == Decimal(result) @pytest.mark.parametrize( "op1, op2, result", ( ("-1", "3", "2"), ("0", "3", "0"), ("1", "3", "1"), ("2", "3", "2"), ("3", "3", "0"), ("3.123", "3", "0.123"), ), ) def test_modulo_2(op1, op2, result): assert mod2(Decimal(op1), Decimal(op2)) == Decimal(result) @pytest.mark.parametrize( "op1, op2, op3, result", ( ("0", "1", "13", "-1"), ("1", "1", "13", "0"), ("2", "1", "13", "0"), ("3", "1", "13", "0"), ("4", "1", "13", "0"), ("5", "1", "13", "0"), ("6", "1", "13", "0"), ("7", "1", "13", "0"), ("8", "1", "13", "0"), ("9", "1", "13", "0"), ("10", "1", "13", "0"), ("11", "1", "13", "0"), ("12", "1", "13", "0"), ("13", "1", "13", "1"), ("13.123", "1", "13", "1"), ), ) def test_f_quotient_3(op1, op2, op3, result): assert quot3(Decimal(op1), Decimal(op2), Decimal(op3)) == Decimal(result) @pytest.mark.parametrize( "op1, op2, op3, result", ( ("0", "1", "13", "12"), ("1", "1", "13", "1"), ("2", "1", "13", "2"), ("3", "1", "13", "3"), ("4", "1", "13", "4"), ("5", "1", "13", "5"), ("6", "1", "13", "6"), ("7", "1", "13", "7"), ("8", "1", "13", "8"), ("9", "1", "13", "9"), ("10", "1", "13", "10"), ("11", "1", "13", "11"), ("12", "1", "13", "12"), ("13", "1", "13", "1"), ("13.123", "1", "13", "1.123"), ), ) def test_modulo_3(op1, op2, op3, result): assert mod3(Decimal(op1), Decimal(op2), Decimal(op3)) == Decimal(result) @pytest.mark.parametrize( "year, month, max_day", ( (1987, 1, 31), (1987, 2, 28), (1987, 3, 31), (1987, 4, 30), (1987, 5, 31), (1987, 6, 30), (1987, 7, 31), (1987, 8, 31), (1987, 9, 30), (1987, 10, 31), (1987, 11, 30), (1987, 12, 31), (1987, 12, 31), (2000, 2, 29), (1900, 2, 28), (2004, 2, 29), ), ) def test_max_day_in_month(year, month, max_day): assert max_day_in_month(Decimal(year), Decimal(month)) == Decimal(max_day) python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_benchmark.py0000644000175000017500000000042214150264231023044 0ustar jdgjdgfrom isodate import parse_duration from isoduration import parse_duration as isodate_parse_duration def test_isoduration(benchmark): benchmark(parse_duration, "P18Y9M4DT11H9M8S") def test_isodate(benchmark): benchmark(isodate_parse_duration, "P18Y9M4DT11H9M8S") python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_formatter.py0000644000175000017500000001364314150264231023126 0ustar jdgjdgimport pytest from isoduration import DurationFormattingException, format_duration from isoduration.types import DateDuration, Duration, TimeDuration @pytest.mark.parametrize( "duration, duration_str", ( # Zero. (Duration(DateDuration(), TimeDuration()), "P0D"), # All fields. ( Duration( DateDuration(years=3, months=6, days=4), TimeDuration(hours=12, minutes=30, seconds=5), ), "P3Y6M4DT12H30M5S", ), ( Duration( DateDuration(years=18, months=9, days=4), TimeDuration(hours=11, minutes=9, seconds=8), ), "P18Y9M4DT11H9M8S", ), # All fields, only date. ( Duration(DateDuration(years=4, months=5, days=18), TimeDuration()), "P4Y5M18D", ), # All fields, only time. ( Duration(DateDuration(), TimeDuration(hours=2, minutes=3, seconds=4)), "PT2H3M4S", ), # Some fields, date only. (Duration(DateDuration(years=4), TimeDuration()), "P4Y"), (Duration(DateDuration(weeks=2), TimeDuration()), "P2W"), (Duration(DateDuration(months=1), TimeDuration()), "P1M"), (Duration(DateDuration(days=6), TimeDuration()), "P6D"), # Some fields, time only. (Duration(DateDuration(), TimeDuration(hours=2)), "PT2H"), (Duration(DateDuration(), TimeDuration(hours=36)), "PT36H"), (Duration(DateDuration(), TimeDuration(minutes=1)), "PT1M"), (Duration(DateDuration(), TimeDuration(seconds=22)), "PT22S"), ( Duration(DateDuration(), TimeDuration(minutes=3, seconds=4)), "PT3M4S", ), ( Duration(DateDuration(), TimeDuration(hours=6, seconds=59)), "PT6H59S", ), # Some fields, date and time. ( Duration( DateDuration(days=1), TimeDuration(hours=2, minutes=3, seconds=4), ), "P1DT2H3M4S", ), ( Duration(DateDuration(weeks=3), TimeDuration(hours=2, seconds=59)), "P3WT2H59S", ), ( Duration(DateDuration(days=1), TimeDuration(hours=2)), "P1DT2H", ), ( Duration(DateDuration(days=1), TimeDuration(hours=12)), "P1DT12H", ), ( Duration(DateDuration(days=23), TimeDuration(hours=23)), "P23DT23H", ), ( Duration(DateDuration(days=1), TimeDuration(hours=2, minutes=3)), "P1DT2H3M", ), # Floating point. (Duration(DateDuration(years=0.5), TimeDuration()), "P0.5Y"), ( Duration(DateDuration(), TimeDuration(hours=8.5, seconds=3)), "PT8.5H3S", ), (Duration(DateDuration(), TimeDuration(hours=2.3)), "PT2.3H"), ( Duration(DateDuration(), TimeDuration(seconds=22.22)), "PT22.22S", ), # Scientific notation. (Duration(DateDuration(years=1e9), TimeDuration()), "P1e+09Y"), (Duration(DateDuration(years=1e-9), TimeDuration()), "P1e-09Y"), (Duration(DateDuration(years=-1e9), TimeDuration()), "-P1e+09Y"), (Duration(DateDuration(), TimeDuration(seconds=90e9)), "PT9e+10S"), ( Duration(DateDuration(years=1e3), TimeDuration(hours=1e7)), "P1000YT1e+07H", ), ( Duration(DateDuration(years=1e3), TimeDuration(hours=1e-7)), "P1000YT1e-07H", ), ( Duration(DateDuration(years=1e3), TimeDuration(hours=-1e7)), "P1000YT-1e+07H", ), # Signs. (Duration(DateDuration(years=-2), TimeDuration()), "-P2Y"), (Duration(DateDuration(weeks=-2), TimeDuration()), "-P2W"), ( Duration(DateDuration(weeks=-2.2), TimeDuration()), "-P2.2W", ), ( Duration( DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=-12, minutes=-30, seconds=-5), ), "-P3Y6M4DT12H30M5S", ), ( Duration( DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=12, minutes=30, seconds=5), ), "P-3Y-6M-4DT12H30M5S", ), ( Duration( DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=-12, minutes=30, seconds=-5), ), "P-3Y-6M-4DT-12H30M-5S", ), ( Duration( DateDuration(days=-1), TimeDuration(hours=-2, minutes=-3, seconds=-4), ), "-P1DT2H3M4S", ), ( Duration( DateDuration(years=-3, months=-6, days=-4), TimeDuration(), ), "-P3Y6M4D", ), ( Duration(DateDuration(), TimeDuration(hours=-6, minutes=-3)), "-PT6H3M", ), ( Duration(DateDuration(), TimeDuration(hours=-6, minutes=3)), "PT-6H3M", ), ( Duration(DateDuration(), TimeDuration(hours=6, minutes=-3)), "PT6H-3M", ), ), ) def test_format_duration(duration, duration_str): assert format_duration(duration) == duration_str @pytest.mark.parametrize( "duration, exception, error_msg", ( ( Duration( DateDuration(years=3, months=6, days=4, weeks=12), TimeDuration(), ), DurationFormattingException, r"Weeks are incompatible with other date designators", ), ), ) def test_format_duration_errors(duration, exception, error_msg): with pytest.raises(exception) as exc: format_duration(duration) assert exc.match(error_msg) python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_parser.py0000644000175000017500000004006614150264231022416 0ustar jdgjdgfrom decimal import Decimal import pytest from isoduration.parser import exceptions, parse_duration from isoduration.types import DateDuration, Duration, TimeDuration @pytest.mark.parametrize( "duration, date_duration, time_duration", ( # Zero. ("P0D", DateDuration(), TimeDuration()), ("PT0S", DateDuration(), TimeDuration()), # All fields. ( "P3Y6M4DT12H30M5S", DateDuration(years=3, months=6, days=4), TimeDuration(hours=12, minutes=30, seconds=5), ), ( "P18Y9M4DT11H9M8S", DateDuration(years=18, months=9, days=4), TimeDuration(hours=11, minutes=9, seconds=8), ), # All fields, only date. ("P4Y5M18D", DateDuration(years=4, months=5, days=18), TimeDuration()), # All fields, only time. ("PT2H3M4S", DateDuration(), TimeDuration(hours=2, minutes=3, seconds=4)), # Some fields, date only. ("P4Y", DateDuration(years=4), TimeDuration()), ("P2W", DateDuration(weeks=2), TimeDuration()), ("P1M", DateDuration(months=1), TimeDuration()), ("P6D", DateDuration(days=6), TimeDuration()), # Some fields, time only. ("PT2H", DateDuration(), TimeDuration(hours=2)), ("PT36H", DateDuration(), TimeDuration(hours=36)), ("PT1M", DateDuration(), TimeDuration(minutes=1)), ("PT22S", DateDuration(), TimeDuration(seconds=22)), ("PT3M4S", DateDuration(), TimeDuration(minutes=3, seconds=4)), ("PT6H59S", DateDuration(), TimeDuration(hours=6, seconds=59)), # Some fields, date and time. ( "P1DT2H3M4S", DateDuration(days=1), TimeDuration(hours=2, minutes=3, seconds=4), ), ( "P3WT2H59S", DateDuration(weeks=3), TimeDuration(hours=2, seconds=59), ), ("P1DT2H", DateDuration(days=1), TimeDuration(hours=2)), ("P1DT12H", DateDuration(days=1), TimeDuration(hours=12)), ("P23DT23H", DateDuration(days=23), TimeDuration(hours=23)), ( "P1DT2H3M", DateDuration(days=1), TimeDuration(hours=2, minutes=3), ), # Floating point. ("P0.5Y", DateDuration(years=Decimal("0.5")), TimeDuration()), ("P0,5Y", DateDuration(years=Decimal("0.5")), TimeDuration()), ( "PT8H3.5S", DateDuration(), TimeDuration(hours=8, seconds=Decimal("3.5")), ), ( "PT8H3,5S", DateDuration(), TimeDuration(hours=8, seconds=Decimal("3.5")), ), ("PT2.3H", DateDuration(), TimeDuration(hours=Decimal("2.3"))), ( "PT22.22S", DateDuration(), TimeDuration(seconds=Decimal("22.22")), ), ("P1.3Y", DateDuration(years=Decimal("1.3")), TimeDuration()), ( "P1Y2.5M", DateDuration(years=Decimal("1"), months=Decimal("2.5")), TimeDuration(), ), ( "P1Y2M6.9D", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6.9")), TimeDuration(), ), ("P14.2W", DateDuration(weeks=Decimal("14.2")), TimeDuration()), ( "P1Y2M6DT6.18H", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6.18")), ), ( "P1Y2M6DT6H18.11M", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6"), minutes=Decimal("18.11")), ), ( "P1Y2M6DT6H18M11.42S", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( "P2M6DT6H18M11.42S", DateDuration(months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( "P1Y6DT6H18M11.42S", DateDuration(years=Decimal("1"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( "P1Y2MT6H18M11.42S", DateDuration(years=Decimal("1"), months=Decimal("2")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( "P1Y2M6DT18M11.42S", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(minutes=Decimal("18"), seconds=Decimal("11.42")), ), ( "P1Y2M6DT6H11.42S", DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6"), seconds=Decimal("11.42")), ), ( "P6DT6H11.42S", DateDuration(days=Decimal("6")), TimeDuration(hours=Decimal("6"), seconds=Decimal("11.42")), ), ( "P1Y6DT18.11M", DateDuration(years=Decimal("1"), days=Decimal("6")), TimeDuration(minutes=Decimal("18.11")), ), # Scientific notation. ( "PT1e3S", DateDuration(), TimeDuration(seconds=Decimal("1000")), ), ( "PT1e-3S", DateDuration(), TimeDuration(seconds=Decimal("0.001")), ), ( "P1E11Y", DateDuration(years=Decimal("1e11")), TimeDuration(), ), ( "P1E+11Y", DateDuration(years=Decimal("1e11")), TimeDuration(), ), ( "P-1E11Y", DateDuration(years=Decimal("-1e11")), TimeDuration(), ), ( "PT1.03e2H", DateDuration(), TimeDuration(hours=Decimal("103")), ), ( "-PT1.03e2H", DateDuration(), TimeDuration(hours=Decimal("-103")), ), ( "P10,8E23W", DateDuration(weeks=Decimal("10.8e23")), TimeDuration(), ), # Leading zeroes. ("PT000022.22", DateDuration(), TimeDuration()), ( "PT000022.22H", DateDuration(), TimeDuration(hours=Decimal("22.22")), ), # Signs. ("+P11D", DateDuration(days=11), TimeDuration()), ("-P2Y", DateDuration(years=-2), TimeDuration()), ("-P2W", DateDuration(weeks=-2), TimeDuration()), ("-P2.2W", DateDuration(weeks=Decimal("-2.2")), TimeDuration()), ( "-P3Y6M4DT12H30M5S", DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=-12, minutes=-30, seconds=-5), ), ( "-P1DT2H3M4S", DateDuration(days=-1), TimeDuration(hours=-2, minutes=-3, seconds=-4), ), ("P-20M", DateDuration(months=-20), TimeDuration()), ("PT-6H3M", DateDuration(), TimeDuration(hours=-6, minutes=3)), ("-PT-6H3M", DateDuration(), TimeDuration(hours=6, minutes=-3)), ("-PT-6H+3M", DateDuration(), TimeDuration(hours=6, minutes=-3)), # Unconventional numbers, beyond usual boundaries. ("P390D", DateDuration(days=390), TimeDuration()), ("P20M", DateDuration(months=20), TimeDuration()), ("P1000W", DateDuration(weeks=1000), TimeDuration()), ("PT72H", DateDuration(), TimeDuration(hours=72)), ("PT1000000M", DateDuration(), TimeDuration(minutes=1000000)), # Alternative format. ( "P0018-09-04T11:09:08", DateDuration(years=18, months=9, days=4), TimeDuration(hours=11, minutes=9, seconds=8), ), ( "P0003-06-04T12:30:00", DateDuration(years=3, months=6, days=4), TimeDuration(hours=12, minutes=30, seconds=0), ), ( "P00030604T123005", DateDuration(years=3, months=6, days=4), TimeDuration(hours=12, minutes=30, seconds=5), ), # Alternative format, with sign. ( "-P0003-06-04T12:30:05", DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=-12, minutes=-30, seconds=-5), ), ( "-P00030604T123005", DateDuration(years=-3, months=-6, days=-4), TimeDuration(hours=-12, minutes=-30, seconds=-5), ), ), ) def test_parse_duration(duration, date_duration, time_duration): assert parse_duration(duration) == Duration(date_duration, time_duration) @pytest.mark.parametrize( "duration, exception, error_msg", ( # EmptyDuration. ("", exceptions.EmptyDuration, r"No duration information provided"), ("P", exceptions.EmptyDuration, r"No duration information provided"), ("ajaksdf", exceptions.EmptyDuration, r"No prefix provided"), ("202hghf01217gg", exceptions.EmptyDuration, r"No prefix provided"), ("2015-2", exceptions.EmptyDuration, r"No prefix provided"), ("2015-01-01", exceptions.EmptyDuration, r"No prefix provided"), ("2015-", exceptions.EmptyDuration, r"No prefix provided"), ("2015-garbage", exceptions.EmptyDuration, r"No prefix provided"), ("0003-06-04T12:30:00", exceptions.EmptyDuration, r"No prefix provided"), ("00030604T123005", exceptions.EmptyDuration, r"No prefix provided"), ("16.263772,-61.2329", exceptions.EmptyDuration, r"No prefix provided"), ("1000", exceptions.EmptyDuration, r"No prefix provided"), ("1Y2M", exceptions.EmptyDuration, r"No prefix provided"), # IncorrectDesignator. ( "P2M1Y", exceptions.IncorrectDesignator, r"Wrong date designator, or designator in the wrong order: Y", ), ( "P1D2H", exceptions.IncorrectDesignator, r"Wrong date designator, or designator in the wrong order: H", ), ( "P4W2D", exceptions.IncorrectDesignator, r"Wrong date designator, or designator in the wrong order: D", ), ( "P4Y2W", exceptions.IncorrectDesignator, r"Week is incompatible with any other date designator", ), ( "P36w", exceptions.IncorrectDesignator, r"Wrong date designator, or designator in the wrong order: w", ), ( "PT3S6M", exceptions.IncorrectDesignator, r"Wrong time designator, or designator in the wrong order: M", ), ( "PT36W", exceptions.IncorrectDesignator, r"Wrong time designator, or designator in the wrong order: W", ), ( "PT11ℵ", exceptions.IncorrectDesignator, r"Wrong time designator, or designator in the wrong order: ℵ", ), ( "PT6H8M43S8S", exceptions.IncorrectDesignator, r"Wrong time designator, or designator in the wrong order: S", ), # NoTime. ("PT", exceptions.NoTime, r"Wanted time, no time provided"), ("P20MT", exceptions.NoTime, r"Wanted time, no time provided"), # UnknownToken. ("P12'3W", exceptions.UnknownToken, r"Token not recognizable: '"), ("P 8W", exceptions.UnknownToken, r"Token not recognizable: "), ("PT(╯°□°)╯︵ ┻━┻", exceptions.UnknownToken, r"Token not recognizable: \("), ("P∰", exceptions.UnknownToken, r"Token not recognizable: ∰"), ("P💩", exceptions.UnknownToken, r"Token not recognizable: 💩"), ("P0018/09/04T11:09:08", exceptions.UnknownToken, r"Token not recognizable: /"), # UnparseableValue. ( "P1YM5D", exceptions.UnparseableValue, r"Value could not be parsed as decimal: ", ), ( "PTS", exceptions.UnparseableValue, r"Value could not be parsed as decimal: ", ), ( "P12..80Y", exceptions.UnparseableValue, r"Value could not be parsed as decimal: 12..80", ), ( "PT11.0.42S", exceptions.UnparseableValue, r"Value could not be parsed as decimal: 11.0.42", ), ( "P1234T", exceptions.UnparseableValue, r"Value could not be parsed as datetime: 1234T", ), ( "P00030604T12300500", exceptions.UnparseableValue, r"Value could not be parsed as datetime: 00030604T12300500", ), ( "P00030604T12300", exceptions.UnparseableValue, r"Value could not be parsed as datetime: 00030604T12300", ), ( "P0003060T123005", exceptions.UnparseableValue, r"Value could not be parsed as datetime: 0003060T123005", ), # InvalidFractional. ( "P1.5Y2M6DT6H18M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2.4M6DT6H18M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6.3DT6H18M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6DT6.2H18M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6DT6H18.1M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1.5Y2M6DT6H18M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2.4M6DT6H18M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6.3DT6H18M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6DT6.2H18M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2M6DT6H18.1M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2.003M6DT6H18.3M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2.003M6.1DT6H18M11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1Y2.003M6DT6H18M11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1.5YT11.42S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P1.5YT11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "P7Y1.5DT11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ( "PT3.5H11S", exceptions.InvalidFractional, r"Only the lowest order component can be fractional", ), ), ) def test_parse_duration_errors(duration, exception, error_msg): with pytest.raises(exception) as exc: parse_duration(duration) assert exc.match(error_msg) python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_properties.py0000644000175000017500000000325514150264231023315 0ustar jdgjdgfrom datetime import timedelta from hypothesis import given from hypothesis.strategies import SearchStrategy, builds, datetimes, decimals from isoduration.formatter import format_duration from isoduration.parser import parse_duration from isoduration.types import DateDuration, Duration, TimeDuration item_st = decimals(min_value=-1_000_000_000_000, max_value=+1_000_000_000_000, places=0) seconds_st = decimals( min_value=-1_000_000_000_000, max_value=+1_000_000_000_000, places=10 ) """ Fractional numbers are only allowed by the standard in the lest significant component. It's a bit difficult to have a strategy modelling this, so we have opted to include fractional numbers just as part of the seconds component. """ date_duration_st: SearchStrategy[DateDuration] = builds( DateDuration, years=item_st, months=item_st, days=item_st ) time_duration_st: SearchStrategy[TimeDuration] = builds( TimeDuration, hours=item_st, minutes=item_st, seconds=seconds_st ) @given(date_duration=date_duration_st, time_duration=time_duration_st) def test_parse_inverse_of_format(date_duration, time_duration): duration = Duration(date_duration, time_duration) assert parse_duration(format_duration(duration)) == duration @given(date_duration=date_duration_st, time_duration=time_duration_st) def test_duration_double_negation(date_duration, time_duration): duration = Duration(date_duration, time_duration) neg_duration = -duration assert -neg_duration == duration @given(datetimes()) def test_duration_addition_identity_element(base_datetime): identity = Duration(DateDuration(), TimeDuration()) assert (base_datetime + identity) - base_datetime < timedelta(seconds=1) python-isoduration-20.11.0+git20211126.ae0bd61/tests/test_parsing_validation.py0000644000175000017500000001462714150264231025003 0ustar jdgjdgfrom decimal import Decimal import pytest from isoduration.parser.exceptions import InvalidFractional from isoduration.parser.validation import validate_fractional from isoduration.types import DateDuration, Duration, TimeDuration @pytest.mark.parametrize( "date_duration, time_duration", ( (DateDuration(), TimeDuration()), (DateDuration(years=Decimal("1.3")), TimeDuration()), (DateDuration(years=Decimal("1"), months=Decimal("2.5")), TimeDuration()), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6.9")), TimeDuration(), ), (DateDuration(weeks=Decimal("14.2")), TimeDuration()), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6.18")), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6"), minutes=Decimal("18.11")), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(minutes=Decimal("18"), seconds=Decimal("11.42")), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration(hours=Decimal("6"), seconds=Decimal("11.42")), ), ( DateDuration(days=Decimal("6")), TimeDuration(hours=Decimal("6"), seconds=Decimal("11.42")), ), ( DateDuration(years=Decimal("1"), days=Decimal("6")), TimeDuration(minutes=Decimal("18.11")), ), ), ) def test_correct_fractional_duration(date_duration, time_duration): validate_fractional(Duration(date_duration, time_duration)) @pytest.mark.parametrize( "date_duration, time_duration", ( ( DateDuration(years=Decimal("1.5"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2.4"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6.3")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6.2"), minutes=Decimal("18"), seconds=Decimal("11") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18.1"), seconds=Decimal("11") ), ), ( DateDuration(years=Decimal("1.5"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2.4"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6.3")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6.2"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1"), months=Decimal("2"), days=Decimal("6")), TimeDuration( hours=Decimal("6"), minutes=Decimal("18.1"), seconds=Decimal("11.42") ), ), ( DateDuration( years=Decimal("1"), months=Decimal("2.003"), days=Decimal("6") ), TimeDuration( hours=Decimal("6"), minutes=Decimal("18.3"), seconds=Decimal("11.42") ), ), ( DateDuration( years=Decimal("1"), months=Decimal("2.003"), days=Decimal("6.1") ), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11") ), ), ( DateDuration( years=Decimal("1"), months=Decimal("2.003"), days=Decimal("6") ), TimeDuration( hours=Decimal("6"), minutes=Decimal("18"), seconds=Decimal("11.42") ), ), ( DateDuration(years=Decimal("1.5")), TimeDuration(seconds=Decimal("11.42")), ), ( DateDuration(years=Decimal("1.5")), TimeDuration(seconds=Decimal("11")), ), ( DateDuration(years=Decimal("7"), days=Decimal("1.5")), TimeDuration(seconds=Decimal("11")), ), ( DateDuration(), TimeDuration(hours=Decimal("3.5"), seconds=Decimal("11")), ), ), ) def test_incorrect_fractional_duration(date_duration, time_duration): with pytest.raises(InvalidFractional): validate_fractional(Duration(date_duration, time_duration)) python-isoduration-20.11.0+git20211126.ae0bd61/VERSION0000644000175000017500000000001014150264231017400 0ustar jdgjdg20.11.0 python-isoduration-20.11.0+git20211126.ae0bd61/scripts/0000755000175000017500000000000014150264231020030 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/scripts/publish.ps10000644000175000017500000000011214150264231022115 0ustar jdgjdg$version = (Get-Content VERSION).Trim() git tag $version git push --tags python-isoduration-20.11.0+git20211126.ae0bd61/scripts/watcher.ps10000644000175000017500000000175614150264231022123 0ustar jdgjdgfunction Add-Watcher { param( [String] $Path = ".", [String] $FileFilter = "*" ) $AttributeFilter = [IO.NotifyFilters]::FileName, [IO.NotifyFilters]::LastWrite $ChangeTypes = [System.IO.WatcherChangeTypes]::All $Timeout = 1000 try { $watcher = New-Object ` -TypeName IO.FileSystemWatcher ` -ArgumentList $Path, $FileFilter ` -Property @{ IncludeSubdirectories = $true NotifyFilter = $AttributeFilter } do { $result = $watcher.WaitForChanged($ChangeTypes, $Timeout) if ($result.TimedOut) { continue } Invoke-Action -Change $result } while ($true) } finally { $watcher.Dispose() } } function Invoke-Action { param ( [Parameter(Mandatory)] [System.IO.WaitForChangedResult] $ChangeInformation ) tox -e linting,py -p auto -o } Add-Watcher -FileFilter *.py python-isoduration-20.11.0+git20211126.ae0bd61/.gitignore0000644000175000017500000000356314150264231020340 0ustar jdgjdg# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Benchmarks .benchmarks/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments ve/ .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ python-isoduration-20.11.0+git20211126.ae0bd61/LICENSE0000644000175000017500000000136014150264231017346 0ustar jdgjdgCopyright (c) 2020 Víctor Muñoz Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. python-isoduration-20.11.0+git20211126.ae0bd61/MANIFEST.in0000644000175000017500000000020614150264231020075 0ustar jdgjdgrecursive-include src *.py include CHANGELOG include LICENSE include README.md include VERSION global-exclude *.py[cod] __pycache__ python-isoduration-20.11.0+git20211126.ae0bd61/requirements/0000755000175000017500000000000014150264231021064 5ustar jdgjdgpython-isoduration-20.11.0+git20211126.ae0bd61/requirements/dev.txt0000644000175000017500000000016714150264231022407 0ustar jdgjdgisodate bandit black dlint flake8 flake8-bugbear hypothesis isort<5 mypy pylint pytest pytest-benchmark pytest-cov tox python-isoduration-20.11.0+git20211126.ae0bd61/pyproject.toml0000644000175000017500000000007014150264231021252 0ustar jdgjdg[tool.black] line-length = 88 target_version = ['py37'] python-isoduration-20.11.0+git20211126.ae0bd61/CHANGELOG0000644000175000017500000000026714150264231017560 0ustar jdgjdgv20.11.0 - 1/11/2020 Initial release. - Support for parsing ISO 8601 durations. - Support for formatting ISO 8601 durations. - Support for arithmetic between datetimes and durations.