# HG changeset patch # User David A. Holland # Date 1407913410 14400 # Node ID bcd1d06838fdb6e4423c0abd10ba2f7a59c01c0a # Parent 73e6dac2939140a157bbf0fbfd74b0de51d2caa7 more better stuff diff -r 73e6dac29391 -r bcd1d06838fd shelltools/query-pr/query.py --- a/shelltools/query-pr/query.py Tue Aug 12 21:55:08 2014 -0400 +++ b/shelltools/query-pr/query.py Wed Aug 13 03:03:30 2014 -0400 @@ -16,6 +16,213 @@ outformat = "text" ############################################################ +# database field access + +# +# Fields of PRs that we might search are spread across a number of +# tables and require varying joins to get them. And, because of +# classication schemes, the set of fields isn't static and we can't +# just assemble a massive view with one column for each field. +# +# The QueryBuilder class knows how to arrange for all known fields to +# be present. +# + +class QueryBuilder: + # these fields are in the PRs table + prtable_fields = [ + "id", "synopsis", "confidential", "state", "locked", + "timeout_date", "timeout_state", + "arrival_schemaversion", "arrival_date", "modified_date", + "closed_date", + "release", "environment" + ] + + # these fields are aliases for others + alias_fields = { + "number" : "id" + "date" : "arrival_date" + } + + def __init__(self): + self.present = {} + self.joined = {} + self.fromitems = [] + self.whereitems = [] + self.order = None + + def setorder(self, order): + self.order = order + + # add to present{} and return the value for convenience (internal) + def makepresent(self, field, name): + self.present[field] = name + return name + + # add a join item (once only) (internal) + def addjoin(self, table, as = None): + if as is not None: + key = table + "-" + as + val = table + " AS " + as + else: + key = table + val = table + if key not in self.joined: + self.joined[key] = True + self.fromitems.append(val) + + # returns a sql expression for the field + def getfield(self, field): + # already-fetched fields + if field in self.present: + return self.present[field] + + # aliases for other fields + if field in alias_fields: + return self.getfield(alias_fields[field]) + + # simple fields directly in the PRs table + if field in prtable_fields: + self.addjoin("PRs") + return self.makepresent(field, "PRs." + field) + + # now it gets more interesting... + if field == "closed": + self.addjoin("PRs") + self.addjoin("states") + self.addwhere("PRs.state = states.name") + return self.makepresent(field, "states.closed") + + # XXX let's pick one set of names and use them everywhere + # (e.g. change "posttime" in the schema to "message_date" + # or something) + if field == "comment_date" or field == "posttime": + self.addjoin("PRs") + self.addjoin("messages") + self.addwhere("PRs.id = messages.pr") + return self.makepresent(field, "messages.posttime") + + if field == "comment" or field == "message" or field == "post": + self.addjoin("PRs") + self.addjoin("messages") + self.addwhere("PRs.id = messages.pr") + return self.makepresent(field, "messages.body") + + if field == "attachment": + self.addjoin("PRs") + self.addjoin("messages") + self.addjoin("attachments") + self.addwhere("PRs.id = messages.pr") + self.addwhere("messages.id = attachments.msgid") + return self.makepresent(field, "attachments.body") + + if field == "patch": + self.addjoin("PRs") + self.addjoin("messages") + self.addjoin("attachments", "patches") + self.addwhere("PRs.id = messages.pr") + self.addwhere("messages.id = patches.msgid") + self.addwhere("patches.mimetype = " + + "'application/x-patch'") + return self.makepresent(field, "patches.body") + + if field == "mimetype": + subquery = "((SELECT mtmessages1.pr as pr, " + + "mtmessages1.mimetype as mimetype " + + "FROM messages as mtmessages1) " + + "UNION " + + "(SELECT mtmessages2.pr as pr, " + + "mtattach2.mimetype as mimetype " + + "FROM messages as mtmessages2, " + + " attachments as mtattach2 " + + "WHERE mtmessages2.id = mtattach2.msgid))" + self.addjoin("PRs") + self.addjoin(subquery, "mimetypes") + self.addwhere("PRs.id = mimetypes.pr") + return self.makepresent(field, "mimetypes.mimetype") + + # XXX: need view userstrings + # select (id, username as name) from users + # union select (id, realname as name) from users + # (allow searching emails? ugh) + if field == "originator" or field == "submitter": + self.addjoin("PRs") + self.addjoin("userstrings", "originators") + self.addwhere("PRs.originator = originators.id") + return self.makepresent(field, "originators.name") + + if field == "reporter" or field == "respondent": + self.addjoin("PRs") + self.addjoin("subscriptions") + self.addjoin("userstrings", "reporters") + self.addwhere("subscriptions.userid = reporters.id") + self.addwhere("subscriptions.reporter") + return self.makepresent(field, "reporters.name") + + if field == "responsible": + self.addjoin("PRs") + self.addjoin("subscriptions") + self.addjoin("userstrings", "responsibles") + self.addwhere("subscriptions.userid = responsibles.id") + self.addwhere("subscriptions.responsible") + return self.makepresent(field, "responsibles.name") + + if field in hierclasses: + col = field + "_data" + self.addjoin("PRs") + self.addjoin("hierclass_data", col) + self.addwhere("PRs.id = %s.pr" % col) + self.addwhere("%s.scheme = '%s'" % (col, field)) + return self.makepresent(field, "%s.value" % col) + + if field in flatclasses: + col = field + "_data" + self.addjoin("PRs") + self.addjoin("flatclass_data", col) + self.addwhere("PRs.id = %s.pr" % col) + self.addwhere("%s.scheme = '%s'" % (col, field)) + return self.makepresent(field, "%s.value" % col) + + if field in textclasses: + col = field + "_data" + self.addjoin("PRs") + self.addjoin("textclass_data", col) + self.addwhere("PRs.id = %s.pr" % col) + self.addwhere("%s.scheme = '%s'" % (col, field)) + return self.makepresent(field, "%s.value" % col) + + if field in tagclasses: + col = field + "_data" + self.addjoin("PRs") + self.addjoin("tagclass_data", col) + self.addwhere("PRs.id = %s.pr" % col) + self.addwhere("%s.scheme = '%s'" % (col, field)) + return self.makepresent(field, "%s.value" % col) + + sys.stderr.write("Unknown field %s" % field) + exit(1) + # end getfield + + # emit sql + def build(self, sels): + s = ", ".join(sels) + f = ", ".join(self.fromitems) + w = " and ".join(self.whereitems) + q = "SELECT %s\nFROM %s\nWHERE %s\n" % (s, f, w) + if self.order is not None: + q = q + "ORDER BY " + self.order + "\n" + return q + # endif + +# end class QueryBuilder + +# XXX we need to add dynamically: +# hierclass_names.name to hierclasses[] +# flatclass_names.name to flatclasses[] +# textclass_names.name to textclasses[] +# tagclass_names.name to tagclasses[] + +############################################################ # database dblink = None @@ -52,6 +259,7 @@ ############################################################ # query class for searches +# XXX: obsolete, remove class Query: __init__(self): @@ -110,6 +318,9 @@ ############################################################ +# this is old code that needs to be merged or deleted into the new stuff +def oldstuff(): + # If we're doing something other than a search, do it now if args.attach is not None: get_attachment(args.attach) @@ -268,11 +479,258 @@ querytexts = [q.textify() for q in queries] return "INTERSECT\n".join(querytexts) +############################################################ +# printing + +class PrintText: + def __init__(self, output): + self.lines = (output == "RAW" or output == "LIST") + def printheader(self, row): + # nothing + def printrow(self, row): + # XXX + print row + def printfooter(self, row): + # nothing +# end class PrintText + +class PrintCsv: + def __init__(self, output): + # nothing + def printheader(self, row): + # XXX + def printrow(self, row): + # XXX + def printfooter(self, row): + # nothing +# end class PrintCsv + +class PrintXml: + def __init__(self, output): + # nothing + def printheader(self, row): + # XXX + def printrow(self, row): + # XXX + def printfooter(self, row): + # XXX +# end class PrintXml + +class PrintJson: + def __init__(self, output): + # nothing + def printheader(self, row): + # XXX + def printrow(self, row): + # XXX + def printfooter(self, row): + # XXX +# end class PrintJson + +class PrintRdf: + def __init__(self, output): + # nothing + def printheader(self, row): + # XXX + def printrow(self, row): + # XXX + def printfooter(self, row): + # XXX +# end class PrintRdf + +class PrintRdflike: + def __init__(self, output): + # nothing + def printheader(self, row): + # XXX + def printrow(self, row): + # XXX + def printfooter(self, row): + # XXX +# end class PrintRdflike + +def print_prs(ids): + if sel.outformat == "TEXT": + mkprinter = PrintText + elif sel.outformat == "CSV": + mkprinter = PrintCsv + elif sel.outformat == "XML": + mkprinter = PrintXml + elif sel.outformat == "JSON": + mkprinter = PrintJson + elif sel.outformat == "RDF": + mkprinter = PrintRdf + elif sel.outformat == "RDFLIKE": + mkprinter = PrintRdflike + else: + assert(False) + + # reset the printer + printer = mkprinter(sel.output) + + if sel.output == "RAW": + printer.printheader(ids[0]) + for id in ids: + printer(id) + printer.printfooter(ids[0]) + return + elif sel.output == "LIST": + # XXX is there a clean way to do this passing the + # whole list of ids at once? + query = "SELECT id, synopsis\n" + + "FROM PRs\n" + + "WHERE id = $1" + elif sel.output == "HEADERS": + query = None # XXX + elif sel.output == "META": + query = None # XXX + elif sel.output == "FULL": + query = None # XXX + else: + assert(False) + + first = True + for id in ids: + results = querydb(query, [id]) + if first: + printer.printheader(results[0]) + first = False + for r in results: + printer.printrow(r) + printer.printfooter(results[0]) +# end print_prs + +# XXX if in public mode we need to check if the PR is public +def print_message(pr, msgnum): + query = "SELECT users.username AS username,\n" + + " users.realname AS realname,\n" + + " messages.id AS id, parent_id,\n" + + " posttime, mimetype, body\n" + + "FROM messages, users\n" + + "WHERE messages.who = users.id\n" + + " AND messages.pr = $1\n" + + " AND messages.number_in_pr = $2\n" + # Note that while pr is safe, msgnum came from the commandline + # and may not be. + results = querydb(query, [pr, msgnum]) + [result] = results + (username, realname, id, parent_id, posttime, mimetype, body) = result + # XXX honor mimetype + # XXX honor output format (e.g. html) + sys.stdout.write("From swallowtail@%s %s\n" % (organization,posttime)) + sys.stdout.write("From: %s (%s)\n" % (username, realname)) + sys.stdout.write("References: %s\n" % parent_id) + sys.stdout.write("Date: %s\n" % posttime) + sys.stdout.write("Content-Type: %s\n" % mimetype) + sys.stdout.write("\n") + sys.stdout.write(body) +# end print_message + +# XXX if in public mode we need to check if the PR is public +def print_attachment(pr, attachnum): + query = "SELECT a.mimetype as mimetype, a.body as body\n" + + "FROM messages, attachments as a\n" + + "WHERE messages.pr = $1\n" + + " AND messages.id = a.msgid\n" + + " AND a.number_in_pr = $2\n" + # Note that while pr is safe, attachnum came from the + # commandline and may not be. + results = querydb(query, [pr, msgnum]) + [result] = results + (mimetype, body) = result + # XXX honor mimetype + # XXX need an http output mode so we can send the mimetype! + sys.stdout.write(body) +# end print_attachment ############################################################ -# arg handling +# AST for query class Invocation: + class Query: + Q_TERM = 1 # XXX unused so far + Q_SQL = 2 + Q_AND = 3 + Q_OR = 4 + Q_TSTRING = 5 + Q_QSTRING = 6 + + def __init__(self, type): + self.type = type + def doterm(term): + self = Query(Q_TERM) + self.term = term + return self + def dosql(s): + self = Query(Q_SQL) + self.sql = s + return self + def doand(qs): + self = Query(Q_AND) + self.args = qs + return self + def door(qs): + self = Query(Q_OR) + self.args = qs + return self + # query term string + def dotstring(q): + self = Query(Q_TSTRING) + self.string = q + return self + # whole query string + def doqstring(q): + self = Query(Q_QSTRING) + self.string = q + return self + # end class Query + + class Order: + def __init__(self, field, rev = False): + self.field = field + self.rev = rev + def dooldest(ign): + return Order("number") + def donewest(ign): + return Order("number", True) + def dostaleness(ign): + return Order("modified_date", True) + def dofield(field): + return Order(field) + def dorevfield(field): + return Order(field, True) + # end class Order + + class Search: + def __init__(self, qs, openonly, publiconly, os) + self.queries = qs + self.openonly = openonly + self.publiconly = publiconly + self.orders = os + # end class Search + + class Selection: + S_PR = 1 + S_MESSAGE = 2 + S_ATTACHMENT = 3 + + def __init__(self, type): + self.type = type + def dopr(output, outformat): + self = Selection(S_PR) + self.output = output + self.outformat = outformat + return self + def domessage(arg): + self = Selection(S_MESSAGE) + self.message = arg + return self + def doattachment(arg): + self = Selection(S_ATTACHMENT) + self.attachment = arg + return self + # end class Selection + class Op: # operation codes OP_FIELDS = 1 @@ -280,8 +738,8 @@ OP_RANGE = 3 OP_SEARCH = 4 - def __init__(self, op): - self.op = op + def __init__(self, type): + self.type = type def dofields(): return Op(OP_FIELDS) @@ -293,18 +751,222 @@ self = Op(OP_RANGE) self.field = field return self - def doquery( + def dosearch(s, sels): + self = Op(OP_SEARCH) + self.search = s + self.sels = sels + return self + # end class Op + + def __init__(self, ops): + self.ops = ops +# end class Invocation + +############################################################ +# run (eval the SQL and print the results) + +def + +def run_sel(sel, ids): + if sel.type == S_PR: + if ids == []: + sys.stderr.write("No PRs matched.\n") + exit(1) + print_prs(ids) + elif sel.type == S_MESSAGE: + if len(ids) <> 1: + sys.stderr.write("Cannot retrieve messages " + + "from multiple PRs.") + exit(1) + print_message(ids[0], sel.message) + elif sel.type == S_ATTACHMENT: + if len(ids) <> 1: + sys.stderr.write("Cannot retrieve attachments " + + "from multiple PRs.") + exit(1) + print_message(ids[0], sel.attachment) + else: + assert(False) + +def run_op(op): + if op.type == OP_FIELDS: + list_fields() + elif op.type == OP_SHOW: + describe_field(op.field) + elif op.type == OP_RANGE: + print_field_range(op.field) + elif op.type == OP_SEARCH: + sql = op.search + args = op.args # XXX not there! + ids = querydb(op.search, args) + for s in op.sels: + run_sel(s, ids) + else: + assert(False) + +def run(ast): + for op in ast.ops: + run_op(op) + +############################################################ +# compile (convert the AST so the searches are pure SQL) + +# +# XXX this doesn't work, we need to keep the interned strings +# on return from compile_query. +# - # output formats - FMT_TXT = 1 - - def __init__(self): - self.op = OP_SEARCH - self.searchstrings = [] - self.searchsql = [] - -# end Invocation +def compile_query(q): + if q.type == Q_QSTRING: + # XXX should use a split that honors quotes + terms = q.string.split() + terms = [dotstring(t) for t in terms] + return compile_query(doand(terms)) + if q.type == Q_TSTRING: + qb = QueryBuilder() + s = q.string + if s ~ "^[0-9]+$": + f = qb.getfield("number") + # Note: s is user-supplied but clean to insert directly + qb.addwhere("%s = %s" % (f, s)) + elif s ~ "^[0-9]+-[0-9]+$": + f = qb.getfield("number") + ss = s.split("-") + # Note: ss[] is user-supplied but clean + qb.addwhere("%s >= %s" % (f, ss[0])) + qb.addwhere("%s <= %s" % (f, ss[1])) + elif s ~ "^[0-9]+-$": + f = qb.getfield("number") + ss = s.split("-") + # Note: ss[] is user-supplied but clean + qb.addwhere("%s >= %s" % (f, ss[0])) + elif s ~ "^-[0-9]+$": + f = qb.getfield("number") + ss = s.split("-") + # Note: ss[] is user-supplied but clean + qb.addwhere("%s <= %s" % (f, ss[1])) + elif s ~ "^[^:]+:[^:]+$": + # XXX honor quoted terms + # XXX = or LIKE? + ss = s.split(":") + # ss[0] is not clean but if it's crap it won't match + f = qb.getfield(ss[0]) + # ss[1] is not clean, so intern it for safety + s = qb.intern(ss[1]) + qb.addwhere("%s = %s" % (f, s)) + elif s ~ "^-[^:]+:[^:]+$" + # XXX honor quoted terms + # XXX <> or NOT LIKE? + ss = s.split(":") + # ss[0] is not clean but if it's crap it won't match + f = qb.getfield(ss[0]) + # ss[1] is not clean, so intern it for safety + s = qb.intern(ss[1]) + qb.addwhere("%s <> %s" % (f, s)) + elif s ~ "^-": + # XXX <> or NOT LIKE? + f = qb.getfield("alltext") + # s is not clean, so intern it for safety + s = qb.intern(s) + qb.addwhere("%s <> %s" % (f, s)) + else: + # XXX = or LIKE? + f = qb.getfield("alltext") + # s is not clean, so intern it for safety + s = qb.intern(s) + qb.addwhere("%s = %s" % (f, s)) + + # XXX also does not handle: + # + # field: with no string (supposed to use a default + # search string) + # + # generated search fields that parse dates: + # {arrived,closed,modified,etc.}-{before,after}:date + # + # stale:time + + return qb.build("PRs.id") + # end Q_TSTRING case + if q.type == Q_OR: + subqueries = ["(" + compile_query(sq) + ")" for sq in q.args] + return " UNION ".join(subqueries) + if q.type == Q_AND: + subqueries = ["(" + compile_query(sq) + ")" for sq in q.args] + return " INTERSECT ".join(subqueries) + if q.type == Q_SQL: + return q.sql + assert(False) +# end compile_query + +def compile_order(qb, o): + str = qb.getfield(o.field) + if o.rev: + str = str + " DESCENDING" + return str + +def compile_search(s): + qb2 = QueryBuilder() + + # multiple query strings are treated as OR + query = door(s.queries) + query = compile_query(q) + + if s.openonly: + qb2.addwhere("not %s" % qb.getfield("closed")) + if s.publiconly: + qb2.addwhere("not %s" % qb.getfield("confidential")) + + orders = [compile_order(qb2, o) for o in s.orders] + order = ", ".join(orders) + if order <> "": + qb2.setorder(order) + + if qb2.nonempty(): + qb2.addjoin(query, "search") + qb2.addjoin("PRs") + qb2.addwhere("search = PRs.id") + query = qb2.build(["search"]) + + return query +# end compile_search + +def compile_op(op): + if op.type == OP_SEARCH: + op.search = compile_search(op.search) + return op + +def compile(ast): + ast.ops = [compile_op(op) for op in ast.ops] + +############################################################ +# arg handling + +# +# I swear, all getopt interfaces suck. +# +# Provide an argparse action for constructing something out of the +# argument value and appending that somewhere, since it can't do this +# on its own. +# +# The way you use this: +# p.add_argument("--foo", action = CtorAppend, dest = 'mylist', +# const = ctor) +# where ctor is a function taking the option arg(s) and producing +# a value to append to mylist. +# +# This itself is mangy even for what it is -- it seems like we should +# be able to pass action=CtorAppend(ctor), but since it has to be a +# class that doesn't work... unless you make a new class for every +# ctor you want to use, which seems completely insane. +# +class CtorAppend(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest) + item = self.const(values) + items.append(item) + setattr(namespace, self.dest, items) def getargs(): p = argparse.ArgumentParser(program_description) @@ -315,199 +977,162 @@ help="Print program version and exit") p.add_argument("--show", nargs=1, - action = 'store', dest='show', - help="Show description of field") + action=CtorAppend, dest='ops', + const=Invocation.Op.doshow + help="Show description of field") p.add_argument("--range", nargs=1, - action = 'store', dest='range', - help="Show range of extant values for field") + action=CtorAppend, dest='ops', + const=Invocation.Op.dorange + help="Show range of extant values for field") p.add_argument("--search", nargs=1, - action = 'store', dest='search', - help="Force string to be read as a search string") + action=CtorAppend, dest='queries', + const=Invocation.Query.doqstring, + help="Force string to be read as a search string") p.add_argument("-s", "--sql", nargs=1, - action = 'store', dest='sql', - help="Supply explicit sql query as search") + action=CtorAppend, dest='queries', + const=Invocation.Query.dosql, + help="Supply explicit sql query as search") p.add_argument("--open", - action = 'store_const', const="True", - dest = 'openonly', - help="Exclude closed PRs (default)") + action='store_const', dest='openonly', const="True", + help="Exclude closed PRs (default)") p.add_argument("--closed", - action = 'store_const', const="False", - dest = 'openonly', - help="Include closed PRs in search") + action='store_const', dest='openonly', const="False", + help="Include closed PRs in search") p.add_argument("--public", - action = 'store_const', const="True", - dest = 'publiconly', - help="Exclude confidential PRs") + action='store_const', dest='publiconly', const="True", + help="Exclude confidential PRs") p.add_argument("--privileged", - action = 'store_const', const="False", - dest = 'publiconly', - help="Allow confidential PRs (default)") + action='store_const', dest='publiconly', const="False", + help="Allow confidential PRs (default)") p.add_argument("--oldest", - action = 'store_const', const="OLDEST", - dest = 'order', - help="Sort output with oldest PRs first") + action=CtorAppend, dest='orders', + const=Invocation.Order.dooldest, + help="Sort output with oldest PRs first") p.add_argument("--newest", - action = 'store_const', const="NEWEST", - dest = 'order', - help="Sort output with newest PRs first") + action=CtorAppend, dest='orders', + const=Invocation.Order.donewest, + help="Sort output with newest PRs first") p.add_argument("--staleness", - action = 'store_const', const="STALENESS", - dest = 'order', - help="Sort output by time since last modification") + action=CtorAppend, dest='orders', + const=Invocation.Order.dostaleness, + help="Sort output by time since last modification") p.add_argument("--orderby", nargs=1, - action = 'store', dest = 'orderfield', - help="Sort output by specific field") + action=CtorAppend, dest='orders', + const=Invocation.Order.dofield, + help="Sort output by specific field") p.add_argument("--revorderby", nargs=1, - action = 'store', dest = 'revorderfield', - help="Sort output by specific field, reversed") + action=CtorAppend, dest='orders', + const=Invocation.Order.dorevfield, + help="Sort output by specific field, reversed") p.add_argument("-m", "--message", nargs=1, - action = 'store', dest = 'message', - help="Print selected message (single PR only)") + action=CtorAppend, dest='selections', + const=Invocation.Selection.domessage, + help="Print selected message (single PR only)") p.add_argument("-a", "--attachment", nargs=1, - action = 'store', dest = 'attachment', - help="Print selected attachment (single PR only)") + action=CtorAppend, dest='selections', + const=Invocation.Selection.doattachment, + help="Print selected attachment (single PR only)") p.add_argument("-r", "--raw", - action = 'store_const', const="RAW", - dest = 'output', - help="Print exactly what the database returns") + action = 'store_const', const="RAW", + dest = 'output', + help="Print exactly what the database returns") p.add_argument("-l", "--list", - action = 'store_const', const="LIST", - dest = 'output', - help="Print in list form (default)") + action = 'store_const', const="LIST", + dest = 'output', + help="Print in list form (default)") p.add_argument("--headers", - action = 'store_const', const="HEADERS", - dest = 'output', - help="Print header information only") + action = 'store_const', const="HEADERS", + dest = 'output', + help="Print header information only") p.add_argument("--meta", - action = 'store_const', const="META", - dest = 'output', - help="Print all metadata") + action = 'store_const', const="META", + dest = 'output', + help="Print all metadata") p.add_argument("--metadata", - action = 'store_const', const="META", - dest = 'output') + action = 'store_const', const="META", + dest = 'output') p.add_argument("-f", "--full", - action = 'store_const', const="FULL", - dest = 'output', - help="Print everything") + action = 'store_const', const="FULL", + dest = 'output', + help="Print everything") p.add_argument("--text", - action = 'store_const', const="TEXT", - dest = 'outformat', - help="Print in text format (default)") + action = 'store_const', const="TEXT", + dest = 'outformat', + help="Print in text format (default)") p.add_argument("--csv", - action = 'store_const', const="CSV", - dest = 'outformat', - help="Print a CSV file") + action = 'store_const', const="CSV", + dest = 'outformat', + help="Print a CSV file") p.add_argument("--xml", - action = 'store_const', const="XML", - dest = 'outformat', - help="Print in XML") + action = 'store_const', const="XML", + dest = 'outformat', + help="Print in XML") p.add_argument("--json", - action = 'store_const', const="JSON", - dest = 'outformat', - help="Print in JSON") + action = 'store_const', const="JSON", + dest = 'outformat', + help="Print in JSON") p.add_argument("--rdf", - action = 'store_const', const="RDF", - dest = 'outbformat', - help="Print in RDF") + action = 'store_const', const="RDF", + dest = 'outbformat', + help="Print in RDF") p.add_argument("--rdflike", - action = 'store_const', const="RDFLIKE", - dest = 'outformat', - help="Print RDF-like text") + action = 'store_const', const="RDFLIKE", + dest = 'outformat', + help="Print RDF-like text") + + p.add_argument("TERM", nargs='*', + action=CtorAppend, dest='queries', + const=Invocation.Query.doqstring, + help="Search term") args = p.parse_args() - p = Invocation() - - if args.show is not None: - do_showfield(args.show) - exit(0) - if args.range is not None: - do_fieldrange(args.range) - exit(0) - - searchstring = args.search - explicitsql = args.sql - - openonly = args.openonly - if openonly is None: - openonly = True - publiconly = args.publiconly - if publiconly is None: - publiconly = False + ops = args.ops + if ops is None: + ops = [] + queries = args.queries + if queries is not None: + openonly = args.openonly + if openonly is None: + openonly = True + publiconly = args.publiconly + if publiconly is None: + publiconly = False + orders = args.orders + if orders is None: + orders = [Invocation.Order.dooldest(None)] + output = args.output + if output is None: + output = "LIST" + outformat = args.outformat + if outformat is None: + outformat = "TEXT" + selections = args.selections + if selections is None: + sel = Invocation.Selection.dopr(output, outformat) + selections = [sel] + search = Search(queries, openonly, publiconly, orders) + op = dosearch(search, selections) + ops.append(op) + # endif - if args.orderfield is not None: - orderby = args.orderfield - orderrev = False - elif args.revorderfield is not None: - orderby = args.revorderfield - orderrev = True - elif args.order == "OLDEST": - orderby = "number" - orderrev = False - elif args.order == "NEWEST": - orderby = "number" - orderrev = True - elif args.order == "STALENESS": - orderby = "last-modified" - orderrev = True - else: - orderby = "number" - orderrev = False - - if args.message is not None: - printwhat = "MESSAGE" - printwhich = args.message - elif args.attachment is not None: - printwhat = "ATTACHMENT" - printwhich = args.attachment - else: - printwhat = "PR" - printwhich = None - - output = args.output - if output is None: - output = "LIST" - - outformat = args.outformat - if outformat is None: - outformat = "TEXT" - - query = buildquery(searchstring, explicitsql, - orderby=orderby, orderrev=orderrev, - openonly=openonly, publiconly=publiconly) - if printwhat == "PR": - printer = buildprinter(output, outformat) - else if printwhat == "MESSAGE": - printer = getmessage(printwhich) - else if printwhat == "ATTACHMENT": - printer = getattachment(printwhich) - - return (query, printer) + return Invocation(ops) # end getargs ############################################################ # main -def main(): +todo = getargs() +opendb() +fetch_classifications() +todo = compile(todo) +run(todo) +closedb() +exit(0) - opendb() - (classification_schemes, classification_schemetypes) = getclassify() - query = getargs(classification_schemes, classification_schemetypes) - ids = querydb(query) - if len(ids) > 0: - show_prs(ids) - else: - sys.stderr.write("No PRs matched.\n") - exit(1) - closedb() - return 0 -# end main - -# real python hackers doubtless laugh at this -exit(main())