Skip to content

Commit 6ce938c

Browse files
authored
Merge pull request #215 from IdentityPython/ft-time_handling_fixes
time handling fixes
2 parents 4bfb809 + 93aae3a commit 6ce938c

File tree

10 files changed

+183
-65
lines changed

10 files changed

+183
-65
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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import threading
33
from datetime import datetime, timedelta
44
from json import dumps
5+
from typing import Any, List, Mapping
56

67
import pkg_resources
78
import pyramid.httpexceptions as exc
@@ -21,8 +22,9 @@
2122
from .logs import get_log
2223
from .pipes import plumbing
2324
from .repo import MDRepository
25+
from .resource import Resource
2426
from .samlmd import entity_display_name
25-
from .utils import b2u, dumptree, duration2timedelta, hash_id, json_serializer
27+
from .utils import b2u, dumptree, duration2timedelta, hash_id, json_serializer, utc_now
2628

2729
log = get_log(__name__)
2830

@@ -387,7 +389,7 @@ def resources_handler(request):
387389
:return: a JSON representation of the set of resources currently loaded by the server
388390
"""
389391

390-
def _info(r):
392+
def _info(r: Resource) -> List[Mapping[str, Any]]:
391393
nfo = r.info
392394
nfo['Valid'] = r.is_valid()
393395
nfo['Parser'] = r.last_parser
@@ -407,7 +409,7 @@ def _info(r):
407409

408410
def pipeline_handler(request):
409411
"""
410-
Implements the /api/resources endpoint
412+
Implements the /api/pipeline endpoint
411413
412414
:param request: the HTTP request
413415
:return: a JSON representation of the active pipeline
@@ -536,8 +538,7 @@ def mkapp(*args, **kwargs):
536538
ctx.add_route('request', '/*path', request_method='GET')
537539
ctx.add_view(request_handler, route_name='request')
538540

539-
start = datetime.utcnow() + timedelta(seconds=1)
540-
log.debug(start)
541+
start = utc_now() + timedelta(seconds=1)
541542
if config.update_frequency > 0:
542543
ctx.registry.scheduler.add_job(
543544
call,

src/pyff/builtins.py

Lines changed: 11 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,18 +1522,19 @@ 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:
1527+
# TODO: if validUntil was not present, valid_until will be the string 'None' here - never the literal None
15251528
try:
1526-
dt = iso8601.parse_date(valid_until)
1527-
dt = dt.replace(tzinfo=None) # make dt "naive" (tz-unaware)
1529+
dt = iso2datetime(valid_until)
15281530
offset = dt - now
1529-
e.set('validUntil', dt.strftime("%Y-%m-%dT%H:%M:%SZ"))
1531+
e.set('validUntil', datetime2iso(dt))
15301532
except ValueError as ex:
15311533
log.error("Unable to parse validUntil: %s (%s)" % (valid_until, ex))
15321534

1533-
# set a reasonable default: 50% of the validity
1535+
# set a reasonable default: 50% of the validity
15341536
# we replace this below if we have cacheDuration set
1537+
# TODO: offset can be None here, if validUntil is not a valid duration or ISO date
15351538
req.state['cache'] = int(total_seconds(offset) / 50)
15361539

15371540
cache_duration = req.args.get('cacheDuration', e.get('cacheDuration', None))

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/pipes.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
Pipes and plumbing. Plumbing instances are sequences of pipes. Each pipe is called in order to load, select,
33
transform, sign or output SAML metadata.
44
"""
5+
from __future__ import annotations
56

67
import os
78
import traceback
9+
from typing import Any, Dict, Optional
810

911
import yaml
12+
from apscheduler.schedulers.background import BackgroundScheduler
1013

1114
from .logs import get_log
15+
from .repo import MDRepository
1216
from .utils import PyffException, is_text, resource_string
1317

1418
log = get_log(__name__)
@@ -202,7 +206,16 @@ class Request(object):
202206
"""
203207

204208
def __init__(
205-
self, pl, md, t=None, name=None, args=None, state=None, store=None, scheduler=None, raise_exceptions=True
209+
self,
210+
pl: Plumbing,
211+
md: MDRepository,
212+
t=None,
213+
name=None,
214+
args=None,
215+
state: Optional[Dict[str, Any]] = None,
216+
store=None,
217+
scheduler: Optional[BackgroundScheduler] = None,
218+
raise_exceptions: bool = True,
206219
):
207220
if not state:
208221
state = dict()
@@ -313,7 +326,7 @@ def process(self, md, args=None, state=None, t=None, store=None, raise_exception
313326
).process(self)
314327

315328

316-
def plumbing(fn):
329+
def plumbing(fn: str) -> Plumbing:
317330
"""
318331
Create a new plumbing instance by parsing yaml from the filename.
319332

src/pyff/resource.py

Lines changed: 11 additions & 9 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()
@@ -220,13 +222,13 @@ def walk(self):
220222
for cn in c.walk():
221223
yield cn
222224

223-
def is_expired(self):
225+
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

229-
def is_valid(self):
231+
def is_valid(self) -> bool:
230232
return not self.is_expired() and self.last_seen is not None and self.last_parser is not None
231233

232234
def add_info(self, info):
@@ -239,7 +241,7 @@ def _replace(self, r):
239241
return
240242
raise ValueError("Resource {} not present - use add_child".format(r.url))
241243

242-
def add_child(self, url, **kwargs):
244+
def add_child(self, url: str, **kwargs) -> Resource:
243245
opts = deepcopy(self.opts)
244246
if 'as' in opts:
245247
del opts['as']
@@ -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)

0 commit comments

Comments
 (0)