Skip to content

Commit ce412c5

Browse files
committed
change to using timezone aware utc_now() functionality in Python 3
All times in SAML are supposed to be "without offset" from UTC, and are typically expressed as ISO8601 formatted strings with a timezone component of "Z". Keep doing that, but use the built-in isoformat() and fromisoformat(), and thoroughly skip microseconds where we might output such times. This removes the need for the external dependency 'iso8601'.
1 parent 5bed935 commit ce412c5

File tree

7 files changed

+58
-38
lines changed

7 files changed

+58
-38
lines changed

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ cachetools
44
gunicorn
55
httplib2 >=0.7.7
66
ipaddr
7-
iso8601 >=0.1.4
87
lxml >=4.1.1
98
mako
109
nose

src/pyff/api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from .repo import MDRepository
2525
from .resource import Resource
2626
from .samlmd import entity_display_name
27-
from .utils import b2u, dumptree, duration2timedelta, hash_id, json_serializer
27+
from .utils import b2u, dumptree, duration2timedelta, hash_id, json_serializer, utc_now
2828

2929
log = get_log(__name__)
3030

@@ -538,8 +538,7 @@ def mkapp(*args, **kwargs):
538538
ctx.add_route('request', '/*path', request_method='GET')
539539
ctx.add_view(request_handler, route_name='request')
540540

