entry modules refactor
This commit is contained in:
213
journal.py
213
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,14 +575,33 @@ def parse_entry(entry):
|
||||
|
||||
return res
|
||||
|
||||
def split_into_blocks(text):
|
||||
return reduce(flip(apply), [
|
||||
def split_post_block(l):
|
||||
res = []
|
||||
|
||||
POST_BLOCK_LENGTH = len('@post 2020-02-02 02:02:02')
|
||||
|
||||
i = 0
|
||||
while i < len(l):
|
||||
curr = l[i]
|
||||
|
||||
if curr.startswith('@post'):
|
||||
res.append(curr[:POST_BLOCK_LENGTH])
|
||||
res.append(curr[POST_BLOCK_LENGTH+1:])
|
||||
else:
|
||||
res.append(curr)
|
||||
|
||||
i += 1
|
||||
|
||||
return res
|
||||
|
||||
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
|
||||
@@ -621,56 +609,32 @@ def parse_entry(entry):
|
||||
# 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] == ' '))),
|
||||
|
||||
# merge notes block
|
||||
## 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],
|
||||
], text)
|
||||
|
||||
def parse_module_block(block):
|
||||
tag = block.split()[0][1:]
|
||||
return entry_modules[tag](block)
|
||||
|
||||
def parse_block(block):
|
||||
if block.startswith('@'):
|
||||
return parse_module_block(block)
|
||||
else:
|
||||
return block
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
34
migrations/2021-06-27_post.py
Normal file
34
migrations/2021-06-27_post.py
Normal file
@@ -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))
|
||||
Reference in New Issue
Block a user