From be14baf096379a75eb1d572ea262d398af511c42 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 19 Dec 2023 14:47:35 +0100 Subject: [PATCH] avoid deprecated datetime.utcnow deprecated in Python 3.12 replace with equivalent utils.utcnow(with_tz=False) --- .../alembic/versions/99a28a4418e1_user_created.py | 8 ++++++-- jupyterhub/apihandlers/auth.py | 5 ++--- jupyterhub/apihandlers/users.py | 7 ++++--- jupyterhub/app.py | 5 +++-- jupyterhub/handlers/base.py | 7 ++++--- jupyterhub/handlers/pages.py | 4 ++-- jupyterhub/orm.py | 15 ++++++++------- jupyterhub/tests/populate_db.py | 9 +++++---- jupyterhub/tests/test_api.py | 2 +- jupyterhub/tests/test_orm.py | 7 ++++--- jupyterhub/user.py | 9 ++++++--- jupyterhub/utils.py | 15 ++++++++++++--- 12 files changed, 57 insertions(+), 36 deletions(-) diff --git a/jupyterhub/alembic/versions/99a28a4418e1_user_created.py b/jupyterhub/alembic/versions/99a28a4418e1_user_created.py index 42ca4b0b17..fc2fca66f3 100644 --- a/jupyterhub/alembic/versions/99a28a4418e1_user_created.py +++ b/jupyterhub/alembic/versions/99a28a4418e1_user_created.py @@ -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 diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index 077fa341a5..6a47bf0fcc 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -2,7 +2,6 @@ # 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 @@ -10,7 +9,7 @@ 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 @@ -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]) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 95eb029840..03e91ad5d8 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -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 @@ -23,6 +23,7 @@ maybe_future, url_escape_path, url_path_join, + utcnow, ) from .base import APIHandler @@ -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): @@ -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, diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 9ac4c81d19..64be37c9d8 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -96,6 +96,7 @@ subdomain_hook_idna, subdomain_hook_legacy, url_path_join, + utcnow, ) common_aliases = { @@ -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. @@ -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: diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 353786cbcb..ed86068f50 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -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 @@ -47,6 +47,7 @@ maybe_future, url_escape_path, url_path_join, + utcnow, ) # pattern for the authentication token header @@ -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) @@ -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 diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 76fef7d7dc..2ab13539a8 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -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 @@ -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) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index e523ed2db5..1076653d8f 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -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 @@ -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): @@ -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( @@ -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)) @@ -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): diff --git a/jupyterhub/tests/populate_db.py b/jupyterhub/tests/populate_db.py index 33b5247b80..4d4697ad3b 100644 --- a/jupyterhub/tests/populate_db.py +++ b/jupyterhub/tests/populate_db.py @@ -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): @@ -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() diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index a9f5ebf399..39e9c0b7b9 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -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) diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 8f3d562e1c..b953d3f909 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -3,7 +3,7 @@ # 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 @@ -11,6 +11,7 @@ from .. import crypto, objects, orm, roles from ..emptyclass import EmptyClass from ..user import User +from ..utils import utcnow from .mocking import MockSpawner @@ -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 @@ -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 diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 0ba868a910..ea0eca6676 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -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 @@ -25,6 +25,7 @@ subdomain_hook_legacy, url_escape_path, url_path_join, + utcnow, ) # detailed messages about the most common failure-to-start errors, @@ -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 @@ -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: diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index a52032839a..426fd30c45 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -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):