From fd40e27be48860417fba4ed11e2672adc82ab7ea Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 21 Mar 2018 14:18:02 +0100 Subject: [PATCH] indicate that REST API timestamps are UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use iso8601 Z suffix for UTC timestamps use dateutil to parse dates from proxy, as well even though CHP uses iso8601 UTC timestamps, we no longer assume CHP, so use more general parsing in our db we are stuck with naïve datetime objects, so use those internally. But ensure we put 'Z' on timestamps we ship externally --- jupyterhub/apihandlers/auth.py | 2 +- jupyterhub/apihandlers/base.py | 6 +++--- jupyterhub/app.py | 16 ++++++++-------- jupyterhub/handlers/base.py | 2 +- jupyterhub/user.py | 1 - jupyterhub/utils.py | 12 +++++++++++- requirements.txt | 1 + 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 839fcc4a..e5b55665 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -8,7 +8,7 @@ import json from urllib.parse import quote from oauth2.web.tornado import OAuth2Handler -from tornado import web, gen +from tornado import web from .. import orm from ..user import User diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index 8223f35c..329d6eb7 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -10,7 +10,7 @@ from tornado import web from .. import orm from ..handlers import BaseHandler -from ..utils import url_path_join +from ..utils import isoformat, url_path_join class APIHandler(BaseHandler): @@ -103,7 +103,7 @@ class APIHandler(BaseHandler): last_activity = user.last_activity # don't call isoformat if last_activity is None if last_activity: - last_activity = last_activity.isoformat() + last_activity = isoformat(last_activity) model = { 'kind': 'user', @@ -122,7 +122,7 @@ class APIHandler(BaseHandler): for name, spawner in user.spawners.items(): last_activity = spawner.orm_spawner.last_activity if last_activity: - last_activity = last_activity.isoformat() + last_activity = isoformat(last_activity) if spawner.ready: servers[name] = s = { 'name': name, diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 1b5ce1e0..31ce2fb1 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -8,7 +8,7 @@ import asyncio import atexit import binascii from concurrent.futures import ThreadPoolExecutor -from datetime import datetime +from datetime import datetime, timezone from getpass import getuser import logging from operator import itemgetter @@ -22,8 +22,8 @@ from urllib.parse import urlparse if sys.version_info[:2] < (3, 3): raise ValueError("Python < 3.3 not supported: %s" % sys.version) +from dateutil.parser import parse as parse_date from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader - from sqlalchemy.exc import OperationalError from tornado.httpclient import AsyncHTTPClient @@ -50,7 +50,7 @@ from .services.service import Service from . import crypto from . import dbutil, orm -from .user import User, UserDict +from .user import UserDict from .oauth.store import make_provider from ._data import DATA_FILES_PATH from .log import CoroutineLogFormatter, log_request @@ -59,7 +59,6 @@ from .traitlets import URLPrefix, Command from .utils import ( maybe_future, url_path_join, - ISO8601_ms, ISO8601_s, print_stacks, print_ps_info, ) # classes for config @@ -1581,10 +1580,11 @@ class JupyterHub(Application): if spawner is None: self.log.warning("Found no spawner for route: %s", route) continue - try: - dt = datetime.strptime(route_data['last_activity'], ISO8601_ms) - except Exception: - dt = datetime.strptime(route_data['last_activity'], ISO8601_s) + dt = parse_date(route_data['last_activity']) + if dt.tzinfo: + # strip timezone info to naïve UTC datetime + dt = dt.astimezone(timezone.utc).replace(tzinfo=None) + if user.last_activity: user.last_activity = max(user.last_activity, dt) else: diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index b5713766..fdd01e86 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -26,7 +26,7 @@ from ..spawner import LocalProcessSpawner from ..utils import maybe_future, url_path_join from ..metrics import ( SERVER_SPAWN_DURATION_SECONDS, ServerSpawnStatus, - PROXY_ADD_DURATION_SECONDS, ProxyAddStatus + PROXY_ADD_DURATION_SECONDS, ProxyAddStatus, ) # pattern for the authentication token header diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 598047cf..681ba8ce 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -10,7 +10,6 @@ from oauth2.error import ClientNotFoundError from sqlalchemy import inspect from tornado import gen from tornado.log import app_log -from traitlets import HasTraits, Any, Dict, default from .utils import maybe_future, url_path_join diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 21cb7ab6..6545ba6d 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -6,6 +6,7 @@ import asyncio from binascii import b2a_hex import concurrent.futures +from datetime import datetime, timezone import random import errno import hashlib @@ -15,7 +16,6 @@ import os import socket import sys import threading -from threading import Thread import uuid import warnings @@ -39,6 +39,16 @@ ISO8601_ms = '%Y-%m-%dT%H:%M:%S.%fZ' ISO8601_s = '%Y-%m-%dT%H:%M:%SZ' +def isoformat(dt): + """Render a datetime object as an ISO 8601 UTC timestamp + + Naïve datetime objects are assumed to be UTC + """ + if dt.tzinfo: + dt = dt.astimezone(timezone.utc).replace(tzinfo=None) + return dt.isoformat() + 'Z' + + def can_connect(ip, port): """Check if we can connect to an ip:port. diff --git a/requirements.txt b/requirements.txt index 7a71f61a..a3a47065 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ tornado>=4.1 jinja2 pamela python-oauth2>=1.0 +python-dateutil SQLAlchemy>=1.1 requests prometheus_client>=0.0.21