def main(): ping = hours_per_ping = settings.gap / 3600 if len(sys.argv) != 3 or settings.beemauth is None: usage() ttlf = sys.argv[1] # tagtime log filename usrslug = sys.argv[2] # like alice/weight m = re.search(r"^(?:.*?(?:\.\/)?data\/)?([^\+\/\.]*)[\+\/]([^\.]*)", usrslug) if not m: usage() usr, slug = m.groups(); beem = beemapi.Beeminder(settings.beemauth, usr) # beef = bee file (cache of data on bmndr) beef = os.path.join(settings.path, '{}+{}.bee'.format(usr, slug)) #if(defined(@beeminder)) { # for backward compatibility # print "Deprecation warning: Get your settings file in line!\n"; # print "Specifically, 'beeminder' should be a hash, not an arry.\n"; # for(@beeminder) { # @stuff = split(/\s+/, $_); # usrslug and tags # $us = shift(@stuff); # $beeminder{$us} = [@stuff]; # } #} crit = settings.beeminder.get(usrslug) if crit is None: raise ValueError("Can't determine which tags match {}".format(usrslug)) # ph (ping hash) maps "y-m-d" to number of pings on that day. # sh (string hash) maps "y-m-d" to the beeminder comment string for that day. # bh (beeminder hash) maps "y-m-d" to the bmndr ID of the datapoint on that day. # ph1 and sh1 are based on the current tagtime log and # ph0 and sh0 are based on the cached .bee file or beeminder-fetched data. ph = defaultdict(int) sh = defaultdict(str) bh = {} ph1 = defaultdict(int) sh1 = defaultdict(str) ph0 = defaultdict(int) sh0 = defaultdict(str) start = time.time() # start and end are the earliest and latest times we will end = 0 # need to care about when updating beeminder. # bflag is true if we need to regenerate the beeminder cache file. reasons we'd # need to: 1. it doesn't exist or is empty; 2. any beeminder IDs are missing # from the cache file; 3. there are multiple datapoints for the same day. try: bflag = not os.stat(beef).st_size except FileNotFoundError: bflag = True bf1 = False bf2 = False bf3 = False bf4 = False # why bflag? if bflag: bf1 = True remember = {} # remember which dates we've already seen in the cache file try: with open(beef, 'r') as B: for line in B: m = re.search(r''' (\d+)\s+ # year (\d+)\s+ # month (\d+)\s+ # day (\S+)\s+ # value "(\d+) # number of pings (?:[^\n\"\(:]*) # currently the string " ping(s)" : # the ": " after " pings" ([^\[]*) # the comment string (no brackets) (?:\[ # if present, bID\:([^\]]*) # the beeminder ID, in brackets \])? # end bracket for "[bID:abc123]" \s*" ''', line, re.VERBOSE) # XXX if not m set an error flag and continue y, m, d, v, p, c, b = m.groups() y = int(y) m = int(m) d = int(d) p = int(p) c = c.strip() ts = '{:04}-{:02}-{:02}'.format(y, m, d) ph0[ts] = p #$ph0{$ts} = $p; #$c =~ s/\s+$//; #m = re.match(r'\s+$/', c) sh0[ts] = c bh[ts] = b t = time.mktime((y, m, d, 0, 0, 0, 0, 0, -1)) if t < start: start = t if t > end: end = t if not b: bflag = True bf2 += 1 if bf2 == 1: print("Problem with this line in cache file:\n{}".format(line)) elif bf2 == 2: print("Additional problems with cache file, which is expected if this " "is your first time updating TagTime with the new Bmndr API.\n") if remember.get(ts): bflag = bf3 = True remember[ts] = True; except IOError: bflag = True bf4 = True if bflag: # re-slurp all the datapoints from beeminder ph0 = defaultdict(int) sh0 = defaultdict(str) bh = {} start = time.time() # reset these since who knows what happened to end = 0 # them when we calculated them from the cache file # we decided to toss. #my $tmp = $beef; $tmp =~ s/(?:[^\/]*\/)*//; # strip path from filename tmp = os.path.basename(beef) if bf1: print("Cache file missing or empty ({}); recreating... ".format(tmp)) elif bf2: print("Cache file doesn't have all the Bmndr IDs; recreating... ") elif bf3: print("Cache file has duplicate Bmndr IDs; recreating... ") elif bf4: print("Couldn't read cache file; recreating... ") else: # this case is impossible print("Recreating Beeminder cache ({})[{bf1}{bf2}{bf3}{bf4}]... ".format( bf1=bf1, bf2=bf2, bf3=bf3, bf4=bf4 )) data = beem.data(slug) print("[Bmndr data fetched]") # take one pass to delete any duplicates on bmndr; must be one datapt per day #i = 0; remember = {} newdata = [] for x in data: tm = time.localtime(x["timestamp"]) y, m, d = tm.tm_year, tm.tm_mon, tm.tm_mday timetuple = time.localtime(x['timestamp']) # XXX okay so we're using localtime here, but # does this change if/when generalized # midnight is rolled out, etc? ts = time.strftime('%Y-%m-%d', timetuple) b = x['id'] if remember.get(ts) is not None: print("Beeminder has multiple datapoints for the same day. " "The other id is {}. Deleting this one:".format(remember[ts])) pprint(x) beem.delete_point(slug, b) else: newdata.append(x) remember[ts] = b #i += 1 data = newdata # for my $x (reverse(@todelete)) { # splice(@$data,$x,1); # } for x in data: # parse the bmndr data into %ph0, %sh0, %bh timetuple = time.localtime(x['timestamp']) y, m, d, *rest = timetuple # XXX see note above about generalized midnight ts = time.strftime('%Y-%m-%d', timetuple) #t = util.pd(ts) # XXX isn't x['timestamp'] the unix time anyway already t = x['timestamp'] if t < start: start = t if t > end: end = t v = x['value'] c = x['comment'] b = x['id'] i = re.search(r'^\d+', c) ph0[ts] = int(i.group(0) if i else 0) # ping count is first thing in the comment sh0[ts] = re.sub(r'[^:]*:\s+', '', c) # drop the "n pings:" comment prefix # This really shouldn't happen. if ts in bh: raise ValueError( "Duplicate cached/fetched id datapoints for {ts}: {bhts}, {b}.\n{val}".format( ts=ts, bhts=bh[ts], b=b, val=pformat(x))) bh[ts] = b try: with open(ttlf) as T: np = 0 # number of lines (pings) in the tagtime log that match for line in T: # parse the tagtime log file m = re.search(r'^(\d+)\s*(.*)$', line) if not m: raise ValueError("Bad line in TagTime log: " + line) t = int(m.group(1)) # timestamp as parsed from the tagtime log ts = time.localtime(t) stuff = m.group(2) # tags and comments for this line of the log tags = util.strip(stuff) if tagmatch(tags, crit, ts): #print('found a match for line: {}'.format(line)) #y, m, d, *rest = time.localtime(t) ymd = time.strftime('%Y-%m-%d', ts) ph1[ymd] += 1 sh1[ymd] += util.stripb(stuff) + ", " np += 1 if t < start: start = t if t > end: end = t except IOError: raise ValueError("Can't open TagTime log file: "+ttlf) # clean up $sh1: trim trailing commas, pipes, and whitespace # for(sort(keys(%sh1))) { $sh1{$_} =~ s/\s*(\||\,)\s*$//; } for key in sorted(sh1.keys()): sh1[key] = re.sub(r'\s*(\||,)\s*$', '', sh1[key]) #print "Processing datapoints in: ", ts($start), " - ", ts($end), "\n"; nquo = 0 # number of datapoints on beeminder with no changes (status quo) ndel = 0 # number of deleted datapoints on beeminder nadd = 0 # number of created datapoints on beeminder nchg = 0 # number of updated datapoints on beeminder minus = 0 # total number of pings decreased from what's on beeminder plus = 0 # total number of pings increased from what's on beeminder ii = 0 for t in range(daysnap(start) - 86400, daysnap(end) + 86401, 86400): timetuple = time.localtime(t) y, m, d, *rest = timetuple ts = time.strftime('%Y-%m-%d', timetuple) b = bh.get(ts, "") p0 = ph0.get(ts, 0) p1 = ph1.get(ts, 0) s0 = sh0.get(ts, "") s1 = sh1.get(ts, "") if p0 == p1 and s0 == s1: # no change to the datapoint on this day if b: nquo += 1 continue if not b and p1 > 0: # no such datapoint on beeminder: CREATE nadd += 1 plus += p1 point = beem.create_point(slug, value=p1*ping, timestamp=t, comment=util.splur(p1, 'ping') + ': ' + s1) bh[ts] = point['id'] #print "Created: $y $m $d ",$p1*$ping," \"$p1 pings: $s1\"\n"; elif p0 > 0 and p1 <= 0: # on beeminder but not in tagtime log: DELETE ndel += 1 minus += p0 beem.delete_point(slug, b) #print "Deleted: $y $m $d ",$p0*$ping," \"$p0 pings: $s0 [bID:$b]\"\n"; elif p0 != p1 or s0 != s1: # bmndr & tagtime log differ: UPDATE nchg += 1 if p1 > p0: plus += p1 - p0 elif p1 < p0: minus += p0 - p1 beem.update_point(slug, b, value=(p1*ping), timestamp=t, comment=util.splur(p1, 'ping') + ': ' + s1) # If this fails, it may well be because the point being updated was deleted/ # replaced on another machine (possibly as the result of a merge) and is no # longer on the server. In which case we should probably fail gracefully # rather than failing with an ERROR (see beemupdate()) and not fixing # the problem, which requires manual cache-deleting intervention. # Restarting the script after deleting the offending cache is one option, # though simply deleting the cache file and waiting for next time is less # Intrusive. Deleting the cache files when merging two TT logs would reduce # the scope for this somewhat. #print "Updated:\n"; #print "$y $m $d ",$p0*$ping," \"$p0 pings: $s0 [bID:$b]\" to:\n"; #print "$y $m $d ",$p1*$ping," \"$p1 pings: $s1\"\n"; else: print("ERROR: can't tell what to do with this datapoint (old/new):\n") print(ts, p0 * ping, " \"{p0} pings: {s0} [bID:{b}]\"".format(p0=p0, s0=s0, b=b)) print(ts, p1 * ping, " \"{p1} pings: {s1}\"\n".format(p1=p1, s1=s1)) with open(beef, 'w') as f: # generate the new cache file for ts in sorted(ph1.keys()): y, m, d = re.split(r'-', ts) p = ph1[ts] v = p * ping c = sh1[ts] b = bh[ts] out = '{y} {m} {d} {v} "{pings}: {c} [bID:{b}]"\n'.format( y=y, m=m, d=d, v=v, pings=util.splur(p, "ping"), c=c, b=b) f.write(out) nd = len(ph1) # number of datapoints if nd != nquo + nchg + nadd: # sanity check print("\nERROR: total != nquo+nchg+nadd ({nd} != {nquo}+{nchg}+{nadd})\n".format( nd=nd, nquo=nquo, nchg=nchg, nadd=nadd)) print("Datapts: {nd} (~{nquo} *{nchg} +{nadd} -{ndel}), ".format( nd=nd, nquo=nquo, nchg=nchg, nadd=nadd, ndel=ndel), "Pings: {np} (+{plus} -{minus}) ".format(np=np, plus=plus, minus=minus)) if isinstance(crit, str): print("w/ tag", crit) elif isinstance(crit, list): print("w/ tags in {", ','.join(crit), "}") elif hasattr(crit, 'search'): print('matching', crit.pattern) elif callable(crit): print('satisfying lambda') else: print("(unknown-criterion: {crit})".format(crit=crit))
# XXX task file tagstr = util.strip(resp).strip() comments = util.stripc(resp).strip() #tagstr = re.sub(r'\b(\d+)\b', lambda m) #$tagstr =~ s//($tags{$1} eq "" ? "$1" : "$1 ").$tags{$1}/eg; #$tagstr =~ s/\b(\d+)\b/tsk $1/; tagstr += autotags tagstr = re.sub(r'\s+', ' ', tagstr) a = util.annotime("{} {} {}".format(t, tagstr, comments), t) if (not tagstr) or\ (not settings.enforcenums or re.search(r'\b(\d+|non|afk)\b', tagstr)): # if enforcenums is enabled, requires a digit or "non" or "afk" to end break print(a) logger.log(a) # Send your TagTime log to Beeminder # (maybe should do this after retro pings too but launch.pl would do that). if settings.beeminder and resp: print(util.divider(" sending your tagtime data to beeminder ")) for key in settings.beeminder: print(key) bm(key) if eflag: print('{}, press enter to dismiss...'.format(util.splur(eflag, 'error'))) tmp = input()
# print "ERROR: Can't read task file ($tskf) again\n"; # $eflag++; # } # } # XXX task file tagstr = util.strip(resp).strip() comments = util.stripc(resp).strip() # tagstr = re.sub(r'\b(\d+)\b', lambda m) # $tagstr =~ s//($tags{$1} eq "" ? "$1" : "$1 ").$tags{$1}/eg; # $tagstr =~ s/\b(\d+)\b/tsk $1/; tagstr += autotags tagstr = re.sub(r"\s+", " ", tagstr) a = util.annotime("{} {} {}".format(t, tagstr, comments), t) if (not tagstr) or (not settings.enforcenums or re.search(r"\b(\d+|non|afk)\b", tagstr)): # if enforcenums is enabled, requires a digit or "non" or "afk" to end break print(a) logger.log(a) # Send your TagTime log to Beeminder # (maybe should do this after retro pings too but launch.pl would do that). if settings.beeminder and resp: print(util.divider(" sending your tagtime data to beeminder ")) for key in settings.beeminder: print(key) bm(key) if eflag: print("{}, press enter to dismiss...".format(util.splur(eflag, "error"))) tmp = input()