Skip to content

Refactor datetime #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d3346ed
Add saml2.datetime module
c00kiemon5ter Jul 4, 2018
b84fc0f
Update assertion to use the datetime module
c00kiemon5ter Jul 4, 2018
eb35f6b
Update authn to use the datetime module
c00kiemon5ter Jul 5, 2018
cbfbb1c
Update cache to use the datetime module
c00kiemon5ter Jul 4, 2018
c22811a
Update cert to use the datetime module
c00kiemon5ter Jul 4, 2018
4a69ad8
Update client to use the datetime module
c00kiemon5ter Jul 4, 2018
44e9912
Update client_base to use the datetime module
c00kiemon5ter Jul 5, 2018
45c5141
Update entity to use the datetime module
c00kiemon5ter Jul 4, 2018
8477c4c
Update httputil and httpbase to use the datetime module
c00kiemon5ter Jul 5, 2018
9dd3200
Update mcache to use the datetime module
c00kiemon5ter Jul 4, 2018
c903afc
Update mdbcache to use the datetime module
c00kiemon5ter Jul 4, 2018
b3f2f6d
Update mdstore to use the datetime module
c00kiemon5ter Jul 4, 2018
e113510
Update metadata to use the datetime module
c00kiemon5ter Jul 4, 2018
6159d9a
Update population to use the datetime module
c00kiemon5ter Jul 4, 2018
9e5f374
Update validate to use the datetime module
c00kiemon5ter Jul 5, 2018
79321d0
Make issue_instant_ok part of validate module
c00kiemon5ter Jul 5, 2018
7f9eea6
Update request to use the datetime module
c00kiemon5ter Jul 4, 2018
e46ad9f
Update response to use the datetime module
c00kiemon5ter Jul 4, 2018
922d21f
Update s_utils to use the datetime module
c00kiemon5ter Jul 4, 2018
4a0cee1
Update sigver to use the datetime module
c00kiemon5ter Jul 4, 2018
ce66189
Update tests
c00kiemon5ter Jul 4, 2018
9b9240e
Remove time_util module
c00kiemon5ter Jul 4, 2018
e6fb4a3
Update documentation
c00kiemon5ter Jul 4, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ scripts =
tools/merge_metadata.py
tools/parse_xsd2.py
install_requires =
aniso8601
cryptography
defusedxml
enum34
future
pyOpenSSL
python-dateutil
Expand Down
52 changes: 52 additions & 0 deletions src/saml2/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Functions to hide compatibility issues between python2 and python3.

This module encapsulates all compatibility issues between python2 and python3.
Many of the compatibility issues are solved by the python-future module. Any
other workarounds will be implemented under this module.
"""

import datetime as _datetime
import time as _time

from aniso8601.timezone import UTCOffset as _Timezone

import future.utils as _future_utils


def timestamp(date_time):
"""Return the POSIX timestamp from the datetime object.

Python3 provides the `.timestamp()` method call on datetime.datetime
objects, but python2 does not. For python2 we must compute the timestamp
ourselves. The formula has been backported from python3.

The parameter `date_time` is expected to be of type: datetime.timedelta

The object returned is of type: float
"""
if hasattr(date_time, 'timestamp'):
timestamp = date_time.timestamp()
else:
timestamp = _time.mktime(date_time.timetuple())
timestamp += date_time.microsecond / 1e6
return timestamp


def _utc_timezone():
"""Return a UTC-timezone tzinfo instance.

Python3 provides a UTC-timezone tzinfo instance through the
datetime.timezone module. Python2 does not define any timezone instance; it
only provides the tzinfo abstract base class. For python2 the instance is
generated with the _Timezone class.
"""
try:
utc_timezone = _datetime.timezone.utc
except AttributeError as e:
utc_timezone = _Timezone(name='UTC', minutes=0)
finally:
return utc_timezone


UTC_TIMEZONE = _utc_timezone()
raise_from = _future_utils.raise_from
202 changes: 202 additions & 0 deletions src/saml2/datetime/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Datetime structures and operations.

This module encapsulates the structures that define datetime, time unit,
duration and timezone objects, their relation and the operations that can be
done upon them. Should these structures change all affected components should
be under this module.

There are three layers of specifications that define the structure and
behaviour of time constructs for SAML2:

- The SAML2-core specification - section 1.3.3 Time Values.
Reference: http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf

- The W3C XML Schema Datatypes - section 3.2.7 dateTime.
Reference: https://www.w3.org/TR/xmlschema-2/#dateTime

[notable] W3C Date and Time Formats defines a profile of ISO 8601.
Reference: https://www.w3.org/TR/NOTE-datetime

- The ISO 8601 standard upon which the dateTime datatype is based.
Reference: https://en.wikipedia.org/wiki/ISO_8601

[notable] Most systems implement rfc3339; a profile of ISO 8601.
Reference: https://tools.ietf.org/html/rfc3339

Finally, further clarification was requested and in the following thread an
answer was given by a member of the SAML Technical Committee:
https://lists.oasis-open.org/archives/saml-dev/201310/msg00001.html

To comply with the specifications, the existing implementations and the
"unofficial errata" in the thread above, the following have been decided:

- all ISO 8601 formats that can be parsed are accepted and converted to UTC.

- if no timezone information is present, it is assumed that the other party is
following the current wording of the SAML2-core specification - the time is
assumed to be in UTC already, but "with no timezone component."

- the datetime object produced are always in UTC timezone, that can be
represented as a string of ISO 8601 combined date and time format with
extended notation, where the timezone component is always present and
represented by the military timezone symbol 'Z'.
"""

