diff options
author | Aaron Brashears <aaronb@lindenlab.com> | 2007-07-16 20:29:28 +0000 |
---|---|---|
committer | Aaron Brashears <aaronb@lindenlab.com> | 2007-07-16 20:29:28 +0000 |
commit | 7964c6f7a5b622d698f7d471690b29122966b1b2 (patch) | |
tree | 78a708bc316db2e2ff3047089a3edf8020c4d7cb /indra/lib/python/indra/ipc | |
parent | 88d7d45316186af931285b7f03d2a78cd04f888f (diff) |
Result of svn merge -r65183:65337 svn+ssh://svn/svn/linden/branches/python-shuffle into release. Also includes untabification of many python files.
Diffstat (limited to 'indra/lib/python/indra/ipc')
-rw-r--r-- | indra/lib/python/indra/ipc/__init__.py | 7 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/compatibility.py | 103 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/httputil.py | 94 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/llmessage.py | 353 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/llsdhttp.py | 257 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/russ.py | 123 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/servicebuilder.py | 52 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/tokenstream.py | 134 | ||||
-rw-r--r-- | indra/lib/python/indra/ipc/xml_rpc.py | 253 |
9 files changed, 1376 insertions, 0 deletions
diff --git a/indra/lib/python/indra/ipc/__init__.py b/indra/lib/python/indra/ipc/__init__.py new file mode 100644 index 0000000000..6576513f9d --- /dev/null +++ b/indra/lib/python/indra/ipc/__init__.py @@ -0,0 +1,7 @@ +"""\ +@file __init__.py +@brief Initialization file for the indra ipc module. + +Copyright (c) 2006-2007, Linden Research, Inc. +$License$ +""" diff --git a/indra/lib/python/indra/ipc/compatibility.py b/indra/lib/python/indra/ipc/compatibility.py new file mode 100644 index 0000000000..a430e46ab4 --- /dev/null +++ b/indra/lib/python/indra/ipc/compatibility.py @@ -0,0 +1,103 @@ +"""\ +@file compatibility.py +@brief Classes that manage compatibility states. + +Copyright (c) 2007, Linden Research, Inc. +$License$ +""" + + +"""Compatibility combination table: + + I M O N S + -- -- -- -- -- +I: I I I I I +M: I M M M M +O: I M O M O +N: I M M N N +S: I M O N S + +""" + +class _Compatibility(object): + def __init__(self, reason): + self.reasons = [ ] + if reason: + self.reasons.append(reason) + + def combine(self, other): + if self._level() <= other._level(): + return self._buildclone(other) + else: + return other._buildclone(self) + + def prefix(self, leadin): + self.reasons = [ leadin + r for r in self.reasons ] + + def same(self): return self._level() >= 1 + def deployable(self): return self._level() > 0 + def resolved(self): return self._level() > -1 + def compatible(self): return self._level() > -2 + + def explain(self): + return self.__class__.__name__ + "\n" + "\n".join(self.reasons) + "\n" + + def _buildclone(self, other=None): + c = self._buildinstance() + c.reasons = self.reasons + if other: + c.reasons = c.reasons + other.reasons + return c + + def _buildinstance(self): + return self.__class__(None) + +# def _level(self): +# raise RuntimeError('implement in subclass') + + +class Incompatible(_Compatibility): + def _level(self): + return -2 + +class Mixed(_Compatibility): + def __init__(self, *inputs): + _Compatibility.__init__(self, None) + for i in inputs: + self.reasons += i.reasons + + def _buildinstance(self): + return self.__class__() + + def _level(self): + return -1 + +class _Aged(_Compatibility): + def combine(self, other): + if self._level() == other._level(): + return self._buildclone(other) + if int(self._level()) == int(other._level()): + return Mixed(self, other) + return _Compatibility.combine(self, other) + +class Older(_Aged): + def _level(self): + return -0.25 + +class Newer(_Aged): + def _level(self): + return 0.25 + +class Same(_Compatibility): + def __init__(self): + _Compatibility.__init__(self, None) + + def _buildinstance(self): + return self.__class__() + + def _level(self): + return 1 + + + + diff --git a/indra/lib/python/indra/ipc/httputil.py b/indra/lib/python/indra/ipc/httputil.py new file mode 100644 index 0000000000..85e4a645bc --- /dev/null +++ b/indra/lib/python/indra/ipc/httputil.py @@ -0,0 +1,94 @@ +"""\ +@file httputil.py +@brief HTTP utilities. HTTP date conversion and non-blocking HTTP +client support. + +Copyright (c) 2006-2007, Linden Research, Inc. +$License$ +""" + + +import os +import time +import urlparse + +import httplib +try: + from mx.DateTime import Parser + + parse_date = Parser.DateTimeFromString +except ImportError: + from dateutil import parser + + parse_date = parser.parse + + +HTTP_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT' + + +to_http_time = lambda t: time.strftime(HTTP_TIME_FORMAT, time.gmtime(t)) +from_http_time = lambda t: int(parse_date(t).gmticks()) + +def host_and_port_from_url(url): + """@breif Simple function to get host and port from an http url. + @return Returns host, port and port may be None. + """ + host = None + port = None + parsed_url = urlparse.urlparse(url) + try: + host, port = parsed_url[1].split(':') + except ValueError: + host = parsed_url[1].split(':') + return host, port + + +def better_putrequest(self, method, url, skip_host=0): + self.method = method + self.path = url + self.old_putrequest(method, url, skip_host) + + +class HttpClient(httplib.HTTPConnection): + """A subclass of httplib.HTTPConnection which works around a bug + in the interaction between eventlet sockets and httplib. httplib relies + on gc to close the socket, causing the socket to be closed too early. + + This is an awful hack and the bug should be fixed properly ASAP. + """ + def __init__(self, host, port=None, strict=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + + def close(self): + pass + + old_putrequest = httplib.HTTPConnection.putrequest + putrequest = better_putrequest + + +class HttpsClient(httplib.HTTPSConnection): + def close(self): + pass + old_putrequest = httplib.HTTPSConnection.putrequest + putrequest = better_putrequest + + + +scheme_to_factory_map = { + 'http': HttpClient, + 'https': HttpsClient, +} + + +def makeConnection(scheme, location, use_proxy): + if use_proxy: + if "http_proxy" in os.environ: + location = os.environ["http_proxy"] + elif "ALL_PROXY" in os.environ: + location = os.environ["ALL_PROXY"] + else: + location = "localhost:3128" #default to local squid + if location.startswith("http://"): + location = location[len("http://"):] + return scheme_to_factory_map[scheme](location) + diff --git a/indra/lib/python/indra/ipc/llmessage.py b/indra/lib/python/indra/ipc/llmessage.py new file mode 100644 index 0000000000..5a9e867ecd --- /dev/null +++ b/indra/lib/python/indra/ipc/llmessage.py @@ -0,0 +1,353 @@ +"""\ +@file llmessage.py +@brief Message template parsing and compatiblity + +Copyright (c) 2007, Linden Research, Inc. +$License$ +""" + +from sets import Set, ImmutableSet + +from compatibility import Incompatible, Older, Newer, Same +from tokenstream import TokenStream + +### +### Message Template +### + +class Template: + def __init__(self): + self.messages = { } + + def addMessage(self, m): + self.messages[m.name] = m + + def compatibleWithBase(self, base): + messagenames = ( + ImmutableSet(self.messages.keys()) + | ImmutableSet(base.messages.keys()) + ) + + compatibility = Same() + for name in messagenames: + selfmessage = self.messages.get(name, None) + basemessage = base.messages.get(name, None) + + if not selfmessage: + c = Older("missing message %s, did you mean to deprecate?" % name) + elif not basemessage: + c = Newer("added message %s" % name) + else: + c = selfmessage.compatibleWithBase(basemessage) + c.prefix("in message %s: " % name) + + compatibility = compatibility.combine(c) + + return compatibility + + + +class Message: + HIGH = "High" + MEDIUM = "Medium" + LOW = "Low" + FIXED = "Fixed" + priorities = [ HIGH, MEDIUM, LOW, FIXED ] + prioritieswithnumber = [ FIXED ] + + TRUSTED = "Trusted" + NOTTRUSTED = "NotTrusted" + trusts = [ TRUSTED, NOTTRUSTED ] + + UNENCODED = "Unencoded" + ZEROCODED = "Zerocoded" + encodings = [ UNENCODED, ZEROCODED ] + + NOTDEPRECATED = "NotDeprecated" + DEPRECATED = "Deprecated" + UDPDEPRECATED = "UDPDeprecated" + deprecations = [ NOTDEPRECATED, UDPDEPRECATED, DEPRECATED ] + # in order of increasing deprecation + + def __init__(self, name, number, priority, trust, coding): + self.name = name + self.number = number + self.priority = priority + self.trust = trust + self.coding = coding + self.deprecateLevel = 0 + self.blocks = [ ] + + def deprecated(self): + return self.deprecateLevel != 0 + + def deprecate(self, deprecation): + self.deprecateLevel = self.deprecations.index(deprecation) + + def addBlock(self, block): + self.blocks.append(block) + + def compatibleWithBase(self, base): + if self.name != base.name: + # this should never happen in real life because of the + # way Template matches up messages by name + return Incompatible("has different name: %s vs. %s in base" + % (self.name, base.name)) + if self.priority != base.priority: + return Incompatible("has different priority: %s vs. %s in base" + % (self.priority, base.priority)) + if self.trust != base.trust: + return Incompatible("has different trust: %s vs. %s in base" + % (self.trust, base.trust)) + if self.coding != base.coding: + return Incompatible("has different coding: %s vs. %s in base" + % (self.coding, base.coding)) + if self.number != base.number: + return Incompatible("has different number: %s vs. %s in base" + % (self.number, base.number)) + + compatibility = Same() + + if self.deprecateLevel != base.deprecateLevel: + if self.deprecateLevel < base.deprecateLevel: + c = Older("is less deprecated: %s vs. %s in base" % ( + self.deprecations[self.deprecateLevel], + self.deprecations[base.deprecateLevel])) + else: + c = Newer("is more deprecated: %s vs. %s in base" % ( + self.deprecations[self.deprecateLevel], + self.deprecations[base.deprecateLevel])) + compatibility = compatibility.combine(c) + + selflen = len(self.blocks) + baselen = len(base.blocks) + samelen = min(selflen, baselen) + + for i in xrange(0, samelen): + selfblock = self.blocks[i] + baseblock = base.blocks[i] + + c = selfblock.compatibleWithBase(baseblock) + if not c.same(): + c = Incompatible("block %d isn't identical" % i) + compatibility = compatibility.combine(c) + + if selflen > baselen: + c = Newer("has %d extra blocks" % (selflen - baselen)) + elif selflen < baselen: + c = Older("missing %d extra blocks" % (baselen - selflen)) + else: + c = Same() + + compatibility = compatibility.combine(c) + return compatibility + + + +class Block(object): + SINGLE = "Single" + MULTIPLE = "Multiple" + VARIABLE = "Variable" + repeats = [ SINGLE, MULTIPLE, VARIABLE ] + repeatswithcount = [ MULTIPLE ] + + def __init__(self, name, repeat, count=None): + self.name = name + self.repeat = repeat + self.count = count + self.variables = [ ] + + def addVariable(self, variable): + self.variables.append(variable) + + def compatibleWithBase(self, base): + if self.name != base.name: + return Incompatible("has different name: %s vs. %s in base" + % (self.name, base.name)) + if self.repeat != base.repeat: + return Incompatible("has different repeat: %s vs. %s in base" + % (self.repeat, base.repeat)) + if self.repeat in Block.repeatswithcount: + if self.count != base.count: + return Incompatible("has different count: %s vs. %s in base" + % (self.count, base.count)) + + compatibility = Same() + + selflen = len(self.variables) + baselen = len(base.variables) + + for i in xrange(0, min(selflen, baselen)): + selfvar = self.variables[i] + basevar = base.variables[i] + + c = selfvar.compatibleWithBase(basevar) + if not c.same(): + c = Incompatible("variable %d isn't identical" % i) + compatibility = compatibility.combine(c) + + if selflen > baselen: + c = Newer("has %d extra variables" % (selflen - baselen)) + elif selflen < baselen: + c = Older("missing %d extra variables" % (baselen - selflen)) + else: + c = Same() + + compatibility = compatibility.combine(c) + return compatibility + + + +class Variable: + U8 = "U8"; U16 = "U16"; U32 = "U32"; U64 = "U64" + S8 = "S8"; S16 = "S16"; S32 = "S32"; S64 = "S64" + F32 = "F32"; F64 = "F64" + LLVECTOR3 = "LLVector3"; LLVECTOR3D = "LLVector3d"; LLVECTOR4 = "LLVector4" + LLQUATERNION = "LLQuaternion" + LLUUID = "LLUUID" + BOOL = "BOOL" + IPADDR = "IPADDR"; IPPORT = "IPPORT" + FIXED = "Fixed" + VARIABLE = "Variable" + types = [ U8, U16, U32, U64, S8, S16, S32, S64, F32, F64, + LLVECTOR3, LLVECTOR3D, LLVECTOR4, LLQUATERNION, + LLUUID, BOOL, IPADDR, IPPORT, FIXED, VARIABLE ] + typeswithsize = [ FIXED, VARIABLE ] + + def __init__(self, name, type, size): + self.name = name + self.type = type + self.size = size + + def compatibleWithBase(self, base): + if self.name != base.name: + return Incompatible("has different name: %s vs. %s in base" + % (self.name, base.name)) + if self.type != base.type: + return Incompatible("has different type: %s vs. %s in base" + % (self.type, base.type)) + if self.type in Variable.typeswithsize: + if self.size != base.size: + return Incompatible("has different size: %s vs. %s in base" + % (self.size, base.size)) + return Same() + + + +### +### Parsing Message Templates +### + +class TemplateParser: + def __init__(self, tokens): + self._tokens = tokens + self._version = 0 + self._numbers = { } + for p in Message.priorities: + self._numbers[p] = 0 + + def parseTemplate(self): + tokens = self._tokens + t = Template() + while True: + if tokens.want("version"): + v = float(tokens.require(tokens.wantFloat())) + self._version = v + t.version = v + continue + + m = self.parseMessage() + if m: + t.addMessage(m) + continue + + if self._version >= 2.0: + tokens.require(tokens.wantEOF()) + break + else: + if tokens.wantEOF(): + break + + tokens.consume() + # just assume (gulp) that this is a comment + # line 468: "sim -> dataserver" + return t + + + def parseMessage(self): + tokens = self._tokens + if not tokens.want("{"): + return None + + name = tokens.require(tokens.wantSymbol()) + priority = tokens.require(tokens.wantOneOf(Message.priorities)) + + if self._version >= 2.0 or priority in Message.prioritieswithnumber: + number = int("+" + tokens.require(tokens.wantInteger()), 0) + else: + self._numbers[priority] += 1 + number = self._numbers[priority] + + trust = tokens.require(tokens.wantOneOf(Message.trusts)) + coding = tokens.require(tokens.wantOneOf(Message.encodings)) + + m = Message(name, number, priority, trust, coding) + + if self._version >= 2.0: + d = tokens.wantOneOf(Message.deprecations) + if d: + m.deprecate(d) + + while True: + b = self.parseBlock() + if not b: + break + m.addBlock(b) + + tokens.require(tokens.want("}")) + + return m + + + def parseBlock(self): + tokens = self._tokens + if not tokens.want("{"): + return None + name = tokens.require(tokens.wantSymbol()) + repeat = tokens.require(tokens.wantOneOf(Block.repeats)) + if repeat in Block.repeatswithcount: + count = int(tokens.require(tokens.wantInteger())) + else: + count = None + + b = Block(name, repeat, count) + + while True: + v = self.parseVariable() + if not v: + break + b.addVariable(v) + + tokens.require(tokens.want("}")) + return b + + + def parseVariable(self): + tokens = self._tokens + if not tokens.want("{"): + return None + name = tokens.require(tokens.wantSymbol()) + type = tokens.require(tokens.wantOneOf(Variable.types)) + if type in Variable.typeswithsize: + size = tokens.require(tokens.wantInteger()) + else: + tokens.wantInteger() # in LandStatRequest: "{ ParcelLocalID S32 1 }" + size = None + tokens.require(tokens.want("}")) + return Variable(name, type, size) + +def parseTemplateString(s): + return TemplateParser(TokenStream().fromString(s)).parseTemplate() + +def parseTemplateFile(f): + return TemplateParser(TokenStream().fromFile(f)).parseTemplate() diff --git a/indra/lib/python/indra/ipc/llsdhttp.py b/indra/lib/python/indra/ipc/llsdhttp.py new file mode 100644 index 0000000000..a2a889742a --- /dev/null +++ b/indra/lib/python/indra/ipc/llsdhttp.py @@ -0,0 +1,257 @@ +"""\ +@file llsdhttp.py +@brief Functions to ease moving llsd over http + +Copyright (c) 2006-2007, Linden Research, Inc. +$License$ +""" + +import os.path +import os +import os +import urlparse + +from indra.base import llsd +from indra.ipc import httputil +LLSD = llsd.LLSD() + + +class ConnectionError(Exception): + def __init__(self, method, host, port, path, status, reason, body): + self.method = method + self.host = host + self.port = port + self.path = path + self.status = status + self.reason = reason + self.body = body + Exception.__init__(self) + + def __str__(self): + return "%s(%r, %r, %r, %r, %r, %r, %r)" % ( + type(self).__name__, + self.method, self.host, self.port, + self.path, self.status, self.reason, self.body) + + +class NotFound(ConnectionError): + pass + +class Forbidden(ConnectionError): + pass + +class MalformedResponse(ConnectionError): + pass + +class NotImplemented(ConnectionError): + pass + +def runConnection(connection, raise_parse_errors=True): + response = connection.getresponse() + if (response.status not in [200, 201, 204]): + klass = {404:NotFound, + 403:Forbidden, + 501:NotImplemented}.get(response.status, ConnectionError) + raise klass( + connection.method, connection.host, connection.port, + connection.path, response.status, response.reason, response.read()) + content_length = response.getheader('content-length') + + if content_length: # Check to see whether it is not None + content_length = int(content_length) + + if content_length: # Check to see whether the length is not 0 + body = response.read(content_length) + else: + body = '' + + if not body.strip(): + #print "No body:", `body` + return None + try: + return LLSD.parse(body) + except Exception, e: + if raise_parse_errors: + print "Exception: %s, Could not parse LLSD: " % (e), body + raise MalformedResponse( + connection.method, connection.host, connection.port, + connection.path, response.status, response.reason, body) + else: + return None + +class FileScheme(object): + """Retarded scheme to local file wrapper.""" + host = '<file>' + port = '<file>' + reason = '<none>' + + def __init__(self, location): + pass + + def request(self, method, fullpath, body='', headers=None): + self.status = 200 + self.path = fullpath.split('?')[0] + self.method = method = method.lower() + assert method in ('get', 'put', 'delete') + if method == 'delete': + try: + os.remove(self.path) + except OSError: + pass # don't complain if already deleted + elif method == 'put': + try: + f = file(self.path, 'w') + f.write(body) + f.close() + except IOError, e: + self.status = 500 + self.raise_connection_error() + elif method == 'get': + if not os.path.exists(self.path): + self.status = 404 + self.raise_connection_error(NotFound) + + def connect(self): + pass + + def getresponse(self): + return self + + def getheader(self, header): + if header == 'content-length': + try: + return os.path.getsize(self.path) + except OSError: + return 0 + + def read(self, howmuch=None): + if self.method == 'get': + try: + return file(self.path, 'r').read(howmuch) + except IOError: + self.status = 500 + self.raise_connection_error() + return '' + + def raise_connection_error(self, klass=ConnectionError): + raise klass( + self.method, self.host, self.port, + self.path, self.status, self.reason, '') + +scheme_to_factory_map = { + 'http': httputil.HttpClient, + 'https': httputil.HttpsClient, + 'file': FileScheme, +} + +def makeConnection(scheme, location, use_proxy): + if use_proxy: + if "http_proxy" in os.environ: + location = os.environ["http_proxy"] + elif "ALL_PROXY" in os.environ: + location = os.environ["ALL_PROXY"] + else: + location = "localhost:3128" #default to local squid + if location.startswith("http://"): + location = location[len("http://"):] + return scheme_to_factory_map[scheme](location) + + +def get(url, headers=None, use_proxy=False): + if headers is None: + headers = {} + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=use_proxy) + fullpath = path + if query: + fullpath += "?"+query + connection.connect() + if headers is None: + headers=dict() + if use_proxy: + headers.update({ "Host" : location }) + connection.request("GET", url, headers=headers) + else: + connection.request("GET", fullpath, headers=headers) + + return runConnection(connection) + +def put(url, data, headers=None): + body = LLSD.toXML(data) + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + if headers is None: + headers = {} + headers.update({'Content-Type': 'application/xml'}) + fullpath = path + if query: + fullpath += "?"+query + connection.request("PUT", fullpath, body, headers) + return runConnection(connection, raise_parse_errors=False) + +def delete(url): + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("DELETE", path+"?"+query) + return runConnection(connection, raise_parse_errors=False) + +def post(url, data=None): + body = LLSD.toXML(data) + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("POST", path+"?"+query, body, {'Content-Type': 'application/xml'}) + return runConnection(connection) + +def postFile(url, filename): + f = open(filename) + body = f.read() + f.close() + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("POST", path+"?"+query, body, {'Content-Type': 'application/octet-stream'}) + return runConnection(connection) + +def getStatus(url, use_proxy=False): + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=use_proxy) + if use_proxy: + connection.request("GET", url, headers={ "Host" : location }) + else: + connection.request("GET", path+"?"+query) + return connection.getresponse().status + +def putStatus(url, data): + body = LLSD.toXML(data) + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("PUT", path+"?"+query, body, {'Content-Type': 'application/xml'}) + return connection.getresponse().status + +def deleteStatus(url): + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("DELETE", path+"?"+query) + return connection.getresponse().status + +def postStatus(url, data): + body = LLSD.toXML(data) + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("POST", path+"?"+query, body) + return connection.getresponse().status + +def postFileStatus(url, filename): + f = open(filename) + body = f.read() + f.close() + scheme, location, path, params, query, id = urlparse.urlparse(url) + connection = makeConnection(scheme, location, use_proxy=False) + connection.request("POST", path+"?"+query, body, {'Content-Type': 'application/octet-stream'}) + response = connection.getresponse() + return response.status, response.read() + +def getFromSimulator(path, use_proxy=False): + return get('http://' + simulatorHostAndPort + path, use_proxy=use_proxy) + +def postToSimulator(path, data=None): + return post('http://' + simulatorHostAndPort + path, data) diff --git a/indra/lib/python/indra/ipc/russ.py b/indra/lib/python/indra/ipc/russ.py new file mode 100644 index 0000000000..f6f67d4314 --- /dev/null +++ b/indra/lib/python/indra/ipc/russ.py @@ -0,0 +1,123 @@ +"""\ +@file russ.py +@brief Recursive URL Substitution Syntax helpers +@author Phoenix + +Many details on how this should work is available on the wiki: +https://wiki.secondlife.com/wiki/Recursive_URL_Substitution_Syntax + +Adding features to this should be reflected in that page in the +implementations section. + +Copyright (c) 2007, Linden Research, Inc. +$License$ +""" + +import urllib +from indra.ipc import llsdhttp + +class UnbalancedBraces(Exception): + pass + +class UnknownDirective(Exception): + pass + +class BadDirective(Exception): + pass + +def format(format_str, context): + """@brief Format format string according to rules for RUSS. +@see https://osiris.lindenlab.com/mediawiki/index.php/Recursive_URL_Substitution_Syntax +@param format_str The input string to format. +@param context A map used for string substitutions. +@return Returns the formatted string. If no match, the braces remain intact. +""" + while True: + #print "format_str:", format_str + all_matches = _find_sub_matches(format_str) + if not all_matches: + break + substitutions = 0 + while True: + matches = all_matches.pop() + # we work from right to left to make sure we do not + # invalidate positions earlier in format_str + matches.reverse() + for pos in matches: + # Use index since _find_sub_matches should have raised + # an exception, and failure to find now is an exception. + end = format_str.index('}', pos) + #print "directive:", format_str[pos+1:pos+5] + if format_str[pos + 1] == '$': + value = context.get(format_str[pos + 2:end]) + elif format_str[pos + 1] == '%': + value = _build_query_string( + context.get(format_str[pos + 2:end])) + elif format_str[pos+1:pos+5] == 'http' or format_str[pos+1:pos+5] == 'file': + value = _fetch_url_directive(format_str[pos + 1:end]) + else: + raise UnknownDirective, format_str[pos:end + 1] + if not value == None: + format_str = format_str[:pos]+str(value)+format_str[end+1:] + substitutions += 1 + + # If there were any substitutions at this depth, re-parse + # since this may have revealed new things to substitute + if substitutions: + break + if not all_matches: + break + + # If there were no substitutions at all, and we have exhausted + # the possible matches, bail. + if not substitutions: + break + return format_str + +def _find_sub_matches(format_str): + """@brief Find all of the substitution matches. +@param format_str the RUSS conformant format string. +@return Returns an array of depths of arrays of positional matches in input. +""" + depth = 0 + matches = [] + for pos in range(len(format_str)): + if format_str[pos] == '{': + depth += 1 + if not len(matches) == depth: + matches.append([]) + matches[depth - 1].append(pos) + continue + if format_str[pos] == '}': + depth -= 1 + continue + if not depth == 0: + raise UnbalancedBraces, format_str + return matches + +def _build_query_string(query_dict): + """\ + @breif given a dict, return a query string. utility wrapper for urllib. + @param query_dict input query dict + @returns Returns an urlencoded query string including leading '?'. + """ + if query_dict: + return '?' + urllib.urlencode(query_dict) + else: + return '' + +def _fetch_url_directive(directive): + "*FIX: This only supports GET" + commands = directive.split('|') + resource = llsdhttp.get(commands[0]) + if len(commands) == 3: + resource = _walk_resource(resource, commands[2]) + return resource + +def _walk_resource(resource, path): + path = path.split('/') + for child in path: + if not child: + continue + resource = resource[child] + return resource diff --git a/indra/lib/python/indra/ipc/servicebuilder.py b/indra/lib/python/indra/ipc/servicebuilder.py new file mode 100644 index 0000000000..d422b2ed42 --- /dev/null +++ b/indra/lib/python/indra/ipc/servicebuilder.py @@ -0,0 +1,52 @@ +"""\ +@file servicebuilder.py +@author Phoenix +@brief Class which will generate service urls. + +Copyright (c) 2007, Linden Research, Inc. +$License$ +""" + +from indra.base import config +from indra.ipc import llsdhttp +from indra.ipc import russ + +services_config = {} +try: + services_config = llsdhttp.get(config.get('services-config')) +except: + pass + +class ServiceBuilder(object): + def __init__(self, services_definition = services_config): + """\ + @brief + @brief Create a ServiceBuilder. + @param services_definition Complete services definition, services.xml. + """ + # no need to keep a copy of the services section of the + # complete services definition, but it doesn't hurt much. + self.services = services_definition['services'] + self.builders = {} + for service in self.services: + service_builder = service.get('service-builder') + if not service_builder: + continue + if isinstance(service_builder, dict): + # We will be constructing several builders + for name, builder in service_builder.items(): + full_builder_name = service['name'] + '-' + name + self.builders[full_builder_name] = builder + else: + self.builders[service['name']] = service_builder + + def buildServiceURL(self, name, context): + """\ + @brief given the environment on construction, return a service URL. + @param name The name of the service. + @param context A dict of name value lookups for the service. + @returns Returns the + """ + base_url = config.get('services-base-url') + svc_path = russ.format(self.builders[name], context) + return base_url + svc_path diff --git a/indra/lib/python/indra/ipc/tokenstream.py b/indra/lib/python/indra/ipc/tokenstream.py new file mode 100644 index 0000000000..f757f2f8af --- /dev/null +++ b/indra/lib/python/indra/ipc/tokenstream.py @@ -0,0 +1,134 @@ +"""\ +@file tokenstream.py +@brief Message template parsing utility class + +Copyright (c) 2007, Linden Research, Inc. +$License$ +""" + +import re + +class _EOF(object): + pass + +EOF = _EOF() + +class _LineMarker(int): + pass + +_commentRE = re.compile(r'//.*') +_symbolRE = re.compile(r'[a-zA-Z_][a-zA-Z_0-9]*') +_integerRE = re.compile(r'(0x[0-9A-Fa-f]+|0\d*|[1-9]\d*)') +_floatRE = re.compile(r'\d+(\.\d*)?') + + +class ParseError(Exception): + def __init__(self, stream, reason): + self.line = stream.line + self.context = stream._context() + self.reason = reason + + def _contextString(self): + c = [ ] + for t in self.context: + if isinstance(t, _LineMarker): + break + c.append(t) + return " ".join(c) + + def __str__(self): + return "line %d: %s @ ... %s" % ( + self.line, self.reason, self._contextString()) + + def __nonzero__(self): + return False + + +def _optionText(options): + n = len(options) + if n == 1: + return '"%s"' % options[0] + return '"' + '", "'.join(options[0:(n-1)]) + '" or "' + options[-1] + '"' + + +class TokenStream(object): + def __init__(self): + self.line = 0 + self.tokens = [ ] + + def fromString(self, string): + return self.fromLines(string.split('\n')) + + def fromFile(self, file): + return self.fromLines(file) + + def fromLines(self, lines): + i = 0 + for line in lines: + i += 1 + self.tokens.append(_LineMarker(i)) + self.tokens.extend(_commentRE.sub(" ", line).split()) + self._consumeLines() + return self + + def consume(self): + if not self.tokens: + return EOF + t = self.tokens.pop(0) + self._consumeLines() + return t + + def _consumeLines(self): + while self.tokens and isinstance(self.tokens[0], _LineMarker): + self.line = self.tokens.pop(0) + + def peek(self): + if not self.tokens: + return EOF + return self.tokens[0] + + def want(self, t): + if t == self.peek(): + return self.consume() + return ParseError(self, 'expected "%s"' % t) + + def wantOneOf(self, options): + assert len(options) + if self.peek() in options: + return self.consume() + return ParseError(self, 'expected one of %s' % _optionText(options)) + + def wantEOF(self): + return self.want(EOF) + + def wantRE(self, re, message=None): + t = self.peek() + if t != EOF: + m = re.match(t) + if m and m.end() == len(t): + return self.consume() + if not message: + message = "expected match for r'%s'" % re.pattern + return ParseError(self, message) + + def wantSymbol(self): + return self.wantRE(_symbolRE, "expected symbol") + + def wantInteger(self): + return self.wantRE(_integerRE, "expected integer") + + def wantFloat(self): + return self.wantRE(_floatRE, "expected float") + + def _context(self): + n = min(5, len(self.tokens)) + return self.tokens[0:n] + + def require(self, t): + if t: + return t + if isinstance(t, ParseError): + raise t + else: + raise ParseError(self, "unmet requirement") + diff --git a/indra/lib/python/indra/ipc/xml_rpc.py b/indra/lib/python/indra/ipc/xml_rpc.py new file mode 100644 index 0000000000..66375b4ac3 --- /dev/null +++ b/indra/lib/python/indra/ipc/xml_rpc.py @@ -0,0 +1,253 @@ +"""\ +@file xml_rpc.py +@brief An implementation of a parser/generator for the XML-RPC xml format. + +Copyright (c) 2006-2007, Linden Research, Inc. +$License$ +""" + + +from greenlet import greenlet + +from mulib import mu + +from xml.sax import handler +from xml.sax import parseString + + +# States +class Expected(object): + def __init__(self, tag): + self.tag = tag + + def __getattr__(self, name): + return type(self)(name) + + def __repr__(self): + return '%s(%r)' % ( + type(self).__name__, self.tag) + + +class START(Expected): + pass + + +class END(Expected): + pass + + +class STR(object): + tag = '' + + +START = START('') +END = END('') + + +class Malformed(Exception): + pass + + +class XMLParser(handler.ContentHandler): + def __init__(self, state_machine, next_states): + handler.ContentHandler.__init__(self) + self.state_machine = state_machine + if not isinstance(next_states, tuple): + next_states = (next_states, ) + self.next_states = next_states + self._character_buffer = '' + + def assertState(self, state, name, *rest): + if not isinstance(self.next_states, tuple): + self.next_states = (self.next_states, ) + for next in self.next_states: + if type(state) == type(next): + if next.tag and next.tag != name: + raise Malformed( + "Expected %s, got %s %s %s" % ( + next, state, name, rest)) + break + else: + raise Malformed( + "Expected %s, got %s %s %s" % ( + self.next_states, state, name, rest)) + + def startElement(self, name, attrs): + self.assertState(START, name.lower(), attrs) + self.next_states = self.state_machine.switch(START, (name.lower(), dict(attrs))) + + def endElement(self, name): + if self._character_buffer.strip(): + characters = self._character_buffer.strip() + self._character_buffer = '' + self.assertState(STR, characters) + self.next_states = self.state_machine.switch(characters) + self.assertState(END, name.lower()) + self.next_states = self.state_machine.switch(END, name.lower()) + + def error(self, exc): + self.bozo = 1 + self.exc = exc + + def fatalError(self, exc): + self.error(exc) + raise exc + + def characters(self, characters): + self._character_buffer += characters + + +def parse(what): + child = greenlet(xml_rpc) + me = greenlet.getcurrent() + startup_states = child.switch(me) + parser = XMLParser(child, startup_states) + try: + parseString(what, parser) + except Malformed: + print what + raise + return child.switch() + + +def xml_rpc(yielder): + yielder.switch(START.methodcall) + yielder.switch(START.methodname) + methodName = yielder.switch(STR) + yielder.switch(END.methodname) + + yielder.switch(START.params) + + root = None + params = [] + while True: + state, _ = yielder.switch(START.param, END.params) + if state == END: + break + + yielder.switch(START.value) + + params.append( + handle(yielder)) + + yielder.switch(END.value) + yielder.switch(END.param) + + yielder.switch(END.methodcall) + ## Resume parse + yielder.switch() + ## Return result to parse + return methodName.strip(), params + + +def handle(yielder): + _, (tag, attrs) = yielder.switch(START) + if tag in ['int', 'i4']: + result = int(yielder.switch(STR)) + elif tag == 'boolean': + result = bool(int(yielder.switch(STR))) + elif tag == 'string': + result = yielder.switch(STR) + elif tag == 'double': + result = float(yielder.switch(STR)) + elif tag == 'datetime.iso8601': + result = yielder.switch(STR) + elif tag == 'base64': + result = base64.b64decode(yielder.switch(STR)) + elif tag == 'struct': + result = {} + while True: + state, _ = yielder.switch(START.member, END.struct) + if state == END: + break + + yielder.switch(START.name) + key = yielder.switch(STR) + yielder.switch(END.name) + + yielder.switch(START.value) + result[key] = handle(yielder) + yielder.switch(END.value) + + yielder.switch(END.member) + ## We already handled </struct> above, don't want to handle it below + return result + elif tag == 'array': + result = [] + yielder.switch(START.data) + while True: + state, _ = yielder.switch(START.value, END.data) + if state == END: + break + + result.append(handle(yielder)) + + yielder.switch(END.value) + + yielder.switch(getattr(END, tag)) + + return result + + +VALUE = mu.tagFactory('value') +BOOLEAN = mu.tagFactory('boolean') +INT = mu.tagFactory('int') +STRUCT = mu.tagFactory('struct') +MEMBER = mu.tagFactory('member') +NAME = mu.tagFactory('name') +ARRAY = mu.tagFactory('array') +DATA = mu.tagFactory('data') +STRING = mu.tagFactory('string') +DOUBLE = mu.tagFactory('double') +METHODRESPONSE = mu.tagFactory('methodResponse') +PARAMS = mu.tagFactory('params') +PARAM = mu.tagFactory('param') + +mu.inline_elements['string'] = True +mu.inline_elements['boolean'] = True +mu.inline_elements['name'] = True + + +def _generate(something): + if isinstance(something, dict): + result = STRUCT() + for key, value in something.items(): + result[ + MEMBER[ + NAME[key], _generate(value)]] + return VALUE[result] + elif isinstance(something, list): + result = DATA() + for item in something: + result[_generate(item)] + return VALUE[ARRAY[[result]]] + elif isinstance(something, basestring): + return VALUE[STRING[something]] + elif isinstance(something, bool): + if something: + return VALUE[BOOLEAN['1']] + return VALUE[BOOLEAN['0']] + elif isinstance(something, int): + return VALUE[INT[something]] + elif isinstance(something, float): + return VALUE[DOUBLE[something]] + +def generate(*args): + params = PARAMS() + for arg in args: + params[PARAM[_generate(arg)]] + return METHODRESPONSE[params] + + +if __name__ == '__main__': + print parse("""<?xml version="1.0"?> <methodCall> <methodName>examples.getStateName</methodName> <params> <param> <value><i4>41</i4></value> </param> </params> </methodCall> +""") + + + + + + + + + |