From c8c142bd1b06588126aa957ad76b42327165f4e7 Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Wed, 16 Oct 2024 23:58:22 +0200 Subject: [PATCH] Obsidian import: More intelligent time behaviour --- obsidian_import/__init__.py | 6 +++--- obsidian_import/obsidian.py | 28 +++++++++++++++++++++++----- personal_data/activity.py | 11 +++++++++++ test/test_obsidian_format.py | 18 ++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 test/test_obsidian_format.py diff --git a/obsidian_import/__init__.py b/obsidian_import/__init__.py index e8cb101..ee775a4 100644 --- a/obsidian_import/__init__.py +++ b/obsidian_import/__init__.py @@ -119,8 +119,9 @@ def import_watched_series_csv(vault: ObsidianVault, rows: Rows) -> int: sample.single_label_with_category('episode.index'), sample.single_label_with_category('episode.name'), ) - return Event(sample.start_at.time(), - sample.end_at.time(), + expected_tz = datetime.timezone(datetime.timedelta(hours=2)) # TODO: Determine this in a more intelligent manner + return Event(sample.start_at.astimezone(expected_tz).replace(second=0,microsecond=0).time(), + sample.end_at.astimezone(expected_tz).replace(second=0,microsecond=0).time(), verb, sample.single_label_with_category('series.name'), comment, @@ -162,6 +163,5 @@ def import_data(obsidian_path: Path, dry_run=True): data_path = Path('output/show_episodes_watched.csv') rows = load_csv_file(data_path) logger.info('Loaded CSV with %d lines', len(rows)) - rows = rows[:7] num_updated = import_watched_series_csv(vault, rows) logger.info('Updated %d files', num_updated) diff --git a/obsidian_import/obsidian.py b/obsidian_import/obsidian.py index b0dc767..be2085b 100644 --- a/obsidian_import/obsidian.py +++ b/obsidian_import/obsidian.py @@ -210,25 +210,43 @@ def format_event_string(event: Event) -> str: ): return event.comment - return f'{event.start_time:%H:%M} | {event.verb} [[{event.subject}]]. {event.comment}'.strip() + buf = [] + buf.append(f'{event.start_time:%H:%M}') + if event.end_time and event.end_time != event.start_time: + buf.append(f'-{event.end_time:%H:%M}') + buf.append(' | ') + buf.append(event.verb) + buf.append(' [[') + buf.append(event.subject) + buf.append(']]. ') + buf.append(event.comment.strip()) + + return ''.join(buf) RE_TIME = r'(\d\d:\d\d(?::\d\d(?:\.\d+?))?)' +RE_VERB = r'(\w+(?:ed|te))' +RE_LINK_MD = r'\[([^\]]*)\]\(?:[^)]*\)' +RE_LINK_WIKI = r'\[\[([^\]]*)\]\]' + +RE_TIME_FORMAT = RE_TIME + r'(?:\s*\-\s*' + RE_TIME + r')?' def parse_event_string(event_str: str) -> Event: if m := re.match( - r'^\s*' + RE_TIME + r'[ :\|-]*(\w+ed)\s+\[([^\]]*)\]\([^)]*\)\.?\s*(.*)$', + r'^\s*' + RE_TIME_FORMAT + r'[ :\|-]*'+RE_VERB+r'\s+'+RE_LINK_MD+r'\.?\s*(.*)$', event_str, ): start = datetime.time.fromisoformat(m.group(1)) - return Event(start, start, m.group(2), m.group(3), m.group(4)) + end = datetime.time.fromisoformat(m.group(2)) if m.group(2) else start + return Event(start, end, m.group(3), m.group(4), m.group(5)) if m := re.match( - r'^\s*' + RE_TIME + '[ :\|-]*(\w+ed)\s+\[\[([^\]]*)\]\]\.?\s*(.*)$', + r'^\s*' + RE_TIME_FORMAT + r'[ :\|-]*'+RE_VERB+r'\s+'+RE_LINK_WIKI+r'\.?\s*(.*)$', event_str, ): start = datetime.time.fromisoformat(m.group(1)) - return Event(start, start, m.group(2), m.group(3), m.group(4)) + end = datetime.time.fromisoformat(m.group(2)) if m.group(2) else start + return Event(start, end, m.group(3), m.group(4), m.group(5)) logger.info('Could not parse format: %s', event_str) return Event(None, None, None, None, event_str) diff --git a/personal_data/activity.py b/personal_data/activity.py index 89e70cd..ba13826 100644 --- a/personal_data/activity.py +++ b/personal_data/activity.py @@ -23,6 +23,10 @@ class ActivitySample: start_at: datetime.datetime | None end_at: datetime.datetime | None + def __post_init__(self): + if self.start_at and self.end_at: + assert self.start_at <= self.end_at + def single_label_with_category(self, category: str) -> str: for label in self.labels: if label.category == category: @@ -35,6 +39,11 @@ class RealizedActivitySample(ActivitySample): start_at: datetime.datetime end_at: datetime.datetime + def __post_init__(self): + assert self.start_at is not None + assert self.end_at is not None + assert self.start_at <= self.end_at + def heuristically_realize_samples( samples: list[ActivitySample], @@ -45,6 +54,8 @@ def heuristically_realize_samples( * No samples overlap. """ + samples.sort(key = lambda x: x.end_at) + previous_sample_end = None for sample in samples: end_at = sample.end_at diff --git a/test/test_obsidian_format.py b/test/test_obsidian_format.py new file mode 100644 index 0000000..86909bf --- /dev/null +++ b/test/test_obsidian_format.py @@ -0,0 +1,18 @@ +import datetime +import pytest + +from obsidian_import import obsidian + + +EXAMPLES = [ + obsidian.Event(datetime.time(12, 0, 0), datetime.time(12, 0, 0), "Ate", + "Lunch", "instantly"), + obsidian.Event(datetime.time(20, 0, 0), datetime.time(22, 0, 0), + "Watched", "Tom and Jerry", "on the *Television*"), + obsidian.Event(None, None, None, None, "Took a walk"), +] + +@pytest.mark.parametrize("event", EXAMPLES) +def test_format_preserves_information(event: obsidian.Event): + formatted = obsidian.format_event_string(event) + assert obsidian.parse_event_string(formatted) == event