second commit

This commit is contained in:
2024-12-27 22:31:23 +09:00
parent 2353324570
commit 10a0f110ca
8819 changed files with 1307198 additions and 28 deletions

View File

@ -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__",
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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."""

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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)