Skip to content

Commit 1390358

Browse files
authored
Merge pull request #34 from simple-repository/feature/utc-timezones
Workaround the fact that sqlite3 doesn't handle timezones well at all
2 parents a4fe5e0 + 6bddc4d commit 1390358

File tree

4 files changed

+75
-5
lines changed

4 files changed

+75
-5
lines changed

simple_repository_browser/fetch_projects.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ def remove_if_found(connection, canonical_name):
3232
def update_summary(
3333
conn, name: str, summary: str, release_date: datetime.datetime, release_version: str
3434
):
35+
# Strip timezone info before storing in SQLite to avoid converter issues.
36+
# We always store naive datetimes which represent UTC.
37+
if release_date.tzinfo is not None:
38+
release_date = release_date.replace(tzinfo=None)
39+
3540
with conn as cursor:
3641
cursor.execute(
3742
"""

simple_repository_browser/model.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ class SearchResultItem:
2424
release_version: str | None = None
2525
release_date: datetime.datetime | None = None
2626

27+
@classmethod
28+
def from_db_row(cls, row: sqlite3.Row) -> "SearchResultItem":
29+
"""
30+
Convert a database row to SearchResultItem with timezone handling.
31+
32+
Naive datetimes from SQLite are interpreted as UTC. We store naive datetimes
33+
to avoid SQLite converter issues (issue #27), but always work with UTC-aware
34+
datetimes in the application.
35+
"""
36+
row_dict = dict(row)
37+
if (
38+
row_dict["release_date"] is not None
39+
and row_dict["release_date"].tzinfo is None
40+
):
41+
row_dict["release_date"] = row_dict["release_date"].replace(
42+
tzinfo=datetime.timezone.utc
43+
)
44+
return cls(**row_dict)
45+
2746

2847
class RepositoryStatsModel(typing.TypedDict):
2948
n_packages: int
@@ -154,7 +173,7 @@ async def project_query(
154173
results = cursor.execute(sql_query, params).fetchall()
155174

156175
# Convert results to SearchResultItem objects
157-
results = [SearchResultItem(*result) for result in results]
176+
results = [SearchResultItem.from_db_row(row) for row in results]
158177

159178
# If the search was for a specific name, then make sure we return it if
160179
# it is in the package repository.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Tests for model timezone handling."""
2+
3+
from datetime import datetime, timezone
4+
from pathlib import Path
5+
import sqlite3
6+
import tempfile
7+
8+
from simple_repository_browser import fetch_projects, model
9+
10+
11+
def test_SearchResultItem__from_db_row__converts_naive_to_utc():
12+
"""Verify SearchResultItem.from_db_row() converts naive datetimes to UTC."""
13+
with tempfile.TemporaryDirectory() as tmpdir:
14+
db_path = Path(tmpdir) / "test.db"
15+
con = sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES)
16+
con.row_factory = sqlite3.Row
17+
fetch_projects.create_table(con)
18+
19+
# Insert with naive datetime (simulating what update_summary stores)
20+
naive_dt = datetime(2023, 1, 15, 12, 30, 0)
21+
con.execute(
22+
"INSERT INTO projects(canonical_name, preferred_name, summary, release_version, release_date) VALUES (?, ?, ?, ?, ?)",
23+
("test-pkg", "Test-Pkg", "A test package", "1.0.0", naive_dt),
24+
)
25+
26+
# Read back using from_db_row
27+
cursor = con.execute(
28+
"SELECT canonical_name, summary, release_version, release_date FROM projects WHERE canonical_name = ?",
29+
("test-pkg",),
30+
)
31+
row = cursor.fetchone()
32+
result = model.SearchResultItem.from_db_row(row)
33+
34+
# Should have UTC timezone
35+
assert result.canonical_name == "test-pkg"
36+
assert result.summary == "A test package"
37+
assert result.release_version == "1.0.0"
38+
assert result.release_date == datetime(
39+
2023, 1, 15, 12, 30, 0, tzinfo=timezone.utc
40+
)
41+
assert result.release_date.tzinfo is not None
42+
43+
con.close()

simple_repository_browser/tests/test_search.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime
12
from pathlib import Path
23
import sqlite3
34
import tempfile
@@ -198,7 +199,7 @@ async def get_project_page(self, name: str):
198199
def test_database(tmp_path: Path):
199200
"""Create a temporary SQLite database with test data for search ordering."""
200201
db_path = tmp_path / "test.db"
201-
con = sqlite3.connect(db_path)
202+
con = sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES)
202203
con.row_factory = sqlite3.Row
203204

204205
# Create projects table matching the real schema
@@ -207,7 +208,7 @@ def test_database(tmp_path: Path):
207208
canonical_name TEXT PRIMARY KEY,
208209
summary TEXT,
209210
release_version TEXT,
210-
release_date TEXT
211+
release_date timestamp
211212
)
212213
""")
213214

@@ -236,10 +237,12 @@ def test_database(tmp_path: Path):
236237
("requests", "HTTP library", "2.28.0", "2022-12-01"),
237238
]
238239

239-
for name, summary, version, date in test_projects:
240+
for name, summary, version, date_str in test_projects:
241+
# Convert date strings to naive datetime objects (representing UTC)
242+
release_date = datetime.strptime(date_str, "%Y-%m-%d")
240243
con.execute(
241244
"INSERT INTO projects (canonical_name, summary, release_version, release_date) VALUES (?, ?, ?, ?)",
242-
(name, summary, version, date),
245+
(name, summary, version, release_date),
243246
)
244247

245248
con.commit()

0 commit comments

Comments
 (0)