avoid deprecated datetime.utcnow

deprecated in Python 3.12

replace with equivalent utils.utcnow(with_tz=False)
This commit is contained in:
Min RK
2023-12-19 14:47:35 +01:00
parent ab82b8e492
commit be14baf096
12 changed files with 57 additions and 36 deletions

View File

@@ -12,17 +12,21 @@ branch_labels = None
depends_on = None depends_on = None
from datetime import datetime from datetime import datetime, timezone
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
def utcnow():
return datetime.now(timezone.utc)._replace(tzinfo=None)
def upgrade(): def upgrade():
op.add_column('users', sa.Column('created', sa.DateTime, nullable=True)) op.add_column('users', sa.Column('created', sa.DateTime, nullable=True))
c = op.get_bind() c = op.get_bind()
# fill created date with current time # fill created date with current time
now = datetime.utcnow() now = utcnow()
c.execute( c.execute(
""" """
UPDATE users UPDATE users

View File

@@ -2,7 +2,6 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import json import json
from datetime import datetime
from unittest import mock from unittest import mock
from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse
@@ -10,7 +9,7 @@ from oauthlib import oauth2
from tornado import web from tornado import web
from .. import orm, roles, scopes 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 from .base import APIHandler, BaseHandler
@@ -39,7 +38,7 @@ class TokenAPIHandler(APIHandler):
self.parsed_scopes = scopes.parse_scopes(self.expanded_scopes) self.parsed_scopes = scopes.parse_scopes(self.expanded_scopes)
# record activity whenever we see a token # 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: if orm_token.user:
orm_token.user.last_activity = now orm_token.user.last_activity = now
model = self.user_model(self.users[orm_token.user]) model = self.user_model(self.users[orm_token.user])

View File

@@ -4,7 +4,7 @@
import asyncio import asyncio
import inspect import inspect
import json import json
from datetime import datetime, timedelta, timezone from datetime import timedelta, timezone
from async_generator import aclosing from async_generator import aclosing
from dateutil.parser import parse as parse_date from dateutil.parser import parse as parse_date
@@ -23,6 +23,7 @@ from ..utils import (
maybe_future, maybe_future,
url_escape_path, url_escape_path,
url_path_join, url_path_join,
utcnow,
) )
from .base import APIHandler from .base import APIHandler
@@ -367,7 +368,7 @@ class UserTokenListAPIHandler(APIHandler):
if not user: if not user:
raise web.HTTPError(404, "No such user: %s" % user_name) raise web.HTTPError(404, "No such user: %s" % user_name)
now = datetime.utcnow() now = utcnow(with_tz=False)
api_tokens = [] api_tokens = []
def sort_key(token): def sort_key(token):
@@ -843,7 +844,7 @@ def _parse_timestamp(timestamp):
# strip timezone info to naive UTC datetime # strip timezone info to naive UTC datetime
dt = dt.astimezone(timezone.utc).replace(tzinfo=None) dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
now = datetime.utcnow() now = utcnow(with_tz=False)
if (dt - now) > timedelta(minutes=59): if (dt - now) > timedelta(minutes=59):
raise web.HTTPError( raise web.HTTPError(
400, 400,

View File

@@ -96,6 +96,7 @@ from .utils import (
subdomain_hook_idna, subdomain_hook_idna,
subdomain_hook_legacy, subdomain_hook_legacy,
url_path_join, url_path_join,
utcnow,
) )
common_aliases = { common_aliases = {
@@ -2093,7 +2094,7 @@ class JupyterHub(Application):
# we don't want to allow user.created to be undefined, # we don't want to allow user.created to be undefined,
# so initialize it to last_activity (if defined) or now. # so initialize it to last_activity (if defined) or now.
if not user.created: if not user.created:
user.created = user.last_activity or datetime.utcnow() user.created = user.last_activity or utcnow(with_tz=False)
db.commit() db.commit()
# The allowed_users set and the users in the db are now the same. # The allowed_users set and the users in the db are now the same.
@@ -3273,7 +3274,7 @@ class JupyterHub(Application):
routes = await self.proxy.get_all_routes() routes = await self.proxy.get_all_routes()
users_count = 0 users_count = 0
active_users_count = 0 active_users_count = 0
now = datetime.utcnow() now = utcnow(with_tz=False)
for prefix, route in routes.items(): for prefix, route in routes.items():
route_data = route['data'] route_data = route['data']
if 'user' not in route_data: if 'user' not in route_data:

View File

@@ -10,7 +10,7 @@ import re
import time import time
import uuid import uuid
import warnings import warnings
from datetime import datetime, timedelta from datetime import timedelta
from http.client import responses from http.client import responses
from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse from urllib.parse import parse_qs, parse_qsl, urlencode, urlparse, urlunparse
@@ -47,6 +47,7 @@ from ..utils import (
maybe_future, maybe_future,
url_escape_path, url_escape_path,
url_path_join, url_path_join,
utcnow,
) )
# pattern for the authentication token header # pattern for the authentication token header
@@ -293,7 +294,7 @@ class BaseHandler(RequestHandler):
recorded (bool): True if activity was recorded, False if not. recorded (bool): True if activity was recorded, False if not.
""" """
if timestamp is None: if timestamp is None:
timestamp = datetime.utcnow() timestamp = utcnow(with_tz=False)
resolution = self.settings.get("activity_resolution", 0) resolution = self.settings.get("activity_resolution", 0)
if not obj.last_activity or resolution == 0: if not obj.last_activity or resolution == 0:
self.log.debug("Recording first activity for %s", obj) self.log.debug("Recording first activity for %s", obj)
@@ -381,7 +382,7 @@ class BaseHandler(RequestHandler):
orm_token = self.get_token() orm_token = self.get_token()
if orm_token is None: if orm_token is None:
return None return None
now = datetime.utcnow() now = utcnow(with_tz=False)
recorded = self._record_activity(orm_token, now) recorded = self._record_activity(orm_token, now)
if orm_token.user: if orm_token.user:
# FIXME: scopes should give us better control than this # FIXME: scopes should give us better control than this

View File

@@ -14,7 +14,7 @@ from tornado.httputil import url_concat
from .. import __version__ from .. import __version__
from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus from ..metrics import SERVER_POLL_DURATION_SECONDS, ServerPollStatus
from ..scopes import needs_scope 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 from .base import BaseHandler
@@ -484,7 +484,7 @@ class TokenPageHandler(BaseHandler):
def sort_key(token): def sort_key(token):
return (token.last_activity or never, token.created or never) 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 # group oauth client tokens by client id
all_tokens = defaultdict(list) all_tokens = defaultdict(list)

View File

@@ -4,7 +4,8 @@
import enum import enum
import json import json
from base64 import decodebytes, encodebytes from base64 import decodebytes, encodebytes
from datetime import datetime, timedelta from datetime import timedelta
from functools import partial
import alembic.command import alembic.command
import alembic.config import alembic.config
@@ -40,10 +41,10 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.types import LargeBinary, Text, TypeDecorator from sqlalchemy.types import LargeBinary, Text, TypeDecorator
from tornado.log import app_log 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 # top-level variable for easier mocking in tests
utcnow = datetime.utcnow utcnow = partial(utcnow, with_tz=False)
class JSONDict(TypeDecorator): class JSONDict(TypeDecorator):
@@ -278,7 +279,7 @@ class User(Base):
return {s.name: s for s in self._orm_spawners} return {s.name: s for s in self._orm_spawners}
admin = Column(Boolean(create_constraint=False), default=False) 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) last_activity = Column(DateTime, nullable=True)
api_tokens = relationship( api_tokens = relationship(
@@ -665,8 +666,8 @@ class APIToken(Hashed, Base):
session_id = Column(Unicode(255), nullable=True) session_id = Column(Unicode(255), nullable=True)
# token metadata for bookkeeping # token metadata for bookkeeping
now = datetime.utcnow # for expiry now = utcnow # for expiry
created = Column(DateTime, default=datetime.utcnow) created = Column(DateTime, default=utcnow)
expires_at = Column(DateTime, default=None, nullable=True) expires_at = Column(DateTime, default=None, nullable=True)
last_activity = Column(DateTime) last_activity = Column(DateTime)
note = Column(Unicode(1023)) note = Column(Unicode(1023))
@@ -855,7 +856,7 @@ class OAuthCode(Expiring, Base):
@staticmethod @staticmethod
def now(): def now():
return datetime.utcnow().timestamp() return utcnow(with_tz=True).timestamp()
@classmethod @classmethod
def find(cls, db, code): def find(cls, db, code):

View File

@@ -4,11 +4,11 @@ Run with old versions of jupyterhub to test upgrade/downgrade
used in test_db.py used in test_db.py
""" """
from datetime import datetime
from functools import partial from functools import partial
import jupyterhub import jupyterhub
from jupyterhub import orm from jupyterhub import orm
from jupyterhub.utils import utcnow
def populate_db(url): def populate_db(url):
@@ -117,10 +117,11 @@ def populate_db(url):
assert user.created assert user.created
assert admin.created assert admin.created
# set last_activity # set last_activity
user.last_activity = datetime.utcnow() now = utcnow().replace(tzinfo=None)
user.last_activity = now
spawner = user.orm_spawners[''] spawner = user.orm_spawners['']
spawner.started = datetime.utcnow() spawner.started = now
spawner.last_activity = datetime.utcnow() spawner.last_activity = now
db.commit() db.commit()

View File

@@ -2444,7 +2444,7 @@ async def test_update_server_activity(app, user, server_name, fresh):
# we use naive utc internally # we use naive utc internally
# initialize last_activity for one named and the default server # initialize last_activity for one named and the default server
for name in ("", "exists"): 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() app.db.commit()
td = timedelta(minutes=1) td = timedelta(minutes=1)

View File

@@ -3,7 +3,7 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import os import os
import socket import socket
from datetime import datetime, timedelta from datetime import timedelta
from unittest import mock from unittest import mock
import pytest import pytest
@@ -11,6 +11,7 @@ import pytest
from .. import crypto, objects, orm, roles from .. import crypto, objects, orm, roles
from ..emptyclass import EmptyClass from ..emptyclass import EmptyClass
from ..user import User from ..user import User
from ..utils import utcnow
from .mocking import MockSpawner from .mocking import MockSpawner
@@ -121,7 +122,7 @@ def test_token_expiry(db):
user = orm.User(name='parker') user = orm.User(name='parker')
db.add(user) db.add(user)
db.commit() db.commit()
now = datetime.utcnow() now = utcnow(with_tz=False)
token = user.new_api_token(expires_in=60) token = user.new_api_token(expires_in=60)
orm_token = orm.APIToken.find(db, token=token) orm_token = orm.APIToken.find(db, token=token)
assert orm_token assert orm_token
@@ -506,7 +507,7 @@ def test_expiring_api_token(app, user):
assert found is orm_token assert found is orm_token
with mock.patch.object( 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) found = orm.APIToken.find(db, token)
assert found is None assert found is None

View File

@@ -3,7 +3,7 @@
import json import json
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import timedelta
from urllib.parse import quote, urlparse from urllib.parse import quote, urlparse
from sqlalchemy import inspect from sqlalchemy import inspect
@@ -25,6 +25,7 @@ from .utils import (
subdomain_hook_legacy, subdomain_hook_legacy,
url_escape_path, url_escape_path,
url_path_join, url_path_join,
utcnow,
) )
# detailed messages about the most common failure-to-start errors, # detailed messages about the most common failure-to-start errors,
@@ -757,7 +758,7 @@ class User:
# update spawner start time, and activity for both spawner and user # update spawner start time, and activity for both spawner and user
self.last_activity = ( self.last_activity = (
spawner.orm_spawner.started spawner.orm_spawner.started
) = spawner.orm_spawner.last_activity = datetime.utcnow() ) = spawner.orm_spawner.last_activity = utcnow(with_tz=False)
db.commit() db.commit()
# wait for spawner.start to return # wait for spawner.start to return
# run optional preparation work to bootstrap the notebook # run optional preparation work to bootstrap the notebook
@@ -964,7 +965,9 @@ class User:
status = await spawner.poll() status = await spawner.poll()
if status is None: if status is None:
await spawner.stop() 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 # remove server entry from db
spawner.server = None spawner.server = None
if not spawner.will_resume: if not spawner.will_resume:

View File

@@ -654,9 +654,18 @@ async def iterate_until(deadline_future, generator):
continue continue
def utcnow(): def utcnow(*, with_tz=True):
"""Return timezone-aware utcnow""" """Return utcnow
return datetime.now(timezone.utc)
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): def _parse_accept_header(accept):