Skip to content

Commit

Permalink
Merge pull request jupyterhub#4665 from minrk/deprecated-utc
Browse files Browse the repository at this point in the history
avoid deprecated datetime.utcnow
  • Loading branch information
minrk authored Jan 11, 2024
2 parents a11816b + be14baf commit 2f091e5
Show file tree
Hide file tree
Showing 12 changed files with 57 additions and 36 deletions.
8 changes: 6 additions & 2 deletions jupyterhub/alembic/versions/99a28a4418e1_user_created.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@
depends_on = None


from datetime import datetime
from datetime import datetime, timezone

import sqlalchemy as sa
from alembic import op


def utcnow():
return datetime.now(timezone.utc)._replace(tzinfo=None)


def upgrade():
op.add_column('users', sa.Column('created', sa.DateTime, nullable=True))
c = op.get_bind()
# fill created date with current time
now = datetime.utcnow()
now = utcnow()
c.execute(
"""
UPDATE users
Expand Down
5 changes: 2 additions & 3 deletions jupyterhub/apihandlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import json
from datetime import datetime
from unittest import mock
from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse

from oauthlib import oauth2
from tornado import web

from .. import orm, roles, scopes
from ..utils import get_browser_protocol, token_authenticated
from ..utils import get_browser_protocol, token_authenticated, utcnow
from .base import APIHandler, BaseHandler


Expand Down Expand Up @@ -39,7 +38,7 @@ def get(self, token):
self.parsed_scopes = scopes.parse_scopes(self.expanded_scopes)

# record activity whenever we see a token
now = orm_token.last_activity = datetime.utcnow()
now = orm_token.last_activity = utcnow(with_tz=False)
if orm_token.user:
orm_token.user.last_activity = now
model = self.user_model(self.users[orm_token.user])
Expand Down
7 changes: 4 additions & 3 deletions jupyterhub/apihandlers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import asyncio
import inspect
import json
from datetime import datetime, timedelta, timezone
from datetime import timedelta, timezone

from async_generator import aclosing
from dateutil.parser import parse as parse_date
Expand All @@ -23,6 +23,7 @@
maybe_future,
url_escape_path,
url_path_join,
utcnow,
)
from .base import APIHandler

Expand Down Expand Up @@ -367,7 +368,7 @@ def get(self, user_name):
if not user:
raise web.HTTPError(404, "No such user: %s" % user_name)

now = datetime.utcnow()
now = utcnow(with_tz=False)
api_tokens = []

def sort_key(token):
Expand Down Expand Up @@ -843,7 +844,7 @@ def _parse_timestamp(timestamp):
# strip timezone info to naive UTC datetime
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)

now = datetime.utcnow()
now = utcnow(with_tz=False)
if (dt - now) > timedelta(minutes=59):
raise web.HTTPError(
400,
Expand Down
5 changes: 3 additions & 2 deletions jupyterhub/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
subdomain_hook_idna,
subdomain_hook_legacy,
url_path_join,
utcnow,
)

common_aliases = {
Expand Down Expand Up @@ -2093,7 +2094,7 @@ async def init_users(self):
# we don't want to allow user.created to be undefined,
# so initialize it to last_activity (if defined) or now.
if not user.created:
user.created = user.last_activity or datetime.utcnow()
user.created = user.last_activity or utcnow(with_tz=False)
db.commit()

# The allowed_users set and the users in the db are now the same.
Expand Down Expand Up @@ -3273,7 +3274,7 @@ async def update_last_activity(self):
routes = await self.proxy.get_all_routes()
users_count = 0
active_users_count = 0
now = datetime.utcnow()
now = utcnow(with_tz=False)
for prefix, route in routes.items():
route_data = route['data']
if 'user' not in route_data:
Expand Down
7 changes: 4 additions & 3 deletions jupyterhub/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import time
import uuid
import warnings
from datetime import datetime, timedelta
from datetime import timedelta
from http.client import responses
from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse

Expand Down Expand Up @@ -47,6 +47,7 @@
maybe_future,
url_escape_path,
url_path_join,
utcnow,
)

# pattern for the authentication token header
Expand Down Expand Up @@ -293,7 +294,7 @@ def _record_activity(self, obj, timestamp=None):
recorded (bool): True if activity was recorded, False if not.
"""
if timestamp is None:
timestamp = datetime.utcnow()
timestamp = utcnow(with_tz=False)
resolution = self.settings.get("activity_resolution", 0)
if not obj.last_activity or resolution == 0:
self.log.debug("Recording first activity for %s", obj)
Expand Down Expand Up @@ -381,7 +382,7 @@ def get_current_user_token(self):
orm_token = self.get_token()
if orm_token is None:
return None
now = datetime.utcnow()
now = utcnow(with_tz=False)
recorded = self._record_activity(orm_token, now)
if orm_token.user:
# FIXME: scopes should give us better control than this
Expand Down
4 changes: 2 additions & 2 deletions jupyterhub/handlers/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .. import __version__
from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus
from ..scopes import needs_scope
from ..utils import maybe_future, url_escape_path, url_path_join
from ..utils import maybe_future, url_escape_path, url_path_join, utcnow
from .base import BaseHandler


Expand Down Expand Up @@ -484,7 +484,7 @@ async def get(self):
def sort_key(token):
return (token.last_activity or never, token.created or never)

now = datetime.utcnow()
now = utcnow(with_tz=False)

# group oauth client tokens by client id
all_tokens = defaultdict(list)
Expand Down
15 changes: 8 additions & 7 deletions jupyterhub/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import enum
import json
from base64 import decodebytes, encodebytes
from datetime import datetime, timedelta
from datetime import timedelta
from functools import partial

import alembic.command
import alembic.config
Expand Down Expand Up @@ -40,10 +41,10 @@
from sqlalchemy.types import LargeBinary, Text, TypeDecorator
from tornado.log import app_log

from .utils import compare_token, hash_token, new_token, random_port
from .utils import compare_token, hash_token, new_token, random_port, utcnow

# top-level variable for easier mocking in tests
utcnow = datetime.utcnow
utcnow = partial(utcnow, with_tz=False)


class JSONDict(TypeDecorator):
Expand Down Expand Up @@ -278,7 +279,7 @@ def orm_spawners(self):
return {s.name: s for s in self._orm_spawners}

admin = Column(Boolean(create_constraint=False), default=False)
created = Column(DateTime, default=datetime.utcnow)
created = Column(DateTime, default=utcnow)
last_activity = Column(DateTime, nullable=True)

api_tokens = relationship(
Expand Down Expand Up @@ -665,8 +666,8 @@ def owner(self):
session_id = Column(Unicode(255), nullable=True)

# token metadata for bookkeeping
now = datetime.utcnow # for expiry
created = Column(DateTime, default=datetime.utcnow)
now = utcnow # for expiry
created = Column(DateTime, default=utcnow)
expires_at = Column(DateTime, default=None, nullable=True)
last_activity = Column(DateTime)
note = Column(Unicode(1023))
Expand Down Expand Up @@ -855,7 +856,7 @@ class OAuthCode(Expiring, Base):

@staticmethod
def now():
return datetime.utcnow().timestamp()
return utcnow(with_tz=True).timestamp()

@classmethod
def find(cls, db, code):
Expand Down
9 changes: 5 additions & 4 deletions jupyterhub/tests/populate_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
used in test_db.py
"""
from datetime import datetime
from functools import partial

import jupyterhub
from jupyterhub import orm
from jupyterhub.utils import utcnow


def populate_db(url):
Expand Down Expand Up @@ -117,10 +117,11 @@ def populate_db(url):
assert user.created
assert admin.created
# set last_activity
user.last_activity = datetime.utcnow()
now = utcnow().replace(tzinfo=None)
user.last_activity = now
spawner = user.orm_spawners['']
spawner.started = datetime.utcnow()
spawner.last_activity = datetime.utcnow()
spawner.started = now
spawner.last_activity = now
db.commit()


Expand Down
2 changes: 1 addition & 1 deletion jupyterhub/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2444,7 +2444,7 @@ async def test_update_server_activity(app, user, server_name, fresh):
# we use naive utc internally
# initialize last_activity for one named and the default server
for name in ("", "exists"):
user.spawners[name].orm_spawner.last_activity = now.replace(tzinfo=None)
user.spawners[name].orm_spawner.last_activity = internal_now
app.db.commit()

td = timedelta(minutes=1)
Expand Down
7 changes: 4 additions & 3 deletions jupyterhub/tests/test_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
# Distributed under the terms of the Modified BSD License.
import os
import socket
from datetime import datetime, timedelta
from datetime import timedelta
from unittest import mock

import pytest

from .. import crypto, objects, orm, roles
from ..emptyclass import EmptyClass
from ..user import User
from ..utils import utcnow
from .mocking import MockSpawner


Expand Down Expand Up @@ -121,7 +122,7 @@ def test_token_expiry(db):
user = orm.User(name='parker')
db.add(user)
db.commit()
now = datetime.utcnow()
now = utcnow(with_tz=False)
token = user.new_api_token(expires_in=60)
orm_token = orm.APIToken.find(db, token=token)
assert orm_token
Expand Down Expand Up @@ -506,7 +507,7 @@ def test_expiring_api_token(app, user):
assert found is orm_token

with mock.patch.object(
orm.APIToken, 'now', lambda: datetime.utcnow() + timedelta(seconds=60)
orm.APIToken, 'now', lambda: utcnow(with_tz=False) + timedelta(seconds=60)
):
found = orm.APIToken.find(db, token)
assert found is None
Expand Down
9 changes: 6 additions & 3 deletions jupyterhub/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import warnings
from collections import defaultdict
from datetime import datetime, timedelta
from datetime import timedelta
from urllib.parse import quote, urlparse

from sqlalchemy import inspect
Expand All @@ -25,6 +25,7 @@
subdomain_hook_legacy,
url_escape_path,
url_path_join,
utcnow,
)

# detailed messages about the most common failure-to-start errors,
Expand Down Expand Up @@ -757,7 +758,7 @@ async def spawn(self, server_name='', options=None, handler=None):
# update spawner start time, and activity for both spawner and user
self.last_activity = (
spawner.orm_spawner.started
) = spawner.orm_spawner.last_activity = datetime.utcnow()
) = spawner.orm_spawner.last_activity = utcnow(with_tz=False)
db.commit()
# wait for spawner.start to return
# run optional preparation work to bootstrap the notebook
Expand Down Expand Up @@ -964,7 +965,9 @@ async def stop(self, server_name=''):
status = await spawner.poll()
if status is None:
await spawner.stop()
self.last_activity = spawner.orm_spawner.last_activity = datetime.utcnow()
self.last_activity = spawner.orm_spawner.last_activity = utcnow(
with_tz=False
)
# remove server entry from db
spawner.server = None
if not spawner.will_resume:
Expand Down
15 changes: 12 additions & 3 deletions jupyterhub/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,9 +654,18 @@ async def iterate_until(deadline_future, generator):
continue


def utcnow():
"""Return timezone-aware utcnow"""
return datetime.now(timezone.utc)
def utcnow(*, with_tz=True):
"""Return utcnow
with_tz (default): returns tz-aware datetime in UTC
if with_tz=False, returns UTC timestamp without tzinfo
(used for most internal timestamp storage because databases often don't preserve tz info)
"""
now = datetime.now(timezone.utc)
if not with_tz:
now = now.replace(tzinfo=None)
return now


def _parse_accept_header(accept):
Expand Down

0 comments on commit 2f091e5

Please sign in to comment.