Improve timezone handling, db models convert naive/timezone-aware
This commit is contained in:
@@ -10,26 +10,20 @@ import datetime
|
|||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
__author__ = "Thomas Oettli <spacefreak@noop.ch>"
|
__author__ = "Thomas Oettli <spacefreak@noop.ch>"
|
||||||
|
|
||||||
# DynDNS-compatible response statuses
|
|
||||||
STATUS_GOOD = "good"
|
|
||||||
STATUS_NOCHG = "nochg"
|
|
||||||
STATUS_BADAUTH = "badauth"
|
|
||||||
STATUS_NOHOST = "nohost"
|
|
||||||
STATUS_DNSERR = "dnserr"
|
|
||||||
STATUS_ABUSE = "abuse"
|
|
||||||
STATUS_BADIP = "badip"
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"app",
|
"app",
|
||||||
"cleanup",
|
"cleanup",
|
||||||
"cli",
|
"cli",
|
||||||
"config",
|
"config",
|
||||||
|
"datetime_aware_utc",
|
||||||
|
"datetime_naive_utc",
|
||||||
"datetime_str",
|
"datetime_str",
|
||||||
"dns",
|
"dns",
|
||||||
"email",
|
"email",
|
||||||
"logging",
|
"logging",
|
||||||
"main",
|
"main",
|
||||||
"models",
|
"models",
|
||||||
|
"now_utc"
|
||||||
"ratelimit",
|
"ratelimit",
|
||||||
"server",
|
"server",
|
||||||
"STATUS_GOOD",
|
"STATUS_GOOD",
|
||||||
@@ -41,13 +35,75 @@ __all__ = [
|
|||||||
"STATUS_BADIP",
|
"STATUS_BADIP",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# DynDNS-compatible response statuses
|
||||||
|
STATUS_GOOD = "good"
|
||||||
|
STATUS_NOCHG = "nochg"
|
||||||
|
STATUS_BADAUTH = "badauth"
|
||||||
|
STATUS_NOHOST = "nohost"
|
||||||
|
STATUS_DNSERR = "dnserr"
|
||||||
|
STATUS_ABUSE = "abuse"
|
||||||
|
STATUS_BADIP = "badip"
|
||||||
|
|
||||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S %Z"
|
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S %Z"
|
||||||
|
|
||||||
# Datetime convention:
|
# Datetime convention:
|
||||||
# All datetime objects in this codebase are naive UTC to match database storage.
|
# All datetime objects in this codebase are timezone-aware.
|
||||||
# - utc_now(): returns naive UTC datetime
|
# - now_utc(): returns timezone-aware UTC datetime
|
||||||
# - datetime_str(): converts naive UTC to display string (adds tzinfo for formatting)
|
# - datetime_str(): converts naive UTC (adds tzinfo for formatting)
|
||||||
|
# or timezone-aware datetime to display string
|
||||||
# - Database stores/returns naive datetimes (always UTC by convention)
|
# - Database stores/returns naive datetimes (always UTC by convention)
|
||||||
|
# - Database models automatically convert between naive/timezone-aware datetimes
|
||||||
|
|
||||||
|
|
||||||
|
def now_utc():
|
||||||
|
"""
|
||||||
|
Get current date and time in UTC.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Timezone-aware datetime object in UTC.
|
||||||
|
"""
|
||||||
|
return datetime.datetime.now(datetime.UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_naive_utc(dt):
|
||||||
|
"""
|
||||||
|
Convert datetime to naive UTC datetime.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt: Datetime object (naive UTC or timezone-aware or None).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Naive datetime object in UTC or None if dt is not a datetime.
|
||||||
|
"""
|
||||||
|
if not isinstance(dt, datetime.datetime):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not dt.tzinfo:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
return dt.astimezone(datetime.UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_aware_utc(dt):
|
||||||
|
"""
|
||||||
|
Convert datetime to UTC datetime.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt: Datetime object (naive UTC or timezone-aware or None).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Timzone-aware datetime object in UTC or None if dt is not a datetime.
|
||||||
|
"""
|
||||||
|
if not isinstance(dt, datetime.datetime):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not dt.tzinfo:
|
||||||
|
return dt.replace(tzinfo=datetime.UTC)
|
||||||
|
|
||||||
|
if dt.tzinfo == datetime.UTC:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
return dt.astimezone(datetime.UTC)
|
||||||
|
|
||||||
|
|
||||||
def datetime_str(dt, utc=False):
|
def datetime_str(dt, utc=False):
|
||||||
@@ -72,13 +128,3 @@ def datetime_str(dt, utc=False):
|
|||||||
return aware_dt.strftime(DATETIME_FORMAT)
|
return aware_dt.strftime(DATETIME_FORMAT)
|
||||||
else:
|
else:
|
||||||
return aware_dt.astimezone().strftime(DATETIME_FORMAT)
|
return aware_dt.astimezone().strftime(DATETIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
def utc_now():
|
|
||||||
"""
|
|
||||||
Get current time as naive UTC datetime.
|
|
||||||
|
|
||||||
Returns naive datetime to match database storage behavior.
|
|
||||||
All naive datetimes in this codebase are assumed to be UTC.
|
|
||||||
"""
|
|
||||||
return datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from . import utc_now
|
from . import now_utc
|
||||||
from .models import Hostname, User
|
from .models import Hostname, User
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ def cleanup_expired(app):
|
|||||||
Returns:
|
Returns:
|
||||||
Number of expired hostnames processed.
|
Number of expired hostnames processed.
|
||||||
"""
|
"""
|
||||||
now = utc_now()
|
now = now_utc()
|
||||||
expired_count = 0
|
expired_count = 0
|
||||||
|
|
||||||
for hostname in Hostname.select().join(User).where(
|
for hostname in Hostname.select().join(User).where(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from . import utc_now
|
from . import datetime_naive_utc, datetime_aware_utc, now_utc
|
||||||
from .dns import encode_dnsname, EncodingError
|
from .dns import encode_dnsname, EncodingError
|
||||||
from peewee import (
|
from peewee import (
|
||||||
AutoField,
|
AutoField,
|
||||||
@@ -78,11 +78,19 @@ class User(BaseModel):
|
|||||||
username = CharField(max_length=64, unique=True)
|
username = CharField(max_length=64, unique=True)
|
||||||
password_hash = CharField(max_length=128)
|
password_hash = CharField(max_length=128)
|
||||||
email = CharField(max_length=255)
|
email = CharField(max_length=255)
|
||||||
created_at = DateTimeField(default=utc_now)
|
created_at = DateTimeField(default=now_utc)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "users"
|
table_name = "users"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.created_at = datetime_aware_utc(self.created_at)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.created_at = datetime_naive_utc(self.created_at)
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Hostname(BaseModel):
|
class Hostname(BaseModel):
|
||||||
"""Hostname model for DNS records."""
|
"""Hostname model for DNS records."""
|
||||||
@@ -104,12 +112,19 @@ class Hostname(BaseModel):
|
|||||||
(('hostname', 'zone'), True),
|
(('hostname', 'zone'), True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.last_ipv4_update = datetime_aware_utc(self.last_ipv4_update)
|
||||||
|
self.last_ipv6_update = datetime_aware_utc(self.last_ipv6_update)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Validate and encode hostname/zone before saving."""
|
"""Validate and encode hostname/zone before saving."""
|
||||||
if self.hostname:
|
if self.hostname:
|
||||||
self.hostname = encode_dnsname(self.hostname)
|
self.hostname = encode_dnsname(self.hostname)
|
||||||
if self.zone:
|
if self.zone:
|
||||||
self.zone = encode_dnsname(self.zone)
|
self.zone = encode_dnsname(self.zone)
|
||||||
|
self.last_ipv4_update = datetime_naive_utc(self.last_ipv4_update)
|
||||||
|
self.last_ipv6_update = datetime_naive_utc(self.last_ipv6_update)
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import ssl
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
now_utc,
|
||||||
datetime_str,
|
datetime_str,
|
||||||
utc_now,
|
|
||||||
STATUS_GOOD,
|
STATUS_GOOD,
|
||||||
STATUS_NOCHG,
|
STATUS_NOCHG,
|
||||||
STATUS_BADAUTH,
|
STATUS_BADAUTH,
|
||||||
@@ -373,7 +373,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return (400, STATUS_BADIP, {})
|
return (400, STATUS_BADIP, {})
|
||||||
|
|
||||||
now = utc_now()
|
now = now_utc()
|
||||||
|
|
||||||
ipv4_changed = False
|
ipv4_changed = False
|
||||||
ipv6_changed = False
|
ipv6_changed = False
|
||||||
|
|||||||
Reference in New Issue
Block a user