"""sparcli -- a SPARQL protocol client

based on
 http://www.w3.org/2001/sw/DataAccess/proto-wd/
 Revision: 1.35 of Date: 2005/05/02 18:26:42

See also handy HTTP tool:
  http://hathawaymix.org/Software/TCPWatch
"""

class Usage(Exception):
    """
    python sparcli.py [OPT] SERVICE OP*
    where OP is select, ask, construct, or describe and
    SERVICE is one of
    --sparqler, --librdf, --sparqlette, --local, or --www2005
    and OPT can be
     --if PATTERN, --then PATTERN, --vars VARS
    where PATTERN is a graph pattern ala { ?book dc:title ?foo }
    and VARS is ala "?book ?foo"

    e.g.
    sparcli.py --sparqlette ask construct select 

    python sparcli.py --if '{ ?a  w5:Author_paper ?p}' --then '{?a w5:Author_paper ?p}' --www2005 construct


    """

    def __str__(self):
        return self.__doc__


__version__ = "$Id: sparcli.py,v 1.11 2005/05/17 21:05:09 connolly Exp $"

from urllib import urlencode
from urllib2 import urlopen, Request

import xmltramp # http://www.aaronsw.com/2002/xmltramp/

import sys

class HTTPBinding:
    def __init__(self, addr):
        if '?' in addr:
            self._addr = addr
        else:
            self._addr = addr + '?'

    def get(self, params, preferredType='*/*'):
        print >>sys.stderr, "@@params", params
        i = self._addr + urlencode(params)
        print >>sys.stderr, "@@get", i, preferredType
        return urlopen(Request(i, headers={'Accept': preferredType}))


class UnexpectedMediaType(Exception):
    def __init__(self, r):
        self._r = r

    def __str__(self):
        return "UnexpectedMediaType: %s" % self._r.info()['content-type']


class SparqlParams:
    """
    mix-in for param handling implementation
    """
    InitParams = []
    BG = 'bg' # subclass should (must?) reassign

    def qParams(self, query, bg):
        params = self.InitParams + [("query", query)]
        if bg:
            for i in bg:
                params.append((self.BG, i))
        
        return params


class SparqlQuery(HTTPBinding, SparqlParams):
    """
    hmm... call it SparqlBindings?
    """

    def selectParams(self, q, b): return self.qParams(q, b)
    def askParams(self, q, b): return self.qParams(q, b)

    def ask(self, q, bg=None):
        r = self.get(self.askParams(q, bg), preferredType="application/xml")
        main, sub = mainSub(r.info()['content-type'])
        if sub == "xml" and main in ("text", "application"):
            x = xmltramp.seed(r)
            b = str(x.results.boolean) == "true"
                
            return b
        else:
            raise UnexpectedMediaType(r)


    def getBindings(self, q, bg=None):
        r = self.get(self.selectParams(q, bg),
                     preferredType="application/xml")
        main, sub = mainSub(r.info()['content-type'])
        if sub == "xml" and main in ("text", "application"):
            x = xmltramp.seed(r)
            return x.results._dir
        else:
            raise UnexpectedMediaType(r)


class SparqlGraph(HTTPBinding, SparqlParams):
    """
    Queries that return graphs: CONSTRUCT, DESCRIBE
    """

    def graphParams(self, q, b): return self.qParams(q, b)
    
    def getGraph(self, q, bg=None):
        r = self.get(self.graphParams(q, bg),
                     preferredType="application/rdf+xml")
        main, sub = mainSub(r.info()['content-type'])
        if (main, sub) == ('application', 'rdf+xml'):
            return r.read()
        else:
            raise UnexpectedMediaType(r)


def mainSub(ty):
    """
    >>> mainSub("text/html; charset='utf-8'")
    ['text', 'html']
    >>> mainSub("text/html")
    ['text', 'html']
    """

    semi = ty.find(";")
    if semi >= 0:
        ty = ty[:semi]
    
    return ty.split('/', 1)



class ProtoWD1_35(SparqlQuery, SparqlGraph):
    BG = 'default-graph-uri'

class Sparqler(SparqlQuery, SparqlGraph):
    InitParams = [('lang', 'SPARQL')]
    BG = 'graph-id'

class LibRDF(SparqlQuery, SparqlGraph):
    """this seems to be the protocol described in
    <http://lists.w3.org/Archives/Public/public-rdf-dawg/2005AprJun/0054.html>
    """
    BG='data'
    InitParams = [('query-lang', 'sparql')]

    def selectParams(self, q, bg):
        ps = self.qParams(q, bg)
        ps.append(('format', 'xml'))
        return ps

    def graphParams(self, q, bg):
        ps = self.qParams(q, bg)
        ps.append(('format', 'rdfxml'))
        return ps

    askParams = selectParams


