indicate that REST API timestamps are UTC

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
This commit is contained in:
Min RK
2018-03-21 14:18:02 +01:00
parent 05b2bf4c96
commit fd40e27be4
7 changed files with 25 additions and 15 deletions

View File

@@ -8,7 +8,7 @@ import json
from urllib.parse import quote from urllib.parse import quote
from oauth2.web.tornado import OAuth2Handler from oauth2.web.tornado import OAuth2Handler
from tornado import web, gen from tornado import web
from .. import orm from .. import orm
from ..user import User from ..user import User

View File

@@ -10,7 +10,7 @@ from tornado import web
from .. import orm from .. import orm
from ..handlers import BaseHandler from ..handlers import BaseHandler
from ..utils import url_path_join from ..utils import isoformat, url_path_join
class APIHandler(BaseHandler): class APIHandler(BaseHandler):
@@ -103,7 +103,7 @@ class APIHandler(BaseHandler):
last_activity = user.last_activity last_activity = user.last_activity
# don't call isoformat if last_activity is None # don't call isoformat if last_activity is None
if last_activity: if last_activity:
last_activity = last_activity.isoformat() last_activity = isoformat(last_activity)
model = { model = {
'kind': 'user', 'kind': 'user',
@@ -122,7 +122,7 @@ class APIHandler(BaseHandler):
for name, spawner in user.spawners.items(): for name, spawner in user.spawners.items():
last_activity = spawner.orm_spawner.last_activity last_activity = spawner.orm_spawner.last_activity
if last_activity: if last_activity:
last_activity = last_activity.isoformat() last_activity = isoformat(last_activity)
if spawner.ready: if spawner.ready:
servers[name] = s = { servers[name] = s = {
'name': name, 'name': name,

View File

@@ -8,7 +8,7 @@ import asyncio
import atexit import atexit
import binascii import binascii
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from datetime import datetime from datetime import datetime, timezone
from getpass import getuser from getpass import getuser
import logging import logging
from operator import itemgetter from operator import itemgetter
@@ -22,8 +22,8 @@ from urllib.parse import urlparse
if sys.version_info[:2] < (3, 3): if sys.version_info[:2] < (3, 3):
raise ValueError("Python < 3.3 not supported: %s" % sys.version) 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 jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import AsyncHTTPClient
@@ -50,7 +50,7 @@ from .services.service import Service
from . import crypto from . import crypto
from . import dbutil, orm from . import dbutil, orm
from .user import User, UserDict from .user import UserDict
from .oauth.store import make_provider from .oauth.store import make_provider
from ._data import DATA_FILES_PATH from ._data import DATA_FILES_PATH
from .log import CoroutineLogFormatter, log_request from .log import CoroutineLogFormatter, log_request
@@ -59,7 +59,6 @@ from .traitlets import URLPrefix, Command
from .utils import ( from .utils import (
maybe_future, maybe_future,
url_path_join, url_path_join,
ISO8601_ms, ISO8601_s,
print_stacks, print_ps_info, print_stacks, print_ps_info,
) )
# classes for config # classes for config
@@ -1581,10 +1580,11 @@ class JupyterHub(Application):
if spawner is None: if spawner is None:
self.log.warning("Found no spawner for route: %s", route) self.log.warning("Found no spawner for route: %s", route)
continue continue
try: dt = parse_date(route_data['last_activity'])
dt = datetime.strptime(route_data['last_activity'], ISO8601_ms) if dt.tzinfo:
except Exception: # strip timezone info to naïve UTC datetime
dt = datetime.strptime(route_data['last_activity'], ISO8601_s) dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
if user.last_activity: if user.last_activity:
user.last_activity = max(user.last_activity, dt) user.last_activity = max(user.last_activity, dt)
else: else:

View File

@@ -26,7 +26,7 @@ from ..spawner import LocalProcessSpawner
from ..utils import maybe_future, url_path_join from ..utils import maybe_future, url_path_join
from ..metrics import ( from ..metrics import (
SERVER_SPAWN_DURATION_SECONDS, ServerSpawnStatus, SERVER_SPAWN_DURATION_SECONDS, ServerSpawnStatus,
PROXY_ADD_DURATION_SECONDS, ProxyAddStatus PROXY_ADD_DURATION_SECONDS, ProxyAddStatus,
) )
# pattern for the authentication token header # pattern for the authentication token header

View File

@@ -10,7 +10,6 @@ from oauth2.error import ClientNotFoundError
from sqlalchemy import inspect from sqlalchemy import inspect
from tornado import gen from tornado import gen
from tornado.log import app_log from tornado.log import app_log
from traitlets import HasTraits, Any, Dict, default
from .utils import maybe_future, url_path_join from .utils import maybe_future, url_path_join

View File

@@ -6,6 +6,7 @@
import asyncio import asyncio
from binascii import b2a_hex from binascii import b2a_hex
import concurrent.futures import concurrent.futures
from datetime import datetime, timezone
import random import random
import errno import errno
import hashlib import hashlib
@@ -15,7 +16,6 @@ import os
import socket import socket
import sys import sys
import threading import threading
from threading import Thread
import uuid import uuid
import warnings import warnings
@@ -39,6 +39,16 @@ ISO8601_ms = '%Y-%m-%dT%H:%M:%S.%fZ'
ISO8601_s = '%Y-%m-%dT%H:%M:%SZ' 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): def can_connect(ip, port):
"""Check if we can connect to an ip:port. """Check if we can connect to an ip:port.

View File

@@ -4,6 +4,7 @@ tornado>=4.1
jinja2 jinja2
pamela pamela
python-oauth2>=1.0 python-oauth2>=1.0
python-dateutil
SQLAlchemy>=1.1 SQLAlchemy>=1.1
requests requests
prometheus_client>=0.0.21 prometheus_client>=0.0.21