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