def main(argv):
    pfxs = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> PREFIX dc:      <http://purl.org/dc/elements/1.1/>"
    ant = "{ ?book dc:creator ?who }"
    conc = "{_:somebody foaf:name ?who; foaf:made ?book}"
    vns = "?book ?who"
    data = ['http://sparql.net/books']

    if '--sparqler' in argv:
        it = Sparqler('http://sparql.org/service?lang=SPARQL&')
    elif '--librdf' in argv:
        it = LibRDF('http://librdf.org/2005/sparqling?')
    elif '--sparqlette' in argv:
        it = ProtoWD1_35('http://www.wasab.dk/morten/2005/04/sparqlette/?')
    elif '--local' in argv:
        it = ProtoWD1_35('http://127.0.0.1:9001/sq')
    elif '--www2005' in argv:
        it = SparqlQuery('http://spam.w3.org/WWW2005?') # doesn't grok BG

        data = []
        pfxs = "PREFIX w5: <http://www.w3.org/2004/10/18-RDF-WWW2005/>"
        vns = "?name ?room ?start ?end"
        conc = "{(?name ?room ?start ?end) a <#Answer>}"
        ant = """
 WHERE {?a  w5:Author_name "Sean M. McNee" .
        ?a  w5:Author_paper ?p .
        ?p  w5:Paper_name ?name .
        ?sp w5:SessionPaper_paper ?p .
        ?sp w5:SessionPaper_session ?s .
        ?s  w5:Session_room ?rm .
        ?rm w5:Room_description ?room .
        ?ps w5:ProgramSession_session ?s .
        ?ps w5:ProgramSession_program ?pg .
        ?pg w5:Program_start ?start .
        ?pg w5:Program_start ?end }
"""
    elif '--zparqler' in sys.argv:
        it = Sparqler('http://demo.asemantics.com/zparqler/exe')
        it.InitParams = [('query-lang', 'http://rdfstore.sourceforge.net/sparql/extended/')]
    else:
        raise Usage()

    if "--if" in argv:
        ant = argv[argv.index("--if") + 1]
    if "--vars" in argv:
        vns = argv[argv.index("--vars") + 1]
    if "--then" in argv:
        conc = argv[argv.index("--then") + 1]

    # peeking at _addr isn't cool, but this is just a diagnostic.
    # if anybody needs it for real, make a method
    print "service:", it._addr


    for op in argv:
        if op == 'select':

            print "\n\n=== select..."
            for solution in it.getBindings("%s SELECT %s WHERE %s" % \
                                      (pfxs, vns, ant),
                                      data):
                print "solution:", solution.__repr__(1)
        elif op == 'ask':
            print "\n\n=== ask..."
            print it.ask("%s ASK WHERE %s" % (pfxs, ant), data)
        elif op == 'construct':
            print "\n\n=== construct..."
            print it.getGraph("%s CONSTRUCT %s WHERE %s" % (pfxs, conc, ant),
                              data)
        elif op == 'describe':
            print "\n\n=== construct..."
            print it.getGraph("%s DESCRIBE %s WHERE %s" % (pfxs, vns, ant),
                              data)


def _test():
    import doctest
    doctest.testmod()


if __name__ == '__main__':
    import sys
    if '--test' in sys.argv:
        _test()
    else:
        main(sys.argv)


# $Log: sparcli.py,v $
# Revision 1.11  2005/05/17 21:05:09  connolly
# sparqlette is now a  ProtoWD1_35
#
# Revision 1.10  2005/05/09 19:36:14  connolly
# - added --zparqler
#
# Revision 1.9  2005/05/09 19:14:14  connolly
# - added describe OP
# - added some examples to Usage
# - renamed QueryInterface to HTTPBinding, suggestive of WSDL
# - split SELECT and CONSTRUCT into separate interfaces
#  - s.select(...) becomes s.getBindings(...)
#  - s.construct(...) becomes s.getGraph(...)
#  - factored qParams out
# - cleaned up code w.r.t. pychecker: s/vars/vns/
#
# Revision 1.8  2005/05/06 20:47:20  connolly
# - handle either results format by not really parsing
#   select results; just return xmltramp Elements
#
# Revision 1.7  2005/05/06 20:27:05  connolly
# - added --then and --vars to go with --if
# - diagnostics to stderr
# - fixed www2005 case (moved InitParams)
# - removed a line of dead code from construct()
#
# Revision 1.6  2005/05/06 20:15:08  connolly
# - refactored Client class
#   - thinking about matching WSDL interfaces
#   - moved protocol parameter handling into classes
# - ask/select/construct is now chosen by command line args
# - added --test option
# - handle bnodes in select()
# - generalized isXML() to mainSub()
#
# Revision 1.5  2005/05/06 17:03:24  connolly
# - --sparqlette working
# - documented Usage
# - added --if option
# - added --www2005 option; not working well yet
# - factored out query parts
# - decoded ASK results; factored out XML media type testing
#
# Revision 1.4  2005/05/05 22:03:22  connolly
# do construct rather than ask and select
#
# Revision 1.3  2005/05/05 20:56:06  connolly
# - local option
# - text/xml
#
# Revision 1.2  2005/05/03 21:42:51  connolly
# - 0054 protocol
# - sparqlette
#
# Revision 1.1  2005/05/03 21:02:39  connolly
# it's starting to work
#
