about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/isodate
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/isodate')
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/__init__.py103
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/duration.py316
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isodates.py203
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isodatetime.py45
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isoduration.py147
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isoerror.py7
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isostrf.py189
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isotime.py155
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/isotzinfo.py91
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/tzinfo.py166
-rw-r--r--.venv/lib/python3.12/site-packages/isodate/version.py16
11 files changed, 1438 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/isodate/__init__.py b/.venv/lib/python3.12/site-packages/isodate/__init__.py
new file mode 100644
index 00000000..d9cca6a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/__init__.py
@@ -0,0 +1,103 @@
+"""
+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__",
+]
diff --git a/.venv/lib/python3.12/site-packages/isodate/duration.py b/.venv/lib/python3.12/site-packages/isodate/duration.py
new file mode 100644
index 00000000..bc8a5cb9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/duration.py
@@ -0,0 +1,316 @@
+"""
+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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/isodates.py b/.venv/lib/python3.12/site-packages/isodate/isodates.py
new file mode 100644
index 00000000..d32fe25e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isodates.py
@@ -0,0 +1,203 @@
+"""
+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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"-(?P<month>[0-9]{2})-(?P<day>[0-9]{2})" % (sign, yeardigits)
+        )
+        #    YYYYMMDD or +- YYYYYYMMDD... basic date format
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"(?P<month>[0-9]{2})(?P<day>[0-9]{2})" % (sign, yeardigits)
+        )
+        # 2. complete week dates:
+        #    YYYY-Www-D or +-YYYYYY-Www-D ... extended week date
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"-W(?P<week>[0-9]{2})-(?P<day>[0-9]{1})" % (sign, yeardigits)
+        )
+        #    YYYYWwwD or +-YYYYYYWwwD ... basic week date
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
+            r"(?P<week>[0-9]{2})(?P<day>[0-9]{1})" % (sign, yeardigits)
+        )
+        # 3. ordinal dates:
+        #    YYYY-DDD or +-YYYYYY-DDD ... extended format
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"-(?P<day>[0-9]{3})" % (sign, yeardigits)
+        )
+        #    YYYYDDD or +-YYYYYYDDD ... basic format
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"(?P<day>[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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"-W(?P<week>[0-9]{2})" % (sign, yeardigits)
+        )
+        #    YYYYWww or +-YYYYYYWww ... basic reduced accuracy week date
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})W"
+            r"(?P<week>[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<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"-(?P<month>[0-9]{2})" % (sign, yeardigits)
+        )
+        #    YYYMM or +-YYYYYYMM ... basic incomplete month date format
+        add_re(
+            r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})"
+            r"(?P<month>[0-9]{2})" % (sign, yeardigits)
+        )
+        # 6. year dates:
+        #    YYYY or +-YYYYYY ... reduced accuracy specific year
+        add_re(r"(?P<sign>[+-]){%d}(?P<year>[0-9]{%d})" % (sign, yeardigits))
+        # 7. century dates:
+        #    YY or +-YYYY ... reduced accuracy specific century
+        add_re(r"(?P<sign>[+-]){%d}" r"(?P<century>[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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/isodatetime.py b/.venv/lib/python3.12/site-packages/isodate/isodatetime.py
new file mode 100644
index 00000000..1b208053
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isodatetime.py
@@ -0,0 +1,45 @@
+"""
+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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/isoduration.py b/.venv/lib/python3.12/site-packages/isodate/isoduration.py
new file mode 100644
index 00000000..4f1755bf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isoduration.py
@@ -0,0 +1,147 @@
+"""
+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<sign>[+-])?"
+    r"P(?!\b)"
+    r"(?P<years>[0-9]+([,.][0-9]+)?Y)?"
+    r"(?P<months>[0-9]+([,.][0-9]+)?M)?"
+    r"(?P<weeks>[0-9]+([,.][0-9]+)?W)?"
+    r"(?P<days>[0-9]+([,.][0-9]+)?D)?"
+    r"((?P<separator>T)(?P<hours>[0-9]+([,.][0-9]+)?H)?"
+    r"(?P<minutes>[0-9]+([,.][0-9]+)?M)?"
+    r"(?P<seconds>[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
diff --git a/.venv/lib/python3.12/site-packages/isodate/isoerror.py b/.venv/lib/python3.12/site-packages/isodate/isoerror.py
new file mode 100644
index 00000000..068429f2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isoerror.py
@@ -0,0 +1,7 @@
+"""
+This module defines all exception classes in the whole package.
+"""
+
+
+class ISO8601Error(ValueError):
+    """Raised when the given ISO string can not be parsed."""
diff --git a/.venv/lib/python3.12/site-packages/isodate/isostrf.py b/.venv/lib/python3.12/site-packages/isodate/isostrf.py
new file mode 100644
index 00000000..455ce97a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isostrf.py
@@ -0,0 +1,189 @@
+"""
+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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/isotime.py b/.venv/lib/python3.12/site-packages/isodate/isotime.py
new file mode 100644
index 00000000..f74ef5dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isotime.py
@@ -0,0 +1,155 @@
+"""
+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<hour>[0-9]{2}):"
+            r"(?P<minute>[0-9]{2}):"
+            r"(?P<second>[0-9]{2}"
+            r"([,.][0-9]+)?)"
+        )
+        #    hhmmss.ss ... basic format
+        add_re(
+            r"T?(?P<hour>[0-9]{2})"
+            r"(?P<minute>[0-9]{2})"
+            r"(?P<second>[0-9]{2}"
+            r"([,.][0-9]+)?)"
+        )
+        # 2. reduced accuracy:
+        #    hh:mm.mm ... extended format
+        add_re(r"T?(?P<hour>[0-9]{2}):" r"(?P<minute>[0-9]{2}" r"([,.][0-9]+)?)")
+        #    hhmm.mm ... basic format
+        add_re(r"T?(?P<hour>[0-9]{2})" r"(?P<minute>[0-9]{2}" r"([,.][0-9]+)?)")
+        #    hh.hh ... basic format
+        add_re(r"T?(?P<hour>[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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/isotzinfo.py b/.venv/lib/python3.12/site-packages/isodate/isotzinfo.py
new file mode 100644
index 00000000..54f36de0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/isotzinfo.py
@@ -0,0 +1,91 @@
+"""
+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<tzname>(Z|(?P<tzsign>[+-])" r"(?P<tzhour>[0-9]{2})(:?(?P<tzmin>[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)
diff --git a/.venv/lib/python3.12/site-packages/isodate/tzinfo.py b/.venv/lib/python3.12/site-packages/isodate/tzinfo.py
new file mode 100644
index 00000000..726c54a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/tzinfo.py
@@ -0,0 +1,166 @@
+"""
+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 "<FixedOffset %r>" % 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()
diff --git a/.venv/lib/python3.12/site-packages/isodate/version.py b/.venv/lib/python3.12/site-packages/isodate/version.py
new file mode 100644
index 00000000..393e7229
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/isodate/version.py
@@ -0,0 +1,16 @@
+# 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.2'
+__version_tuple__ = version_tuple = (0, 7, 2)