Merge pull request 'next-release' (#47) from next-release into main
All checks were successful
Build Docker Image / build (push) Successful in 46s
All checks were successful
Build Docker Image / build (push) Successful in 46s
Reviewed-on: #47
This commit is contained in:
commit
a80addc742
@ -59,6 +59,13 @@ class MyCog(commands.Cog):
|
||||
- **Image**: `manticorum67/major-domo-discordapp` (no dash between discord and app)
|
||||
- **Health**: Process liveness only (no HTTP endpoint)
|
||||
- **CI/CD**: Gitea Actions on PR to `main` — builds Docker image, auto-generates CalVer version (`YYYY.MM.BUILD`) on merge
|
||||
|
||||
### Release Workflow
|
||||
1. Create feature/fix branches off `next-release` (e.g., `fix/scorebug-bugs`)
|
||||
2. When done, merge the branch into `next-release` — this is the staging branch where changes accumulate
|
||||
3. When ready to release, open a PR from `next-release` → `main`
|
||||
4. CI builds Docker image on PR; CalVer tag is created on merge
|
||||
5. Deploy the new image to production (see `/deploy` skill)
|
||||
- **Other services on same host**: `sba_db_api`, `sba_postgres`, `sba_redis`, `sba-website-sba-web-1`, `pd_api`
|
||||
|
||||
### Logs
|
||||
|
||||
@ -8,6 +8,8 @@ Runs on a schedule to increment weeks and process contested transactions.
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, UTC
|
||||
|
||||
from utils.timezone import now_chicago
|
||||
from typing import Dict, List, Tuple, Set, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
@ -325,7 +327,7 @@ class TransactionFreezeTask:
|
||||
self.logger.warning("Could not get current league state")
|
||||
return
|
||||
|
||||
now = datetime.now(UTC)
|
||||
now = now_chicago()
|
||||
self.logger.info(
|
||||
f"Weekly loop check",
|
||||
datetime=now.isoformat(),
|
||||
@ -920,6 +922,7 @@ class TransactionFreezeTask:
|
||||
is_div_week = current.week in [1, 3, 6, 14, 16, 18]
|
||||
|
||||
weekly_str = (
|
||||
f"**Week**: {current.week}\n"
|
||||
f"**Season**: {season_str}\n"
|
||||
f"**Time of Day**: {night_str} / {night_str if is_div_week else day_str} / "
|
||||
f"{night_str} / {day_str}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
129
tests/test_utils_timezone.py
Normal file
129
tests/test_utils_timezone.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""
|
||||
Tests for timezone utility module.
|
||||
|
||||
Validates centralized timezone helpers that ensure scheduling logic uses
|
||||
explicit America/Chicago timezone rather than relying on OS defaults.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from utils.timezone import (
|
||||
CHICAGO_TZ,
|
||||
now_utc,
|
||||
now_chicago,
|
||||
to_chicago,
|
||||
to_discord_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class TestChicagoTZ:
|
||||
"""Tests for the CHICAGO_TZ constant."""
|
||||
|
||||
def test_chicago_tz_is_zoneinfo(self):
|
||||
"""CHICAGO_TZ should be a ZoneInfo instance for America/Chicago."""
|
||||
assert isinstance(CHICAGO_TZ, ZoneInfo)
|
||||
assert str(CHICAGO_TZ) == "America/Chicago"
|
||||
|
||||
|
||||
class TestNowUtc:
|
||||
"""Tests for now_utc()."""
|
||||
|
||||
def test_returns_aware_datetime(self):
|
||||
"""now_utc() should return a timezone-aware datetime."""
|
||||
result = now_utc()
|
||||
assert result.tzinfo is not None
|
||||
|
||||
def test_returns_utc(self):
|
||||
"""now_utc() should return a datetime in UTC."""
|
||||
result = now_utc()
|
||||
assert result.tzinfo == timezone.utc
|
||||
|
||||
|
||||
class TestNowChicago:
|
||||
"""Tests for now_chicago()."""
|
||||
|
||||
def test_returns_aware_datetime(self):
|
||||
"""now_chicago() should return a timezone-aware datetime."""
|
||||
result = now_chicago()
|
||||
assert result.tzinfo is not None
|
||||
|
||||
def test_returns_chicago_tz(self):
|
||||
"""now_chicago() should return a datetime in America/Chicago timezone."""
|
||||
result = now_chicago()
|
||||
assert result.tzinfo == CHICAGO_TZ
|
||||
|
||||
|
||||
class TestToChicago:
|
||||
"""Tests for to_chicago()."""
|
||||
|
||||
def test_converts_utc_datetime(self):
|
||||
"""to_chicago() should correctly convert a UTC datetime to Chicago time."""
|
||||
# 2024-07-15 18:00 UTC = 2024-07-15 13:00 CDT (UTC-5 during summer)
|
||||
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
result = to_chicago(utc_dt)
|
||||
assert result.hour == 13
|
||||
assert result.tzinfo == CHICAGO_TZ
|
||||
|
||||
def test_handles_naive_datetime_assumes_utc(self):
|
||||
"""to_chicago() should treat naive datetimes as UTC."""
|
||||
naive_dt = datetime(2024, 7, 15, 18, 0, 0)
|
||||
result = to_chicago(naive_dt)
|
||||
# Same as converting from UTC
|
||||
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
expected = to_chicago(utc_dt)
|
||||
assert result.hour == expected.hour
|
||||
assert result.tzinfo == CHICAGO_TZ
|
||||
|
||||
def test_preserves_already_chicago(self):
|
||||
"""to_chicago() on an already-Chicago datetime should be a no-op."""
|
||||
chicago_dt = datetime(2024, 7, 15, 13, 0, 0, tzinfo=CHICAGO_TZ)
|
||||
result = to_chicago(chicago_dt)
|
||||
assert result.hour == 13
|
||||
assert result.tzinfo == CHICAGO_TZ
|
||||
|
||||
def test_winter_offset(self):
|
||||
"""to_chicago() should use CST (UTC-6) during winter months."""
|
||||
# 2024-01-15 18:00 UTC = 2024-01-15 12:00 CST (UTC-6 during winter)
|
||||
utc_dt = datetime(2024, 1, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
result = to_chicago(utc_dt)
|
||||
assert result.hour == 12
|
||||
|
||||
|
||||
class TestToDiscordTimestamp:
|
||||
"""Tests for to_discord_timestamp()."""
|
||||
|
||||
def test_default_style(self):
|
||||
"""to_discord_timestamp() should use 'f' style by default."""
|
||||
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
result = to_discord_timestamp(dt)
|
||||
unix_ts = int(dt.timestamp())
|
||||
assert result == f"<t:{unix_ts}:f>"
|
||||
|
||||
def test_relative_style(self):
|
||||
"""to_discord_timestamp() with style='R' should produce relative format."""
|
||||
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
result = to_discord_timestamp(dt, style="R")
|
||||
unix_ts = int(dt.timestamp())
|
||||
assert result == f"<t:{unix_ts}:R>"
|
||||
|
||||
def test_all_styles(self):
|
||||
"""to_discord_timestamp() should support all Discord timestamp styles."""
|
||||
dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
unix_ts = int(dt.timestamp())
|
||||
for style in ("R", "f", "F", "t", "T", "d", "D"):
|
||||
result = to_discord_timestamp(dt, style=style)
|
||||
assert result == f"<t:{unix_ts}:{style}>"
|
||||
|
||||
def test_naive_datetime_assumes_utc(self):
|
||||
"""to_discord_timestamp() should treat naive datetimes as UTC."""
|
||||
naive_dt = datetime(2024, 7, 15, 18, 0, 0)
|
||||
aware_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
assert to_discord_timestamp(naive_dt) == to_discord_timestamp(aware_dt)
|
||||
|
||||
def test_chicago_datetime_same_instant(self):
|
||||
"""to_discord_timestamp() should produce the same unix timestamp regardless of tz."""
|
||||
utc_dt = datetime(2024, 7, 15, 18, 0, 0, tzinfo=timezone.utc)
|
||||
chicago_dt = utc_dt.astimezone(CHICAGO_TZ)
|
||||
assert to_discord_timestamp(utc_dt) == to_discord_timestamp(chicago_dt)
|
||||
@ -10,7 +10,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, UTC
|
||||
from typing import Dict, Any, Optional, Union
|
||||
|
||||
# Context variable for request tracking across async calls
|
||||
@ -32,7 +32,7 @@ class JSONFormatter(logging.Formatter):
|
||||
"""Format log record as JSON with context information."""
|
||||
# Base log object
|
||||
log_obj: dict[str, JSONValue] = {
|
||||
"timestamp": datetime.now().isoformat() + "Z",
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
|
||||
54
utils/timezone.py
Normal file
54
utils/timezone.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
Timezone Utilities
|
||||
|
||||
Centralized timezone handling for the Discord bot. The SBA league operates
|
||||
in America/Chicago time, but the production container may have ambiguous
|
||||
timezone config. These helpers ensure scheduling logic uses explicit timezones
|
||||
rather than relying on the OS default.
|
||||
|
||||
- Internal storage/logging: UTC
|
||||
- Scheduling checks (freeze/thaw): America/Chicago
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# League timezone — all scheduling decisions use this
|
||||
CHICAGO_TZ = ZoneInfo("America/Chicago")
|
||||
|
||||
|
||||
def now_utc() -> datetime:
|
||||
"""Return the current time as a timezone-aware UTC datetime."""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def now_chicago() -> datetime:
|
||||
"""Return the current time as a timezone-aware America/Chicago datetime."""
|
||||
return datetime.now(CHICAGO_TZ)
|
||||
|
||||
|
||||
def to_chicago(dt: datetime) -> datetime:
|
||||
"""Convert a datetime to America/Chicago.
|
||||
|
||||
If *dt* is naive (no tzinfo), it is assumed to be UTC.
|
||||
"""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(CHICAGO_TZ)
|
||||
|
||||
|
||||
def to_discord_timestamp(dt: datetime, style: str = "f") -> str:
|
||||
"""Format a datetime as a Discord dynamic timestamp.
|
||||
|
||||
Args:
|
||||
dt: A datetime (naive datetimes are assumed UTC).
|
||||
style: Discord timestamp style letter.
|
||||
R = relative, f = long date/short time, F = long date/time,
|
||||
t = short time, T = long time, d = short date, D = long date.
|
||||
|
||||
Returns:
|
||||
A string like ``<t:1234567890:f>``.
|
||||
"""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return f"<t:{int(dt.timestamp())}:{style}>"
|
||||
Loading…
Reference in New Issue
Block a user