Files
agendula/scripts/make_import_fixture.py
Jean-Luc Makiola 96a2995df4 build: add lib-recur to :app, and a dmfs v23 fixture for the import
The own store expands recurrences itself, so lib-recur is a direct
dependency now rather than something :provider drags in. Still pinned at
0.12.2 — 0.16.0 removed RecurrenceSet.

scripts/make_import_fixture.py writes the tasks.db the one-shot import
will be tested against: the provider's DATABASE_VERSION 23 schema, seeded
with the cases the import has to get right (a task with no UID, a deleted
row, a recurring series, an all-day task, a subtask, an alarm property,
and a list under a real CalDAV account). The provider is being deleted, so
a fixture is the only way to keep testing against the schema it wrote.
2026-08-13 15:58:52 +02:00

143 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Build the dmfs `tasks.db` fixture the one-shot import is tested against.
The provider is being deleted (docs/OWN-STORE.md phase 5), so the import cannot
keep a real v0.3.x database around by asking the provider to create one. This
writes the schema the provider's TaskDatabaseHelper produces at DATABASE_VERSION
23 — tables `Lists`, `Tasks`, `Properties` only, which are the three the import
reads — and seeds a spread that covers what the import has to get right.
Regenerate with: python3 scripts/make_import_fixture.py
"""
from __future__ import annotations
import os
import sqlite3
import sys
OUT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"app/src/androidTest/assets/tasks-v23.db",
)
DDL = [
"""CREATE TABLE Lists (
_id INTEGER PRIMARY KEY AUTOINCREMENT,
account_name TEXT, account_type TEXT, list_name TEXT, list_color INTEGER,
list_access_level INTEGER, visible INTEGER, sync_enabled INTEGER,
list_owner TEXT, _dirty INTEGER DEFAULT 0, _sync_id TEXT,
sync_version TEXT, sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT,
sync5 TEXT, sync6 TEXT, sync7 TEXT, sync8 TEXT)""",
"""CREATE TABLE Tasks (
_id INTEGER PRIMARY KEY AUTOINCREMENT, version INTEGER DEFAULT 0,
list_id INTEGER NOT NULL, title TEXT, location TEXT, geo TEXT,
description TEXT, url TEXT, organizer TEXT, priority INTEGER,
task_color INTEGER, class INTEGER, completed INTEGER,
completed_is_allday INTEGER, percent_complete INTEGER,
status INTEGER DEFAULT 0, is_new INTEGER, is_closed INTEGER,
dtstart INTEGER, created INTEGER, last_modified INTEGER,
is_allday INTEGER, tz TEXT, due INTEGER, duration TEXT, rdate TEXT,
exdate TEXT, rrule TEXT, parent_id INTEGER, sorting TEXT,
has_alarms INTEGER, has_properties INTEGER, pinned INTEGER,
original_instance_sync_id TEXT, original_instance_id INTEGER,
original_instance_time INTEGER, original_instance_allday INTEGER,
_dirty INTEGER DEFAULT 1, _deleted INTEGER DEFAULT 0, _sync_id TEXT,
_uid TEXT, sync_version TEXT, sync1 TEXT, sync2 TEXT, sync3 TEXT,
sync4 TEXT, sync5 TEXT, sync6 TEXT, sync7 TEXT, sync8 TEXT)""",
"""CREATE TABLE Properties (
property_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
mimetype INTEGER, prop_version INTEGER,
data0 TEXT, data1 TEXT, data2 TEXT, data3 TEXT, data4 TEXT, data5 TEXT,
data6 TEXT, data7 TEXT, data8 TEXT, data9 TEXT, data10 TEXT,
data11 TEXT, data12 TEXT, data13 TEXT, data14 TEXT, data15 TEXT,
prop_sync1 TEXT, prop_sync2 TEXT, prop_sync3 TEXT, prop_sync4 TEXT,
prop_sync5 TEXT, prop_sync6 TEXT, prop_sync7 TEXT, prop_sync8 TEXT)""",
]
LOCAL = ("Local", "org.dmfs.account.LOCAL")
CALDAV = ("me@example.org", "bitfire.at.davdroid")
ALARM_MIMETYPE = "vnd.android.cursor.item/alarm"
# 2026-01-15T09:00:00Z, and a day in millis.
T0 = 1_768_467_600_000
DAY = 86_400_000
def seed(db: sqlite3.Connection) -> None:
lists = db.executemany(
"INSERT INTO Lists (_id, account_name, account_type, list_name, list_color,"
" visible, sync_enabled, list_owner) VALUES (?,?,?,?,?,?,?,?)",
[
(1, *LOCAL, "Personal", 0xFF7A5C6B, 1, 1, None),
(2, *LOCAL, "Hidden list", 0xFF445566, 0, 1, None),
# An external account inside our own authority: only reachable if the
# user pointed DAVx5 at us. Imported as a local list, UID preserved.
(3, *CALDAV, "Work", 0xFF2244AA, 1, 1, "Me"),
],
)
del lists
def task(**kw):
cols = ", ".join(kw)
marks = ", ".join("?" * len(kw))
db.execute(f"INSERT INTO Tasks ({cols}) VALUES ({marks})", tuple(kw.values()))
task(_id=1, list_id=1, title="Buy milk", due=T0 + DAY, status=0,
_uid="a1b2c3d4-0000-4000-8000-000000000001", created=T0, last_modified=T0)
# No UID: the import mints one.
task(_id=2, list_id=1, title="Call the dentist", due=T0 + 2 * DAY, status=1,
percent_complete=40, created=T0, last_modified=T0)
task(_id=3, list_id=1, title="Gather receipts", parent_id=1, status=0,
_uid="a1b2c3d4-0000-4000-8000-000000000003", created=T0, last_modified=T0)
task(_id=4, list_id=1, title="Renew domain", status=2, percent_complete=100,
completed=T0 - DAY, is_closed=1,
_uid="a1b2c3d4-0000-4000-8000-000000000004", created=T0, last_modified=T0)
task(_id=5, list_id=1, title="Water the plants", dtstart=T0, due=T0 + 3600_000,
rrule="FREQ=WEEKLY;BYDAY=MO,TH", tz="Europe/Berlin",
_uid="a1b2c3d4-0000-4000-8000-000000000005", created=T0, last_modified=T0)
task(_id=6, list_id=1, title="Team offsite", dtstart=T0 - T0 % DAY,
due=T0 - T0 % DAY + DAY, is_allday=1,
_uid="a1b2c3d4-0000-4000-8000-000000000006", created=T0, last_modified=T0)
# Deleted-but-unsynced: gone as far as the user is concerned, so not imported.
task(_id=7, list_id=1, title="Cancelled thing", _deleted=1,
_uid="a1b2c3d4-0000-4000-8000-000000000007", created=T0, last_modified=T0)
task(_id=8, list_id=2, title="Task in a hidden list", status=0,
_uid="a1b2c3d4-0000-4000-8000-000000000008", created=T0, last_modified=T0)
task(_id=9, list_id=3, title="Ship the release", due=T0 + 5 * DAY, status=0,
_uid="a1b2c3d4-0000-4000-8000-000000000009", created=T0, last_modified=T0,
_sync_id="https://dav.example.org/tasks/9.ics")
db.executemany(
"INSERT INTO Properties (property_id, task_id, mimetype, data0, data1, data2, data3)"
" VALUES (?,?,?,?,?,?,?)",
[
# data0 minutes before, data1 reference (1 = DUE), data3 alarm type.
(1, 1, ALARM_MIMETYPE, "30", "1", None, "1"),
(2, 9, ALARM_MIMETYPE, "1440", "1", "Ship it", "1"),
# A non-alarm property the import must skip.
(3, 1, "vnd.android.cursor.item/category", "Errands", None, None, None),
],
)
def main() -> int:
os.makedirs(os.path.dirname(OUT), exist_ok=True)
if os.path.exists(OUT):
os.remove(OUT)
db = sqlite3.connect(OUT)
try:
for statement in DDL:
db.execute(statement)
db.execute("PRAGMA user_version = 23")
seed(db)
db.commit()
finally:
db.close()
print(f"wrote {OUT}")
return 0
if __name__ == "__main__":
sys.exit(main())