Skip to content

Commit 6f560fa

Browse files
Timezone fixes to avoid future depreciation
1 parent 4ee8458 commit 6f560fa

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

DATETIME_MODERNIZATION.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# DateTime Modernization Update
2+
3+
## Summary
4+
Updated the Subscription Tracker codebase to use modern Python datetime practices by replacing deprecated `datetime.utcnow()` with `datetime.now(timezone.utc)`.
5+
6+
## Changes Made
7+
8+
### ✅ Files Updated
9+
10+
1. **`app/webhooks.py`**
11+
- Added `timezone` import
12+
- Updated webhook timestamp generation
13+
- Updated last_used timestamp tracking
14+
- Fixed Discord, Slack, and Generic webhook timestamp formats
15+
16+
2. **`app/currency.py`**
17+
- Added `timezone` import
18+
- Updated cache age calculations for currency rates
19+
- Fixed timezone-aware datetime comparisons
20+
21+
3. **`app/models.py`**
22+
- Added `timezone` import
23+
- Updated database column defaults for:
24+
- `Webhook.created_at`
25+
- `ExchangeRate.created_at`
26+
- `PaymentMethod.created_at`
27+
- Fixed `ExchangeRate.save_rates()` method
28+
29+
### 🔧 Technical Details
30+
31+
#### Before (Deprecated):
32+
```python
33+
datetime.utcnow() # Returns naive datetime in UTC
34+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
35+
```
36+
37+
#### After (Modern):
38+
```python
39+
datetime.now(timezone.utc) # Returns timezone-aware datetime in UTC
40+
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
41+
```
42+
43+
### 📝 Key Improvements
44+
45+
1. **Timezone Awareness**: All UTC timestamps are now timezone-aware
46+
2. **Future Compatibility**: Avoids deprecation warnings in Python 3.12+
47+
3. **Database Compatibility**: Lambda functions ensure fresh timestamps per record
48+
4. **Consistency**: Uniform approach across all modules
49+
50+
### 🔍 Remaining `datetime.now()` Usage
51+
52+
The following `datetime.now()` calls remain unchanged as they are used for local time operations:
53+
54+
- **Display formatting**: User-facing timestamp displays
55+
- **Local date comparisons**: Subscription expiry checks relative to user's local date
56+
- **Health check timestamps**: Application status timestamps
57+
- **Circuit breaker timestamps**: Internal timeout tracking
58+
59+
These are intentionally kept as local time since they represent user-facing functionality or internal timers.
60+
61+
### 🧪 Testing
62+
63+
After this update, ensure:
64+
65+
1. **Webhook timestamps** appear correctly in your webhook destinations
66+
2. **Database records** have proper created_at timestamps
67+
3. **Currency rate caching** works correctly with age calculations
68+
4. **No deprecation warnings** appear in Python 3.12+ environments
69+
70+
### 🚀 Migration Notes
71+
72+
This is a **forward-compatible** change:
73+
- Existing database records are unaffected
74+
- All new records will use timezone-aware timestamps
75+
- No data migration required
76+
- Backward compatible with existing functionality
77+
78+
### 📚 References
79+
80+
- [Python datetime documentation](https://docs.python.org/3/library/datetime.html)
81+
- [PEP 615 – Support for the IANA Time Zone Database](https://peps.python.org/pep-0615/)
82+
- [Python 3.12 datetime deprecations](https://docs.python.org/3.12/whatsnew/3.12.html#deprecated)
83+
84+
## Verification
85+
86+
Run the application and verify:
87+
```bash
88+
# No deprecation warnings should appear
89+
docker-compose up --build
90+
91+
# Test webhook functionality
92+
./test-webhook.sh discord "your-webhook-url"
93+
94+
# Check database timestamps are timezone-aware
95+
# (New records should have proper UTC timestamps)
96+
```
97+
98+
This update ensures the Subscription Tracker remains compatible with current and future Python versions while maintaining all existing functionality.

app/currency.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import requests
22
import json
33
import os
4-
from datetime import datetime, date
4+
from datetime import datetime, date, timezone
55
from flask import current_app
66
import xml.etree.ElementTree as ET
77
from decimal import Decimal, getcontext, InvalidOperation
@@ -60,7 +60,7 @@ def get_exchange_rates(self, base_currency: str = 'EUR', force_refresh: bool = F
6060
if not force_refresh and primary_provider:
6161
record = ExchangeRate.query.filter_by(date=date.today(), base_currency=base_currency, provider=primary_provider).first()
6262
if record:
63-
age_min = (datetime.utcnow() - record.created_at).total_seconds() / 60.0
63+
age_min = (datetime.now(timezone.utc) - record.created_at).total_seconds() / 60.0
6464
if age_min <= refresh_minutes:
6565
try:
6666
self.last_provider = primary_provider
@@ -79,7 +79,7 @@ def get_exchange_rates(self, base_currency: str = 'EUR', force_refresh: bool = F
7979
if not force_refresh:
8080
cached = ExchangeRate.query.filter_by(date=date.today(), base_currency=base_currency, provider=provider).first()
8181
if cached:
82-
age_min = (datetime.utcnow() - cached.created_at).total_seconds() / 60.0
82+
age_min = (datetime.now(timezone.utc) - cached.created_at).total_seconds() / 60.0
8383
if age_min <= refresh_minutes:
8484
try:
8585
self.last_provider = provider

app/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, date
1+
from datetime import datetime, date, timezone
22
from flask_login import UserMixin
33
from werkzeug.security import generate_password_hash, check_password_hash
44
from app import db, login_manager
@@ -62,7 +62,7 @@ class Webhook(db.Model):
6262
custom_headers = db.Column(db.Text) # JSON string for custom headers
6363
is_active = db.Column(db.Boolean, default=True)
6464
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
65-
created_at = db.Column(db.DateTime, default=datetime.utcnow)
65+
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
6666
last_used = db.Column(db.DateTime)
6767

6868
# Relationship
@@ -106,7 +106,7 @@ class ExchangeRate(db.Model):
106106
base_currency = db.Column(db.String(3), nullable=False, default='EUR')
107107
provider = db.Column(db.String(40), nullable=False, default='legacy') # data source identifier
108108
rates_json = db.Column(db.Text, nullable=False) # JSON string of exchange rates
109-
created_at = db.Column(db.DateTime, default=datetime.utcnow)
109+
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
110110

111111
__table_args__ = (
112112
db.UniqueConstraint('date', 'base_currency', 'provider', name='uq_rate_date_base_provider'),
@@ -136,7 +136,7 @@ def save_rates(cls, rates, base_currency='EUR', provider='unknown'):
136136
existing_rate = cls.query.filter_by(date=today, base_currency=base_currency, provider=provider).first()
137137
if existing_rate:
138138
existing_rate.rates_json = json.dumps(rates)
139-
existing_rate.created_at = datetime.utcnow()
139+
existing_rate.created_at = datetime.now(timezone.utc)
140140
else:
141141
new_rate = cls(
142142
date=today,
@@ -154,7 +154,7 @@ class PaymentMethod(db.Model):
154154
last_four = db.Column(db.String(4), nullable=True)
155155
notes = db.Column(db.Text, nullable=True)
156156
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
157-
created_at = db.Column(db.DateTime, default=datetime.utcnow)
157+
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
158158

159159
# Relationship back to user
160160
user = db.relationship('User', backref=db.backref('payment_methods', lazy=True))

app/webhooks.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import requests
1616
import json
1717
import logging
18-
from datetime import datetime
18+
from datetime import datetime, timezone
1919
from typing import Dict, List, Optional, Any
2020
from flask import current_app
2121

@@ -58,7 +58,7 @@ def send(self, message: str, title: str = None, color: str = None) -> Dict[str,
5858
response.raise_for_status()
5959

6060
# Update last_used timestamp
61-
self.webhook.last_used = datetime.utcnow()
61+
self.webhook.last_used = datetime.now(timezone.utc)
6262
from app import db
6363
db.session.commit()
6464

@@ -111,7 +111,7 @@ def prepare_payload(self, message: str, title: str = None, color: str = None) ->
111111

112112
embed = {
113113
"description": message,
114-
"timestamp": datetime.utcnow().isoformat(),
114+
"timestamp": datetime.now(timezone.utc).isoformat(),
115115
"footer": {
116116
"text": "Subscription Tracker"
117117
}
@@ -151,7 +151,7 @@ def prepare_payload(self, message: str, title: str = None, color: str = None) ->
151151

152152
attachment = {
153153
"text": message,
154-
"ts": int(datetime.utcnow().timestamp()),
154+
"ts": int(datetime.now(timezone.utc).timestamp()),
155155
"footer": "Subscription Tracker"
156156
}
157157

@@ -239,7 +239,7 @@ class GenericWebhookSender(WebhookSender):
239239
def prepare_payload(self, message: str, title: str = None, color: str = None) -> Dict[str, Any]:
240240
payload = {
241241
"text": message,
242-
"timestamp": datetime.utcnow().isoformat()
242+
"timestamp": datetime.now(timezone.utc).isoformat()
243243
}
244244

245245
if title:

0 commit comments

Comments
 (0)