#!/usr/bin/python # Copyright (C) Mark Eichin. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the 3-clause BSD terms are met. # (License subject to change.) """Hiveminder Braindump entry tool. Takes one item on the command line, or "-" to take a list as input. """ __revision__ = "$Id$" __version__ = "%s/%s" % (__revision__.split()[3], __revision__.split()[2]) import os import urllib import optparse import sys import subprocess import time import string try: import cElementTree as etree except ImportError: try: import elementtree.ElementTree as etree except ImportError: import xml.etree.ElementTree as etree # In the interests of keeping hive-braindump.py self-contained, for the moment # I'm including encode_locator and decode_locator inline, as lifted from todo.py. # Given that the protocol now passes them as decimal numbers, though, it may # be possible to drop them again... # Roughly this translates numbers into base 32... # would be base 36 except that 0->O, 1->I, S->F, B->P # to do this with existing encoders, we need to pad rather than replace rep_digits = string.digits + string.uppercase compressed_digits = rep_digits.translate(string.maketrans("",""), "01SB") + "____" map_digits = rep_digits.translate(string.maketrans("01SB", "OIFP")) loc_to_rep = string.maketrans(compressed_digits, rep_digits) rep_to_loc = string.maketrans(rep_digits, compressed_digits) rep_to_map = string.maketrans(rep_digits, map_digits) def encode_locator(number): """Number::RecordLocator encoder""" # this *should* be as simple as: # return something(locator, 32).translate(rep_to_loc) # but while was proposed for 2.5 it doesn't seem to have happened... res = [] while number: number, digit = divmod(number, 32) res.append(digit) return "".join(compressed_digits[digit] for digit in reversed(res)) def decode_locator(locator): """Number::RecordLocator decoder""" # normalize to upper case # then translate to the digit subset # then compact into 32-contiguous values for int return int(locator.upper().translate(rep_to_map).translate(loc_to_rep), 32) # tests, from Number-RecordLocator-0.001/t/00.load.t assert encode_locator(1) == "3", "We skip one and zero so should end up with 3 when encoding 1" assert encode_locator(12354) == 'F44' assert encode_locator(123456) == '5RL2' assert decode_locator('5RL2') == 123456 assert decode_locator(encode_locator(123456)) == 123456 assert decode_locator('1234') == decode_locator('I234') assert decode_locator('10SB') == decode_locator('IOFP') def my_sid(known_sid=[]): """fetch my sid from ~/.hiveminder""" if not known_sid: hive_file = os.path.expanduser("~/.hiveminder") if file(hive_file).readline().startswith("---"): for line in file(hive_file): if line.startswith("sid:"): known_sid.append(line.strip().split(":", 1)[-1].strip()) break elif file(hive_file).readline().startswith("{"): for line in "".join(file(hive_file).readlines()).split(","): line = line.strip() if line.startswith("sid:"): known_sid.append(line.strip().split(":", 1)[-1].strip()) break else: raise Exception("no sid found!") if not known_sid: # still! raise Exception("no sid parser worked!") return known_sid[0] def braindump(items): """do a braindump of these items""" base_url = "http://hiveminder.com/__jifty/webservices/xml" uo = urllib.FancyURLopener() uo.addheader("Cookie", "JIFTY_SID_HIVEMINDER=%s" % my_sid()) # "J:A:F-text-quickcreate": "do this now %s [deleteme]" % time.ctime(), u = uo.open(base_url, data=urllib.urlencode({ "J:A-quickcreate": "BTDT::Action::ParseTasksMagically", "J:A:F-text-quickcreate": "\r\n".join(items) })) xmlresponse = u.read() try: response = etree.fromstring(xmlresponse) except Exception, xerr: # xml.parsers.expat.ExpatError print "got", xerr, "parsing", xmlresponse raise success = response.find("result/success").text if success and int(success): message = response.find("result/message").text ids = map(lambda ids: "#"+encode_locator(int(ids.text)), response.findall("result/content/ids")) if ids: return message + ": " + ", ".join(ids) else: return message else: raise Exception("braindump failed, %s" % response.find("result/message").text) # TODO: figure out a general abstraction for this kind of prompt/queue def we_have_net(): """Do we (probably) have net?""" # *also* need reachability... return "default" in subprocess.Popen(["netstat", "-r"], stdout=subprocess.PIPE).communicate()[0] # DiskQueue (or PersistQueue) should be a common package # (I implemented one in pagefeeder too) queue_dir = os.path.expanduser("~/.hiveminder_outbound") if not os.path.isdir(queue_dir): os.mkdir(queue_dir) def enqueue(item): """stuff an item on the queue""" while True: qname = os.path.join(queue_dir, "h%s" % time.time()) if not os.path.exists(qname): break # This limits us to a maximum queuing rate of 100/sec. Boo Hoo. qfile = file(qname, "w") for line in item: print >> qfile, line qfile.close() def queue_empty(): return 0 == len(list(qitem for qitem in os.listdir(queue_dir) if qitem.startswith("h"))) def dequeue(): """return one item (and cleanup handle) from the queue""" first_file = sorted((os.path.join(queue_dir, qitem) for qitem in os.listdir(queue_dir) if qitem.startswith("h")), key=lambda p: float(os.path.basename(p).replace("h","")))[0] def cleanup(path=first_file): os.remove(path) return first_file, cleanup def test_queue(): if not queue_empty(): raise Exception("can't test with a live queue!") enqueue(["a", "fred"]) path, cleaner = dequeue() assert map(string.rstrip, file(path).readlines()) == ["a", "fred"] cleaner() enqueue(["b", "fred"]) enqueue(["c", "fred"]) # raw_input("foo") path, cleaner = dequeue() assert map(string.rstrip, file(path).readlines()) == ["b", "fred"] cleaner() path, cleaner = dequeue() assert map(string.rstrip, file(path).readlines()) == ["c", "fred"] cleaner() def paint_tags(lines, tags, groups): """paint tags on lines in braindump syntax""" tags = (tags or []) + map(lambda g: "group:" + g, groups or []) if not tags: return lines # tagstring = "[" + " ".join(tags) + "] " # allow more advanced strings in tags... tagstring = " ".join("[%s]" % tag for tag in tags) return [tagstring + " " + line for line in lines] def try_to_braindump(lines, tags, groups, force_queue=False): """try to braindump the whole queue""" if lines: enqueue(paint_tags(lines, tags, groups)) results = [] if we_have_net() and not force_queue: while not queue_empty(): path, cleaner = dequeue() lines = map(string.rstrip, file(path).readlines()) try: result = braindump(lines) results.append(result) cleaner() except Exception, exc: print "braindump failed", exc, "queuing" break else: if lines: # a lie, we already did, but it fits the user model better print "queuing" return results if __name__ == "__main__": parser = optparse.OptionParser(usage=__doc__, version="%%prog %s" % __version__) parser.allow_interspersed_args = False # works better for commandline items # parser.add_option("--emacs") parser.add_option("--tag", action="append", help="add this tag (for easier aliases)") parser.add_option("--group", action="append", help="add this group (for easier aliases)") parser.add_option("--test-queue", action="store_true") parser.add_option("--force-queue", action="store_true") parser.add_option("--flush-queue", action="store_true") options, args = parser.parse_args() if options.test_queue: sys.exit(test_queue()) if options.force_queue and options.flush_queue: sys.exit("don't try to force and flush at the same time...") if args == ["-"]: if os.isatty(1): print "Enter content, c-d or a blank line to finish" lines = [] while True: try: line = raw_input() except EOFError: break if not line: break lines.append(line) if os.isatty(1): print "posting...", sys.stdout.flush() results = try_to_braindump(lines, tags=options.tag, groups=options.group, force_queue=options.force_queue) elif args: results = try_to_braindump([" ".join(args)], tags=options.tag, groups=options.group, force_queue=options.force_queue) elif options.flush_queue: if options.tag: print "warning, ignoring tag, flushing everything" results = try_to_braindump([], [], [], force_queue=options.force_queue) else: parser.print_help() sys.exit(1) for result in results[:-1]: print "[from queue]", result for result in results[-1:]: print result