3
0
Fork 0

feat: drop timezone, assume UTC for all datetime values in DB

i guess it was worth a try, but preserving system timezone was doomed
to failure since only postgres actually supports it.  from now on all
DateTime columns will be naive, but understood / assumed to be
UTC-local
This commit is contained in:
Lance Edgar 2025-12-15 13:18:08 -06:00
parent 900937826c
commit a5d641d5bc
7 changed files with 201 additions and 21 deletions

View file

@ -862,7 +862,8 @@ class AppHandler: # pylint: disable=too-many-public-methods
:returns: Text to display. :returns: Text to display.
""" """
return humanize.naturaltime(value) # TODO: this now assumes naive UTC value incoming...
return humanize.naturaltime(value, when=self.make_utc(tzinfo=False))
############################## ##############################
# getters for other handlers # getters for other handlers

View file

@ -24,7 +24,6 @@
Batch Handlers Batch Handlers
""" """
import datetime
import os import os
import shutil import shutil
@ -481,7 +480,7 @@ class BatchHandler(GenericHandler): # pylint: disable=too-many-public-methods
result = self.execute( # pylint: disable=assignment-from-none result = self.execute( # pylint: disable=assignment-from-none
batch, user=user, progress=progress, **kwargs batch, user=user, progress=progress, **kwargs
) )
batch.executed = datetime.datetime.now() batch.executed = self.app.make_utc()
batch.executed_by = user batch.executed_by = user
return result return result

View file

@ -0,0 +1,186 @@
"""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,
)

View file

@ -39,14 +39,13 @@ So a user's permissions are "inherited" from the role(s) to which they
belong. belong.
""" """
import datetime
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db.util import uuid_column, uuid_fk_column from wuttjamaican.db.util import uuid_column, uuid_fk_column
from wuttjamaican.db.model.base import Base from wuttjamaican.db.model.base import Base
from wuttjamaican.util import make_utc
class Role(Base): # pylint: disable=too-few-public-methods class Role(Base): # pylint: disable=too-few-public-methods
@ -343,9 +342,9 @@ class UserAPIToken(Base): # pylint: disable=too-few-public-methods
) )
created = sa.Column( created = sa.Column(
sa.DateTime(timezone=True), sa.DateTime(),
nullable=False, nullable=False,
default=datetime.datetime.now, default=make_utc,
doc=""" doc="""
Date/time when the token was created. Date/time when the token was created.
""", """,

View file

@ -34,6 +34,7 @@ from sqlalchemy.ext.orderinglist import ordering_list
from wuttjamaican.db.model.base import uuid_column from wuttjamaican.db.model.base import uuid_column
from wuttjamaican.db.model.auth import User from wuttjamaican.db.model.auth import User
from wuttjamaican.db.util import UUID from wuttjamaican.db.util import UUID
from wuttjamaican.util import make_utc
class BatchMixin: class BatchMixin:
@ -223,9 +224,7 @@ class BatchMixin:
status_code = sa.Column(sa.Integer(), nullable=True) status_code = sa.Column(sa.Integer(), nullable=True)
status_text = sa.Column(sa.String(length=255), nullable=True) status_text = sa.Column(sa.String(length=255), nullable=True)
created = sa.Column( created = sa.Column(sa.DateTime(), nullable=False, default=make_utc)
sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now
)
created_by_uuid = sa.Column(UUID(), nullable=False) created_by_uuid = sa.Column(UUID(), nullable=False)
@declared_attr @declared_attr
@ -238,7 +237,7 @@ class BatchMixin:
cascade_backrefs=False, cascade_backrefs=False,
) )
executed = sa.Column(sa.DateTime(timezone=True), nullable=True) executed = sa.Column(sa.DateTime(), nullable=True)
executed_by_uuid = sa.Column(UUID(), nullable=True) executed_by_uuid = sa.Column(UUID(), nullable=True)
@declared_attr @declared_attr
@ -428,8 +427,5 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods
status_text = sa.Column(sa.String(length=255), nullable=True) status_text = sa.Column(sa.String(length=255), nullable=True)
modified = sa.Column( modified = sa.Column(
sa.DateTime(timezone=True), sa.DateTime(), nullable=True, default=make_utc, onupdate=make_utc
nullable=True,
default=datetime.datetime.now,
onupdate=datetime.datetime.now,
) )

View file

@ -24,14 +24,13 @@
Upgrade Model Upgrade Model
""" """
import datetime
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from wuttjamaican.enum import UpgradeStatus from wuttjamaican.enum import UpgradeStatus
from wuttjamaican.db.util import uuid_column, uuid_fk_column from wuttjamaican.db.util import uuid_column, uuid_fk_column
from wuttjamaican.db.model.base import Base from wuttjamaican.db.model.base import Base
from wuttjamaican.util import make_utc
class Upgrade(Base): # pylint: disable=too-few-public-methods class Upgrade(Base): # pylint: disable=too-few-public-methods
@ -44,9 +43,9 @@ class Upgrade(Base): # pylint: disable=too-few-public-methods
uuid = uuid_column() uuid = uuid_column()
created = sa.Column( created = sa.Column(
sa.DateTime(timezone=True), sa.DateTime(),
nullable=False, nullable=False,
default=datetime.datetime.now, default=make_utc,
doc=""" doc="""
When the upgrade record was created. When the upgrade record was created.
""", """,
@ -98,7 +97,7 @@ class Upgrade(Base): # pylint: disable=too-few-public-methods
) )
executed = sa.Column( executed = sa.Column(
sa.DateTime(timezone=True), sa.DateTime(),
nullable=True, nullable=True,
doc=""" doc="""
When the upgrade was executed. When the upgrade was executed.

View file

@ -571,7 +571,7 @@ app_title = WuttaTest
now = datetime.datetime.now() now = datetime.datetime.now()
result = self.app.render_time_ago(now) result = self.app.render_time_ago(now)
self.assertEqual(result, "now") self.assertEqual(result, "now")
humanize.naturaltime.assert_called_once_with(now) humanize.naturaltime.assert_called_once()
def test_get_person(self): def test_get_person(self):
people = self.app.get_people_handler() people = self.app.get_people_handler()