diff --git a/CHANGELOG.md b/CHANGELOG.md index 916c550..648dd7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,6 @@ All notable changes to WuttJamaican will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.25.0 (2025-12-15) - -### Feat - -- drop timezone, assume UTC for all datetime values in DB -- add `make_utc()` function, app method - ## v0.24.1 (2025-10-29) ### Fix diff --git a/pyproject.toml b/pyproject.toml index fec7d85..7f9906b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttJamaican" -version = "0.25.0" +version = "0.24.1" description = "Base package for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@wuttaproject.org"}] diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 0056cb8..c10e6bc 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -39,7 +39,6 @@ from wuttjamaican.util import ( load_object, make_title, make_full_name, - make_utc, make_uuid, make_true_uuid, progress_loop, @@ -527,14 +526,6 @@ class AppHandler: # pylint: disable=too-many-public-methods """ return make_full_name(*parts) - def make_utc(self, dt=None, tzinfo=False): - """ - This returns a datetime local to the UTC timezone. It is a - convenience wrapper around - :func:`~wuttjamaican.util.make_utc()`. - """ - return make_utc(dt=dt, tzinfo=tzinfo) - def make_true_uuid(self): """ Generate a new UUID value. @@ -862,8 +853,7 @@ class AppHandler: # pylint: disable=too-many-public-methods :returns: Text to display. """ - # TODO: this now assumes naive UTC value incoming... - return humanize.naturaltime(value, when=self.make_utc(tzinfo=False)) + return humanize.naturaltime(value) ############################## # getters for other handlers diff --git a/src/wuttjamaican/batch.py b/src/wuttjamaican/batch.py index 733c48b..54c9ad7 100644 --- a/src/wuttjamaican/batch.py +++ b/src/wuttjamaican/batch.py @@ -24,6 +24,7 @@ Batch Handlers """ +import datetime import os import shutil @@ -480,7 +481,7 @@ class BatchHandler(GenericHandler): # pylint: disable=too-many-public-methods result = self.execute( # pylint: disable=assignment-from-none batch, user=user, progress=progress, **kwargs ) - batch.executed = self.app.make_utc() + batch.executed = datetime.datetime.now() batch.executed_by = user return result diff --git a/src/wuttjamaican/db/alembic/versions/b59a34266288_drop_time_zones.py b/src/wuttjamaican/db/alembic/versions/b59a34266288_drop_time_zones.py deleted file mode 100644 index c78e547..0000000 --- a/src/wuttjamaican/db/alembic/versions/b59a34266288_drop_time_zones.py +++ /dev/null @@ -1,186 +0,0 @@ -"""drop time zones - -Revision ID: b59a34266288 -Revises: efdcb2c75034 -Create Date: 2025-12-14 19:10:11.627188 - -""" - -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import wuttjamaican.db.util -from sqlalchemy.dialects import postgresql -from wuttjamaican.util import make_utc - -# revision identifiers, used by Alembic. -revision: str = "b59a34266288" -down_revision: Union[str, None] = "efdcb2c75034" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # upgrade.created - op.add_column("upgrade", sa.Column("created_new", sa.DateTime(), nullable=True)) - upgrade = sa.sql.table( - "upgrade", - sa.sql.column("uuid"), - sa.sql.column("created"), - sa.sql.column("created_new"), - ) - cursor = op.get_bind().execute(upgrade.select()) - for row in cursor.fetchall(): - op.get_bind().execute( - upgrade.update() - .where(upgrade.c.uuid == row.uuid) - .values({"created_new": make_utc(row.created)}) - ) - op.drop_column("upgrade", "created") - op.alter_column( - "upgrade", - "created_new", - new_column_name="created", - nullable=False, - existing_type=sa.DateTime(), - existing_nullable=True, - ) - - # upgrade.executed - op.add_column("upgrade", sa.Column("executed_new", sa.DateTime(), nullable=True)) - upgrade = sa.sql.table( - "upgrade", - sa.sql.column("uuid"), - sa.sql.column("executed"), - sa.sql.column("executed_new"), - ) - cursor = op.get_bind().execute(upgrade.select()) - for row in cursor.fetchall(): - if row.executed: - op.get_bind().execute( - upgrade.update() - .where(upgrade.c.uuid == row.uuid) - .values({"executed_new": make_utc(row.executed)}) - ) - op.drop_column("upgrade", "executed") - op.alter_column( - "upgrade", - "executed_new", - new_column_name="executed", - existing_type=sa.DateTime(), - existing_nullable=True, - ) - - # user_api_token.created - op.add_column( - "user_api_token", sa.Column("created_new", sa.DateTime(), nullable=True) - ) - user_api_token = sa.sql.table( - "user_api_token", - sa.sql.column("uuid"), - sa.sql.column("created"), - sa.sql.column("created_new"), - ) - cursor = op.get_bind().execute(user_api_token.select()) - for row in cursor.fetchall(): - op.get_bind().execute( - user_api_token.update() - .where(user_api_token.c.uuid == row.uuid) - .values({"created_new": make_utc(row.created)}) - ) - op.drop_column("user_api_token", "created") - op.alter_column( - "user_api_token", - "created_new", - new_column_name="created", - nullable=False, - existing_type=sa.DateTime(), - existing_nullable=True, - ) - - -def downgrade() -> None: - - # user_api_token.created - op.add_column( - "user_api_token", - sa.Column("created_old", sa.DateTime(timezone=True), nullable=True), - ) - user_api_token = sa.sql.table( - "user_api_token", - sa.sql.column("uuid"), - sa.sql.column("created"), - sa.sql.column("created_old"), - ) - cursor = op.get_bind().execute(user_api_token.select()) - for row in cursor.fetchall(): - op.get_bind().execute( - user_api_token.update() - .where(user_api_token.c.uuid == row.uuid) - .values({"created_old": row.created}) - ) - op.drop_column("user_api_token", "created") - op.alter_column( - "user_api_token", - "created_old", - new_column_name="created", - nullable=False, - existing_type=sa.DateTime(timezone=True), - existing_nullable=True, - ) - - # upgrade.executed - op.add_column( - "upgrade", sa.Column("executed_old", sa.DateTime(timezone=True), nullable=True) - ) - upgrade = sa.sql.table( - "upgrade", - sa.sql.column("uuid"), - sa.sql.column("executed"), - sa.sql.column("executed_old"), - ) - cursor = op.get_bind().execute(upgrade.select()) - for row in cursor.fetchall(): - if row.executed: - op.get_bind().execute( - upgrade.update() - .where(upgrade.c.uuid == row.uuid) - .values({"executed_old": row.executed}) - ) - op.drop_column("upgrade", "executed") - op.alter_column( - "upgrade", - "executed_old", - new_column_name="executed", - existing_type=sa.DateTime(timezone=True), - existing_nullable=True, - ) - - # upgrade.created - op.add_column( - "upgrade", sa.Column("created_old", sa.DateTime(timezone=True), nullable=True) - ) - upgrade = sa.sql.table( - "upgrade", - sa.sql.column("uuid"), - sa.sql.column("created"), - sa.sql.column("created_old"), - ) - cursor = op.get_bind().execute(upgrade.select()) - for row in cursor.fetchall(): - op.get_bind().execute( - upgrade.update() - .where(upgrade.c.uuid == row.uuid) - .values({"created_old": row.created}) - ) - op.drop_column("upgrade", "created") - op.alter_column( - "upgrade", - "created_old", - new_column_name="created", - nullable=False, - existing_type=sa.DateTime(timezone=True), - existing_nullable=True, - ) diff --git a/src/wuttjamaican/db/model/auth.py b/src/wuttjamaican/db/model/auth.py index 2a48369..5bb4c45 100644 --- a/src/wuttjamaican/db/model/auth.py +++ b/src/wuttjamaican/db/model/auth.py @@ -39,13 +39,14 @@ So a user's permissions are "inherited" from the role(s) to which they belong. """ +import datetime + import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db.util import uuid_column, uuid_fk_column from wuttjamaican.db.model.base import Base -from wuttjamaican.util import make_utc class Role(Base): # pylint: disable=too-few-public-methods @@ -342,9 +343,9 @@ class UserAPIToken(Base): # pylint: disable=too-few-public-methods ) created = sa.Column( - sa.DateTime(), + sa.DateTime(timezone=True), nullable=False, - default=make_utc, + default=datetime.datetime.now, doc=""" Date/time when the token was created. """, diff --git a/src/wuttjamaican/db/model/batch.py b/src/wuttjamaican/db/model/batch.py index c9ab097..ad2ef3c 100644 --- a/src/wuttjamaican/db/model/batch.py +++ b/src/wuttjamaican/db/model/batch.py @@ -34,7 +34,6 @@ from sqlalchemy.ext.orderinglist import ordering_list from wuttjamaican.db.model.base import uuid_column from wuttjamaican.db.model.auth import User from wuttjamaican.db.util import UUID -from wuttjamaican.util import make_utc class BatchMixin: @@ -224,7 +223,9 @@ class BatchMixin: status_code = sa.Column(sa.Integer(), nullable=True) status_text = sa.Column(sa.String(length=255), nullable=True) - created = sa.Column(sa.DateTime(), nullable=False, default=make_utc) + created = sa.Column( + sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now + ) created_by_uuid = sa.Column(UUID(), nullable=False) @declared_attr @@ -237,7 +238,7 @@ class BatchMixin: cascade_backrefs=False, ) - executed = sa.Column(sa.DateTime(), nullable=True) + executed = sa.Column(sa.DateTime(timezone=True), nullable=True) executed_by_uuid = sa.Column(UUID(), nullable=True) @declared_attr @@ -427,5 +428,8 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods status_text = sa.Column(sa.String(length=255), nullable=True) modified = sa.Column( - sa.DateTime(), nullable=True, default=make_utc, onupdate=make_utc + sa.DateTime(timezone=True), + nullable=True, + default=datetime.datetime.now, + onupdate=datetime.datetime.now, ) diff --git a/src/wuttjamaican/db/model/upgrades.py b/src/wuttjamaican/db/model/upgrades.py index ae035bb..7c94d3f 100644 --- a/src/wuttjamaican/db/model/upgrades.py +++ b/src/wuttjamaican/db/model/upgrades.py @@ -24,13 +24,14 @@ Upgrade Model """ +import datetime + import sqlalchemy as sa from sqlalchemy import orm from wuttjamaican.enum import UpgradeStatus from wuttjamaican.db.util import uuid_column, uuid_fk_column from wuttjamaican.db.model.base import Base -from wuttjamaican.util import make_utc class Upgrade(Base): # pylint: disable=too-few-public-methods @@ -43,9 +44,9 @@ class Upgrade(Base): # pylint: disable=too-few-public-methods uuid = uuid_column() created = sa.Column( - sa.DateTime(), + sa.DateTime(timezone=True), nullable=False, - default=make_utc, + default=datetime.datetime.now, doc=""" When the upgrade record was created. """, @@ -97,7 +98,7 @@ class Upgrade(Base): # pylint: disable=too-few-public-methods ) executed = sa.Column( - sa.DateTime(), + sa.DateTime(timezone=True), nullable=True, doc=""" When the upgrade was executed. diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index fd63ff2..6c03e2b 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -24,7 +24,6 @@ WuttJamaican - utilities """ -import datetime import importlib import logging import os @@ -190,50 +189,6 @@ def make_full_name(*parts): return " ".join(parts) -def make_utc(dt=None, tzinfo=False): - """ - This returns a datetime local to the UTC timezone. By default it - will be a *naive* datetime; the common use case is to convert as - needed for sake of writing to the database. - - See also the shortcut - :meth:`~wuttjamaican.app.AppHandler.make_utc()` method on the app - handler. - - :param dt: Optional :class:`python:datetime.datetime` instance. - If not specified, the current time will be used. - - :param tzinfo: Boolean indicating whether the return value should - have its :attr:`~python:datetime.datetime.tzinfo` attribute - set. This is false by default in which case the return value - will be naive. - - :returns: :class:`python:datetime.datetime` instance local to UTC. - """ - # use current time if none provided - if dt is None: - now = datetime.datetime.now(datetime.timezone.utc) - if tzinfo: - return now - return now.replace(tzinfo=None) - - # otherwise may need to convert timezone - if dt.tzinfo: - if dt.tzinfo is not datetime.timezone.utc: - dt = dt.astimezone(datetime.timezone.utc) - if tzinfo: - return dt - return dt.replace(tzinfo=None) - - # naive value returned as-is.. - if not tzinfo: - return dt - - # ..unless tzinfo is wanted, in which case this assumes naive - # value is in the UTC timezone - return dt.replace(tzinfo=datetime.timezone.utc) - - def make_true_uuid(): """ Generate a new v7 UUID value. diff --git a/tests/test_app.py b/tests/test_app.py index 936d79d..2a0c89e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -426,11 +426,6 @@ app_title = WuttaTest name = self.app.make_full_name("Fred", "", "Flintstone", "") self.assertEqual(name, "Fred Flintstone") - def test_make_utc(self): - dt = self.app.make_utc() - self.assertIsInstance(dt, datetime.datetime) - self.assertIsNone(dt.tzinfo) - def test_make_uuid(self): uuid = self.app.make_uuid() self.assertEqual(len(uuid), 32) @@ -571,7 +566,7 @@ app_title = WuttaTest now = datetime.datetime.now() result = self.app.render_time_ago(now) self.assertEqual(result, "now") - humanize.naturaltime.assert_called_once() + humanize.naturaltime.assert_called_once_with(now) def test_get_person(self): people = self.app.get_people_handler() diff --git a/tests/test_util.py b/tests/test_util.py index e2b9095..a078100 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,6 +1,5 @@ # -*- coding: utf-8; -*- -import datetime import sys from unittest import TestCase from unittest.mock import patch, MagicMock @@ -165,53 +164,6 @@ class TestLoadObject(TestCase): self.assertIs(result, TestCase) -class TestMakeUTC(TestCase): - - def test_current_time(self): - - # no tzinfo by default - dt = mod.make_utc() - self.assertIsInstance(dt, datetime.datetime) - self.assertIsNone(dt.tzinfo) - now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) - self.assertAlmostEqual(int(dt.timestamp()), int(now.timestamp())) - - # with tzinfo - dt = mod.make_utc(tzinfo=True) - self.assertIsInstance(dt, datetime.datetime) - self.assertIs(dt.tzinfo, datetime.timezone.utc) - now = datetime.datetime.now(datetime.timezone.utc) - self.assertAlmostEqual(int(dt.timestamp()), int(now.timestamp())) - - def test_convert_with_tzinfo(self): - sample = datetime.datetime( - 2024, 9, 15, 8, 30, tzinfo=datetime.timezone(-datetime.timedelta(hours=5)) - ) - - # no tzinfo by default - dt = mod.make_utc(sample) - self.assertEqual(dt, datetime.datetime(2024, 9, 15, 13, 30, tzinfo=None)) - - # with tzinfo - dt = mod.make_utc(sample, tzinfo=True) - self.assertEqual( - dt, datetime.datetime(2024, 9, 15, 13, 30, tzinfo=datetime.timezone.utc) - ) - - def test_convert_without_tzinfo(self): - sample = datetime.datetime(2024, 9, 15, 8, 30) - - # no tzinfo by default - dt = mod.make_utc(sample) - self.assertEqual(dt, datetime.datetime(2024, 9, 15, 8, 30, tzinfo=None)) - - # with tzinfo - dt = mod.make_utc(sample, tzinfo=True) - self.assertEqual( - dt, datetime.datetime(2024, 9, 15, 8, 30, tzinfo=datetime.timezone.utc) - ) - - class TestMakeUUID(TestCase): def test_basic(self):