541-
start = datetime.utcnow() + timedelta(seconds=1)
542-
log.debug(start)
541+
start = utc_now() + timedelta(seconds=1)
543542
if config.update_frequency > 0:
544543
ctx.registry.scheduler.add_job(
545544
call,

src/pyff/builtins.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
import ipaddr
1818
import six
1919
import xmlsec
20-
from iso8601 import iso8601
2120
from lxml.etree import DocumentInvalid
2221
from six.moves.urllib_parse import quote_plus, urlparse
2322

2423
from pyff.pipes import registry
2524

26-
from .constants import NS, config
25+
from .constants import NS
2726
from .decorators import deprecated
2827
from .exceptions import MetadataException
2928
from .logs import get_log
@@ -42,12 +41,15 @@
4241
sort_entities,
4342
)
4443
from .utils import (
44+
datetime2iso,
4545
dumptree,
4646
duration2timedelta,
4747
hash_id,
48+
iso2datetime,
4849
root,
4950
safe_write,
5051
total_seconds,
52+
utc_now,
5153
validate_document,
5254
with_tree,
5355
xslt_transform,
@@ -1503,7 +1505,7 @@ def finalize(req, *opts):
15031505
if name:
15041506
e.set('Name', name)
15051507

1506-
now = datetime.utcnow()
1508+
now = utc_now()
15071509

15081510
mdid = req.args.get('ID', 'prefix _')
15091511
if re.match('(\s)*prefix(\s)*', mdid):
@@ -1520,17 +1522,16 @@ def finalize(req, *opts):
15201522
offset = duration2timedelta(valid_until)
15211523
if offset is not None:
15221524
dt = now + offset
1523-
e.set('validUntil', dt.strftime("%Y-%m-%dT%H:%M:%SZ"))
1525+
e.set('validUntil', datetime2iso(dt))
15241526
elif valid_until is not None:
15251527
try:
1526-
dt = iso8601.parse_date(valid_until)
1527-
dt = dt.replace(tzinfo=None) # make dt "naive" (tz-unaware)
1528+
dt = iso2datetime(valid_until)
15281529
offset = dt - now
1529-
e.set('validUntil', dt.strftime("%Y-%m-%dT%H:%M:%SZ"))
1530+
e.set('validUntil', datetime2iso(dt))
15301531
except ValueError as ex:
15311532
log.error("Unable to parse validUntil: %s (%s)" % (valid_until, ex))
15321533

1533-
# set a reasonable default: 50% of the validity
1534+
# set a reasonable default: 50% of the validity
15341535
# we replace this below if we have cacheDuration set
15351536
req.state['cache'] = int(total_seconds(offset) / 50)
15361537

src/pyff/parse.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from .constants import NS
77
from .logs import get_log
8-
from .utils import find_matching_files, first_text, parse_xml, root, unicode_stream
8+
from .utils import find_matching_files, parse_xml, root, unicode_stream, utc_now
99

1010
__author__ = 'leifj'
1111

@@ -66,7 +66,7 @@ def parse(self, resource, content):
6666

6767
resource.never_expires = True
6868
resource.expire_time = None
69-
resource.last_seen = datetime.now()
69+
resource.last_seen = utc_now().replace(microsecond=0)
7070

7171
return dict()
7272

@@ -98,7 +98,7 @@ def parse(self, resource, content):
9898
fp = fingerprints[0]
9999
log.debug("XRD: {} verified by {}".format(link_href, fp))
100100
resource.add_child(link_href, verify=fp)
101-
resource.last_seen = datetime.now()
101+
resource.last_seen = utc_now().replace(microsecond=0)
102102
resource.expire_time = None
103103
resource.never_expires = True
104104
return info

src/pyff/resource.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
An abstraction layer for metadata fetchers. Supports both synchronous and asynchronous fetchers with cache.
44
55
"""
6+
from __future__ import annotations
67

78
import os
89
from collections import deque
910
from copy import deepcopy
1011
from datetime import datetime
1112
from threading import Condition, Lock
13+
from typing import Optional
1214

1315
import requests
1416

@@ -17,7 +19,7 @@
1719
from .fetch import make_fetcher
1820
from .logs import get_log
1921
from .parse import parse_resource
20-
from .utils import Watchable, hex_digest, img_to_data, non_blocking_lock, url_get
22+
from .utils import Watchable, hex_digest, img_to_data, non_blocking_lock, url_get, utc_now
2123

2224
requests.packages.urllib3.disable_warnings()
2325

@@ -133,9 +135,9 @@ def __init__(self, url=None, **kwargs):
133135
self.t = None
134136
self.type = "text/plain"
135137
self.etag = None
136-
self.expire_time = None
137-
self.never_expires = False
138-
self.last_seen = None
138+
self.expire_time: Optional[datetime] = None
139+
self.never_expires: bool = False
140+
self.last_seen: Optional[datetime] = None
139141
self.last_parser = None
140142
self._infos = deque(maxlen=config.info_buffer_size)
141143
self.children = deque()
@@ -223,7 +225,7 @@ def walk(self):
223225
def is_expired(self) -> bool:
224226
if self.never_expires:
225227
return False
226-
now = datetime.now()
228+
now = utc_now()
227229
return self.expire_time is not None and self.expire_time < now
228230

229231
def is_valid(self) -> bool:
@@ -301,7 +303,7 @@ def parse(self, getter):
301303
info.update(parse_info)
302304

303305
if self.t is not None:
304-
self.last_seen = datetime.now()
306+
self.last_seen = utc_now().replace(microsecond=0)
305307
if self.post and isinstance(self.post, list):
306308
for cb in self.post:
307309
if self.t is not None:

src/pyff/samlmd.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Lambda,
1818
b2u,
1919
check_signature,
20+
datetime2iso,
2021
dumptree,
2122
duration2timedelta,
2223
filter_lang,
@@ -34,6 +35,7 @@
3435
subdomains,
3536
unicode_stream,
3637
url2host,
38+
utc_now,
3739
validate_document,
3840
xml_error,
3941
)
@@ -172,7 +174,10 @@ def parse(self, resource, content):
172174
)
173175

174176
if expire_time_offset is not None:
175-
expire_time = datetime.now() + expire_time_offset
177+
now = utc_now()
178+
now = now.replace(microsecond=0)
179+
180+
expire_time = now + expire_time_offset
176181
resource.expire_time = expire_time
177182
info['Expiration Time'] = str(expire_time)
178183

@@ -217,13 +222,13 @@ def parse(self, resource, content):
217222
info['NextUpdate'] = next_update
218223
resource.expire_time = iso2datetime(next_update)
219224
elif config.respect_cache_duration:
220-
now = datetime.utcnow()
225+
now = utc_now()
221226
now = now.replace(microsecond=0)
222227
next_update = now + duration2timedelta(config.default_cache_duration)
223228
info['NextUpdate'] = next_update
224229
resource.expire_time = next_update
225230

226-
info['Expiration Time'] = str(resource.expire_time)
231+
info['Expiration Time'] = 'None' if not resource.expire_time else resource.expire_time.isoformat()
227232
info['IssuerName'] = first_text(relt, "{%s}IssuerName" % NS['ser'])
228233
info['SchemeIdentifier'] = first_text(relt, "{%s}SchemeIdentifier" % NS['ser'])
229234
info['SchemeTerritory'] = first_text(relt, "{%s}SchemeTerritory" % NS['ser'])
@@ -266,7 +271,7 @@ def _update_entities(_t, **kwargs):
266271
r.add_via(Lambda(_update_entities, **args))
267272

268273
log.debug("Done parsing eIDAS MetadataServiceList")
269-
resource.last_seen = datetime.now()
274+
resource.last_seen = utc_now().replace(microsecond=0)
270275
resource.expire_time = None
271276
return info
272277

@@ -280,10 +285,9 @@ def metadata_expiration(t):
280285
cache_duration = config.default_cache_duration
281286
valid_until = relt.get('validUntil', None)
282287
if valid_until is not None:
283-
now = datetime.utcnow()
288+
now = utc_now().replace(microsecond=0)
284289
vu = iso2datetime(valid_until)
285-
now = now.replace(microsecond=0)
286-
vu = vu.replace(microsecond=0, tzinfo=None)
290+
vu = vu.replace(microsecond=0)
287291
return vu - now
288292
elif config.respect_cache_duration:
289293
cache_duration = relt.get('cacheDuration', config.default_cache_duration)
@@ -1037,8 +1041,7 @@ def set_pubinfo(e, publisher=None, creation_instant=None):
10371041
raise MetadataException("At least publisher must be provided")
10381042

10391043
if creation_instant is None:
1040-
now = datetime.utcnow()
1041-
creation_instant = now.strftime("%Y-%m-%dT%H:%M:%SZ")
1044+
creation_instant = datetime2iso(utc_now())
10421045

10431046
ext = entity_extensions(e)
10441047
pi = ext.find(".//{%s}PublicationInfo" % NS['mdrpi'])
@@ -1080,10 +1083,8 @@ def expiration(t):
10801083
cache_duration = config.default_cache_duration
10811084
valid_until = relt.get('validUntil', None)
10821085
if valid_until is not None:
1083-
now = datetime.utcnow()
1086+
now = utc_now().replace(microsecond=0)
10841087
vu = iso2datetime(valid_until)
1085-
now = now.replace(microsecond=0)
1086-
vu = vu.replace(microsecond=0, tzinfo=None)
10871088
return vu - now
10881089
elif config.respect_cache_duration:
10891090
cache_duration = relt.get('cacheDuration', config.default_cache_duration)

src/pyff/utils.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from time import gmtime, strftime
2727
from typing import Optional, Union
2828

29-
import iso8601
3029
import pkg_resources
3130
import requests
3231
import xmlsec
@@ -177,8 +176,19 @@ def ts_now() -> int:
177176
return int(time.time())
178177

179178

180-
def iso2datetime(s):
181-
return iso8601.parse_date(s)
179+
def iso2datetime(s: str) -> datetime:
180+
# TODO: All timestamps in SAML are supposed to be without offset from UTC - raise exception if it is not?
181+
if s.endswith('Z'):
182+
s = s[:-1] + '+00:00'
183+
return datetime.fromisoformat(s)
184+
185+
186+
def datetime2iso(dt: datetime) -> str:
187+
s = dt.replace(microsecond=0).isoformat()
188+
# Use 'Z' instead of +00:00 suffix for UTC times
189+
if s.endswith('+00:00'):
190+
s = s[:-6] + 'Z'
191+
return s
182192

183193

184194
def first_text(elt, tag, default=None):
@@ -444,13 +454,16 @@ def valid_until_ts(elt, default_ts: int) -> int:
444454
ts = default_ts
445455
valid_until = elt.get("validUntil", None)
446456
if valid_until is not None:
447-
dt = iso8601.parse_date(valid_until)
457+
try:
458+
dt = datetime.fromtimestamp(valid_until)
459+
except Exception:
460+
dt = None
448461
if dt is not None:
449462
ts = totimestamp(dt)
450463

451464
cache_duration = elt.get("cacheDuration", None)
452465
if cache_duration is not None:
453-
dt = datetime.utcnow() + duration2timedelta(cache_duration)
466+
dt = utc_now() + duration2timedelta(cache_duration)
454467
if dt is not None:
455468
ts = totimestamp(dt)
456469

@@ -951,3 +964,8 @@ def notify(self, *args, **kwargs):
951964
except BaseException as ex:
952965
log.debug(traceback.format_exc())
953966
log.warn(ex)
967+
968+
969+
def utc_now() -> datetime:
970+
""" Return current time with tz=UTC """
971+
return datetime.now(tz=timezone.utc)

0 commit comments

Comments
 (0)