diff --git a/obsidian_import/__init__.py b/obsidian_import/__init__.py index 51dbe1e..f0123cd 100644 --- a/obsidian_import/__init__.py +++ b/obsidian_import/__init__.py @@ -5,7 +5,7 @@ Sub-module for importing time-based data into Obsidian. import dataclasses import datetime -from collections.abc import Iterator +from collections.abc import Iterator, Iterable from logging import getLogger from pathlib import Path from typing import Any @@ -119,7 +119,6 @@ class EventContent: subject: str comment: str - def import_activity_sample_csv( vault: ObsidianVault, rows: Rows, @@ -141,16 +140,9 @@ def import_activity_sample_csv( def map_to_event(sample: RealizedActivitySample) -> Event: content = content_mapper(sample) - 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(), + sample.start_at, + sample.end_at, verb=content.verb, subject=escape_for_obsidian_link(content.subject), comment=content.comment, diff --git a/obsidian_import/obsidian.py b/obsidian_import/obsidian.py index 39697df..9450ad6 100644 --- a/obsidian_import/obsidian.py +++ b/obsidian_import/obsidian.py @@ -6,6 +6,8 @@ from decimal import Decimal from logging import getLogger from pathlib import Path from typing import Any +from zoneinfo import ZoneInfo +import enforce_typing import frontmatter import marko @@ -16,10 +18,11 @@ logger = getLogger(__name__) StatisticKey = str +@enforce_typing.enforce_types @dataclasses.dataclass(frozen=True, order=True) class Event: - start_time: datetime.time | None - end_time: datetime.time | None + start_time: datetime.datetime | None + end_time: datetime.datetime | None verb: str | None subject: str | None comment: str @@ -29,13 +32,13 @@ class Event: assert ':' not in self.subject assert '/' not in self.subject - @dataclasses.dataclass(frozen=True) class FileContents: frontmatter: dict[str, Any] blocks_pre_events: list events: frozenset[Event] blocks_post_events: list + timezone: ZoneInfo @dataclasses.dataclass(frozen=False) @@ -54,9 +57,6 @@ FILE_FORMAT = """ {blocks_post_events} """ -MIDNIGHT = datetime.time(0, 0, 0) - - class ObsidianVault: def __init__( self, @@ -136,6 +136,8 @@ class ObsidianVault: return contents.events def _load_date_contents(self, date: datetime.date) -> FileContents | None: + timezone = ZoneInfo('Europe/Copenhagen') # TODO: Parameterize in an intelligent manner + file_path = self._date_file_path(date) text = self._load_file_text(file_path) or self._load_file_text( self._daily_template_path(), @@ -147,9 +149,9 @@ class ObsidianVault: ast = MARKDOWN_PARSER.parse(str(file_frontmatter)) (pre_events, list_block_items, post_events) = find_events_list_block(ast) events = frozenset( - parse_event_string(list_item) for list_item in list_block_items + parse_event_string(list_item, date, timezone) for list_item in list_block_items ) - return FileContents(file_frontmatter.metadata, pre_events, events, post_events) + return FileContents(file_frontmatter.metadata, pre_events, events, post_events, timezone) def _save_date_contents(self, date: datetime.date, contents: FileContents) -> None: blocks_pre_events = ''.join( @@ -162,8 +164,9 @@ class ObsidianVault: events = list(contents.events) events.sort(key=lambda x: x.subject or '') events.sort(key=lambda x: x.verb or '') - events.sort(key=lambda x: x.start_time or x.end_time or MIDNIGHT) - block_events = '\n'.join('- ' + format_event_string(e) for e in events) + date_sentinel = datetime.datetime(1900, 1, 1, 1, 1, 1, tzinfo=contents.timezone) + events.sort(key=lambda x: x.start_time or x.end_time or date_sentinel) + block_events = '\n'.join('- ' + format_event_string(e, tz = contents.timezone) for e in events) post = frontmatter.Post( content=FILE_FORMAT.format( @@ -240,7 +243,7 @@ def find_events_list_block(ast) -> tuple[list, list[str], list]: return (blocks, [], []) -def format_event_string(event: Event) -> str: +def format_event_string(event: Event, tz: ZoneInfo) -> str: assert event is not None if ( event.start_time is None @@ -251,9 +254,9 @@ def format_event_string(event: Event) -> str: return event.comment buf = [] - buf.append(f'{event.start_time:%H:%M}') + buf.append(f'{event.start_time.astimezone(tz):%H:%M}') if event.end_time and event.end_time != event.start_time: - buf.append(f'-{event.end_time:%H:%M}') + buf.append(f'-{event.end_time.astimezone(tz):%H:%M}') buf.append(' | ') buf.append(event.verb) buf.append(' [[') @@ -271,7 +274,9 @@ RE_LINK_WIKI = r'\[\[([^\]:/]*)\]\]' RE_TIME_FORMAT = RE_TIME + r'(?:\s*\-\s*' + RE_TIME + r')?' -def parse_event_string(event_str: str) -> Event: +def parse_event_string(event_str: str, date: datetime.date, timezone: ZoneInfo) -> Event: + """Parses event string for the given date. + """ if m := re.match( r'^\s*' + RE_TIME_FORMAT @@ -282,10 +287,9 @@ def parse_event_string(event_str: str) -> Event: + r'\.?\s*(.*)$', event_str, ): - start = datetime.time.fromisoformat(m.group(1)) - 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( + start_time = datetime.time.fromisoformat(m.group(1)) + end_time = datetime.time.fromisoformat(m.group(2)) if m.group(2) else start_time + elif m := re.match( r'^\s*' + RE_TIME_FORMAT + r'[ :\|-]*' @@ -295,8 +299,13 @@ def parse_event_string(event_str: str) -> Event: + r'\.?\s*(.*)$', event_str, ): - start = datetime.time.fromisoformat(m.group(1)) - 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) + start_time = datetime.time.fromisoformat(m.group(1)) + end_time = datetime.time.fromisoformat(m.group(2)) if m.group(2) else start_time + else: + logger.info('Could not parse format: %s', event_str) + return Event(None, None, None, None, event_str) + + start = datetime.datetime.combine(date, start_time, timezone) + end = datetime.datetime.combine(date, end_time, timezone) + + return Event(start, end, m.group(3), m.group(4), m.group(5))