From b2924168a9d65bf8b0a6d6b209b284bb45b69bde Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 4 Apr 2012 19:00:17 +0200 Subject: [PATCH] Refractored into a Journal and Entry class, cleaned up, documented. --- jrnl.py | 285 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 181 insertions(+), 104 deletions(-) diff --git a/jrnl.py b/jrnl.py index 23cd73bf..3bd3cc8e 100755 --- a/jrnl.py +++ b/jrnl.py @@ -3,6 +3,7 @@ import parsedatetime.parsedatetime as pdt import parsedatetime.parsedatetime_consts as pdc +import re import argparse from datetime import datetime import time @@ -12,132 +13,208 @@ config = { 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", + 'tagsymbols': '#@' } -def read_file(filename=None): - filename = filename or config['journal'] - f = open(filename) - journal = [] +class Entry: + def __init__(self, journal, date=None, title="", body=""): + self.journal = journal # Reference to journal mainly to access it's config + self.date = date + self.title = title.strip() + self.body = body.strip() + self.tags = self.parse_tags() - date_length = len(datetime.today().strftime(config['timeformat'])) - date = None - body = "" - title = "" - for line in f.readlines(): - if line: - try: - new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], config['timeformat']))) - # make a journal entry of the current stuff first - if date: - journal.append((date, title.strip(), body.strip())) - # Start constructing current entry - title = line[date_length+1:] - body = "" - date = new_date - except ValueError: - body += line - journal.append((date, title.strip(), body.strip())) - f.close() - return journal + def parse_tags(self): + fulltext = " ".join([self.title, self.body]).lower() + tags = re.findall(r"([%s]\w+)" % self.journal.config['tagsymbols'], fulltext) + self.tags = set(tags) -def print_journal(journal): - for date, title, body in sorted(journal): - print "Date:", date.strftime(config['timeformat']) - print "Title:", title - if body: print "Body:", body - print "-------------------------------------------" + def __str__(self): + date_str = self.date.strftime(self.journal.config['timeformat']) + body_wrapper = "\n" if self.body else "" + body = body_wrapper + self.body.strip() + space = "\n\n" -def write_file(journal, filename=None): - filename = filename or config['journal'] - f = open(filename, 'w') - for date, title, body in sorted(journal): - body = ("\n%s\n\n" % body if body else "\n\n") - f.write("%(date)s %(title)s %(body)s" % { - 'date': date.strftime(config['timeformat']), - 'title': title, + return "%(date)s %(title)s %(body)s %(space)s" % { + 'date': date_str, + 'title': self.title, 'body': body, - }) - f.close() + 'space': space + } -def parse_entry(log, date=None): - # Set up date parser - consts = pdc.Constants() - consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday - dateparse = pdt.Calendar(consts) + def __repr__(self): + return str(self) - if not date: - #see whether we find anything in the beginning of our log - if log.find(":") > 0: - date = log[:log.find(":")] - dtest, flag = dateparse.parse(date) - if flag: # can parse successfully - log = log[log.find(":")+1:].strip() + +class Journal: + def __init__(self, config): + self.config = config + + # Set up date parser + consts = pdc.Constants() + consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday + self.dateparse = pdt.Calendar(consts) + + self.entries = self.open() + self.sort() + + def open(self, filename=None): + """Opens the journal file defined in the config and parses it into a list of Entries. + Entries have the form (date, title, body).""" + filename = filename or self.config['journal'] + + # Entries start with a line that looks like 'date title' - let's figure out how + # long the date will be by constructing one + date_length = len(datetime.today().strftime(self.config['timeformat'])) + + # Initialise our current entry + entries = [] + current_entry = None + + journal_file = open(filename) + for line in journal_file.readlines(): + if line: + try: + new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], config['timeformat']))) + # make a journal entry of the current stuff first + if new_date and current_entry: + entries.append(current_entry) + # Start constructing current entry + current_entry = Entry(self, date=new_date, title=line[date_length+1:]) + except ValueError: + # Happens when we can't parse the start of the line as an date. + # In this case, just append line to our body. + current_entry.body += line + # Append last entry + entries.append(current_entry) + journal_file.close() + for entry in entries: + entry.parse_tags() + return entries + + def __str__(self): + """Prettyprints the journal's entries""" + sep = "-"*60+"\n" + return sep.join([str(e) for e in self.entries]) + + def __repr__(self): + return "" % len(self.entries) + + def write(self, filename = None): + """Dumps the journal into the config file, overwriting it""" + filename = filename or self.config['journal'] + journal_file = open(filename, 'w') + for entry in self.entries: + journal_file.write(entry) + journal_file.close() + + def sort(self): + self.entries = sorted(self.entries, key=lambda entry: entry.date) + + def limit(self, n=None): + """Removes all but the last n entries""" + if n: + self.entries = self.entries[-n:] + + def filter(self, tags=[], start_date=None, end_date=None, strict=False): + """Removes all entries from the journal that don't match the filter. + + tags is a list of tags, each being a string that starts with one of the + tag symbols defined in the config, e.g. ["@John", "#WorldDomination"]. + + start_date and end_date define a timespan by which to filter. + + If strict is True, all tags must be present in an entry. If false, the + entry is kept if any tag is present.""" + search_tags = set(tags) + # If strict mode is on, all tags have to be present in entry + tagged = search_tags.issubset if strict else search_tags.intersection + + result = [ + entry for entry in self.entries + if (not tags or tagged(entry.tags)) + and (not start_date or entry.date > start_date) + and (not end_date or entry.date < start_date) + ] + self.entries = result + + def parse_date(self, date): + """Parses a string containing a fuzzy date and returns a datetime.datetime object""" + if not date: + return None + elif type(date) is datetime: + return date + + date, flag = self.dateparse.parse(date) + + if not flag: # Oops, unparsable. + return None + + if flag is 1: # Date found, but no time. Use the default time. + date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute']) else: - date = "now" + date = datetime(*date[:6]) - # Parse date - date, flag = dateparse.parse(date) - if flag is 1: # set to 9 am - date = datetime(*date[:3], hour=config['default_hour'], minute=config['default_minute']) - else: - date = datetime(*date[:6]) + return date - # Split log into title and body - body = "" - title_end = len(log) - for separator in ".?!": - sep_pos = log.find(separator) - if 1 < sep_pos < title_end: - title_end = sep_pos - title = log[:title_end+1] - body = log[title_end+1:].strip() - return date, title, body + def new_entry(self, raw, date=None): + """Constructs a new entry from some raw text input. + If a date is given, it will parse and use this, otherwise scan for a date in the input first.""" + if not date: + if raw.find(":") > 0: + date = self.parse_date(raw[:raw.find(":")]) + if date: # Parsed successfully, strip that from the raw text + raw = raw[raw.find(":")+1:].strip() -def filter_journal(journal, tags=[], people=[]): - tags = [tag[1:].lower() if tag.startswith("#") else tag.lower() for tag in tags] - people = [person[:1].lower() if person.startswith("@") else person.lower() for person in people] - - def _has_tag(entry, tags, symbol="@"): - date, title, body = entry - fulltext = " ".join([title, body]).lower() - has = False - for tag in tags: - if symbol+tag in fulltext: - has = True - return has - - result = [entry for entry in journal - if _has_tag(entry, people) - or _has_tag(entry, tags, symbol="#") - ] - return result + if not date: # Still nothing? Meh, just live in the moment. + date = self.parse_date("now") + # Split raw text into title and body + body = "" + title_end = len(raw) + for separator in ".?!": + sep_pos = raw.find(separator) + if 1 < sep_pos < title_end: + title_end = sep_pos + title = raw[:title_end+1] + body = raw[title_end+1:].strip() + self.entries.append(Entry(self, date, title, body)) + self.sort() if __name__ == "__main__": parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') - composing.add_argument('log', metavar='text', nargs="*", help='Log entry') + composing.add_argument('text', metavar='text', nargs="*", help='Log entry') reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') - reading.add_argument('-tags', dest='tags', metavar="#tag", default=[], help='Tags by which to filter', nargs="*") - reading.add_argument('-people', dest='people', metavar="@person", default=[], help='People by which to filter', nargs="+") - reading.add_argument('-n', dest='limit', metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) + reading.add_argument('-from', dest='start_date', metavar="date", help='View entries after this date') + reading.add_argument('-to', dest='end_date', metavar="date", help='View entries before this date') + reading.add_argument('-n', dest='limit', default=None, metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) args = parser.parse_args() # open journal - journal = read_file() + journal = Journal(config=config) + + # Guess mode + compose = True + if args.start_date or args.end_date or args.limit: + # Any sign of displaying stuff? + compose = False + elif not args.date and args.text and all(word[0] in config['tagsymbols'] for word in args.text): + # No date and only tags? + print args.text, all(word[0] in config['tagsymbols'] for word in args.text) + compose = False + # Writing mode - if not args.log and not args.people and not args.limit and not args.tags: - args.log = [raw_input("Compose Entry: ")] - elif args.log: # Write mode - raw = " ".join(args.log).strip() - entry = parse_entry(log=raw, date=args.date) - journal.append(entry) - print_journal(journal) - write_file(journal) + if compose: + if not args.text: + args.text = [raw_input("Compose Entry: ")] + raw = " ".join(args.text).strip() + journal.new_entry(raw, args.date) + journal.write() + else: # read mode - journal = filter_journal(journal, tags=args.tags, people=args.people) - if args.limit: - journal = journal[:-limit] - print_journal(journal) + journal.filter(tags=args.text, start_date=args.start_date, end_date=args.end_date) + journal.limit(args.limit) + print journal