From 798d367d54a6c6379ad355bd8345fa40e31e7fe9 Mon Sep 17 00:00:00 2001
From: Jacek Antonelli
Date: Sat, 6 Sep 2008 18:24:57 -0500
Subject: Second Life viewer sources 1.21.0-RC
---
linden/indra/lib/python/indra/base/cllsd_test.py | 51 +++
linden/indra/lib/python/indra/base/config.py | 236 +++++++++---
linden/indra/lib/python/indra/base/llsd.py | 110 +++---
linden/indra/lib/python/indra/base/lluuid.py | 32 +-
linden/indra/lib/python/indra/ipc/llsdhttp.py | 13 +-
linden/indra/lib/python/indra/ipc/mysql_pool.py | 100 +++--
linden/indra/lib/python/indra/ipc/siesta.py | 402 +++++++++++++++++++++
linden/indra/lib/python/indra/ipc/siesta_test.py | 214 +++++++++++
.../lib/python/indra/util/fastest_elementtree.py | 52 +++
linden/indra/lib/python/indra/util/llmanifest.py | 217 ++++++-----
linden/indra/lib/python/indra/util/named_query.py | 7 +-
linden/indra/lib/python/indra/util/term.py | 222 ++++++++++++
12 files changed, 1399 insertions(+), 257 deletions(-)
create mode 100644 linden/indra/lib/python/indra/base/cllsd_test.py
create mode 100644 linden/indra/lib/python/indra/ipc/siesta.py
create mode 100644 linden/indra/lib/python/indra/ipc/siesta_test.py
create mode 100644 linden/indra/lib/python/indra/util/fastest_elementtree.py
create mode 100644 linden/indra/lib/python/indra/util/term.py
(limited to 'linden/indra/lib')
diff --git a/linden/indra/lib/python/indra/base/cllsd_test.py b/linden/indra/lib/python/indra/base/cllsd_test.py
new file mode 100644
index 0000000..3af59e7
--- /dev/null
+++ b/linden/indra/lib/python/indra/base/cllsd_test.py
@@ -0,0 +1,51 @@
+from indra.base import llsd, lluuid
+from datetime import datetime
+import cllsd
+import time, sys
+
+class myint(int):
+ pass
+
+values = (
+ '&<>',
+ u'\u81acj',
+ llsd.uri('http://foo<'),
+ lluuid.LLUUID(),
+ llsd.LLSD(['thing']),
+ 1,
+ myint(31337),
+ sys.maxint + 10,
+ llsd.binary('foo'),
+ [],
+ {},
+ {u'f&\u1212': 3},
+ 3.1,
+ True,
+ None,
+ datetime.fromtimestamp(time.time()),
+ )
+
+def valuator(values):
+ for v in values:
+ yield v
+
+longvalues = () # (values, list(values), iter(values), valuator(values))
+
+for v in values + longvalues:
+ print '%r => %r' % (v, cllsd.llsd_to_xml(v))
+
+a = [[{'a':3}]] * 1000000
+
+s = time.time()
+print hash(cllsd.llsd_to_xml(a))
+e = time.time()
+t1 = e - s
+print t1
+
+s = time.time()
+print hash(llsd.LLSDXMLFormatter()._format(a))
+e = time.time()
+t2 = e - s
+print t2
+
+print 'Speedup:', t2 / t1
diff --git a/linden/indra/lib/python/indra/base/config.py b/linden/indra/lib/python/indra/base/config.py
index 3865f5e..9c37ecf 100644
--- a/linden/indra/lib/python/indra/base/config.py
+++ b/linden/indra/lib/python/indra/base/config.py
@@ -26,74 +26,224 @@ THE SOFTWARE.
$/LicenseInfo$
"""
-from os.path import dirname, join, realpath
+import copy
+import os
+import traceback
+import time
import types
+
+from os.path import dirname, getmtime, join, realpath
from indra.base import llsd
-_g_config_dict = None
-
-def load(indra_xml_file=None):
- global _g_config_dict
- if _g_config_dict == None:
- if indra_xml_file is None:
- ## going from:
- ## "/opt/linden/indra/lib/python/indra/base/config.py"
- ## to:
- ## "/opt/linden/etc/indra.xml"
- indra_xml_file = realpath(
- dirname(realpath(__file__)) + "../../../../../../etc/indra.xml")
- config_file = file(indra_xml_file)
- _g_config_dict = llsd.LLSD().parse(config_file.read())
+_g_config = None
+
+class IndraConfig(object):
+ """
+ IndraConfig loads a 'indra' xml configuration file
+ and loads into memory. This representation in memory
+ can get updated to overwrite values or add new values.
+
+ The xml configuration file is considered a live file and changes
+ to the file are checked and reloaded periodically. If a value had
+ been overwritten via the update or set method, the loaded values
+ from the file are ignored (the values from the update/set methods
+ override)
+ """
+ def __init__(self, indra_config_file):
+ self._indra_config_file = indra_config_file
+ self._reload_check_interval = 30 # seconds
+ self._last_check_time = 0
+ self._last_mod_time = 0
+
+ self._config_overrides = {}
+ self._config_file_dict = {}
+ self._combined_dict = {}
+
+ self._load()
+
+ def _load(self):
+ if self._indra_config_file is None:
+ return
+
+ config_file = open(self._indra_config_file)
+ self._config_file_dict = llsd.parse(config_file.read())
+ self._combine_dictionaries()
config_file.close()
- #print "loaded config from",indra_xml_file,"into",_g_config_dict
-def dump(indra_xml_file, indra_cfg={}, update_in_mem=False):
+ self._last_mod_time = self._get_last_modified_time()
+ self._last_check_time = time.time() # now
+
+ def _get_last_modified_time(self):
+ """
+ Returns the mtime (last modified time) of the config file,
+ if such exists.
+ """
+ if self._indra_config_file is not None:
+ return os.path.getmtime(self._indra_config_file)
+
+ return 0
+
+ def _combine_dictionaries(self):
+ self._combined_dict = {}
+ self._combined_dict.update(self._config_file_dict)
+ self._combined_dict.update(self._config_overrides)
+
+ def _reload_if_necessary(self):
+ now = time.time()
+
+ if (now - self._last_check_time) > self._reload_check_interval:
+ self._last_check_time = now
+ try:
+ modtime = self._get_last_modified_time()
+ if modtime > self._last_mod_time:
+ self._load()
+ except OSError, e:
+ if e.errno == errno.ENOENT: # file not found
+ # someone messed with our internal state
+ # or removed the file
+
+ print 'WARNING: Configuration file has been removed ' + (self._indra_config_file)
+ print 'Disabling reloading of configuration file.'
+
+ traceback.print_exc()
+
+ self._indra_config_file = None
+ self._last_check_time = 0
+ self._last_mod_time = 0
+ else:
+ raise # pass the exception along to the caller
+
+ def __getitem__(self, key):
+ self._reload_if_necessary()
+
+ return self._combined_dict[key]
+
+ def get(self, key, default = None):
+ try:
+ return self.__getitem__(key)
+ except KeyError:
+ return default
+
+ def __setitem__(self, key, value):
+ """
+ Sets the value of the config setting of key to be newval
+
+ Once any key/value pair is changed via the set method,
+ that key/value pair will remain set with that value until
+ change via the update or set method
+ """
+ self._config_overrides[key] = value
+ self._combine_dictionaries()
+
+ def set(self, key, newval):
+ return self.__setitem__(key, newval)
+
+ def update(self, new_conf):
+ """
+ Load an XML file and apply its map as overrides or additions
+ to the existing config. Update can be a file or a dict.
+
+ Once any key/value pair is changed via the update method,
+ that key/value pair will remain set with that value until
+ change via the update or set method
+ """
+ if isinstance(new_conf, dict):
+ overrides = new_conf
+ else:
+ # assuming that it is a filename
+ config_file = open(new_conf)
+ overrides = llsd.parse(config_file.read())
+ config_file.close()
+
+ self._config_overrides.update(overrides)
+ self._combine_dictionaries()
+
+ def as_dict(self):
+ """
+ Returns immutable copy of the IndraConfig as a dictionary
+ """
+ return copy.deepcopy(self._combined_dict)
+
+def load(indra_xml_file = None):
+ global _g_config
+
+ if indra_xml_file is None:
+ ## going from:
+ ## "/opt/linden/indra/lib/python/indra/base/config.py"
+ ## to:
+ ## "/opt/linden/etc/indra.xml"
+ indra_xml_file = realpath(
+ dirname(realpath(__file__)) + "../../../../../../etc/indra.xml")
+
+ try:
+ _g_config = IndraConfig(indra_xml_file)
+ except IOError:
+ # indra.xml was not openable, so let's initialize with an empty dict
+ # some code relies on config behaving this way
+ _g_config = IndraConfig(None)
+
+def dump(indra_xml_file, indra_cfg = None, update_in_mem=False):
'''
Dump config contents into a file
Kindof reverse of load.
Optionally takes a new config to dump.
Does NOT update global config unless requested.
'''
- global _g_config_dict
+ global _g_config
+
if not indra_cfg:
- indra_cfg = _g_config_dict
+ if _g_config is None:
+ return
+
+ indra_cfg = _g_config.as_dict()
+
if not indra_cfg:
return
+
config_file = open(indra_xml_file, 'w')
_config_xml = llsd.format_xml(indra_cfg)
config_file.write(_config_xml)
config_file.close()
+
if update_in_mem:
update(indra_cfg)
def update(new_conf):
- """Load an XML file and apply its map as overrides or additions
- to the existing config. The dataserver does this with indra.xml
- and dataserver.xml."""
- global _g_config_dict
- if _g_config_dict == None:
- _g_config_dict = {}
- if isinstance(new_conf, dict):
- overrides = new_conf
- else:
- config_file = file(new_conf)
- overrides = llsd.LLSD().parse(config_file.read())
- config_file.close()
-
- _g_config_dict.update(overrides)
+ global _g_config
+
+ if _g_config is None:
+ # To keep with how this function behaved
+ # previously, a call to update
+ # before the global is defined
+ # make a new global config which does not
+ # load data from a file.
+ _g_config = IndraConfig(None)
+
+ return _g_config.update(new_conf)
def get(key, default = None):
- global _g_config_dict
- if _g_config_dict == None:
+ global _g_config
+
+ if _g_config is None:
load()
- return _g_config_dict.get(key, default)
+
+ return _g_config.get(key, default)
def set(key, newval):
- global _g_config_dict
- if _g_config_dict == None:
- load()
- _g_config_dict[key] = newval
+ """
+ Sets the value of the config setting of key to be newval
+
+ Once any key/value pair is changed via the set method,
+ that key/value pair will remain set with that value until
+ change via the update or set method or program termination
+ """
+ global _g_config
+
+ if _g_config is None:
+ _g_config = IndraConfig(None)
+
+ _g_config.set(key, newval)
-def as_dict():
- global _g_config_dict
- return _g_config_dict
+def get_config():
+ global _g_config
+ return _g_config
diff --git a/linden/indra/lib/python/indra/base/llsd.py b/linden/indra/lib/python/indra/base/llsd.py
index cd23551..995ace7 100644
--- a/linden/indra/lib/python/indra/base/llsd.py
+++ b/linden/indra/lib/python/indra/base/llsd.py
@@ -33,14 +33,7 @@ import time
import types
import re
-#from cElementTree import fromstring ## This does not work under Windows
-try:
- ## This is the old name of elementtree, for use with 2.3
- from elementtree.ElementTree import fromstring
-except ImportError:
- ## This is the name of elementtree under python 2.5
- from xml.etree.ElementTree import fromstring
-
+from indra.util.fastest_elementtree import fromstring
from indra.base import lluuid
int_regex = re.compile("[-+]?\d+")
@@ -67,6 +60,39 @@ BOOL_TRUE = ('1', '1.0', 'true')
BOOL_FALSE = ('0', '0.0', 'false', '')
+def format_datestr(v):
+ """ Formats a datetime object into the string format shared by xml and notation serializations."""
+ second_str = ""
+ if v.microsecond > 0:
+ seconds = v.second + float(v.microsecond) / 1000000
+ second_str = "%05.2f" % seconds
+ else:
+ second_str = "%d" % v.second
+ return '%s%sZ' % (v.strftime('%Y-%m-%dT%H:%M:'), second_str)
+
+
+def parse_datestr(datestr):
+ """Parses a datetime object from the string format shared by xml and notation serializations."""
+ if datestr == "":
+ return datetime.datetime(1970, 1, 1)
+
+ match = re.match(date_regex, datestr)
+ if not match:
+ raise LLSDParseError("invalid date string '%s'." % datestr)
+
+ year = int(match.group('year'))
+ month = int(match.group('month'))
+ day = int(match.group('day'))
+ hour = int(match.group('hour'))
+ minute = int(match.group('minute'))
+ second = int(match.group('second'))
+ seconds_float = match.group('second_float')
+ microsecond = 0
+ if seconds_float:
+ microsecond = int(seconds_float[1:]) * 10000
+ return datetime.datetime(year, month, day, hour, minute, second, microsecond)
+
+
def bool_to_python(node):
val = node.text or ''
if val in BOOL_TRUE:
@@ -99,8 +125,7 @@ def date_to_python(node):
val = node.text or ''
if not val:
val = "1970-01-01T00:00:00Z"
- return datetime.datetime(
- *time.strptime(val, '%Y-%m-%dT%H:%M:%SZ')[:6])
+ return parse_datestr(val)
def uri_to_python(node):
val = node.text or ''
@@ -194,7 +219,7 @@ class LLSDXMLFormatter(object):
def URI(self, v):
return self.elt('uri', self.xml_esc(str(v)))
def DATE(self, v):
- return self.elt('date', v.strftime('%Y-%m-%dT%H:%M:%SZ'))
+ return self.elt('date', format_datestr(v))
def ARRAY(self, v):
return self.elt('array', ''.join([self.generate(item) for item in v]))
def MAP(self, v):
@@ -215,8 +240,12 @@ class LLSDXMLFormatter(object):
def format(self, something):
return '' + self.elt("llsd", self.generate(something))
+_g_xml_formatter = None
def format_xml(something):
- return LLSDXMLFormatter().format(something)
+ global _g_xml_formatter
+ if _g_xml_formatter is None:
+ _g_xml_formatter = LLSDXMLFormatter()
+ return _g_xml_formatter.format(something)
class LLSDXMLPrettyFormatter(LLSDXMLFormatter):
def __init__(self, indent_atom = None):
@@ -333,13 +362,7 @@ class LLSDNotationFormatter(object):
def URI(self, v):
return 'l"%s"' % str(v).replace("\\", "\\\\").replace('"', '\\"')
def DATE(self, v):
- second_str = ""
- if v.microsecond > 0:
- seconds = v.second + float(v.microsecond) / 1000000
- second_str = "%05.2f" % seconds
- else:
- second_str = "%d" % v.second
- return 'd"%s%sZ"' % (v.strftime('%Y-%m-%dT%H:%M:'), second_str)
+ return 'd"%s"' % format_datestr(v)
def ARRAY(self, v):
return "[%s]" % ','.join([self.generate(item) for item in v])
def MAP(self, v):
@@ -548,10 +571,11 @@ class LLSDNotationParser(object):
integer: i####
real: r####
uuid: u####
- string: "g'day" | 'have a "nice" day' | s(size)"raw data"
+ string: "g\'day" | 'have a "nice" day' | s(size)"raw data"
uri: l"escaped"
date: d"YYYY-MM-DDTHH:MM:SS.FFZ"
- binary: b##"ff3120ab1" | b(size)"raw data" """
+ binary: b##"ff3120ab1" | b(size)"raw data"
+ """
def __init__(self):
pass
@@ -614,7 +638,6 @@ class LLSDNotationParser(object):
elif cc == 'b':
raise LLSDParseError("binary notation not yet supported")
else:
- print cc
raise LLSDParseError("invalid token at index %d: %d" % (
self._index - 1, ord(cc)))
@@ -695,25 +718,7 @@ class LLSDNotationParser(object):
delim = self._buffer[self._index]
self._index += 1
datestr = self._parse_string(delim)
-
- if datestr == "":
- return datetime.datetime(1970, 1, 1)
-
- match = re.match(date_regex, datestr)
- if not match:
- raise LLSDParseError("invalid date string '%s'." % datestr)
-
- year = int(match.group('year'))
- month = int(match.group('month'))
- day = int(match.group('day'))
- hour = int(match.group('hour'))
- minute = int(match.group('minute'))
- second = int(match.group('second'))
- seconds_float = match.group('second_float')
- microsecond = 0
- if seconds_float:
- microsecond = int(seconds_float[1:]) * 10000
- return datetime.datetime(year, month, day, hour, minute, second, microsecond)
+ return parse_datestr(datestr)
def _parse_real(self):
match = re.match(real_regex, self._buffer[self._index:])
@@ -738,7 +743,7 @@ class LLSDNotationParser(object):
return int( self._buffer[start:end] )
def _parse_string(self, delim):
- """ string: "g'day" | 'have a "nice" day' | s(size)"raw data" """
+ """ string: "g\'day" | 'have a "nice" day' | s(size)"raw data" """
rv = ""
if delim in ("'", '"'):
@@ -908,22 +913,17 @@ class LLSD(object):
undef = LLSD(None)
-# register converters for stacked, if stacked is available
+# register converters for llsd in mulib, if it is available
try:
- from mulib import stacked
+ from mulib import stacked, mu
stacked.NoProducer() # just to exercise stacked
+ mu.safe_load(None) # just to exercise mu
except:
- print "Couldn't import mulib.stacked, not registering LLSD converters"
+ # mulib not available, don't print an error message since this is normal
+ pass
else:
- def llsd_convert_json(llsd_stuff, request):
- callback = request.get_header('callback')
- if callback is not None:
- ## See Yahoo's ajax documentation for information about using this
- ## callback style of programming
- ## http://developer.yahoo.com/common/json.html#callbackparam
- req.write("%s(%s)" % (callback, simplejson.dumps(llsd_stuff)))
- else:
- req.write(simplejson.dumps(llsd_stuff))
+ mu.add_parser(parse, 'application/llsd+xml')
+ mu.add_parser(parse, 'application/llsd+binary')
def llsd_convert_xml(llsd_stuff, request):
request.write(format_xml(llsd_stuff))
@@ -932,8 +932,6 @@ else:
request.write(format_binary(llsd_stuff))
for typ in [LLSD, dict, list, tuple, str, int, float, bool, unicode, type(None)]:
- stacked.add_producer(typ, llsd_convert_json, 'application/json')
-
stacked.add_producer(typ, llsd_convert_xml, 'application/llsd+xml')
stacked.add_producer(typ, llsd_convert_xml, 'application/xml')
stacked.add_producer(typ, llsd_convert_xml, 'text/xml')
diff --git a/linden/indra/lib/python/indra/base/lluuid.py b/linden/indra/lib/python/indra/base/lluuid.py
index dd336f0..0756889 100644
--- a/linden/indra/lib/python/indra/base/lluuid.py
+++ b/linden/indra/lib/python/indra/base/lluuid.py
@@ -74,21 +74,29 @@ class UUID(object):
hexip = ''.join(["%04x" % long(i) for i in ip.split('.')])
lastid = ''
- def __init__(self, string_with_uuid=None):
+ def __init__(self, possible_uuid=None):
"""
- Initialize to first valid UUID in string argument,
- or to null UUID if none found or string is not supplied.
+ Initialize to first valid UUID in argument (if a string),
+ or to null UUID if none found or argument is not supplied.
+
+ If the argument is a UUID, the constructed object will be a copy of it.
"""
self._bits = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
- if string_with_uuid:
- uuid_match = UUID.uuid_regex.search(string_with_uuid)
- if uuid_match:
- uuid_string = uuid_match.group()
- s = string.replace(uuid_string, '-', '')
- self._bits = _int2binstr(string.atol(s[:8],16),4) + \
- _int2binstr(string.atol(s[8:16],16),4) + \
- _int2binstr(string.atol(s[16:24],16),4) + \
- _int2binstr(string.atol(s[24:],16),4)
+ if possible_uuid is None:
+ return
+
+ if isinstance(possible_uuid, type(self)):
+ self.set(possible_uuid)
+ return
+
+ uuid_match = UUID.uuid_regex.search(possible_uuid)
+ if uuid_match:
+ uuid_string = uuid_match.group()
+ s = string.replace(uuid_string, '-', '')
+ self._bits = _int2binstr(string.atol(s[:8],16),4) + \
+ _int2binstr(string.atol(s[8:16],16),4) + \
+ _int2binstr(string.atol(s[16:24],16),4) + \
+ _int2binstr(string.atol(s[24:],16),4)
def __len__(self):
"""
diff --git a/linden/indra/lib/python/indra/ipc/llsdhttp.py b/linden/indra/lib/python/indra/ipc/llsdhttp.py
index 24cad61..1cf1146 100644
--- a/linden/indra/lib/python/indra/ipc/llsdhttp.py
+++ b/linden/indra/lib/python/indra/ipc/llsdhttp.py
@@ -48,7 +48,11 @@ put_ = suite.put_
request = suite.request
request_ = suite.request_
-for x in (httpc.ConnectionError, httpc.NotFound, httpc.Forbidden):
+# import every httpc error exception into our namespace for convenience
+for x in httpc.status_to_error_map.itervalues():
+ globals()[x.__name__] = x
+
+for x in (httpc.ConnectionError,):
globals()[x.__name__] = x
@@ -60,21 +64,22 @@ def postFile(url, filename):
return post_(url, llsd_body)
+# deprecated in favor of get_
def getStatus(url, use_proxy=False):
status, _headers, _body = get_(url, use_proxy=use_proxy)
return status
-
+# deprecated in favor of put_
def putStatus(url, data):
status, _headers, _body = put_(url, data)
return status
-
+# deprecated in favor of delete_
def deleteStatus(url):
status, _headers, _body = delete_(url)
return status
-
+# deprecated in favor of post_
def postStatus(url, data):
status, _headers, _body = post_(url, data)
return status
diff --git a/linden/indra/lib/python/indra/ipc/mysql_pool.py b/linden/indra/lib/python/indra/ipc/mysql_pool.py
index 01e31bb..507b185 100644
--- a/linden/indra/lib/python/indra/ipc/mysql_pool.py
+++ b/linden/indra/lib/python/indra/ipc/mysql_pool.py
@@ -1,6 +1,6 @@
"""\
@file mysql_pool.py
-@brief Uses saranwrap to implement a pool of nonblocking database connections to a mysql server.
+@brief Thin wrapper around eventlet.db_pool that chooses MySQLdb and Tpool.
$LicenseInfo:firstyear=2007&license=mit$
@@ -26,44 +26,14 @@ THE SOFTWARE.
$/LicenseInfo$
"""
-import os
-
-from eventlet.pools import Pool
-from eventlet.processes import DeadProcess
-from indra.ipc import saranwrap
-
import MySQLdb
+from eventlet import db_pool
-# method 2: better -- admits the existence of the pool
-# dbp = my_db_connector.get()
-# dbh = dbp.get()
-# dbc = dbh.cursor()
-# dbc.execute(named_query)
-# dbc.close()
-# dbp.put(dbh)
-
-class DatabaseConnector(object):
- """\
-@brief This is an object which will maintain a collection of database
-connection pools keyed on host,databasename"""
+class DatabaseConnector(db_pool.DatabaseConnector):
def __init__(self, credentials, min_size = 0, max_size = 4, *args, **kwargs):
- """\
- @brief constructor
- @param min_size the minimum size of a child pool.
- @param max_size the maximum size of a child pool."""
- self._min_size = min_size
- self._max_size = max_size
- self._args = args
- self._kwargs = kwargs
- self._credentials = credentials # this is a map of hostname to username/password
- self._databases = {}
-
- def credentials_for(self, host):
- if host in self._credentials:
- return self._credentials[host]
- else:
- return self._credentials.get('default', None)
+ super(DatabaseConnector, self).__init__(MySQLdb, credentials, min_size, max_size, conn_pool=db_pool.ConnectionPool, *args, **kwargs)
+ # get is extended relative to eventlet.db_pool to accept a port argument
def get(self, host, dbname, port=3306):
key = (host, dbname, port)
if key not in self._databases:
@@ -77,28 +47,44 @@ connection pools keyed on host,databasename"""
return self._databases[key]
-
-class ConnectionPool(Pool):
+class ConnectionPool(db_pool.TpooledConnectionPool):
"""A pool which gives out saranwrapped MySQLdb connections from a pool
"""
- def __init__(self, min_size = 0, max_size = 4, *args, **kwargs):
- self._args = args
- self._kwargs = kwargs
- Pool.__init__(self, min_size, max_size)
- def create(self):
- return saranwrap.wrap(MySQLdb).connect(*self._args, **self._kwargs)
-
- def put(self, conn):
- # rollback any uncommitted changes, so that the next process
- # has a clean slate. This also pokes the process to see if
- # it's dead or None
- try:
- conn.rollback()
- except (AttributeError, DeadProcess), e:
- conn = self.create()
- # TODO figure out if we're still connected to the database
- if conn is not None:
- Pool.put(self, conn)
- else:
- self.current_size -= 1
+ def __init__(self, min_size = 0, max_size = 4, *args, **kwargs):
+ super(ConnectionPool, self).__init__(MySQLdb, min_size, max_size, *args, **kwargs)
+
+ def get(self):
+ conn = super(ConnectionPool, self).get()
+ # annotate the connection object with the details on the
+ # connection; this is used elsewhere to check that you haven't
+ # suddenly changed databases in midstream while making a
+ # series of queries on a connection.
+ arg_names = ['host','user','passwd','db','port','unix_socket','conv','connect_timeout',
+ 'compress', 'named_pipe', 'init_command', 'read_default_file', 'read_default_group',
+ 'cursorclass', 'use_unicode', 'charset', 'sql_mode', 'client_flag', 'ssl',
+ 'local_infile']
+ # you could have constructed this connectionpool with a mix of
+ # keyword and non-keyword arguments, but we want to annotate
+ # the connection object with a dict so it's easy to check
+ # against so here we are converting the list of non-keyword
+ # arguments (in self._args) into a dict of keyword arguments,
+ # and merging that with the actual keyword arguments
+ # (self._kwargs). The arg_names variable lists the
+ # constructor arguments for MySQLdb Connection objects.
+ converted_kwargs = dict([ (arg_names[i], arg) for i, arg in enumerate(self._args) ])
+ converted_kwargs.update(self._kwargs)
+ conn.connection_parameters = converted_kwargs
+ return conn
+
+ def clear(self):
+ """ Close all connections that this pool still holds a reference to, leaving it empty."""
+ for conn in self.free_items:
+ try:
+ conn.close()
+ except:
+ pass # even if stuff happens here, we still want to at least try to close all the other connections
+ self.free_items.clear()
+
+ def __del__(self):
+ self.clear()
diff --git a/linden/indra/lib/python/indra/ipc/siesta.py b/linden/indra/lib/python/indra/ipc/siesta.py
new file mode 100644
index 0000000..5fbea29
--- /dev/null
+++ b/linden/indra/lib/python/indra/ipc/siesta.py
@@ -0,0 +1,402 @@
+from indra.base import llsd
+from webob import exc
+import webob
+import re, socket
+
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from StringIO import StringIO
+
+try:
+ import cjson
+ json_decode = cjson.decode
+ json_encode = cjson.encode
+ JsonDecodeError = cjson.DecodeError
+ JsonEncodeError = cjson.EncodeError
+except ImportError:
+ import simplejson
+ json_decode = simplejson.loads
+ json_encode = simplejson.dumps
+ JsonDecodeError = ValueError
+ JsonEncodeError = TypeError
+
+
+llsd_parsers = {
+ 'application/json': json_decode,
+ 'application/llsd+binary': llsd.parse_binary,
+ 'application/llsd+notation': llsd.parse_notation,
+ 'application/llsd+xml': llsd.parse_xml,
+ 'application/xml': llsd.parse_xml,
+ }
+
+
+def mime_type(content_type):
+ '''Given a Content-Type header, return only the MIME type.'''
+
+ return content_type.split(';', 1)[0].strip().lower()
+
+class BodyLLSD(object):
+ '''Give a webob Request or Response an llsd property.
+
+ Getting the llsd property parses the body, and caches the result.
+
+ Setting the llsd property formats a payload, and the body property
+ is set.'''
+
+ def _llsd__get(self):
+ '''Get, set, or delete the LLSD value stored in this object.'''
+
+ try:
+ return self._llsd
+ except AttributeError:
+ if not self.body:
+ raise AttributeError('No llsd attribute has been set')
+ else:
+ mtype = mime_type(self.content_type)
+ try:
+ parser = llsd_parsers[mtype]
+ except KeyError:
+ raise exc.HTTPUnsupportedMediaType(
+ 'Content type %s not supported' % mtype).exception
+ try:
+ self._llsd = parser(self.body)
+ except (llsd.LLSDParseError, JsonDecodeError, TypeError), err:
+ raise exc.HTTPBadRequest(
+ 'Could not parse body: %r' % err.args).exception
+ return self._llsd
+
+ def _llsd__set(self, val):
+ req = getattr(self, 'request', None)
+ if req is not None:
+ formatter, ctype = formatter_for_request(req)
+ self.content_type = ctype
+ else:
+ formatter, ctype = formatter_for_mime_type(
+ mime_type(self.content_type))
+ self.body = formatter(val)
+
+ def _llsd__del(self):
+ if hasattr(self, '_llsd'):
+ del self._llsd
+
+ llsd = property(_llsd__get, _llsd__set, _llsd__del)
+
+
+class Response(webob.Response, BodyLLSD):
+ '''Response class with LLSD support.
+
+ A sensible default content type is used.
+
+ Setting the llsd property also sets the body. Getting the llsd
+ property parses the body if necessary.
+
+ If you set the body property directly, the llsd property will be
+ deleted.'''
+
+ default_content_type = 'application/llsd+xml'
+
+ def _body__set(self, body):
+ if hasattr(self, '_llsd'):
+ del self._llsd
+ super(Response, self)._body__set(body)
+
+ def cache_forever(self):
+ self.cache_expires(86400 * 365)
+
+ body = property(webob.Response._body__get, _body__set,
+ webob.Response._body__del,
+ webob.Response._body__get.__doc__)
+
+
+class Request(webob.Request, BodyLLSD):
+ '''Request class with LLSD support.
+
+ Sensible content type and accept headers are used by default.
+
+ Setting the llsd property also sets the body. Getting the llsd
+ property parses the body if necessary.
+
+ If you set the body property directly, the llsd property will be
+ deleted.'''
+
+ default_content_type = 'application/llsd+xml'
+ default_accept = ('application/llsd+xml; q=0.5, '
+ 'application/llsd+notation; q=0.3, '
+ 'application/llsd+binary; q=0.2, '
+ 'application/xml; q=0.1, '
+ 'application/json; q=0.0')
+
+ def __init__(self, environ=None, *args, **kwargs):
+ if environ is None:
+ environ = {}
+ else:
+ environ = environ.copy()
+ if 'CONTENT_TYPE' not in environ:
+ environ['CONTENT_TYPE'] = self.default_content_type
+ if 'HTTP_ACCEPT' not in environ:
+ environ['HTTP_ACCEPT'] = self.default_accept
+ super(Request, self).__init__(environ, *args, **kwargs)
+
+ def _body__set(self, body):
+ if hasattr(self, '_llsd'):
+ del self._llsd
+ super(Request, self)._body__set(body)
+
+ def path_urljoin(self, *parts):
+ return '/'.join([path_url.rstrip('/')] + list(parts))
+
+ body = property(webob.Request._body__get, _body__set,
+ webob.Request._body__del, webob.Request._body__get.__doc__)
+
+ def create_response(self, llsd=None, status='200 OK',
+ conditional_response=webob.NoDefault):
+ resp = self.ResponseClass(status=status, request=self,
+ conditional_response=conditional_response)
+ resp.llsd = llsd
+ return resp
+
+ def curl(self):
+ '''Create and fill out a pycurl easy object from this request.'''
+
+ import pycurl
+ c = pycurl.Curl()
+ c.setopt(pycurl.URL, self.url())
+ if self.headers:
+ c.setopt(pycurl.HTTPHEADER,
+ ['%s: %s' % (k, self.headers[k]) for k in self.headers])
+ c.setopt(pycurl.FOLLOWLOCATION, True)
+ c.setopt(pycurl.AUTOREFERER, True)
+ c.setopt(pycurl.MAXREDIRS, 16)
+ c.setopt(pycurl.NOSIGNAL, True)
+ c.setopt(pycurl.READFUNCTION, self.body_file.read)
+ c.setopt(pycurl.SSL_VERIFYHOST, 2)
+
+ if self.method == 'POST':
+ c.setopt(pycurl.POST, True)
+ post301 = getattr(pycurl, 'POST301', None)
+ if post301 is not None:
+ # Added in libcurl 7.17.1.
+ c.setopt(post301, True)
+ elif self.method == 'PUT':
+ c.setopt(pycurl.PUT, True)
+ elif self.method != 'GET':
+ c.setopt(pycurl.CUSTOMREQUEST, self.method)
+ return c
+
+Request.ResponseClass = Response
+Response.RequestClass = Request
+
+
+llsd_formatters = {
+ 'application/json': json_encode,
+ 'application/llsd+binary': llsd.format_binary,
+ 'application/llsd+notation': llsd.format_notation,
+ 'application/llsd+xml': llsd.format_xml,
+ 'application/xml': llsd.format_xml,
+ }
+
+
+def formatter_for_mime_type(mime_type):
+ '''Return a formatter that encodes to the given MIME type.
+
+ The result is a pair of function and MIME type.'''
+
+ try:
+ return llsd_formatters[mime_type], mime_type
+ except KeyError:
+ raise exc.HTTPInternalServerError(
+ 'Could not use MIME type %r to format response' %
+ mime_type).exception
+
+
+def formatter_for_request(req):
+ '''Return a formatter that encodes to the preferred type of the client.
+
+ The result is a pair of function and actual MIME type.'''
+
+ for ctype in req.accept.best_matches('application/llsd+xml'):
+ try:
+ return llsd_formatters[ctype], ctype
+ except KeyError:
+ pass
+ else:
+ raise exc.HTTPNotAcceptable().exception
+
+
+def wsgi_adapter(func, environ, start_response):
+ '''Adapt a Siesta callable to act as a WSGI application.'''
+
+ try:
+ req = Request(environ)
+ resp = func(req, **req.urlvars)
+ if not isinstance(resp, webob.Response):
+ try:
+ formatter, ctype = formatter_for_request(req)
+ resp = req.ResponseClass(formatter(resp), content_type=ctype)
+ resp._llsd = resp
+ except (JsonEncodeError, TypeError), err:
+ resp = exc.HTTPInternalServerError(
+ detail='Could not format response')
+ except exc.HTTPException, e:
+ resp = e
+ except socket.error, e:
+ resp = exc.HTTPInternalServerError(detail=e.args[1])
+ return resp(environ, start_response)
+
+
+def llsd_callable(func):
+ '''Turn a callable into a Siesta application.'''
+
+ def replacement(environ, start_response):
+ return wsgi_adapter(func, environ, start_response)
+
+ return replacement
+
+
+def llsd_method(http_method, func):
+ def replacement(environ, start_response):
+ if environ['REQUEST_METHOD'] == http_method:
+ return wsgi_adapter(func, environ, start_response)
+ return exc.HTTPMethodNotAllowed()(environ, start_response)
+
+ return replacement
+
+
+http11_methods = 'OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT'.split()
+http11_methods.sort()
+
+def llsd_class(cls):
+ '''Turn a class into a Siesta application.
+
+ A new instance is created for each request. A HTTP method FOO is
+ turned into a call to the handle_foo method of the instance.'''
+
+ def foo(req, **kwargs):
+ instance = cls()
+ method = req.method.lower()
+ try:
+ handler = getattr(instance, 'handle_' + method)
+ except AttributeError:
+ allowed = [m for m in http11_methods
+ if hasattr(instance, 'handle_' + m.lower())]
+ raise exc.HTTPMethodNotAllowed(
+ headers={'Allowed': ', '.join(allowed)}).exception
+ return handler(req, **kwargs)
+
+ def replacement(environ, start_response):
+ return wsgi_adapter(foo, environ, start_response)
+
+ return replacement
+
+
+def curl(reqs):
+ import pycurl
+
+ m = pycurl.CurlMulti()
+ curls = [r.curl() for r in reqs]
+ io = {}
+ for c in curls:
+ fp = StringIO()
+ hdr = StringIO()
+ c.setopt(pycurl.WRITEFUNCTION, fp.write)
+ c.setopt(pycurl.HEADERFUNCTION, hdr.write)
+ io[id(c)] = fp, hdr
+ m.handles = curls
+ try:
+ while True:
+ ret, num_handles = m.perform()
+ if ret != pycurl.E_CALL_MULTI_PERFORM:
+ break
+ finally:
+ m.close()
+
+ for req, c in zip(reqs, curls):
+ fp, hdr = io[id(c)]
+ hdr.seek(0)
+ status = hdr.readline().rstrip()
+ headers = []
+ name, values = None, None
+
+ # XXX We don't currently handle bogus header data.
+
+ for line in hdr.readlines():
+ if not line[0].isspace():
+ if name:
+ headers.append((name, ' '.join(values)))
+ name, value = line.strip().split(':', 1)
+ value = [value]
+ else:
+ values.append(line.strip())
+ if name:
+ headers.append((name, ' '.join(values)))
+
+ resp = c.ResponseClass(fp.getvalue(), status, headers, request=req)
+
+
+route_re = re.compile(r'''
+ \{ # exact character "{"
+ (\w+) # variable name (restricted to a-z, 0-9, _)
+ (?:([:~])([^}]+))? # optional :type or ~regex part
+ \} # exact character "}"
+ ''', re.VERBOSE)
+
+predefined_regexps = {
+ 'uuid': r'[a-f0-9][a-f0-9-]{31,35}',
+ 'int': r'\d+',
+ }
+
+def compile_route(route):
+ fp = StringIO()
+ last_pos = 0
+ for match in route_re.finditer(route):
+ fp.write(re.escape(route[last_pos:match.start()]))
+ var_name = match.group(1)
+ sep = match.group(2)
+ expr = match.group(3)
+ if expr:
+ if sep == ':':
+ expr = predefined_regexps[expr]
+ # otherwise, treat what follows '~' as a regexp
+ else:
+ expr = '[^/]+'
+ expr = '(?P<%s>%s)' % (var_name, expr)
+ fp.write(expr)
+ last_pos = match.end()
+ fp.write(re.escape(route[last_pos:]))
+ return '^%s$' % fp.getvalue()
+
+class Router(object):
+ '''WSGI routing class. Parses a URL and hands off a request to
+ some other WSGI application. If no suitable application is found,
+ responds with a 404.'''
+
+ def __init__(self):
+ self.routes = []
+ self.paths = []
+
+ def add(self, route, app, methods=None):
+ self.paths.append(route)
+ self.routes.append((re.compile(compile_route(route)), app,
+ methods and dict.fromkeys(methods)))
+
+ def __call__(self, environ, start_response):
+ path_info = environ['PATH_INFO']
+ request_method = environ['REQUEST_METHOD']
+ allowed = []
+ for regex, app, methods in self.routes:
+ m = regex.match(path_info)
+ if m:
+ if not methods or request_method in methods:
+ environ['paste.urlvars'] = m.groupdict()
+ return app(environ, start_response)
+ else:
+ allowed += methods
+ if allowed:
+ allowed = dict.fromkeys(allows).keys()
+ allowed.sort()
+ resp = exc.HTTPMethodNotAllowed(
+ headers={'Allowed': ', '.join(allowed)})
+ else:
+ resp = exc.HTTPNotFound()
+ return resp(environ, start_response)
diff --git a/linden/indra/lib/python/indra/ipc/siesta_test.py b/linden/indra/lib/python/indra/ipc/siesta_test.py
new file mode 100644
index 0000000..177ea71
--- /dev/null
+++ b/linden/indra/lib/python/indra/ipc/siesta_test.py
@@ -0,0 +1,214 @@
+from indra.base import llsd, lluuid
+from indra.ipc import siesta
+import datetime, math, unittest
+from webob import exc
+
+
+class ClassApp(object):
+ def handle_get(self, req):
+ pass
+
+ def handle_post(self, req):
+ return req.llsd
+
+
+def callable_app(req):
+ if req.method == 'UNDERPANTS':
+ raise exc.HTTPMethodNotAllowed()
+ elif req.method == 'GET':
+ return None
+ return req.llsd
+
+
+class TestBase:
+ def test_basic_get(self):
+ req = siesta.Request.blank('/')
+ self.assertEquals(req.get_response(self.server).body,
+ llsd.format_xml(None))
+
+ def test_bad_method(self):
+ req = siesta.Request.blank('/')
+ req.environ['REQUEST_METHOD'] = 'UNDERPANTS'
+ self.assertEquals(req.get_response(self.server).status_int,
+ exc.HTTPMethodNotAllowed.code)
+
+ json_safe = {
+ 'none': None,
+ 'bool_true': True,
+ 'bool_false': False,
+ 'int_zero': 0,
+ 'int_max': 2147483647,
+ 'int_min': -2147483648,
+ 'long_zero': 0,
+ 'long_max': 2147483647L,
+ 'long_min': -2147483648L,
+ 'float_zero': 0,
+ 'float': math.pi,
+ 'float_huge': 3.14159265358979323846e299,
+ 'str_empty': '',
+ 'str': 'foo',
+ u'unic\u1e51de_empty': u'',
+ u'unic\u1e51de': u'\u1e4exx\u10480',
+ }
+ json_safe['array'] = json_safe.values()
+ json_safe['tuple'] = tuple(json_safe.values())
+ json_safe['dict'] = json_safe.copy()
+
+ json_unsafe = {
+ 'uuid_empty': lluuid.UUID(),
+ 'uuid_full': lluuid.UUID('dc61ab0530200d7554d23510559102c1a98aab1b'),
+ 'binary_empty': llsd.binary(),
+ 'binary': llsd.binary('f\0\xff'),
+ 'uri_empty': llsd.uri(),
+ 'uri': llsd.uri('http://www.secondlife.com/'),
+ 'datetime_empty': datetime.datetime(1970,1,1),
+ 'datetime': datetime.datetime(1999,9,9,9,9,9),
+ }
+ json_unsafe.update(json_safe)
+ json_unsafe['array'] = json_unsafe.values()
+ json_unsafe['tuple'] = tuple(json_unsafe.values())
+ json_unsafe['dict'] = json_unsafe.copy()
+ json_unsafe['iter'] = iter(json_unsafe.values())
+
+ def _test_client_content_type_good(self, content_type, ll):
+ def run(ll):
+ req = siesta.Request.blank('/')
+ req.environ['REQUEST_METHOD'] = 'POST'
+ req.content_type = content_type
+ req.llsd = ll
+ req.accept = content_type
+ resp = req.get_response(self.server)
+ self.assertEquals(resp.status_int, 200)
+ return req, resp
+
+ if False and isinstance(ll, dict):
+ def fixup(v):
+ if isinstance(v, float):
+ return '%.5f' % v
+ if isinstance(v, long):
+ return int(v)
+ if isinstance(v, (llsd.binary, llsd.uri)):
+ return v
+ if isinstance(v, (tuple, list)):
+ return [fixup(i) for i in v]
+ if isinstance(v, dict):
+ return dict([(k, fixup(i)) for k, i in v.iteritems()])
+ return v
+ for k, v in ll.iteritems():
+ l = [k, v]
+ req, resp = run(l)
+ self.assertEquals(fixup(resp.llsd), fixup(l))
+
+ run(ll)
+
+ def test_client_content_type_json_good(self):
+ self._test_client_content_type_good('application/json', self.json_safe)
+
+ def test_client_content_type_llsd_xml_good(self):
+ self._test_client_content_type_good('application/llsd+xml',
+ self.json_unsafe)
+
+ def test_client_content_type_llsd_notation_good(self):
+ self._test_client_content_type_good('application/llsd+notation',
+ self.json_unsafe)
+
+ def test_client_content_type_llsd_binary_good(self):
+ self._test_client_content_type_good('application/llsd+binary',
+ self.json_unsafe)
+
+ def test_client_content_type_xml_good(self):
+ self._test_client_content_type_good('application/xml',
+ self.json_unsafe)
+
+ def _test_client_content_type_bad(self, content_type):
+ req = siesta.Request.blank('/')
+ req.environ['REQUEST_METHOD'] = 'POST'
+ req.body = '\0invalid nonsense under all encodings'
+ req.content_type = content_type
+ self.assertEquals(req.get_response(self.server).status_int,
+ exc.HTTPBadRequest.code)
+
+ def test_client_content_type_json_bad(self):
+ self._test_client_content_type_bad('application/json')
+
+ def test_client_content_type_llsd_xml_bad(self):
+ self._test_client_content_type_bad('application/llsd+xml')
+
+ def test_client_content_type_llsd_notation_bad(self):
+ self._test_client_content_type_bad('application/llsd+notation')
+
+ def test_client_content_type_llsd_binary_bad(self):
+ self._test_client_content_type_bad('application/llsd+binary')
+
+ def test_client_content_type_xml_bad(self):
+ self._test_client_content_type_bad('application/xml')
+
+ def test_client_content_type_bad(self):
+ req = siesta.Request.blank('/')
+ req.environ['REQUEST_METHOD'] = 'POST'
+ req.body = 'XXX'
+ req.content_type = 'application/nonsense'
+ self.assertEquals(req.get_response(self.server).status_int,
+ exc.HTTPUnsupportedMediaType.code)
+
+ def test_request_default_content_type(self):
+ req = siesta.Request.blank('/')
+ self.assertEquals(req.content_type, req.default_content_type)
+
+ def test_request_default_accept(self):
+ req = siesta.Request.blank('/')
+ from webob import acceptparse
+ self.assertEquals(str(req.accept).replace(' ', ''),
+ req.default_accept.replace(' ', ''))
+
+ def test_request_llsd_auto_body(self):
+ req = siesta.Request.blank('/')
+ req.llsd = {'a': 2}
+ self.assertEquals(req.body, '')
+
+ def test_request_llsd_mod_body_changes_llsd(self):
+ req = siesta.Request.blank('/')
+ req.llsd = {'a': 2}
+ req.body = '1337'
+ self.assertEquals(req.llsd, 1337)
+
+ def test_request_bad_llsd_fails(self):
+ def crashme(ctype):
+ def boom():
+ class foo(object): pass
+ req = siesta.Request.blank('/')
+ req.content_type = ctype
+ req.llsd = foo()
+ for mime_type in siesta.llsd_parsers:
+ self.assertRaises(TypeError, crashme(mime_type))
+
+
+class ClassServer(TestBase, unittest.TestCase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ self.server = siesta.llsd_class(ClassApp)
+
+
+class CallableServer(TestBase, unittest.TestCase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ self.server = siesta.llsd_callable(callable_app)
+
+
+class RouterServer(unittest.TestCase):
+ def test_router(self):
+ def foo(req, quux):
+ print quux
+
+ r = siesta.Router()
+ r.add('/foo/{quux:int}', siesta.llsd_callable(foo), methods=['GET'])
+ req = siesta.Request.blank('/foo/33')
+ req.get_response(r)
+
+ req = siesta.Request.blank('/foo/bar')
+ self.assertEquals(req.get_response(r).status_int,
+ exc.HTTPNotFound.code)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/linden/indra/lib/python/indra/util/fastest_elementtree.py b/linden/indra/lib/python/indra/util/fastest_elementtree.py
new file mode 100644
index 0000000..64aed09
--- /dev/null
+++ b/linden/indra/lib/python/indra/util/fastest_elementtree.py
@@ -0,0 +1,52 @@
+"""\
+@file fastest_elementtree.py
+@brief Concealing some gnarly import logic in here. This should export the interface of elementtree.
+
+$LicenseInfo:firstyear=2006&license=mit$
+
+Copyright (c) 2006-2008, Linden Research, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+$/LicenseInfo$
+"""
+
+# Using celementree might cause some unforeseen problems so here's a
+# convenient off switch.
+
+# *NOTE: turned off cause of problems. :-( *TODO: debug
+use_celementree = False
+
+try:
+ if not use_celementree:
+ raise ImportError()
+ from cElementTree import * ## This does not work under Windows
+except ImportError:
+ try:
+ if not use_celementree:
+ raise ImportError()
+ ## This is the name of cElementTree under python 2.5
+ from xml.etree.cElementTree import *
+ except ImportError:
+ try:
+ ## This is the old name of elementtree, for use with 2.3
+ from elementtree.ElementTree import *
+ except ImportError:
+ ## This is the name of elementtree under python 2.5
+ from xml.etree.ElementTree import *
+
diff --git a/linden/indra/lib/python/indra/util/llmanifest.py b/linden/indra/lib/python/indra/util/llmanifest.py
index 89c14e8..4675177 100644
--- a/linden/indra/lib/python/indra/util/llmanifest.py
+++ b/linden/indra/lib/python/indra/util/llmanifest.py
@@ -34,7 +34,6 @@ import fnmatch
import getopt
import glob
import os
-import os.path
import re
import shutil
import sys
@@ -42,10 +41,10 @@ import tarfile
import errno
def path_ancestors(path):
- path = os.path.normpath(path)
+ drive, path = os.path.splitdrive(os.path.normpath(path))
result = []
- while len(path) > 0:
- result.append(path)
+ while len(path) > 0 and path != os.path.sep:
+ result.append(drive+path)
path, sub = os.path.split(path)
return result
@@ -57,13 +56,13 @@ def proper_windows_path(path, current_platform = sys.platform):
drive_letter = None
rel = None
match = re.match("/cygdrive/([a-z])/(.*)", path)
- if(not match):
+ if not match:
match = re.match('([a-zA-Z]):\\\(.*)', path)
- if(not match):
+ if not match:
return None # not an absolute path
drive_letter = match.group(1)
rel = match.group(2)
- if(current_platform == "cygwin"):
+ if current_platform == "cygwin":
return "/cygdrive/" + drive_letter.lower() + '/' + rel.replace('\\', '/')
else:
return drive_letter.upper() + ':\\' + rel.replace('/', '\\')
@@ -98,6 +97,7 @@ def get_channel(srctree):
return channel
+DEFAULT_SRCTREE = os.path.dirname(sys.argv[0])
DEFAULT_CHANNEL = 'Second Life Release'
ARGUMENTS=[
@@ -118,10 +118,12 @@ ARGUMENTS=[
Example use: %(name)s --arch=i686
On Linux this would try to use Linux_i686Manifest.""",
default=""),
+ dict(name='build', description='Build directory.', default=DEFAULT_SRCTREE),
dict(name='configuration',
description="""The build configuration used. Only used on OS X for
now, but it could be used for other platforms as well.""",
default="Universal"),
+ dict(name='dest', description='Destination directory.', default=DEFAULT_SRCTREE),
dict(name='grid',
description="""Which grid the client will try to connect to. Even
though it's not strictly a grid, 'firstlook' is also an acceptable
@@ -144,6 +146,15 @@ ARGUMENTS=[
description="""The current platform, to be used for looking up which
manifest class to run.""",
default=get_default_platform),
+ dict(name='source',
+ description='Source directory.',
+ default=DEFAULT_SRCTREE),
+ dict(name='artwork', description='Artwork directory.', default=DEFAULT_SRCTREE),
+ dict(name='touch',
+ description="""File to touch when action is finished. Touch file will
+ contain the name of the final package in a form suitable
+ for use by a .bat file.""",
+ default=None),
dict(name='version',
description="""This specifies the version of Second Life that is
being packaged up.""",
@@ -167,63 +178,75 @@ def usage(srctree=""):
default,
arg['description'] % nd)
-def main(argv=None, srctree='.', dsttree='./dst'):
- if(argv == None):
- argv = sys.argv
-
+def main():
option_names = [arg['name'] + '=' for arg in ARGUMENTS]
option_names.append('help')
- options, remainder = getopt.getopt(argv[1:], "", option_names)
- if len(remainder) >= 1:
- dsttree = remainder[0]
-
- print "Source tree:", srctree
- print "Destination tree:", dsttree
+ options, remainder = getopt.getopt(sys.argv[1:], "", option_names)
# convert options to a hash
- args = {}
+ args = {'source': DEFAULT_SRCTREE,
+ 'artwork': DEFAULT_SRCTREE,
+ 'build': DEFAULT_SRCTREE,
+ 'dest': DEFAULT_SRCTREE }
for opt in options:
args[opt[0].replace("--", "")] = opt[1]
+ for k in 'artwork build dest source'.split():
+ args[k] = os.path.normpath(args[k])
+
+ print "Source tree:", args['source']
+ print "Artwork tree:", args['artwork']
+ print "Build tree:", args['build']
+ print "Destination tree:", args['dest']
+
# early out for help
- if args.has_key('help'):
+ if 'help' in args:
# *TODO: it is a huge hack to pass around the srctree like this
- usage(srctree)
+ usage(args['source'])
return
# defaults
for arg in ARGUMENTS:
- if not args.has_key(arg['name']):
+ if arg['name'] not in args:
default = arg['default']
if hasattr(default, '__call__'):
- default = default(srctree)
+ default = default(args['source'])
if default is not None:
args[arg['name']] = default
# fix up version
- if args.has_key('version') and type(args['version']) == str:
+ if isinstance(args.get('version'), str):
args['version'] = args['version'].split('.')
# default and agni are default
if args['grid'] in ['default', 'agni']:
args['grid'] = ''
- if args.has_key('actions'):
+ if 'actions' in args:
args['actions'] = args['actions'].split()
# debugging
for opt in args:
print "Option:", opt, "=", args[opt]
- wm = LLManifest.for_platform(args['platform'], args.get('arch'))(srctree, dsttree, args)
+ wm = LLManifest.for_platform(args['platform'], args.get('arch'))(args)
wm.do(*args['actions'])
+
+ # Write out the package file in this format, so that it can easily be called
+ # and used in a .bat file - yeah, it sucks, but this is the simplest...
+ touch = args.get('touch')
+ if touch:
+ fp = open(touch, 'w')
+ fp.write('set package_file=%s\n' % wm.package_file)
+ fp.close()
+ print 'touched', touch
return 0
class LLManifestRegistry(type):
def __init__(cls, name, bases, dct):
super(LLManifestRegistry, cls).__init__(name, bases, dct)
match = re.match("(\w+)Manifest", name)
- if(match):
+ if match:
cls.manifests[match.group(1).lower()] = cls
class LLManifest(object):
@@ -235,15 +258,18 @@ class LLManifest(object):
return self.manifests[platform.lower()]
for_platform = classmethod(for_platform)
- def __init__(self, srctree, dsttree, args):
+ def __init__(self, args):
super(LLManifest, self).__init__()
self.args = args
self.file_list = []
self.excludes = []
self.actions = []
- self.src_prefix = [srctree]
- self.dst_prefix = [dsttree]
+ self.src_prefix = [args['source']]
+ self.artwork_prefix = [args['artwork']]
+ self.build_prefix = [args['build']]
+ self.dst_prefix = [args['dest']]
self.created_paths = []
+ self.package_name = "Unknown"
def default_grid(self):
return self.args.get('grid', None) == ''
@@ -260,16 +286,20 @@ class LLManifest(object):
in the file list by path()."""
self.excludes.append(glob)
- def prefix(self, src='', dst=None):
+ def prefix(self, src='', build=None, dst=None):
""" Pushes a prefix onto the stack. Until end_prefix is
called, all relevant method calls (esp. to path()) will prefix
paths with the entire prefix stack. Source and destination
prefixes can be different, though if only one is provided they
are both equal. To specify a no-op, use an empty string, not
None."""
- if(dst == None):
+ if dst is None:
dst = src
+ if build is None:
+ build = src
self.src_prefix.append(src)
+ self.artwork_prefix.append(src)
+ self.build_prefix.append(build)
self.dst_prefix.append(dst)
return True # so that you can wrap it in an if to get indentation
@@ -281,14 +311,24 @@ class LLManifest(object):
exception is raised."""
# as an error-prevention mechanism, check the prefix and see if it matches the source or destination prefix. If not, improper nesting may have occurred.
src = self.src_prefix.pop()
+ artwork = self.artwork_prefix.pop()
+ build = self.build_prefix.pop()
dst = self.dst_prefix.pop()
- if descr and not(src == descr or dst == descr):
+ if descr and not(src == descr or build == descr or dst == descr):
raise ValueError, "End prefix '" + descr + "' didn't match '" +src+ "' or '" +dst + "'"
def get_src_prefix(self):
""" Returns the current source prefix."""
return os.path.join(*self.src_prefix)
+ def get_artwork_prefix(self):
+ """ Returns the current artwork prefix."""
+ return os.path.join(*self.artwork_prefix)
+
+ def get_build_prefix(self):
+ """ Returns the current build prefix."""
+ return os.path.join(*self.build_prefix)
+
def get_dst_prefix(self):
""" Returns the current destination prefix."""
return os.path.join(*self.dst_prefix)
@@ -298,6 +338,11 @@ class LLManifest(object):
relative to the source directory."""
return os.path.join(self.get_src_prefix(), relpath)
+ def build_path_of(self, relpath):
+ """Returns the full path to a file or directory specified
+ relative to the build directory."""
+ return os.path.join(self.get_build_prefix(), relpath)
+
def dst_path_of(self, relpath):
"""Returns the full path to a file or directory specified
relative to the destination directory."""
@@ -329,13 +374,13 @@ class LLManifest(object):
lines = []
while True:
lines.append(fd.readline())
- if(lines[-1] == ''):
+ if lines[-1] == '':
break
else:
print lines[-1],
output = ''.join(lines)
status = fd.close()
- if(status):
+ if status:
raise RuntimeError(
"Command %s returned non-zero status (%s) \noutput:\n%s"
% (command, status, output) )
@@ -356,7 +401,7 @@ class LLManifest(object):
f.close()
def replace_in(self, src, dst=None, searchdict={}):
- if(dst == None):
+ if dst == None:
dst = src
# read src
f = open(self.src_path_of(src), "rbU")
@@ -369,11 +414,11 @@ class LLManifest(object):
self.created_paths.append(dst)
def copy_action(self, src, dst):
- if(src and (os.path.exists(src) or os.path.islink(src))):
+ if src and (os.path.exists(src) or os.path.islink(src)):
# ensure that destination path exists
self.cmakedirs(os.path.dirname(dst))
self.created_paths.append(dst)
- if(not os.path.isdir(src)):
+ if not os.path.isdir(src):
self.ccopy(src,dst)
else:
# src is a dir
@@ -408,7 +453,7 @@ class LLManifest(object):
print "Cleaning up " + c
def process_file(self, src, dst):
- if(self.includes(src, dst)):
+ if self.includes(src, dst):
# print src, "=>", dst
for action in self.actions:
methodname = action + "_action"
@@ -416,26 +461,29 @@ class LLManifest(object):
if method is not None:
method(src, dst)
self.file_list.append([src, dst])
+ return 1
else:
- print "Excluding: ", src, dst
-
+ sys.stdout.write(" (excluding %r, %r)" % (src, dst))
+ sys.stdout.flush()
+ return 0
def process_directory(self, src, dst):
- if(not self.includes(src, dst)):
- print "Excluding: ", src, dst
- return
+ if not self.includes(src, dst):
+ sys.stdout.write(" (excluding %r, %r)" % (src, dst))
+ sys.stdout.flush()
+ return 0
names = os.listdir(src)
self.cmakedirs(dst)
errors = []
+ count = 0
for name in names:
srcname = os.path.join(src, name)
dstname = os.path.join(dst, name)
if os.path.isdir(srcname):
- self.process_directory(srcname, dstname)
+ count += self.process_directory(srcname, dstname)
else:
- self.process_file(srcname, dstname)
-
-
+ count += self.process_file(srcname, dstname)
+ return count
def includes(self, src, dst):
if src:
@@ -446,9 +494,9 @@ class LLManifest(object):
def remove(self, *paths):
for path in paths:
- if(os.path.exists(path)):
+ if os.path.exists(path):
print "Removing path", path
- if(os.path.isdir(path)):
+ if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
@@ -457,17 +505,17 @@ class LLManifest(object):
""" Copy a single file or symlink. Uses filecmp to skip copying for existing files."""
if os.path.islink(src):
linkto = os.readlink(src)
- if(os.path.islink(dst) or os.path.exists(dst)):
+ if os.path.islink(dst) or os.path.exists(dst):
os.remove(dst) # because symlinking over an existing link fails
os.symlink(linkto, dst)
else:
# Don't recopy file if it's up-to-date.
# If we seem to be not not overwriting files that have been
# updated, set the last arg to False, but it will take longer.
- if(os.path.exists(dst) and filecmp.cmp(src, dst, True)):
+ if os.path.exists(dst) and filecmp.cmp(src, dst, True):
return
# only copy if it's not excluded
- if(self.includes(src, dst)):
+ if self.includes(src, dst):
try:
os.unlink(dst)
except OSError, err:
@@ -481,7 +529,7 @@ class LLManifest(object):
feature that the destination directory can exist. It
is so dumb that Python doesn't come with this. Also it
implements the excludes functionality."""
- if(not self.includes(src, dst)):
+ if not self.includes(src, dst):
return
names = os.listdir(src)
self.cmakedirs(dst)
@@ -512,7 +560,7 @@ class LLManifest(object):
def find_existing_file(self, *list):
for f in list:
- if(os.path.exists(f)):
+ if os.path.exists(f):
return f
# didn't find it, return last item in list
if len(list) > 0:
@@ -535,62 +583,63 @@ class LLManifest(object):
def wildcard_regex(self, src_glob, dst_glob):
- # print "regex_pair:", src_glob, dst_glob
src_re = re.escape(src_glob)
src_re = src_re.replace('\*', '([-a-zA-Z0-9._ ]+)')
dst_temp = dst_glob
i = 1
- while(dst_temp.count("*") > 0):
+ while dst_temp.count("*") > 0:
dst_temp = dst_temp.replace('*', '\g<' + str(i) + '>', 1)
i = i+1
- # print "regex_result:", src_re, dst_temp
return re.compile(src_re), dst_temp
def check_file_exists(self, path):
- if(not os.path.exists(path) and not os.path.islink(path)):
+ if not os.path.exists(path) and not os.path.islink(path):
raise RuntimeError("Path %s doesn't exist" % (
os.path.normpath(os.path.join(os.getcwd(), path)),))
wildcard_pattern = re.compile('\*')
def expand_globs(self, src, dst):
- def fw_slash(str):
- return str.replace('\\', '/')
- def os_slash(str):
- return str.replace('/', os.path.sep)
- dst = fw_slash(dst)
- src = fw_slash(src)
src_list = glob.glob(src)
- src_re, d_template = self.wildcard_regex(src, dst)
+ src_re, d_template = self.wildcard_regex(src.replace('\\', '/'),
+ dst.replace('\\', '/'))
for s in src_list:
- s = fw_slash(s)
- d = src_re.sub(d_template, s)
- #print "s:",s, "d_t", d_template, "dst", dst, "d", d
- yield os_slash(s), os_slash(d)
+ d = src_re.sub(d_template, s.replace('\\', '/'))
+ yield os.path.normpath(s), os.path.normpath(d)
def path(self, src, dst=None):
- print "Processing", src, "=>", dst
+ sys.stdout.write("Processing %s => %s ... " % (src, dst))
+ sys.stdout.flush()
if src == None:
raise RuntimeError("No source file, dst is " + dst)
if dst == None:
dst = src
dst = os.path.join(self.get_dst_prefix(), dst)
- src = os.path.join(self.get_src_prefix(), src)
- # expand globs
- if(self.wildcard_pattern.search(src)):
- for s,d in self.expand_globs(src, dst):
- self.process_file(s, d)
- else:
- # if we're specifying a single path (not a glob),
- # we should error out if it doesn't exist
- self.check_file_exists(src)
- # if it's a directory, recurse through it
- if(os.path.isdir(src)):
- self.process_directory(src, dst)
+ def try_path(src):
+ # expand globs
+ count = 0
+ if self.wildcard_pattern.search(src):
+ for s,d in self.expand_globs(src, dst):
+ count += self.process_file(s, d)
else:
- self.process_file(src, dst)
-
+ # if we're specifying a single path (not a glob),
+ # we should error out if it doesn't exist
+ self.check_file_exists(src)
+ # if it's a directory, recurse through it
+ if os.path.isdir(src):
+ count += self.process_directory(src, dst)
+ else:
+ count += self.process_file(src, dst)
+ return count
+ try:
+ count = try_path(os.path.join(self.get_src_prefix(), src))
+ except RuntimeError:
+ try:
+ count = try_path(os.path.join(self.get_artwork_prefix(), src))
+ except RuntimeError:
+ count = try_path(os.path.join(self.get_build_prefix(), src))
+ print "%d files" % count
def do(self, *actions):
self.actions = actions
diff --git a/linden/indra/lib/python/indra/util/named_query.py b/linden/indra/lib/python/indra/util/named_query.py
index 680d1f9..20f2ec7 100644
--- a/linden/indra/lib/python/indra/util/named_query.py
+++ b/linden/indra/lib/python/indra/util/named_query.py
@@ -47,7 +47,7 @@ except NameError:
from indra.base import llsd
from indra.base import config
-NQ_FILE_SUFFIX = config.get('named-query-file-suffix', '')
+NQ_FILE_SUFFIX = config.get('named-query-file-suffix', '.nq')
NQ_FILE_SUFFIX_LEN = len(NQ_FILE_SUFFIX)
_g_named_manager = None
@@ -60,6 +60,11 @@ def _init_g_named_manager(sql_dir = None):
because it's tricky to control the config from inside a test."""
if sql_dir is None:
sql_dir = config.get('named-query-base-dir')
+
+ # extra fallback directory in case config doesn't return what we want
+ if sql_dir is None:
+ sql_dir = os.path.dirname(__file__) + "../../../../web/dataservice/sql"
+
global _g_named_manager
_g_named_manager = NamedQueryManager(
os.path.abspath(os.path.realpath(sql_dir)))
diff --git a/linden/indra/lib/python/indra/util/term.py b/linden/indra/lib/python/indra/util/term.py
new file mode 100644
index 0000000..8238b78
--- /dev/null
+++ b/linden/indra/lib/python/indra/util/term.py
@@ -0,0 +1,222 @@
+'''
+@file term.py
+@brief a better shutil.copytree replacement
+
+$LicenseInfo:firstyear=2007&license=mit$
+
+Copyright (c) 2007-2008, Linden Research, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+$/LicenseInfo$
+'''
+
+#http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
+
+import sys, re
+
+class TerminalController:
+ """
+ A class that can be used to portably generate formatted output to
+ a terminal.
+
+ `TerminalController` defines a set of instance variables whose
+ values are initialized to the control sequence necessary to
+ perform a given action. These can be simply included in normal
+ output to the terminal:
+
+ >>> term = TerminalController()
+ >>> print 'This is '+term.GREEN+'green'+term.NORMAL
+
+ Alternatively, the `render()` method can used, which replaces
+ '${action}' with the string required to perform 'action':
+
+ >>> term = TerminalController()
+ >>> print term.render('This is ${GREEN}green${NORMAL}')
+
+ If the terminal doesn't support a given action, then the value of
+ the corresponding instance variable will be set to ''. As a
+ result, the above code will still work on terminals that do not
+ support color, except that their output will not be colored.
+ Also, this means that you can test whether the terminal supports a
+ given action by simply testing the truth value of the
+ corresponding instance variable:
+
+ >>> term = TerminalController()
+ >>> if term.CLEAR_SCREEN:
+ ... print 'This terminal supports clearning the screen.'
+
+ Finally, if the width and height of the terminal are known, then
+ they will be stored in the `COLS` and `LINES` attributes.
+ """
+ # Cursor movement:
+ BOL = '' #: Move the cursor to the beginning of the line
+ UP = '' #: Move the cursor up one line
+ DOWN = '' #: Move the cursor down one line
+ LEFT = '' #: Move the cursor left one char
+ RIGHT = '' #: Move the cursor right one char
+
+ # Deletion:
+ CLEAR_SCREEN = '' #: Clear the screen and move to home position
+ CLEAR_EOL = '' #: Clear to the end of the line.
+ CLEAR_BOL = '' #: Clear to the beginning of the line.
+ CLEAR_EOS = '' #: Clear to the end of the screen
+
+ # Output modes:
+ BOLD = '' #: Turn on bold mode
+ BLINK = '' #: Turn on blink mode
+ DIM = '' #: Turn on half-bright mode
+ REVERSE = '' #: Turn on reverse-video mode
+ NORMAL = '' #: Turn off all modes
+
+ # Cursor display:
+ HIDE_CURSOR = '' #: Make the cursor invisible
+ SHOW_CURSOR = '' #: Make the cursor visible
+
+ # Terminal size:
+ COLS = None #: Width of the terminal (None for unknown)
+ LINES = None #: Height of the terminal (None for unknown)
+
+ # Foreground colors:
+ BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
+
+ # Background colors:
+ BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
+ BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
+
+ _STRING_CAPABILITIES = """
+ BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
+ CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
+ BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
+ HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
+ _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
+ _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
+
+ def __init__(self, term_stream=sys.stdout):
+ """
+ Create a `TerminalController` and initialize its attributes
+ with appropriate values for the current terminal.
+ `term_stream` is the stream that will be used for terminal
+ output; if this stream is not a tty, then the terminal is
+ assumed to be a dumb terminal (i.e., have no capabilities).
+ """
+ # Curses isn't available on all platforms
+ try: import curses
+ except: return
+
+ # If the stream isn't a tty, then assume it has no capabilities.
+ if not term_stream.isatty(): return
+
+ # Check the terminal type. If we fail, then assume that the
+ # terminal has no capabilities.
+ try: curses.setupterm()
+ except: return
+
+ # Look up numeric capabilities.
+ self.COLS = curses.tigetnum('cols')
+ self.LINES = curses.tigetnum('lines')
+
+ # Look up string capabilities.
+ for capability in self._STRING_CAPABILITIES:
+ (attrib, cap_name) = capability.split('=')
+ setattr(self, attrib, self._tigetstr(cap_name) or '')
+
+ # Colors
+ set_fg = self._tigetstr('setf')
+ if set_fg:
+ for i,color in zip(range(len(self._COLORS)), self._COLORS):
+ setattr(self, color, curses.tparm(set_fg, i) or '')
+ set_fg_ansi = self._tigetstr('setaf')
+ if set_fg_ansi:
+ for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
+ setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
+ set_bg = self._tigetstr('setb')
+ if set_bg:
+ for i,color in zip(range(len(self._COLORS)), self._COLORS):
+ setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
+ set_bg_ansi = self._tigetstr('setab')
+ if set_bg_ansi:
+ for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
+ setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
+
+ def _tigetstr(self, cap_name):
+ # String capabilities can include "delays" of the form "$<2>".
+ # For any modern terminal, we should be able to just ignore
+ # these, so strip them out.
+ import curses
+ cap = curses.tigetstr(cap_name) or ''
+ return re.sub(r'\$<\d+>[/*]?', '', cap)
+
+ def render(self, template):
+ """
+ Replace each $-substitutions in the given template string with
+ the corresponding terminal control string (if it's defined) or
+ '' (if it's not).
+ """
+ return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
+
+ def _render_sub(self, match):
+ s = match.group()
+ if s == '$$': return s
+ else: return getattr(self, s[2:-1])
+
+#######################################################################
+# Example use case: progress bar
+#######################################################################
+
+class ProgressBar:
+ """
+ A 3-line progress bar, which looks like::
+
+ Header
+ 20% [===========----------------------------------]
+ progress message
+
+ The progress bar is colored, if the terminal supports color
+ output; and adjusts to the width of the terminal.
+ """
+ BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
+ HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
+
+ def __init__(self, term, header):
+ self.term = term
+ if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
+ raise ValueError("Terminal isn't capable enough -- you "
+ "should use a simpler progress dispaly.")
+ self.width = self.term.COLS or 75
+ self.bar = term.render(self.BAR)
+ self.header = self.term.render(self.HEADER % header.center(self.width))
+ self.cleared = 1 #: true if we haven't drawn the bar yet.
+ self.update(0, '')
+
+ def update(self, percent, message):
+ if self.cleared:
+ sys.stdout.write(self.header)
+ self.cleared = 0
+ n = int((self.width-10)*percent)
+ sys.stdout.write(
+ self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
+ (self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
+ self.term.CLEAR_EOL + message.center(self.width))
+
+ def clear(self):
+ if not self.cleared:
+ sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
+ self.term.UP + self.term.CLEAR_EOL +
+ self.term.UP + self.term.CLEAR_EOL)
+ self.cleared = 1
--
cgit v1.1