This repository has been archived on 2026-04-05. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
things-3-report/dump.py
2023-07-14 18:19:42 +09:00

184 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import things
import argparse
import re
import random
from datetime import datetime, timedelta
with open('./emojis.txt', 'r') as emoji_file:
EMOJIS = [l.strip() for l in emoji_file.readlines()]
def mine_tags(tags, place):
if place is None or not 'tags' in place:
return tags
tags.extend(place['tags'])
def has_tag(task, tag):
area = things.areas(uuid=task['area']) if 'area' in task else None
project = things.projects(uuid=task['project']) if 'project' in task else None
heading_project = things.projects(uuid=things.get(uuid=task['heading'])['project']) if 'heading' in task else None
project_area = things.areas(uuid=project['area']) if project is not None and 'area' in project else None
tags = []
mine_tags(tags, task)
mine_tags(tags, area)
mine_tags(tags, heading_project)
mine_tags(tags, project)
mine_tags(tags, project_area)
return tag in tags
def tasks_to_heirarchy(top_level_comment, tasks):
heir = {}
for task in tasks:
task_leaf = (task, {})
if 'project' not in task and 'heading' not in task:
heir[task['uuid']] = task_leaf
elif 'heading' in task:
h_uuid = task['heading']
head = things.get(h_uuid)
p_uuid = head['project'] # This must exist
proj = things.get(p_uuid)
proj_title, proj_sublevel = heir[p_uuid] if p_uuid in heir else (proj, {})
head_title, head_sublevel = proj_sublevel[h_uuid] if h_uuid in proj_sublevel else (head, {})
head_sublevel[task['uuid']] = task_leaf
proj_sublevel[head['uuid']] = (head_title, head_sublevel)
heir[p_uuid] = (proj_title, proj_sublevel)
else: # The project must not be None here
p_uuid = task['project']
proj = things.get(p_uuid)
proj_title, proj_sublevel = heir[p_uuid] if p_uuid in heir else (proj, {})
proj_sublevel[task['uuid']] = task_leaf
heir[p_uuid] = (proj_title, proj_sublevel)
return (top_level_comment, heir)
def parse_note(note):
note_block_pattern = r"```report\n([\s\S]*?)\n?```"
return ' '.join(re.findall(note_block_pattern, note))
def make_recursive_formatter(formatter):
def recursive_format(structure, depth):
(node, branches) = structure
notes = parse_note(node['notes'])
node_str = formatter.node(node, notes)
if len(branches) == 1:
item = list(branches.values())[0]
node_str += formatter.single(recursive_format(item, depth))
elif len(branches) > 1:
new_depth = depth + 2
bullets = [formatter.bullet(f"{recursive_format(item, new_depth)}", new_depth) for item in branches.values()]
node_str += ''.join(map(lambda b: f'\n{b}', bullets))
return node_str
return recursive_format
class TodayFormatter:
def node(self, node, notes):
return f"{node['title']}{'' if notes == '' else f' {notes}'}"
def single(self, subtext):
return f" > {subtext}"
def bullet(self, subtext, depth):
space = ''.join([' '] * depth)
return f"{space}- {subtext}"
class LogbookFormatter:
def node(self, node, notes):
node_text = f"{node['title']}{'' if notes == '' or node['status'] == 'canceled' else f' {notes}'}"
if node['status'] == 'canceled':
node_text = f"~{node_text}~"
return node_text
def single(self, subtext):
return f" > {subtext}"
def bullet(self, subtext, depth):
space = ''.join([' '] * depth)
return f"{space}- {subtext}"
# Note: the characters on the RHS are all "lookalikes". The do not == the left side
VOWEL_MAP = {
'a': 'а',
'e': 'e',
'i': 'і',
'o': 'о',
'u': 'ս',
}
def sanitize_name(name):
for normal_ch, special_ch in VOWEL_MAP.items():
name = name.replace(normal_ch, special_ch)
return name
def sanitize_string(s, mapping):
for original_str, new_str in mapping.items():
s = s.replace(original_str, new_str)
return s
def sanitize_mentions(task):
if 'tags' not in task:
return
people_tags = { t[1:]: sanitize_name(t[1:]) for t in task['tags'] if t[0] == '@' }
# For now just sanitize title and notes (headings might also make sense?)
task['title'] = sanitize_string(task['title'], people_tags)
task['notes'] = sanitize_string(task['notes'], people_tags)
def same_date(d1, d2):
day1 = str(d1).split()[0]
day2 = str(d2).split()[0]
return day1 == day2
def generate_signoff_message(target_tag):
format_tasks = make_recursive_formatter(LogbookFormatter())
import datetime
today = datetime.datetime.today()
reportable = list(filter(lambda t: has_tag(t, target_tag) and same_date(today, t['stop_date']) and t['status'] == 'completed', things.logbook()))
for task in reportable:
sanitize_mentions(task)
structured_tasks = tasks_to_heirarchy({ 'title': 'Stopping now', 'notes': '', 'status': '' }, sorted(reportable, key=by_modified_timestamp))
return format_tasks(structured_tasks, 0)
def by_modified_timestamp(val):
return datetime.fromisoformat(val['modified'])
def generate_today_message(target_tag):
format_tasks = make_recursive_formatter(TodayFormatter())
reportable = list(filter(lambda t: has_tag(t, target_tag), things.today()))
for task in reportable:
sanitize_mentions(task)
top_level_comment = ' '.join(map(lambda e: f':{e}:', random.choices(EMOJIS, k=3)))
structured_tasks = tasks_to_heirarchy({ 'title': top_level_comment, 'notes': '' }, sorted(reportable, key=by_modified_timestamp))
return format_tasks(structured_tasks, 0)
def generate_track_message(target_tag):
t = datetime.today() - timedelta(days=14)
projects = filter(lambda x: x['type'] == 'project', things.logbook())
recent_projects = filter(lambda x: datetime.fromisoformat(x['modified']) > t, projects)
selected_projects = filter(lambda x: 'tags' in x and target_tag in x['tags'], recent_projects)
return '\n'.join(map(lambda x: x['title'], selected_projects))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Generate iknow_vacation_remote style messages from Things 3 Tasks")
parser.add_argument('tags', metavar='TAG', type=str, nargs="+", help="a word in the overall tag")
parser.add_argument('--signoff', action=argparse.BooleanOptionalAction, help="Get tasks from the logbook and generate output for a signoff message")
parser.add_argument('--track', action=argparse.BooleanOptionalAction, help="Get tasks from the logbook and generate output for a track meeting")
args = parser.parse_args()
target_tag = ' '.join(args.tags)
if args.signoff:
print(generate_signoff_message(target_tag))
elif args.track:
print(generate_track_message(target_tag))
else:
print(generate_today_message(target_tag))