From 954e0c12aff84c81541069628c85729ca17a6569 Mon Sep 17 00:00:00 2001 From: olari Date: Sun, 27 Jun 2021 15:18:48 +0300 Subject: [PATCH] entry modules refactor --- journal.py | 247 ++++++++++++++-------------------- migrations/2021-06-27_post.py | 34 +++++ 2 files changed, 138 insertions(+), 143 deletions(-) create mode 100644 migrations/2021-06-27_post.py diff --git a/journal.py b/journal.py index 8c6dacd..0cfcf72 100644 --- a/journal.py +++ b/journal.py @@ -11,6 +11,7 @@ import random import re import sys import textwrap +import traceback ### GLOBALS @@ -18,6 +19,9 @@ JOURNAL_PATH = Path.home() / '.journal.json' ### UTILS +def compose(*fns): + return partial(reduce, flip(apply), fns) + def nth_or_default(n, l, default): return l[n] if n < len(l) else default @@ -287,60 +291,8 @@ def create_sticky(journal, date): ### ENTRY MODULES -def parse_post(block): - block = block.removeprefix('@post').strip() - - try: - timestamp = int(parse_timestamp(block[:19]).timestamp()) - block = block[19:] - except: - timestamp = None - - content = block.strip() - - result = {} - if content: - result['content'] = content - if timestamp: - result['timestamp'] = timestamp - return result - -def generate_post(block): - result = '@post' - - if ts := block.get('timestamp'): - result += f' {format_timestamp(ts)}' - - if content := block.get('content'): - result += f' {content}' - - return wrap_text(result) - -def parse_notes(block): - tag, source, title = block.splitlines() - return {'source': source, 'title': title} - -def generate_notes(block): - parts = ['@notes'] - - if source := block.get('source'): - parts.append(source) - if title := block.get('title'): - parts.append(title) - - return '\n'.join(parts) - -def parse_diet(block): - tag, amount, food = block.split() - amount = int(amount.removesuffix('g')) - return {'amount': amount, 'food': food} - -def generate_diet(block): - _, amount, food = block.values() - return f'@diet {amount}g {food}' - def parse_timer(block): - tag, *rest = block.split() + rest = block.split() name = None timestamp = None @@ -358,7 +310,7 @@ def parse_timer(block): return result def generate_timer(block): - parts = [f'@{block["type"]}'] + parts = [] if name := block.get('name'): parts.append(name) @@ -369,7 +321,7 @@ def generate_timer(block): return ' '.join(parts) def parse_exercise(block): - tag, *parts = block.split() + parts = block.split() if parts[0] == 'walk': kind, minutes, distance, steps = parts @@ -394,25 +346,62 @@ def parse_exercise(block): def generate_exercise(block): if block['kind'] == 'walk': - return f'@exercise walk {block["minutes"]}min {block["distance"]}km {block["steps"]}steps' + return f'walk {block["minutes"]}min {block["distance"]}km {block["steps"]}steps' elif block['kind'] == 'calisthenics': - return f'@exercise calisthenics {block["sets"]}x{block["reps"]} {block["exercise"]}' + return f'calisthenics {block["sets"]}x{block["reps"]} {block["exercise"]}' assert False -def parse_notify(block): - tag, day, *rest = block.split() +DEFAULT_PARSER = lambda b: {'value': b} +DEFAULT_GENERATOR = lambda b: b['value'] - return {'day': day.strip(), 'message': ' '.join(rest)} +entry_modules = { + 'diet': ( + lambda b: {'amount': int(b.split()[0].removesuffix('g')), 'food': b.split()[1].strip()}, + lambda b: f'{b["amount"]}g {b["food"]}'), + 'exercise': (parse_exercise, generate_exercise), + 'behavior': (DEFAULT_PARSER, DEFAULT_GENERATOR), -def generate_notify(block): - return f'@notify {block["day"]} {block["message"]}' + 'hide': (lambda _: {}, lambda _: ''), + 'info': (DEFAULT_PARSER, compose(DEFAULT_GENERATOR, wrap_text)), -def generate_default(block): - return f'@{block["type"]} {block["value"]}' + 'post': ( + lambda b: {'timestamp': int(parse_timestamp(b.removeprefix('@post ').strip()).timestamp())}, + lambda b: format_timestamp(b["timestamp"]) + ), + 'notes': ( + compose( + lambda b: b.splitlines(), + lambda s: {'source': s[0], 'title': s[1]}, + ), + lambda b: f'{b["source"]}\n{b["title"]}' + ), -def generate_info(block): - return wrap_text(f'@info {block["value"]}') + 'task': (DEFAULT_PARSER, DEFAULT_GENERATOR), + 'start': (parse_timer, generate_timer), + 'stop': (parse_timer, generate_timer), + 'done': (parse_timer, generate_timer), + + 'notify': ( + compose( + lambda b: b.split(maxsplit=1), + lambda s: {'day': s[0], 'message': s[1]} + ), + lambda b: f'{b["day"]} {b["message"]}' + ), +} + +def parse_entry_module(block: str) -> dict: + tag = block.split()[0].removeprefix('@') + block = block.removeprefix(f'@{tag}').strip() + + return {'type': tag} | entry_modules[tag][0](block) + +def generate_entry_module(block: dict) -> str: + if block['type'] == 'notes': + return f'@notes\n{entry_modules[block["type"]][1](block)}' + + return f'@{block["type"]} {entry_modules[block["type"]][1](block)}' ### READ-ONLY STATS SECTION FUNCTIONS @@ -475,7 +464,6 @@ def generate_stats(page): return result - ### PAGE FUNCTIONS def create_header(journal, date): @@ -537,25 +525,6 @@ def create_entry(journal, date): } def parse_entry(entry): - def create_entry_module_parser(name, handler=None): - handler = handler or (lambda b: {'value': b.removeprefix(f'@{name} ')}) - return (name, lambda b: {'type': name} | handler(b)) - - entry_modules = dict([ - create_entry_module_parser('hide', lambda _: {}), - create_entry_module_parser('post', parse_post), - create_entry_module_parser('info'), - create_entry_module_parser('notes', parse_notes), - create_entry_module_parser('behavior'), - create_entry_module_parser('diet', parse_diet), - create_entry_module_parser('task'), - create_entry_module_parser('start', parse_timer), - create_entry_module_parser('stop', parse_timer), - create_entry_module_parser('done', parse_timer), - create_entry_module_parser('exercise', parse_exercise), - create_entry_module_parser('notify', parse_notify), - ]) - def merge_notes_block(l): res = [] @@ -606,71 +575,66 @@ def parse_entry(entry): return res - def split_into_blocks(text): - return reduce(flip(apply), [ - # split the text into sections by newline and tag symbol, keeping the separators - partial(split_keep, ('\n', '@')), + def split_post_block(l): + res = [] - # merge sequential newlines together into a single whitespace block - partial(merge_if, lambda p, c: p == c == '\n'), + POST_BLOCK_LENGTH = len('@post 2020-02-02 02:02:02') - # attach escaped tags - partial(merge_if, lambda p, c: c == '@' and p[-1] == '\\'), - # attach tag - partial(merge_if, lambda p, c: p == '@'), - # attach tags which do not come after newline or another tag - partial(merge_if, lambda p, c: c[0] == '@' and not (not p[-1] != '\n' or (p[0] == '@' and p[-1] == ' '))), + i = 0 + while i < len(l): + curr = l[i] - # merge notes block - merge_notes_block, - # strip all non-whitespace blocks - partial(map, lambda s: s if s.isspace() else s.rstrip()), list, - # merge escaped tags with following text - partial(merge_if, lambda p, c: p.endswith('\\@')), - # merge wrapped lines - merge_wrapped_lines, - # remove trailing whitespace block - lambda b: b if b and not all(c == '\n' for c in b[-1]) else b[:-1], - ], text) + if curr.startswith('@post'): + res.append(curr[:POST_BLOCK_LENGTH]) + res.append(curr[POST_BLOCK_LENGTH+1:]) + else: + res.append(curr) - def parse_module_block(block): - tag = block.split()[0][1:] - return entry_modules[tag](block) + i += 1 + + return res - def parse_block(block): - if block.startswith('@'): - return parse_module_block(block) - else: - return block + split_into_blocks = compose( + # split the text into sections by newline and tag symbol, keeping the separators + partial(split_keep, ('\n', '@')), + + # merge sequential newlines together into a single whitespace block + partial(merge_if, lambda p, c: p == c == '\n'), + + ## TAG PARSING + # attach escaped tags + partial(merge_if, lambda p, c: c == '@' and p[-1] == '\\'), + # attach tag + partial(merge_if, lambda p, c: p == '@'), + # attach tags which do not come after newline or another tag + partial(merge_if, lambda p, c: c[0] == '@' and not (not p[-1] != '\n' or (p[0] == '@' and p[-1] == ' '))), + + ## SPECIAL BLOCK PARSING + # merge notes block (because it spans 3 lines or 5 blocks) + merge_notes_block, + # split post block (because next block could be attached to it) + split_post_block, + + # strip all non-whitespace blocks + partial(map, lambda s: s if s.isspace() else s.rstrip()), list, + # merge escaped tags with following text + partial(merge_if, lambda p, c: p.endswith('\\@')), + + # merge wrapped lines + merge_wrapped_lines, + + # remove trailing whitespace block + lambda b: b if b and not all(c == '\n' for c in b[-1]) else b[:-1], + ) timestamp, content = entry return { 'timestamp': int(parse_timestamp(timestamp.strip()).timestamp()), - 'blocks': [parse_block(b) for b in split_into_blocks(content)], + 'blocks': [parse_entry_module(b) if b.startswith('@') else b for b in split_into_blocks(content)], } def generate_entry(entry): - entry_modules = { - 'diet': generate_diet, - 'exercise': generate_exercise, - 'behavior': generate_default, - - 'hide': lambda _: '@hide', - 'info': generate_info, - - 'post': generate_post, - - 'notes': generate_notes, - - 'task': generate_default, - 'start': generate_timer, - 'stop': generate_timer, - 'done': generate_timer, - - 'notify': generate_notify, - } - def format_block(block, is_first): def format_text(text): if all(c == '\n' for c in block): @@ -691,10 +655,7 @@ def generate_entry(entry): return text - def format_module(module): - return entry_modules[module['type']](module) - - formatted = format_text(block) if isinstance(block, str) else format_module(block) + formatted = format_text(block) if isinstance(block, str) else generate_entry_module(block) if result[-1] != '\n' and not all(c == '\n' for c in formatted): formatted = ' ' + formatted @@ -816,7 +777,7 @@ def open_journal(date): new_journal = import_journal(tmpdir) break except Exception as e: - print('Error:', e) + traceback.print_exc() input('Press enter to try again...') save_journal(new_journal) diff --git a/migrations/2021-06-27_post.py b/migrations/2021-06-27_post.py new file mode 100644 index 0000000..4442984 --- /dev/null +++ b/migrations/2021-06-27_post.py @@ -0,0 +1,34 @@ +from copy import deepcopy +from pathlib import Path +from shutil import copy +import json + +journal_path = Path.home() / '.journal.json' + +copy(str(journal_path), str(journal_path.with_suffix('.bkp'))) + +journal = json.loads(journal_path.read_text()) +new_journal = deepcopy(journal) + +for day in journal['days']: + new_entries = [] + for entry in journal['days'][day]['entries']: + new_blocks = [] + for block in entry['blocks']: + if not isinstance(block, str) and block['type'] == 'post': + new_blocks.append({ + 'type': 'post', + 'timestamp': block.get('timestamp', entry['timestamp'] + 30) + }) + + if content := block.get('content'): + new_blocks.append(content) + else: + new_blocks.append(block) + + entry['blocks'] = new_blocks + new_entries.append(entry) + + new_journal['days'][day]['entries'] = new_entries + +journal_path.write_text(json.dumps(new_journal))