mirror of
https://github.com/jbranchaud/til
synced 2026-08-31 16:51:46 +00:00
Add Register SQLite Adapter To Serialize Datetimes as a Python TIL
This commit is contained in:
@@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/).
|
|||||||
|
|
||||||
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter).
|
||||||
|
|
||||||
_1874 TILs and counting..._
|
_1875 TILs and counting..._
|
||||||
|
|
||||||
See some of the other learning resources I work on:
|
See some of the other learning resources I work on:
|
||||||
|
|
||||||
@@ -1126,6 +1126,7 @@ If you've learned something here, support my efforts writing daily TILs by
|
|||||||
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
- [Parse Relative Time To datetime Object](python/parse-relative-time-to-datetime-object.md)
|
||||||
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
|
- [Publish A Package To A Test Env As A Dry Run](python/publish-a-package-to-a-test-env-as-a-dry-run.md)
|
||||||
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
- [Reclassify Certain Packages As Dev Dependencies](python/reclassify-certain-packages-as-dev-dependencies.md)
|
||||||
|
- [Register SQLite Adapter To Serialize Datetimes](python/register-sqlite-adapter-to-serialize-datetimes.md)
|
||||||
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
|
- [Resurface Exceptions Swallowed By Click Under Test](python/resurface-exceptions-swallowed-by-click-under-test.md)
|
||||||
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
|
- [Select Implementation With Class Registry](python/select-implementation-with-class-registry.md)
|
||||||
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
- [Set Up Pyright Type Checking In GitHub](python/set-up-pyright-type-checking-in-github.md)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Register SQLite Adapter To Serialize Datetimes
|
||||||
|
|
||||||
|
SQLite doesn't have a datetime or timestamp data type, so that information has
|
||||||
|
to be stored as `text` or a unix epoch `int`. That means when using Python's
|
||||||
|
`sqlite3` module to perform writes I need to tell it what shape to write a
|
||||||
|
datetime value.
|
||||||
|
|
||||||
|
I could manually convert `datetime` values everywhere they are involved in
|
||||||
|
_write_ operations.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Prepare `sessions` insert payload
|
||||||
|
session_data = {
|
||||||
|
"active": 1 if active else 0,
|
||||||
|
"project_id": project_id,
|
||||||
|
"start_time": datetime.isoformat(session.start_time),
|
||||||
|
"end_time": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.end_time:
|
||||||
|
session_data["end_time"] = datetime.isoformat(session.end_time)
|
||||||
|
|
||||||
|
# Insert the new active session
|
||||||
|
cursor = self.conn.execute(
|
||||||
|
"""
|
||||||
|
insert into sessions (active, project_id, start_time, end_time)
|
||||||
|
values (:active, :project_id, :start_time, :end_time)
|
||||||
|
returning id;
|
||||||
|
""",
|
||||||
|
session_data,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
I've used `datetime.isoformat` above which formats `datetime` objects like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> datetime.now().isoformat()
|
||||||
|
'2026-08-28T11:52:04.709907'
|
||||||
|
```
|
||||||
|
|
||||||
|
I'd like to make two improvements.
|
||||||
|
|
||||||
|
1. I want these `datetime` values to be formatted instead like `2026-08-28T18:15:27.213Z`.
|
||||||
|
2. I want `datetime` values to be serialized automatically in the specific shape
|
||||||
|
without having to manually convert them everywhere.
|
||||||
|
|
||||||
|
I can achieve both of these things by [registering an adapter with `sqlite3`](https://docs.python.org/3/library/sqlite3.html#how-to-register-adapter-callables)
|
||||||
|
that handles the serialize of `datetime` objects.
|
||||||
|
|
||||||
|
First, I define a function that can perform the `datetime` to `str` conversion.
|
||||||
|
I decided to put this in `db.py` where I have some other database-specific
|
||||||
|
functions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def to_db(dt: datetime) -> str:
|
||||||
|
if dt.tzinfo is None or dt.utcoffset() is None:
|
||||||
|
raise ValueError(f"Unable to store naive datetime: {dt!r}")
|
||||||
|
dt = dt.astimezone(timezone.utc)
|
||||||
|
return f"{dt:%Y-%m-%dT%H:%M:%S}.{dt.microsecond // 1000:03d}Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I register the adapter before creating the connection that gets used for
|
||||||
|
database interactions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from sqlite3 import Connection
|
||||||
|
|
||||||
|
def initialize_conn(db_file: Path) -> Connection:
|
||||||
|
# register adapters
|
||||||
|
sqlite3.register_adapter(datetime, to_db)
|
||||||
|
|
||||||
|
conn: Connection = sqlite3.connect(db_file)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
return conn
|
||||||
|
```
|
||||||
|
|
||||||
|
Then I can run write operations with `datetime` objects knowing they will be
|
||||||
|
correctly serialized.
|
||||||
|
|
||||||
|
```python
|
||||||
|
with self.conn:
|
||||||
|
query = "update sessions set active = :active, end_time = :end_time where active = 1;"
|
||||||
|
self.conn.execute(
|
||||||
|
query,
|
||||||
|
{"active": 0, "end_time": session.end_time},
|
||||||
|
)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user