import enum as _enum
from datetime import datetime as _datetime

from aniso8601 import parse_datetime as _datetime_parser

import saml2.compat
from saml2.datetime import duration
from saml2.datetime import errors
from saml2.datetime import timezone


def parse(data):
"""Return a datetime object in UTC timezone from the given data.

If timezone information is available the datetime object will be converted
to UTC timezone. If no timezone information is available, it will be
assumed to be in UTC timezone and that information will be added.

The parameter `data` is expected to be of type:
- datetime.datetime: a datetime.datetime object
- str: a string in ISO 8601 combined date and time format with extended
notation
- int: a number representing a POSIX timestamp
- float: a number representing a POSIX timestamp

The object returned is of type: datetime.datetime
Copy link
Contributor

@johanlundberg johanlundberg Jul 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your current comments about arguments and types reads better but would you (also?) consider type hints for IDEs?

Something like:

    """Return a datetime object in UTC timezone from the given data.
    *snip*
    :param data: Representation of time
    :type data: datetime.datetime|str|int|float

    :return: Datetime object
    :rtype: datetime.datetime
    """

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like Python3 type hints, as they are inline and they refer to actual types - in contrast to types written as strings (wether in docstrings or comments).

I was thinking about experimenting with function annotations which should work with Python2 and still allow us to signal actual types rather than strings. It does have the drawback that we must type the parameter names, but I think it is still better.

In the meantime, I can do the conversion to the format that the rest of the code follows.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also prefer the Python 3 annotations and I haven't seen the function annotations that you link to. I wasn't thrilled about the syntax of those either but if you find it workable in your experiment I would be for them.

"""
try:
parse = _parsers[type(data)]
except KeyError as e:
saml2.compat.raise_from(errors.DatetimeFactoryError(data), e)

try:
value = parse(data)
except (ValueError, TypeError, NotImplementedError) as e:
saml2.compat.raise_from(errors.DatetimeParseError(data), e)

utc_timezone = timezone.UTC_TIMEZONE
if value.tzinfo is None:
value = value.replace(tzinfo=utc_timezone)
if value.tzinfo is not utc_timezone:
value = value.astimezone(utc_timezone)

return value


def fromtimestamp(timestamp):
"""Return a datetime object in UTC timezone from the given POSIX timestamp.

The parameter `timestamp` is expected to be of type: int|float

The object returned is of type: datetime.datetime
"""
return _datetime.fromtimestamp(timestamp, timezone.UTC_TIMEZONE)


def to_string(date_time_obj):
"""Return an ISO 8601 string representation of the datetime object.

Return the given datetime object -as returned by the `parse` function-
represented as a string of ISO 8601 combined date and time format with
extended notation, where the timezone component is always present and
represented by the military timezone symbol 'Z'.

The parameter `date_time_obj` is expected to be of type: datetime.datetime

The object returned is of type: str
"""
return date_time_obj.isoformat().replace(
timezone.UTC_OFFSET_SYMBOL,
timezone.UTC_MILITARY_TIMEZONE_SYMBOL)


class unit(_enum.Enum):
"""Time unit representations and constructors.

Available units are:
- days
- seconds
- microseconds
- milliseconds
- minutes
- hours
- weeks

Both plural and singular forms are available. Time units can be used to
create objects that describe a period of time or signify the type of unit
of a given amount.

Usage example:

* The difference between two datetime objects is a period of time:

```
import saml2.datetime

dt1 = saml2.datetime.parse('2018-01-25T08:45:00Z')
dt2 = saml2.datetime.parse('2018-01-25T08:40:00Z')
delta = dt1 - dt2
period = saml2.datetime.unit.minute(5)

assert period == delta
```

* Signify the type of unit for an amount:

```
import saml2.datetime
from saml2.datetime import duration

period = saml2.datetime.duration.parse({
saml2.datetime.unit.seconds: 5
})

assert saml2.datetime.unit.seconds(5) == period
```

The object returned is of type: datetime.timedelta
"""

day = 'days'
days = 'days'
second = 'seconds'
seconds = 'seconds'
microsecond = 'microseconds'
microseconds = 'microseconds'
millisecond = 'milliseconds'
milliseconds = 'milliseconds'
minute = 'minutes'
minutes = 'minutes'
hour = 'hours'
hours = 'hours'
week = 'weeks'
weeks = 'weeks'

def __str__(self):
"""Return the string representation time unit types.

The object returned is of type: str
"""
return self.value

def __call__(self, amount):
"""Return a period object of the specified time unit and amount.

The parameter `amount` is expected to be of type: int|float

The object returned is of type: datetime.timedelta
"""
return duration.parse({self.value: amount})


_parsers = {
_datetime: lambda x: x,
str: _datetime_parser,
int: fromtimestamp,
float: fromtimestamp,
}
26 changes: 26 additions & 0 deletions src/saml2/datetime/asn1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""This module provides a parser for datetime strings in ANS.1 UTCTime format.

The parser produces datetime objects that can be used with the other datetime
modules.
"""

from datetime import datetime as _datetime

import saml2.datetime


_ASN1_UTCTime_FORMAT = '%Y%m%d%H%M%SZ'


def parse(data):
"""Return a datetime object from the given ASN.1 UTCTime formatted string.

The datetime object will be in UTC timezone.

The parameter `data` is expected to be of type: str

The object returned is of type: datetime.datetime
"""
value = _datetime.strptime(data, _ASN1_UTCTime_FORMAT)
value = saml2.datetime.parse(value)
return value
Loading