Initial commit

This commit is contained in:
Correl Roush 2010-05-27 22:38:06 -04:00
commit f797615413
80 changed files with 13529 additions and 0 deletions

27
default.py Normal file
View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
import os
import xbmc
import xbmcgui
__scriptname__ = "Transmission"
__author__ = "Correl"
__url__ = ""
__svn_url__ = ""
__credits__ = ""
__version__ = "0.5a"
__XBMC_Revision__ = "22240"
BASE_RESOURCE_PATH = xbmc.translatePath( os.path.join( os.getcwd(), 'resources', 'lib' ) )
sys.path.append (BASE_RESOURCE_PATH)
__language__ = xbmc.Language(os.getcwd()).getLocalizedString
KEY_BUTTON_BACK = 275
KEY_KEYBOARD_ESC = 61467
if __name__ == '__main__':
from gui import TransmissionGUI
w = TransmissionGUI("script-Transmission-main.xml",os.getcwd() ,"default")
w.doModal()
del w

BIN
default.tbn Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<strings>
<string id="0">Transmission</string>
<!-- Buttons -->
<string id="101">Add</string>
<string id="102">Remove</string>
<string id="103">Pause</string>
<string id="104">Start</string>
<string id="105">Pause All</string>
<string id="106">Start All</string>
<string id="107">Exit</string>
<!-- Settings -->
<string id="1000">RPC Settings</string>
<string id="1001">Host</string>
<string id="1002">Port</string>
<string id="1003">User</string>
<string id="1004">Password</string>
</strings>

View file

@ -0,0 +1,8 @@
"""Common data-modeling Python types
The idea of the basictypes package is to provide
types which provide enough metadata to allow an
application to use introspection to perform much
of the housekeeping required to create business
applications.
"""

View file

@ -0,0 +1,274 @@
"""Stand-alone type-definition objects for basic data types
Rather than add class-methods to the built-in data types
module designs stand in datatypes which can be used to
define basicproperty property classes.
"""
from basictypes import latebind, datatypedefinition, registry, booleanfix
import types, traceback
class Object_DT( datatypedefinition.BaseType_DT ):
"""Generic "object" data-type
"""
dataType = 'object'
baseType = object
def coerce(cls, value):
"""Coerce value to the appropriate data type"""
if cls.check( value):
return value
raise TypeError( """Don't know how to convert %r to a %s"""%( value, cls.dataType))
coerce = classmethod( coerce )
registry.registerDT( object, Object_DT )
class String_DT( datatypedefinition.BaseType_DT ):
"""String (Unicode) data-type specifier"""
dataType = "str"
baseType = unicode
def coerce(cls, value):
"""Coerce value to Unicode value
Accepted values:
None -- u""
str instance -- value.decode()
int,float,long,complex -- unicode(value)
"""
if cls.check( value ):
return value
if value is None:
return u""
if isinstance( value, str ):
value = value.decode()
if isinstance( value, (int, float, long, complex)):
value = unicode( value)
### XXX Should be raising an error here!
return value
coerce = classmethod( coerce )
registry.registerDT( unicode, String_DT )
class Numeric_DT( datatypedefinition.BaseType_DT ):
def coerce(cls, value):
"""Coerce value to Numeric value (using baseType)
Accepted values:
"", "0" -- 0
numeric values
ascii strings -- base type attempts to interpret
"""
if cls.check( value):
return value
try:
if value in ("0.0","0"):
value = 0
except TypeError, err:
# something which doesn't return integer on comparison
# such as a pg_numeric data-type
pass
try:
return cls.baseType(value)
except Exception, err:
# is this potentially a floating-point-formatted long or int?
try:
test = float(value)
newValue = cls.baseType(round(test,0))
except Exception:
raise ValueError( """Fail: Coerce %r -> %r: %s"""%(
value, cls.dataType, err,
))
else:
if test == newValue:
return newValue
else:
raise ValueError( """Fail: Coerce %r -> %s: Data loss would occur"""%( test, cls.dataType))
coerce = classmethod( coerce )
class Int_DT( Numeric_DT ):
"""Integer data-type specifier"""
dataType = "int"
baseType = int
registry.registerDT( int, Int_DT )
class Float_DT( Numeric_DT ):
"""Integer data-type specifier"""
dataType = "float"
baseType = float
registry.registerDT( float, Float_DT )
class Long_DT( Numeric_DT ):
"""Long-integer data-type specifier"""
dataType = "long"
baseType = long
registry.registerDT( long, Long_DT )
class Boolean_DT( datatypedefinition.BaseType_DT ):
"""Boolean-integer data-type specifier"""
dataType = 'bool'
falseValues = (
0,
None,
'0',
'zero',
'null',
'none',
'false',
'f',
'no',
'',
)
baseType = booleanfix.bool
def coerce(cls, value):
"""Coerce value to Boolean value
Accepted Values:
(any value in self.falseValues) -- False
__nonzero__ False -- False
otherwise True
"""
# interpret as a Boolean...
if cls.check( value ):
return value
test = value
if type(value) in (str, unicode):
test = str(test.lower())
if test in cls.falseValues:
return booleanfix.False
elif not test:
return booleanfix.False
else:
return booleanfix.True
coerce = classmethod( coerce )
def check( cls, value ):
"""Determine whether value conforms to definition"""
if not isinstance( value, cls.baseType ):
return 0
if value not in (booleanfix.False,booleanfix.True):
return 0
return 1
check = classmethod( check )
try:
registry.registerDT( bool, Boolean_DT )
except NameError:
pass
registry.registerDT( booleanfix.bool, Boolean_DT )
class StringLocale_DT( datatypedefinition.BaseType_DT ):
"""String data-type specifier"""
dataType = "str.locale"
baseType = str
def coerce(cls, value):
"""Coerce the value to string (true string) value
Acceptable Values:
Unicode -- value.encode()
None -- ""
integer, float, long, complex -- str(value)
"""
if cls.check( value ):
return value
if value is None:
return ""
if isinstance( value, unicode ):
value = value.encode()
if isinstance( value, (int, float, long, complex)):
value = str( value)
return value
coerce = classmethod( coerce )
registry.registerDT( str, StringLocale_DT )
class ClassName_DT( datatypedefinition.BaseType_DT ):
"""Class-name data-type specifier"""
dataType = 'str.classname'
baseType = str
def coerce( cls, value ):
"""Coerce to a string
Acceptable Values:
class name (string or Unicode, Unicode will be encoded)
class object (with __module__ and __name__)
"""
if cls.check( value ):
return value
if hasattr( value, "__module__") and hasattr(value, "__name__"):
return ".".join( (value.__module__, value.__name__))
elif isinstance( value, str ):
return value
elif isinstance( value, unicode):
return value.encode()
else:
raise ValueError( """Unable to convert value %r to a class specifier"""%(value))
coerce = classmethod( coerce )
class Class_DT( datatypedefinition.BaseType_DT ):
"""Class-object data-type specifier"""
dataType = 'class'
baseType = (
type,
types.ClassType,
types.FunctionType,
types.MethodType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.InstanceType,
types.LambdaType,
types.UnboundMethodType,
)
def coerce( cls, value ):
"""Coerce to a class
Acceptable Values:
Unicode/string class name
a class
"""
if cls.check( value ):
return value
if isinstance( value, unicode ):
value = str(value)
if isinstance( value, str):
try:
return latebind.bind( value )
except ImportError, error:
raise ValueError(
"""ImportError loading class %r: %s"""%(
value, error
)
)
except Exception, error:
traceback.print_exc()
raise ValueError(
"""%s loading class from specifier %r: %s"""%(
error.__class__.__name__, value, error,
)
)
else:
raise TypeError( """Unable to convert value %s (%s) to a class"""%(value, ))
coerce = classmethod( coerce )
## def factories( cls ):
## """Determine a sequence of factory objects"""
## return [_classFactory]
## factories = classmethod( factories )
##
##def _classFactory( ):
## """Create a new default class object"""
## return type( "", (object,), {} )
class List_DT( datatypedefinition.BaseType_DT ):
"""List-of-objects data-type (no coercion of items)
Conceptually this is a listof_objects, but that would
make an inefficient type for such a common datatype.
"""
baseType = list
def coerce(cls, value ):
"""Attempt to coerce the value to a list
Strings and unicode values are converted to
[ value ]. Anything else which can be processed with
list( value ) is, everything else raises errors
when list(value) is called.
"""
if cls.check( value ):
return value
if isinstance( value, (str,unicode)):
value = [value]
value = list(value)
return value
coerce = classmethod( coerce )
registry.registerDT( list, List_DT )

View file

@ -0,0 +1,66 @@
"""Hack to provide Python 2.3-style boolean operation in 2.2
"""
try:
if str(True) != 'True':
# Python 2.2.3 has True, but it's just 1 and 0 refs...
raise NameError
True = True
False = False
bool = bool
except NameError:
class bool(int):
def __new__(cls, val=0):
# This constructor always returns an existing instance
if val:
return True
else:
return False
def __repr__(self):
if self:
return "True"
else:
return "False"
__str__ = __repr__
def __and__(self, other):
if isinstance(other, bool):
return bool(int(self) & int(other))
else:
return int.__and__(self, other)
__rand__ = __and__
def __or__(self, other):
if isinstance(other, bool):
return bool(int(self) | int(other))
else:
return int.__or__(self, other)
__ror__ = __or__
def __xor__(self, other):
if isinstance(other, bool):
return bool(int(self) ^ int(other))
else:
return int.__xor__(self, other)
__rxor__ = __xor__
# Bootstrap truth values through sheer willpower
False = int.__new__(bool, 0==1)
True = int.__new__(bool, 1==1)
def install():
"""Install the enhanced bool, True and False in __builtin__"""
import __builtin__
__builtin__.True = True
__builtin__.False = False
__builtin__.bool = bool
#install()
if __name__ == "__main__":
assert True == 1
assert False == 0

View file

@ -0,0 +1,303 @@
"""Boundary objects for checking data values
You use a boundary object by passing a sequence of boundaries
in to a BasicProperty as the keyword argument "boundaries".
Boundaries must conform to the interface described by the
Boundary class, but do not necessarily need to be derived from
them. For instance, if you would like to define boundaries
as functions or methods, feel free.
The Boundary will always be called with all three arguments
from BasicProperty, the allowance for property or client being
None is just a convenience if you want to re-use the boundary
checking code.
NOTE:
The API for Boundary objects changed the order of the
arguments in version 0.6.2! If you were somehow using
Boundary objects before this you *must* change your code
to use the new API!
"""
NULL = []
from basictypes import latebind
class Boundary(object):
"""Base class from which boundary conditions should be derived"""
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions
Your boundary should override this method, check the value
and raise appropriate BoundaryError's if the value is
outside of bounds
"""
class Type(Boundary):
"""Restrict the type of the value to a subclass of a boundary type (or types)
Type provides a way of checking that a value
is an instance of a particular type, or one of a set of
types. Objects are within the boundary if:
isinstance( object, boundaryTypes )
"""
__resolved = 0
def __init__(self, boundaryType):
"""Initialize the Type object with a boundaryType specifier
The boundaryType specifier can be any of the following:
string -- dotted-name specifying the full class name
(including module) for a single class/type
for example "wxPython.wx.wxFramePtr"
This specification allows for late-binding of the
data type, which avoids mutual import problems in certain
situations.
Note: if this specification is used, the type boundary
may raise BoundaryTypeError exceptions on the first
__call__ of the Boundary when the attempt is made
to import the class/type.
class -- a single class/type object,
for example str or wxPython.wx.wxFramePtr
tuple -- a tuple of class/type objects,
for example ( str, list, tuple, unicode ) or
string specifiers (as above)
"""
self.boundaryType = boundaryType
def __repr__( self ):
return """<Type(Boundary): type=%s>"""%(self.boundaryType,)
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions"""
if not self.__resolved:
self.boundaryType = self.resolveBoundary( self.boundaryType, property, client, value )
self.__resolved = 1
if not isinstance(value, self.boundaryType):
raise BoundaryTypeError(
property, self, client, value,
"Value %s was not of required type %s"%( repr(value), self.boundaryType)
)
def resolveBoundary( self, boundarySpecifier, property, client, value ):
"""Resolve a particular boundary specifier into a boundary class"""
try:
return latebind.bind( boundarySpecifier )
except (ImportError, AttributeError):
raise BoundaryTypeError(
property, self, client, value,
"Class/type %s could not be imported"""%(boundarySpecifier,)
)
class Range(Boundary):
"""Restrict the value to between/above/below boundary minimum and/or maximum values
The Range allows you to constrain values based on
comparisons with minimum and/or maximum values. The class
allows for specifying one or both of the boundary conditions.
Note: minimum and maximum are included in the set of valid values
Note: although the obvious use for Range boundaries is for
simple data types (integers, floats, strings), there is nothing
in the Boundary itself which restricts the use to simple data
types. All that is required is the ability to compare instances
using the < and > operators
"""
def __init__(self, minimum = NULL, maximum = NULL):
"""Specify minimum and/or maximum, if one or both is left off, that bound is not checked
Note: minimum and maximum are included in the set of valid values
(i.e. the range is inclusive of the end points)
"""
self.minimum = minimum
self.maximum = maximum
def __repr__( self ):
return """<Range(Boundary): min=%s max=%s>"""%(repr(self.minimum), repr(self.maximum))
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions"""
if self.minimum is not NULL and value < self.minimum:
raise BoundaryValueError(
property, self, client, value,
"Value was < minimum"
)
if self.maximum is not NULL and value > self.maximum:
raise BoundaryValueError(
property, self, client, value,
"Value was > maximum"
)
class Function( Boundary ):
"""Boundary where function( value ) must return given value"""
TRUE_VALUES = []
FALSE_VALUES = []
def __init__(self, function, required=TRUE_VALUES):
"""Specify the function, and the required result to pass the test
function -- the boundary function, taking a single value as parameter
required -- if Function.TRUE_VALUES, test is that the value returned
from function is "true" in the Python sense,
if Function.FALSE_VALUES, test for "falseness" in Python sense,
otherwise, require that result value be == to required to pass
"""
self.function = function
self.required = required
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions"""
result = self.function( value )
if self.required is self.TRUE_VALUES:
if not result:
raise BoundaryValueError(
property, self, client, value,
"%s(%s) gave false value, required a true value"%(
getattr(self.function, '__name__', self.function),
repr(value)[:50],
)
)
elif self.required is self.FALSE_VALUES:
if result:
raise BoundaryValueError(
property, self, client, value,
"%s(%s) gave true value, required a false value"%(
getattr(self.function, '__name__', self.function),
repr(value)[:50],
)
)
elif self.required != result:
raise BoundaryValueError(
property, self, client, value,
"%s(%s) gave value different from required value %r"%(
getattr(self.function, '__name__', self.function),
repr(value)[:50],
self.required,
)
)
class Length( Boundary ):
"""Restrict the length of value to between/above/below boundary minimum and/or maximum lengths
Conceptually, Length boundary is a very minor sub-class of
Range, where the result of passing the value to
a function (in this case len) is compared with the boundary
values, rather then the initial value. This implementation
doesn't currently take advantage of this abstraction.
"""
def __init__(self, minimum = NULL, maximum = NULL):
"""Specify minimum and/or maximum, if one or both is left off, that bound is not checked"""
self.minimum = minimum
self.maximum = maximum
def __repr__( self ):
return """<Length(Boundary): min=%s max=%s>"""%(self.minimum, self.maximum)
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions"""
length = len(value)
if self.minimum is not NULL and length < self.minimum:
raise BoundaryValueError(
property, self, client, value,
"Value was shorter than minimum, length == %s"%(len(value))
)
if self.maximum is not NULL and length > self.maximum:
raise BoundaryValueError(
property, self, client, value,
"Value was longer than maximum, length == %s"%(len(value))
)
class NotNull( Boundary ):
"""Require that value evaluate to true (non-null)
"""
def __call__(self, value, property=None, client=None):
"""Check value against boundary conditions"""
if not value:
raise BoundaryValueError(
property, self, client, value,
"""Value was "null" (evaluates as false)"""
)
class ForEach( Boundary ):
"""For iterable objects, checks a given boundary for each item in object
The ForEach boundary is used to apply another Boundary
object to each object in an iterable value. This allows you
to define checks such as this:
constraints = [
Type( list ),
ForEach( Type( int )),
ForEach( Range( min=0, max=100 )),
]
which would require that the property value be a list of
integers from 0 to 100 (inclusive).
"""
def __init__(self, base):
self.base = base
def __repr__( self ):
return """<ForItemInList %s>"""%( repr(self.base))
def __call__(self, value, property=None, client=None):
"""Check each item in value against base boundary condition"""
try:
index = 0
for item in value:
self.base( item, property, client )
index = index + 1
except BoundaryError, error:
error.boundary = self
error.message = error.message + """ (Offending element was %s (index %s))"""%(item,index)
error.index = index
error.value = value
raise error
class BoundaryError:
"""Base class for all Boundary exceptions
This class keeps references to the objects involved in the
transgression of the boundary. This allows for higher level
systems (such as a GUI application) to provide interactive
support for fixing the boundary transgression.
"""
index = None
def __init__( self, property, boundary, client, value, message="" ):
"""Initialize the error, just stores the references to minimize overhead where the error isn't actually needed"""
self.property, self.boundary, self.client, self.value, self.message = property, boundary, client, value, message
def __repr__( self ):
"""Get a short user-friendly representation of the error"""
return """%s val=%s type=%s prop=%s bound=%s obj=%s msg=%s"""%(
self.__class__.__name__,
repr( self.value ),
type(self.value),
self.property,
self.boundary,
self.client,
self.message,
)
def __str__( self ):
"""Get a full user-friendly string representation of the error"""
return """%s: value %s (type %s) for property %s failed boundary check %s for object %s with message %s"""%(
self.__class__.__name__,
repr( self.value ),
repr( type(self.value)),
self.property,
self.boundary,
self.client,
self.message,
)
class BoundaryTypeError( BoundaryError, TypeError ):
"""A Boundary object which checks data type found a non-conforming value/type
This error is primarily raised by the TypeBoundary class.
It can be caught explicitly, or as a TypeError, depending
on your application's requirements.
"""
class BoundaryValueError( BoundaryError, ValueError ):
"""A Boundary object which checks data value found a non-conforming value
This error is raised by most Boundary classes.
It can be caught explicitly, or as a TypeError, depending
on your application's requirements.
"""

View file

@ -0,0 +1,76 @@
"""Simple class providing formatting of byte values (gigabytes, megabytes, etceteras)"""
from basictypes import basic_types
class Bytes( long ):
"""Special data-type for byte values"""
KILOBYTES = 1024.0
MEGABYTES = KILOBYTES*1024
GIGABYTES = MEGABYTES*1024
TERABYTES = GIGABYTES*1024
displayNames = [
(TERABYTES, 'TB'),
(GIGABYTES, 'GB'),
(MEGABYTES, 'MB'),
(KILOBYTES, 'KB'),
(0, 'B'),
]
def coerce( cls, value ):
"""Coerce the value to byte value"""
if isinstance( value, cls ):
return value
elif isinstance( value, (str,unicode)):
value = value.strip().upper()
for multiplier,name in cls.displayNames:
if value.endswith( name ):
value = (value[:-len(name)]).strip()
try:
return cls( long( value ) * multiplier )
except ValueError, err:
try:
return cls( long(float(value)*multiplier))
except ValueError, err:
raise ValueError(
"""Unable to coerce to a Bytes type, invalid numeric component: %r"""%(
value,
)
)
# had no recognised suffix, try to convert directly with long
# numeric or string with right format will succeed,
# everything else will go boom
result = cls( value )
return result
coerce = classmethod( coerce )
def format( cls, value, multiplier=None, asBits=False ):
"""Format as a string which is back-coercable
multiplier -- pass in the appropriate multiplier for
the value (i.e. request 'KB' to get back as kilobytes,
default (None) indicates that the nearest should
be used
asBits -- if True, format a Byte value as bits, suitable
for display in a "bandwidth" setting, as distinct
from a simple measure of bytes.
"""
if value < 0:
value = abs(value)
neg = '-'
else:
neg = ""
if asBits:
value = value * 8
for threshold, name in cls.displayNames:
if value >= threshold:
if threshold:
value = value/threshold
value = '%3.1f'%(value,)
if asBits:
name = name[:-1] + name[-1].lower()
return '%s%s %s'%( neg, value, name)
raise RuntimeError( """A value %r both > 0 and < 0 was encountered?"""%(value,))
format = classmethod( format )
# backwards compatibility
Bytes_DT = Bytes

View file

@ -0,0 +1,309 @@
"""Preliminary callable-object modelling classes"""
from basicproperty import propertied, basic, common
import inspect
from basictypes import list_types
__NULL__ = []
class Argument( propertied.Propertied ):
"""Representation of a single argument on a callable object"""
name = common.StringLocaleProperty(
'name', """The argument's name, as a simple string""",
)
default = basic.BasicProperty(
'default', """Default-value for the argument, may be NULL/unavailable""",
)
baseType = basic.BasicProperty(
'baseType', """Base data-type for the argument, may be NULL/unavailable""",
)
def __init__(self, name, default =__NULL__, baseType=__NULL__, **named):
"""Initialize the Callable object
name -- the argument name
default -- if provided, will provide the default value
for the argument
baseType -- if provided, will allow for type checking
and coercion of arguments before calling the callable
object.
"""
if default is not __NULL__:
named ["default"] = default
if baseType is not __NULL__:
named ["baseType"] = baseType
super (Argument, self).__init__(
name = name,
**named
)
def __str__(self,):
"""Create a friendly string representation"""
fragments = [repr(self.name)]
if hasattr( self, "default"):
fragments.append (repr(self.default))
if hasattr( self, "baseType"):
fragments.append (repr(self.baseType))
return """%s(%s)"""%(
self.__class__.__name__,
", ".join(fragments),
)
__repr__=__str__
def __eq__( self, other ):
"""Determine whether other is our equivalent
returns true if other is of the same class, with
the same primary attributes
"""
if self.__class__ is not other.__class__:
return 0
NULL = []
for nm in ['name','default','baseType']:
if hasattr( self, nm) and not hasattr( other, nm):
return 0
elif not hasattr( self, nm) and hasattr( other, nm):
return 0
elif hasattr( self, nm ):
if getattr( self, nm) != getattr(other,nm):
return 0
return 1
### Data-type API
def check( cls, value ):
"""Strict check to see if value is an instance of cls"""
return isinstance( value, cls)
check = classmethod(check)
def coerce( cls, value ):
"""Coerce value to a cls instance
Accepted forms:
("name",)
("name",default)
("name",default,baseType)
"name"
{ ** } # passed to the initialiser
"""
if cls.check( value ):
return value
if isinstance( value, (tuple, list)) and value and len(value) < 4:
items = {}
for item,name in zip(value,['name','default','baseType'][:len(value)]):
items[name] = item
return cls( **items )
elif isinstance( value, str ):
return cls( name = value )
elif isinstance( value, dict ):
return cls( **value )
raise TypeError( """Don't know how to convert %r to a %s object"""%( value, cls.__name__))
coerce = classmethod(coerce)
listof_Arguments = list_types.listof(
Argument,
name = "listof_Arguments",
dataType = 'list.Arguments',
)
class Callable( propertied.Propertied ):
"""Modelling of a callable Python object"""
name = common.StringProperty(
'name', """The callable object's-name (may be different from underlying object)""",
)
implementation = basic.BasicProperty(
"implementation", """The underlying implementation (callable Python object)""",
)
arguments = common.ListProperty(
'arguments', """Argument-list for the callable object""",
baseType = listof_Arguments,
)
shortHelp = common.StringProperty(
'shortHelp', """Short help-string suitable for tooltips/status-bars""",
)
longHelp = common.StringProperty(
'longHelp', """Longer help-string suitable for context-sensitive help""",
)
coerce = common.BooleanProperty (
"coerce","""Whether to coerce arguments if possible""",
defaultValue = 0,
)
def __init__(
self, implementation, name=__NULL__,
arguments=__NULL__,
shortHelp = __NULL__, longHelp=__NULL__,
**named
):
"""Initialize the Callable object
implementation -- a callable python object
name -- if provided, will override the given name
arguments -- if provided, will override calculated arguments
shortHelp -- short help-string, first line of __doc__ if not given
longHelp -- long help-string, entire __doc__ string if not given
"""
if name is __NULL__:
name = self._name( implementation )
if arguments is __NULL__:
arguments = self._arguments (implementation)
if shortHelp is __NULL__:
shortHelp = self._shortHelp(implementation)
if longHelp is __NULL__:
longHelp = self._longHelp(implementation)
super (Callable, self).__init__(
implementation = implementation,
name = name,
arguments = arguments,
**named
)
def __str__(self):
"""Return a friendly string representation"""
return """%s( %s )"""% (self.__class__.__name__, self.implementation)
def __call__( self, *arguments, **named ):
"""Do the actual calling of the callable object"""
set = {}
for argument,value in zip(arguments,self.arguments):
set[argument.name] = (argument,value)
# XXX potentially there are missing positional arguments!
if named:
nameSet = dict([(arg.name,arg) for arg in self.arguments])
for key,value in named.items():
if set.has_key( key ):
raise ValueError("""Redefinition of argument order for argument %s"""%(set.get(key)))
else:
# note that argument may be None
set [key] = nameSet.get(key), value
for key,(argument,value) in set.items():
if self.coerce and argument and argument.baseType and hasattr(argument.baseType, "coerce"):
value = argument.baseType.coerce(argument)
set[key] = value
# XXX Should keep arguments in order to allow for *args set :(
return self.implementation( **set )
def getArgument( self, name ):
"""Retieve an argument by name"""
for argument in self.arguments:
if argument.name == name:
return argument
raise KeyError( """%r object doesn't have a %s argument"""%(self, name))
def _name( self, value ):
"""Try to find a decent name for a callable object"""
name = "<unknown>"
for attribute in [ '__name__','name','func_name','co_name','__file__',"friendlyName"]:
if hasattr( value, attribute):
v = getattr( value, attribute)
if isinstance( v, (str,unicode)):
name = v
if '.' in name:
return name.split('.')[-1]
return name
def _shortHelp( self, value ):
"""Try to find the short-docstring for an object"""
if hasattr( value, '__doc__') and value.__doc__:
return value.__doc__.split( '\n')[0]
else:
return ""
def _longHelp( self, value ):
"""Try to find the short-docstring for an object"""
if hasattr( value, '__doc__') and value.__doc__:
return value.__doc__
else:
return ""
def _useCall( self, value ):
"""Can we use __call__ to call this object?
returns true if we should be able to use it
"""
return (
# must have __call__
hasattr( value, '__call__') and
(
# call should be a function or method...
hasattr( value.__call__, 'im_func') or
hasattr( value.__call__, 'im_code')
)
)
def _arguments( self, value ):
"""Get a list of arguments for a callable object"""
if self._useCall( value ):
value = value.__call__
if hasattr(value, 'im_func'):
# receiver is a method. Drop the first argument, usually 'self'.
func = value.im_func
arguments = inspect.getargspec( func )
if value.im_self is not None:
# a bound instance or class method
arguments = inspect.getargspec( func )
del arguments[0][0]
else:
# an un-bound method
pass
elif hasattr(value, 'func_code') or hasattr(value, 'im_code'):
# receiver is a function.
func = value
arguments = inspect.getargspec( func )
else:
raise ValueError('unknown reciever type %s %s'%(receiver, type(receiver)))
names, vararg, varnamed, defaults = arguments
defaults = defaults or ()
result = [ Argument( name = name ) for name in names ]
for name,default in zip( names[-len(defaults):],defaults):
for item in result:
if item.name == name:
item.default = default
return result
def check( cls, value ):
"""Strict check to see if value is an instance of cls"""
return isinstance( value, cls)
check = classmethod(check)
def coerce( cls, value ):
"""Coerce value to a Callable-object"""
if cls.check( value ):
return value
if callable( value ):
return cls(
implementation = value,
)
else:
raise TypeError( "Don't know how to convert %r to a %s object"%(
value, cls.__name__,
))
coerce = classmethod(coerce)
def __eq__( self, other ):
"""Determine whether other is our equivalent
returns true if other is of the same class, with
the same primary attributes
"""
if self.__class__ is not other.__class__:
return 0
NULL = []
for nm in ['name','implementation','arguments']:
if hasattr( self, nm) and not hasattr( other, nm):
return 0
elif not hasattr( self, nm) and hasattr( other, nm):
return 0
elif hasattr( self, nm ):
if getattr( self, nm) != getattr(other,nm):
return 0
return 1
Callables = list_types.listof(
Callable,
name = "Callables",
dataType = 'list.Callables',
)
##class Curry( propertied.Propertied ):
## """A curried Callable with particular arguments pre-set"""
## values = common.DictionaryProperty(
## "values", """Partial value-set to be applied to callable""",
## )
## implementation = basic.BasicProperty(
## 'implementation', """The underlying implementation of the curry""",
## baseType = callable.Callable,
## )
##

View file

@ -0,0 +1,91 @@
"""Interface/base class for data type definitions"""
class DataTypeDefinition( object ):
"""Interface for explicit data type definitions
The data-type definition allows for creating
stand-alone mechanisms for annotating a
particular type without actually modifying the
type itself.
The API for the DataTypeDefinition can easily
be implemented for new classes, but it is
desirable to allow, for instance, built-in
and extension classes to be annotated without
requiring explicit support in those classes
for basictypes.
dataType "" -- required dotted-string identifier for data-type
coerce( cls, value ) -- coerce loose value to type
check( cls, value ) -- strict check for conformance
factories( cls ) -- list of factory objects for the type
commonValues( cls ) -- list of common values for the type
format( cls, value ) -- get a coercable representation of value
"""
def coerce(cls, value):
"""Coerce value to the appropriate data type
This is a method for "fuzzy" conversion of values,
and it should be fairly forgiving. With that said,
it should not be so forgiving that will allow user
errors to be ignored.
"""
if cls.check( value ):
return value
raise TypeError ("""Value %r is not appropriate for data type %s"""%(value, self))
coerce = classmethod(coerce)
def factories( cls ):
"""Determine a sequence of factory objects
Factory objects are used to generate new instances
conforming to this definition. For many datatypes
this is simply the class itself. For others,
it is the list of all sub-classes, or all
specifically-registered sub-classes, or something
entirely different.
XXX The factory object's API has not yet been determined
"""
return ()
factories = classmethod(factories)
def check( cls, value ):
"""Determine whether value conforms to definition
This method is used to determine whether a particular
value conforms to this definition. This is a strict
check, that is, it should return false if the value is
in any way non-conformant, so that coercian can be
attempted.
Note:
Must be callable as definition.check( value ), which
requires classmethods for class-based definitions.
Note:
Because this method is called from coerce and from
basicproperty objects, it should be as minimal as
possible to avoid the possibility of infinite
recursion errors.
"""
return 1
check = classmethod(check)
class BaseType_DT( DataTypeDefinition ):
"""Abstract base DataTypeDefinition w/ "defer-to-base" implementation
"""
baseType = None
def check( cls, value ):
"""Determine whether value conforms to definition"""
if not isinstance( value, cls.baseType ):
return 0
return 1
check = classmethod( check )
def factories( cls ):
"""Determine a sequence of factory objects"""
if callable( cls.baseType ):
return [cls.baseType]
return []
factories = classmethod( factories )
def __new__( cls, *args, **named ):
"""Create a new instance of our base-type"""
return cls.baseType( *args, **named )

View file

@ -0,0 +1,108 @@
"""Stand-alone type-definition objects for date-based data types
Three possible sources (1 implemented):
* mx.DateTime (prefered, and implemented)
* Python 2.3 datetime
* standard time module (least interesting)
XXX Would be nice to get a Python 2.3 datetime module
implementation, but it's pretty low on my list of
priorities
"""
try:
from datemx_types import *
from mx import DateTime as mx_DateTime
haveMX = 1
DateTime_DT = mxDateTime_DT
DateTimeDelta_DT = mxDateTimeDelta_DT
TimeOfDay = mxTimeOfDay
DateTime = mx_DateTime.DateTimeFrom
DateTimeDelta = mx_DateTime.DateTimeDelta
now = mx_DateTime.now
today = mx_DateTime.today
except ImportError:
haveMX = 0
haveImplementation = haveMX # or havePy23 or haveTimeModule
# month enumeration...
from basictypes import enumeration
import calendar
def allInstances( cls ):
"""Return cls instances for each of this class's set"""
items = [
(choice.value, cls( name= choice.name))
for choice in cls.set.values()
]
items.sort()
items = [ v[1] for v in items ]
return items
class WeekDay( enumeration.Enumeration ):
"""Locale-specific day-of-week enumeration
Uses both calendar and mx.DateTime's standard of
Monday = 0, Sunday = 6
"""
dataType = 'enumeration.weekday'
set = enumeration.EnumerationSet.coerce(
zip(
calendar.day_name,
range(len(calendar.day_name))
)
)
allInstances = classmethod( allInstances )
class WeekDayAbbr( enumeration.Enumeration ):
"""Locale-specific day-of-week (abbreviated) enumeration
Uses both calendar and mx.DateTime's standard of
Mon = 0, Sun = 6
"""
dataType = 'enumeration.weekday.abbr'
set = enumeration.EnumerationSet.coerce(
zip(
calendar.day_abbr,
range(len(calendar.day_abbr))
)
)
allInstances = classmethod( allInstances )
class Month( enumeration.Enumeration ):
"""Locale-specific month enumeration
Uses calendar/mx.DateTime standard of January=1,
December = 12
"""
dataType = 'enumeration.month'
data = zip(
calendar.month_name[1:],
range(len(calendar.month_name))[1:]
)
set = enumeration.EnumerationSet.coerce(
data
)
allInstances = classmethod( allInstances )
class MonthAbbr( enumeration.Enumeration ):
"""Locale-specific month (abbreviated) enumeration
Uses calendar/mx.DateTime standard of January=1,
December = 12
"""
dataType = 'enumeration.month.abbr'
set = enumeration.EnumerationSet.coerce(
zip(
calendar.month_abbr[1:],
range(len(calendar.month_abbr))[1:]
)
)
allInstances = classmethod( allInstances )
del calendar
del enumeration

View file

@ -0,0 +1,196 @@
"""Python 2.3 datetime (+dateutils) implementation
"""
import datetime
from dateutil import parser
from dateutil import relativedelta
from basictypes import datatypedefinition, registry
import time, re, traceback
class DateTime( datetime.datetime ):
"""Datatype for the standard Python 2.3+ datetime.datetime type
"""
dataType = 'datetime.datetime'
__slots__ = ()
## def __new__( cls, value=None ):
## if value is None:
## return cls.copy(cls.now())
## elif isinstance( value, datetime.datetime
def check (cls, value):
"""Determine whether value conforms to definition"""
return isinstance( value, cls )
check = classmethod (check)
def copy( cls, source ):
"""Produce a version of given datetime as this class' instance"""
return cls(
source.year,
source.month,
source.day,
source.hour,
source.minute,
source.second,
source.microsecond,
source.tzinfo
)
copy = classmethod( copy )
def coerce(cls, value):
"""Coerce value to the appropriate data type
Accepts:
datetime.datetime instances
datetime.date instances (assumes midnight)
datetime.time instances (assumes today for local tz (watch out!))
string/unicode (using parser.parse)
float (interpreted as time module times)
time.struct_time instances (note that DST setting is not retained!)
"""
if cls.check( value ):
return value
elif isinstance( value, datetime.datetime ):
return cls.copy( value )
elif isinstance( value, datetime.date ):
return cls( value.year, value.month, value.day, tzinfo=value.tzinfo )
elif isinstance( value, datetime.time ):
# XXX May be corner cases here where due to timezone,
# today is actually tomorrow or yesterday...
# what we'd really like to do is figure out what day it is
# in the value's timezone right now and use *that*
return cls.combine(
datetime.date.today(),
value
)
if isinstance(value, (str,unicode)):
return cls.copy( parser.parse( value ) )
elif isinstance(value, float):
# interpreted as a local-time second-since-the-epoch
return cls.fromtimestamp( value )
elif isinstance( value, time.struct_time ):
# no built-in function for this!?!
return cls(
value[0], # year
value[1], # month
value[2], # day
value[3], # hour
value[4], # minute
int(value[5]), # second
int((value[5]%1.0)*1000000), # microseconds
# XXX note that we lose the daylight savings time info!
)
elif type(value).__name__ == 'DateTime':
return cls(
value.year,
value.month,
value.day,
value.hour,
value.minute,
int(value.second),
int(round((value.second%1)*1000, 0)),
# tz is the *name* of the timezone, which we likely won't have...
)
else:
raise TypeError (
"""Could not convert %r (type %s) to DateTime type"""% (value,type (value))
)
coerce = classmethod (coerce)
def asMx( self ):
"""Produce an mxDateTime instance for this value"""
from mx.DateTime import DateTime
return DateTime.DateTime(
source.year,
source.month,
source.day,
source.hour,
source.minute,
source.second + (source.microsecond/1000.0),
#source.tzinfo
)
class _TimeParser(object):
"""Class providing time-parsing functionality"""
HOUR_RE = '\d+'
MINUTE_RE = '\d+'
SECOND_RE = '(\d+([.]\d+)?)|([.]\d+)'
PM_RE = 'p[.]?[m]?[.]?'
AM_RE = 'a[.]?[m]?[.]?'
TEMPLATE_RE = """
(?P<hour>%(hour)s)
(
[:,.]
(?P<minute>%(minute)s)?
(
[:,.]
(?P<second>%(second)s)
)?
)?
[ \t]*
(
(?P<am>%(am)s)
|
(?P<pm>%(pm)s)
)?
"""
def parse( cls, text ):
"""Takes user input and returns partial value dict
Defaults to 24 hours clock
Example inputs:
2:13pm -> 14:13:00
2:13 -> 2:13:00
14:13:00 -> 14:13:00
3pm -> 15:00:00
4 -> 04:00:00
AM and PM formats:
a, p, am, pm, a.m., p.m.,
"""
re_fragments = {
'hour':cls.HOUR_RE, 'minute':cls.MINUTE_RE, 'second':cls.SECOND_RE,
'am':cls.AM_RE, 'pm':cls.PM_RE,
}
searcher = re.compile( cls.TEMPLATE_RE % re_fragments, re.IGNORECASE|re.VERBOSE )
result = searcher.search( text )
if result:
if len( result.group(0)) != len(text.strip()):
raise ValueError( """Could not parse the entirety of %r as a TimeOfDay value, parsed %r"""% (text, result.group(0)))
if result.group('minute'):
minute = int( result.group('minute'), 10)
else:
minute = 0
values = {
'hour': int( result.group('hour'), 10),
'minute': minute,
'second': float( result.group('second') or 0),
}
if result.group( 'pm'):
# Forces the value to be in the PM, regardless
# of whether it already is (so 14pm works fine).
if (values['hour']%24) != 12:
values['hour'] = (values['hour'] % 12)+12
if result.group( 'am'):
# 12am gets subtraction...
if (values['hour'] %24) == 12:
values['hour'] = 0
return values
raise ValueError( """Unable to parse value %r into a TimeOfDay value"""%(text,))
parse = classmethod (parse)
class _TimeDeltaParser( _TimeParser ):
"""Time parser with negative-value support"""
HOUR_RE = '[+ -]*\d+'
def parse( cls, text ):
"""Takes user input and returns partial value dict
This just adds support for negative values, which
consists of negating all values if the hour value is
negative.
"""
values = super( _TimeDeltaParser, cls).parse( text )
if values['hour'] < 0:
values['minute'] = -values['minute']
values['second'] = -values['second']
return values
parse = classmethod( parse )

View file

@ -0,0 +1,383 @@
"""mxDateTime-based date value-types
XXX Still need:
full RelativeDateTime definition
"""
from mx.DateTime import *
from basictypes import datatypedefinition, registry
import time, re, traceback
__all__ = ( "mxDateTime_DT", "mxDateTimeDelta_DT", "mxTimeOfDay", 'RelativeDateTime')
class mxDateTime_DT( datatypedefinition.BaseType_DT ):
"""Data type for an mx.DateTime.DateTime value
"""
baseType = DateTimeType
dataType = 'datetime.mx'
def check (cls, value):
"""Determine whether value conforms to definition"""
return isinstance( value, DateTimeType)
check = classmethod (check)
def coerce(cls, value):
"""Coerce value to the appropriate data type
Will accept:
DateTimeType
string or Unicode representations (DateTimeFrom)
float (DateTimeFromTicks)
time.struct_time (mktime)
"""
if cls.check( value ):
return value
if isinstance( value, unicode):
value = value.encode()
if isinstance(value, str):
# need to parse the data...
# XXX this isn't good, DateTimeFrom only raise an exception
# if the format is close enough to "right" that it can determine
# a format which "should" work. As a result, it will ignore
# such things as "2" or "23" and just treat them as though
# they were start-of-today
return DateTimeFrom(value)
elif isinstance(value, float):
# interpreted as a local-time second-since-the-epoch
return DateTimeFromTicks( value )
elif isinstance( value, time.struct_time ):
return mktime( value )
else:
raise TypeError (
"""Could not convert %r (type %s) to mxDateTime type"""% (value,type (value))
)
coerce = classmethod (coerce)
## def factories ( cls ):
## """Get the factories for this data type"""
## return [now,today]
## factories = classmethod (factories)
# def __store__( cls, value ):
# """Return a stable, low-level representation of the value
#
# In this case, an ISO date-string for the UTC value of the
# DateTime
# """
# gm = value.gmtime()
# return gm.Format('%Y-%m-%dT%H:%M:')+str(gm.second)
# __store__ = classmethod( __store__ )
# def __unstore__( cls, value ):
# """Take our opaque date-store value and convert to an instance
# """
# gm = DateTimeFrom( value )
# return gm.localtime()
# __unstore__ = classmethod( __unstore__ )
registry.registerDT( DateTimeType, mxDateTime_DT )
class _TimeParser(object):
"""Class providing time-parsing functionality"""
HOUR_RE = '\d+'
MINUTE_RE = '\d+'
SECOND_RE = '(\d+([.]\d+)?)|([.]\d+)'
PM_RE = 'p[.]?[m]?[.]?'
AM_RE = 'a[.]?[m]?[.]?'
TEMPLATE_RE = """
(?P<hour>%(hour)s)
(
[:,.]
(?P<minute>%(minute)s)?
(
[:,.]
(?P<second>%(second)s)
)?
)?
[ \t]*
(
(?P<am>%(am)s)
|
(?P<pm>%(pm)s)
)?
"""
def parse( cls, text ):
"""Takes user input and returns partial value dict
Defaults to 24 hours clock
Example inputs:
2:13pm -> 14:13:00
2:13 -> 2:13:00
14:13:00 -> 14:13:00
3pm -> 15:00:00
4 -> 04:00:00
AM and PM formats:
a, p, am, pm, a.m., p.m.,
"""
re_fragments = {
'hour':cls.HOUR_RE, 'minute':cls.MINUTE_RE, 'second':cls.SECOND_RE,
'am':cls.AM_RE, 'pm':cls.PM_RE,
}
searcher = re.compile( cls.TEMPLATE_RE % re_fragments, re.IGNORECASE|re.VERBOSE )
result = searcher.search( text )
if result:
if len( result.group(0)) != len(text.strip()):
raise ValueError( """Could not parse the entirety of %r as a TimeOfDay value, parsed %r"""% (text, result.group(0)))
if result.group('minute'):
minute = int( result.group('minute'), 10)
else:
minute = 0
values = {
'hour': int( result.group('hour'), 10),
'minute': minute,
'second': float( result.group('second') or 0),
}
if result.group( 'pm'):
# Forces the value to be in the PM, regardless
# of whether it already is (so 14pm works fine).
if (values['hour']%24) != 12:
values['hour'] = (values['hour'] % 12)+12
if result.group( 'am'):
# 12am gets subtraction...
if (values['hour'] %24) == 12:
values['hour'] = 0
return values
raise ValueError( """Unable to parse value %r into a TimeOfDay value"""%(text,))
parse = classmethod (parse)
class _TimeDeltaParser( _TimeParser ):
"""Time parser with negative-value support"""
HOUR_RE = '[+ -]*\d+'
def parse( cls, text ):
"""Takes user input and returns partial value dict
This just adds support for negative values, which
consists of negating all values if the hour value is
negative.
"""
values = super( _TimeDeltaParser, cls).parse( text )
if values['hour'] < 0:
values['minute'] = -values['minute']
values['second'] = -values['second']
return values
parse = classmethod( parse )
class mxDateTimeDelta_DT( datatypedefinition.BaseType_DT ):
"""Data type for an mx.DateTime.DateTimeDelta value
"""
baseType = DateTimeDeltaType
dataType = 'datetimedelta.mx'
def check (cls, value):
"""Determine whether value conforms to definition"""
return isinstance( value, DateTimeDeltaType)
check = classmethod (check)
def coerce(cls, value):
"""Coerce value to the appropriate data type
Will accept:
DateTimeType
string or Unicode representations (DateTimeFrom)
float (DateTimeFromTicks)
"""
if cls.check( value ):
return value
if isinstance(value, (str,unicode)):
# need to parse the data...
return cls.parse(value)
elif isinstance (value, (tuple,list)):
return DateTimeDelta( * value )
elif isinstance( value, float ):
return DateTimeDelta( 0,0,0, value )
else:
raise TypeError (
"""Could not convert %r (type %s) to mxDateTime type"""% (value,type (value))
)
coerce = classmethod (coerce)
## def factories ( cls ):
## """Get factories for this data type"""
## return []
## factories = classmethod (factories)
def parse( cls, text ):
"""Takes text (user input) and returns a DateTimeDelta object
Example inputs:
2 hours, 3 days, 45 minutes
3d2h45m
45m
2hours
H:M:S (i.e. standard time format)
XXX should eventually allow:
2,3,45 -> directly to the constructor (d,h,m,s)
2h 15 -> imply order based on previous item
2.5 -> fractional day or hour (see next note)
XXX should we take bare integer as _day_, not hour, as currently???
seems more useful as-is, but it's not really in line
with the base type
"""
if ':' in text:
try:
values = _TimeDeltaParser.parse( text )
return DateTimeDelta(
0, # days
values['hour'],
values['minute'],
values['second'],
)
except (TypeError, ValueError):
traceback.print_exc()
units = [ 'd','h','m','s',]
basePattern = '(\d+([.]\d+)?)\W*%s'
fragments = []
for unit in units:
result = re.compile( basePattern%unit, re.I ).search( text )
if result:
fragment = result.group( 1 )
if not result.group( 2 ):
fragments.append( int(fragment))
else:
fragments.append( float(fragment))
else:
fragments.append( 0 )
if fragments == [0,0,0,0] and text.strip():
try:
fragments[1] = int( text )
except ValueError:
pass
fragments = cls._normalise(fragments)
return DateTimeDelta( * fragments )
parse = classmethod (parse)
def _normalise( cls, value ):
"""Local utility function... Push overflows into higher units, push fractions into lower units"""
value = list(value)
d,h,m,s = range(4)
# push up
for a,b,divisor in [(s,m,60),(m,h,60),(h,d,24)]:
if value[a] > divisor: # more than x in y
value[b] = value[b] + int( value[a]/divisor)
value[a] = value[a] % divisor
# push down
for a,b,divisor in [(h,d,24),(m,h,60),(s,m,60),]:
if value[b] % 1.0: # fraction present
value[a] = value[a] + (value[b]%1.0 * divisor)
value[b] = int(value[b])
return tuple(value)
_normalise = classmethod (_normalise)
def format( cls, value ):
"""Format as a string which is back-coercable"""
result = []
for (attr,fmt,always) in (('day','%i',0),('hour','%02i',1),('minute','%02i',1),('second','%0.2f',0)):
v = getattr( value, attr )
if v or always:
result.append( fmt%v+attr[0] )
return ", ".join( result )
format = classmethod( format )
registry.registerDT( DateTimeDeltaType, mxDateTimeDelta_DT )
class mxTimeOfDay( _TimeParser, RelativeDateTime ):
"""Representation of a time during a particular day
This implementation is simply a sub-class of
RelativeDateTime which provides the functionality
of a data-type definition
"""
dataType = 'timeofday.mx'
## Apparently the object type's __init__ is overriding the RelativeDateTime's
__init__ = RelativeDateTime.__init__
def __repr__( self ):
"""Get a code-like representation of the time of day"""
return """%s( %r )"""%( self.__class__.__name__, self.__class__.format( self ))
def __str__( self ):
"""Get the string representation of the time of day"""
return self.__class__.format( self )
def __eq__( self, other ):
"""Are we equal to the other value?"""
if not isinstance( other, RelativeDateTime ):
return 0
try:
for attr in (
'hour','minute','second',
'year','month','day',
'hours','minutes','seconds',
'years','months','days',
):
if getattr( self, attr) != getattr( other, attr ):
return 0
return 1
except (ValueError,TypeError,AttributeError):
return 0
### Data type definition API
def check( cls, value ):
"""Check that this is a RDT with only hour, minute and second"""
if isinstance( value, cls ):
for attribute in [ 'year','month','day','years','months','days','hours','minutes','seconds']:
if getattr(value, attribute):
return 0
return 1
return 0
check = classmethod( check )
def coerce( cls, value ):
"""Coerce the value to our internal format (RelativeDateTime)
Accepts:
RelativeDateTime with only hour, minute, and second values
tuple/list with up to 4 values for (default 0):
hour, minute, second, millisecond
RelativeDateTime with more data (copies to new with just time data)
DateTime (creates a RelativeDateTime for the DateTime's TimeOfDay)
float, int or long -> bare hours values
"""
def normalise( hour=0, minute=0, second=0.0 ):
day, hour,minute,second = mxDateTimeDelta_DT._normalise(
[0,hour,minute,second]
)
hour = hour % 24
return hour, minute, second
if cls.check( value ):
return value
elif isinstance( value, RelativeDateTime ):
# create new with just time-of-day values
hour, minute,second = normalise( value.hour, value.minute, value.second )
elif isinstance( value, (str,unicode)):
hour, minute,second = normalise( **cls.parse( value ))
elif isinstance( value, (tuple, list)):
# new RDT from:
# up to 4 values, hour, minute, second, millisecond
hour, minute, second, millisecond = list(value) + [0,0,0,0][len(value):]
if millisecond:
second = second + (millisecond/1000.0)
hour, minute,second = normalise( hour, minute, second )
elif isinstance( value, (float,int,long)):
# interpreted as an hour value (can include fractions)
hour, minute,second = normalise( value )
else:
try:
DT = mxDateTime_DT.coerce( value )
hour, minute,second = normalise( DT.hour, DT.minute, DT.second )
except TypeError:
raise TypeError(
"""Unable to extract a time-of-day from value %r"""%(value)
)
# almost every path gets here...
return cls(
hour = hour,
minute= minute,
second = second
)
coerce = classmethod( coerce )
def format( cls, value ):
"""Format as a string which is back-coercable"""
result = []
for (attr,fmt,always) in (('hour','%02i',1),('minute','%02i',1)):
v = getattr( value, attr )
if v or always:
result.append( fmt%(v or 0) )
if value.second:
if value.second < 10.0:
result.append( '0'+str(value.second))
else:
result.append( str(value.second) )
return ":".join( result )
format = classmethod( format )

View file

@ -0,0 +1,57 @@
"""Logging facilities for basictypes
If the logging package (from Python 2.3) is available,
we use it for our logging needs, otherwise we use a
simplistic locally-defined class for logging.
"""
import traceback, cStringIO
def getException(error):
"""Get formatted exception"""
exception = str(error)
file = cStringIO.StringIO()
try:
traceback.print_exc( limit=10, file = file )
exception = file.getvalue()
finally:
file.close()
return exception
try:
import logging
Log = logging.getLogger
logging.basicConfig()
WARN = logging.WARN
ERROR = logging.ERROR
INFO = logging.INFO
DEBUG = logging.DEBUG
logging.Logger.getException = staticmethod( getException )
logging.Logger.err = logging.Logger.error
except ImportError:
# does not have the logging package installed
import sys
DEBUG = 10
INFO = 20
WARN = 30
ERROR = 40
class Log( object ):
"""Stand-in logging facility"""
level = WARN
def __init__( self, name ):
self.name = name
def debug(self, message, *arguments):
if self.level <= DEBUG:
sys.stderr.write( 'DEBUG:%s:%s\n'%(self.name, message%arguments))
def warn( self, message, *arguments ):
if self.level <= WARN:
sys.stderr.write( 'WARN:%s:%s\n'%(self.name, message%arguments))
def error( self, message, *arguments ):
if self.level <= ERROR:
sys.stderr.write( 'ERR :%s:%s\n'%(self.name, message%arguments))
def info( self, message, *arguments ):
if self.level <= INFO:
sys.stderr.write( 'INFO:%s:%s\n'%(self.name, message%arguments))
def setLevel( self, level ):
self.level = level
getException = staticmethod( getException )

View file

@ -0,0 +1,21 @@
"""Wrapper for Python 2.4 (which also works with 2.3) decimal datatype
You can find the 2.3 decimal module described in the PEP for it
here:
http://www.python.org/peps/pep-0327.html
This is a floating-point decimal data-type, not the "fixedpoint" module.
"""
from basictypes import basic_types
try:
import decimal
except ImportError, err:
decimal = None
if decimal:
class DecimalDT( basic_types.Numeric_DT ):
"""Numeric data-type descriptor for the new standard Decimal type"""
dataType = "decimal"
baseType = decimal.Decimal
basic_types.registry.registerDT( decimal.Decimal, DecimalDT )

View file

@ -0,0 +1,37 @@
"""String sub-class representing a domain name"""
class DomainName( str ):
"""Domain-name data-type"""
def __new__( cls, value ):
"""Create a new DomainName from a (non-null) string value"""
if not value:
raise ValueError( """Null domain-name specified""" )
else:
return str.__new__( cls, value )
def check( cls, value ):
"""Check whether value is a valid domain-name
Just checks that the value is an instance of the class.
"""
if isinstance( value, cls ):
return 1
return 0
check = classmethod( check )
def coerce( cls, value ):
"""Coerce value to a string domain-name
Will accept a string value, a unicode value (encoded to
utf-8), must be a non-null value.
"""
if cls.check( value ):
return value
if not isinstance( value, (str,unicode)):
raise TypeError( """Don't know how to convert %r type %r to a domain name object"""%(
value, value.__class__,
))
if isinstance( value, unicode ):
value = value.encode( 'utf-8')
if not value:
raise ValueError( """Null domain-name %r specified"""%(value,) )
return cls( value )
coerce = classmethod( coerce )

View file

@ -0,0 +1,282 @@
"""Simple Enumeration (choice-from-set) data-type"""
from basicproperty import basic, propertied
from basictypes import basic_types
def defaultFriendly(property, client):
"""Determine the default friendly name (the name)"""
return client.name
class EnumerationChoice( propertied.Propertied ):
"""A particular choice within an enumeration set
The enumeration choice is a particular choice
stored within the enumeration set. Its name
is used to index the choice within its set, while
its value is the actual value being enumerated.
"""
name = basic.BasicProperty(
"name","""The internal name/key used to identify the choice""",
baseType = basic_types.String_DT,
)
value = basic.BasicProperty(
"value","""The data value associated with this choice""",
)
friendlyName = basic.BasicProperty(
"friendlyName","""Friendly name used to describe this choice to users""",
setDefaultOnGet = 0,
defaultFunction = defaultFriendly,
baseType = basic_types.String_DT,
)
def __repr__( self ):
"""Get a code-like representation of this choice"""
if self.friendlyName != self.name:
return """%s( name=%r, value=%r, friendlyName=%r)"""%(
self.__class__.__name__,
self.name,
self.value,
self.friendlyName,
)
else:
return """%s( name=%r, value=%r)"""%(
self.__class__.__name__,
self.name,
self.value,
)
class EnumerationSet( dict ):
"""EnumerationSet classes (set from which values may be chosen)
The struct mimics a C enumeration with
names mapping to integer values.
Note:
name values must be hashable
XXX Needed features:
* ordered sets (e.g. month names)
* multiple input (name -> value) mappings
* preferred name-set (value -> name) mappings
* set-union, difference
"""
choiceClass = EnumerationChoice
def getName( self, value ):
"""Get the name of a choice whose value matches value or None"""
for choice in self.values():
if choice.value == value:
return choice.name
return None
def new( self, **namedarguments ):
"""Add a new choice to this enumeration
namedarguments -- passed to self.choiceClass initialiser
"""
choice = self.choiceClass( **namedarguments)
self.append( choice )
return choice
def append( self, choice ):
"""Register a choice with the set"""
self[choice.name] = choice
__iter__ = dict.itervalues
def coerce( cls, value ):
"""Coerce a value to an enumeration-set value
Accepted value formats:
None # empty dictionary/set
[ stringValue, ... ] # value == name
{ 'name': value, ... }
[ (name,value), ... ]
[ choiceClass(), ... ]
[ { }, ... ] # arguments for choice-class
"""
if cls.check( value ):
return value
elif value is None:
value = ()
if isinstance( value, dict ):
value = value.items()
try:
set = [ cls.coerceSingle(item) for item in value ]
return cls(
[(choice.name,choice) for choice in set]
)
except (TypeError, KeyError,ValueError), err:
raise TypeError( """Couldn't coerce %r to a %s value: %s"""%(value,cls.__name__, err))
coerce = classmethod( coerce )
def check( cls, value ):
"""Check whether item is compatible with this set"""
return isinstance( value, cls )
check = classmethod( check )
def checkSingle( cls, item ):
"""Check whether item is compatible with this set"""
return isinstance( item, cls.choiceClass )
checkSingle = classmethod( checkSingle )
def coerceSingle( cls, item ):
"""Coerce an individual value/values to an item
This doesn't actually add the item, as the cls
doesn't store such data.
Accepted formats:
'key' # str or unicode only, converted to unicode for key
('key',value)
{'name':'key','value':value,...} # passed directly to the initialiser
choiceClass instance
"""
if cls.checkSingle( item ):
return item
elif isinstance( item, (str,unicode)):
return cls.choiceClass( name = item, value=item )
elif isinstance( item, (tuple,list)) and len(item) == 2:
if cls.checkSingle( item[1] ):
return item[1].clone( name = item[0])
else:
return cls.choiceClass( name = item[0], value=item[1])
elif isinstance( item, dict ):
return cls.choiceClass( **item )
else:
raise TypeError( """%r unknown item-type"""%item)
coerceSingle = classmethod( coerceSingle )
class Enumeration (propertied.Propertied):
"""A choice from an enumerated set of data values
This class also operates as the base-type for the
enumeration properties, via the data-type-definition
API.
"""
dataType = "enumeration"
## set must be class-data, not just instance data
## should probably be a metaclass property of EnumerationSet type
set = None
name = basic.BasicProperty(
"name", """Data-value choice within one of our sets""",
defaultValue = "",
baseType = unicode,
)
def __init__( self, name="", *arguments, **named ):
if not isinstance( name, (str,unicode)):
name = self.__class__.set.getName( name )
super( Enumeration, self).__init__( name=name, *arguments, **named )
if not self.choice():
raise ValueError( """Name %r is not part of %s"""%(self.name, self.__class__.__name__))
def choice( self ):
"""Get the choice object associated with this value or None"""
return self.set.get( self.name )
def value( self ):
"""Get the value associated with this choice"""
choice = self.choice( )
if choice is not None:
return choice.value
raise ValueError ("""Could not get value for name %r for %s"""%(self.name,self.__class__.__name__))
def __cmp__( self, other ):
"""Compare this value to another value"""
if isinstance( other, Enumeration):
return cmp( self.value(), other.value())
else:
return cmp( self.value(), other )
def __repr__( self ):
"""Return a code-like representation of this object"""
return """%s( name=%r)"""%( self.__class__.__name__, self.name )
def __str__( self ):
"""Return the enumeration value as a name"""
return self.name or self.value()
### Data-type-definition API
def check( cls, value ):
"""Check whether value is of cls type, and has the same set"""
return isinstance( value, cls ) and cls.set == value.set
check = classmethod( check )
def coerce (cls, value):
"""Coerce a value into an Enumeration value
Accepted types:
Enumeration objects
integers/longs
([name,name,name],remainder) tuples
[name,name,name,value] lists (values are |'d together)
"""
if cls.check( value ):
return value
elif isinstance( value, (str, unicode)):
return cls.parse( value)
else:
return cls.fromValue( value )
coerce = classmethod( coerce )
def fromValue( cls, value ):
"""Create from an integer value"""
name = cls.set.getName( value )
if name is None:
raise ValueError( """Value %r is not part of %s"""%(value, cls.__name__))
else:
return cls( name = name )
fromValue = classmethod( fromValue )
def parse ( cls, value):
"""Create from a string value
Possible formats:
"coreName"
"23"
"friendlyName"
"""
value = value.strip ()
current = cls.set.get( value)
if current is not None:
return cls( name = value )
else:
return cls.fromValue( value )
parse = classmethod (parse)
def allInstances( cls ):
"""Return cls instances for each of this class's set"""
items = [
(choice.friendlyName, cls( name= choice.name))
for choice in cls.set.values()
]
items.sort()
items = [ v[1] for v in items ]
return items
allInstances = classmethod( allInstances )
def new( dataType, names, values ):
"""Utility function to create a new enumeration set"""
enum = EnumerationSet.coerce(
map(None, names, values ),
)
enum.dataType = dataType
return enum
class EnumerationProperty( object ):
"""Mix-in for Enumeration properties to return/accept enums"""
def _getValue( self, client ):
"""Retrieve the current value of the property for the client
returns an instance of self.baseType if possible
"""
raw = super( EnumerationProperty,self)._getValue( client )
base = self.getBaseType()
if base:
return base.fromValue( raw )
return raw
def _setValue( self, client, value ):
"""Set the current value of the property for the client
accepts instances of self.baseType as well as raw values
"""
if isinstance(value, Enumeration ):
value = value.value()
return super( EnumerationProperty,self)._setValue( client, value )
##
##try:
## from wxoo.resources import enumeration32_png, enumeration16_png
## from wxoo import typeregistry
##except ImportError:
## pass
##else:
## typeregistry.TYPEICONSNORMAL.Register( Enumeration, enumeration32_png.getBitmap())
## typeregistry.TYPEICONSSMALL.Register( Enumeration, enumeration16_png.getBitmap())
##

View file

@ -0,0 +1,20 @@
"""(preliminary) Model of a Factory Callable object"""
from basicproperty import propertied, basic, common
from basictypes import list_types, callable
class Factory( callable.Callable ):
"""An object intended to create instances of a type
The factory allows you to create instances of a type
through the GUI. Most factories will allow for
entirely-default calling (i.e. Factory() creates a
new, valid instance). Others may require interactive
definition of certain parameters.
"""
listof_Factories = list_types.listof(
Factory,
name = "listof_Factories",
dataType = 'list.Factories',
)

View file

@ -0,0 +1,257 @@
"""Interfaces for basictypes and basicproperty
XXX Should we do adapters from basicproperty objects to
zope schema field objects?
"""
from zope.interface import Interface, Attribute
### Really generic interfaces...
class IName( Interface ):
"""Provide a generic name for an object"""
name = Attribute (
"name",
"""Provides an internal name for an object, may be
used for user interface, but this is of secondary
importance to internal use. Should be, as much as
possible, a valid name in most programming contexts.
""",
)
class IFriendlyName( Interface):
"""Provide a friendly name for an object"""
friendlyName = Attribute (
"friendlyName",
"""user-friendly name for use in UIs and the like,
defaults to the current value of name""",
)
class IPyName( Interface):
"""Object providing a generic name as __name__"""
__name__ = Attribute (
"__name__",
"""Provides an internal name for an object, may be
used for user interface, but this is of secondary
importance to internal use. Should be, as much as
possible, a valid name in most programming contexts.
""",
)
class IPyDocumented(Interface):
"""Object providing documentation strings"""
__doc__ = Attribute (
"__doc__", """Documentation string for the object""",
)
class ICloneProperties(Interface):
"""Live-object duplication mechanism with property substitution"""
def clone( **newValues ):
"""Clone the object, with (optional) new property values
"""
### General DataTypeDefinition interfaces
class ITypeCoercion( Interface ):
"""Convert/coerce a value to instance of type"""
def coerce( value ):
"""Coerce value to the appropriate data type
This is a method for "fuzzy" conversion of values,
and it should be fairly forgiving. With that said,
it should not be so forgiving that will allow user
errors to be ignored.
"""
class ITypeCheck( Interface ):
"""Interface checking whether value is proper instance of type"""
def check (value):
"""Determine whether value value is proper instance of type
This method is used to determine whether a particular
value conforms to this type's restrictions. This is
a strict check, that is, it should return false if
the value is in any way non-conformant, so that
coercian can be attempted.
"""
class ITypeFactories(Interface):
"""Interface providing factory instances for a given type"""
def factories( ):
"""Determine a sequence of factory objects for type
Factory objects are used to generate new instances
conforming to this definition. For many datatypes
this is simply the class itself. For others,
it is the list of all sub-classes, or all
specifically-registered sub-classes, or something
entirely different.
"""
class IDataTypeDeclaration (Interface):
"""Object provides a dataType compatible with wxoo"""
dataType = Attribute ("dataType","""The string data type specifier
The specifier is used throughout wxoo to identify
an abstract data type for processing.
""")
class ITypeBaseType (Interface):
"""Objects with base data types for dependent objects"""
baseType = Attribute (
"baseType","""A type or type-like object providing type-like services""",
)
### Propertied object interfaces
class ITypeProperties(Interface):
"""Allows retrieval of property-set for a type"""
def getProperties( ):
"""Get the properties for the type"""
### Callable-object interfaces
class ICallableArgument( IName ):
"""Describes an argument to a callable object
Note that ITypeBaseType may be provided by particular
subclasses to allow for type checking.
"""
default = Attribute(
'default', """Default-value for the argument, may be NULL/unavailable""",
)
def __eq__( other ):
"""Determine whether other is our equivalent
returns true if other is of the same class, with
the same primary attributes
"""
class ICallable (IName):
"""Describes and provides access to a callable object"""
name = Attribute(
'name', """The callable-object's name""",
)
arguments = Attribute(
'arguments', """Argument-list for the callable object""",
)
shortHelp = Attribute(
'shortHelp', """Short help-string suitable for tooltips/status-bars""",
)
longHelp = Attribute(
'longHelp', """Longer help-string suitable for context-sensitive help""",
)
coerce = Attribute(
"coerce","""Whether to coerce arguments if possible""",
)
def __init__(
implementation, name=None,
arguments=None,
shortHelp = None, longHelp=None,
**named
):
"""Initialize the Callable object
implementation -- a callable python object
name -- if provided, will override the given name
arguments -- if provided, will override calculated arguments
shortHelp -- short help-string, first line of __doc__ if not given
longHelp -- long help-string, entire __doc__ string if not given
"""
def __call__( *arguments, **named ):
"""Do the actual calling of the callable object"""
def getArgument( name ):
"""Retieve an argument-wrapper object by name"""
### Boundary-related interfaces
class IBoundary (Interface):
"""A boundary object for checking a value"""
def __call__( value, client = None, property = None ):
"""Check value against boundary conditions
Raises BoundaryError exceptions if the value
is not within the bounds defined by the boundary.
"""
class IBoundaryError (Interface):
"""Provides rich information about a boundary error"""
def __init__( property, boundary, client, value, message="" ):
"""Initialize the error with data values"""
property = Attribute ("property","""The property passed to the boundary's __call__ method""")
boundary = Attribute ("boundary","""The boundary object raising the exception""")
client = Attribute ("client","""The client passed to the boundary's __call__ method""")
value = Attribute ("value","""The value which failed the boundary check""")
message = Attribute ("message","""Human-friendly string describing the error""")
### XXX Enumeration-related interfaces, when we get those sorted out
### Property-related interfaces...
class IPropertyDefaults(Interface):
"""Property interface providing default-value semantics"""
setDefaultOnGet = Attribute (
"setDefaultOnGet",
"""if true (default), then retrieving a
default value causes the value to be explicitly set as the
current value""",
)
default = Attribute (
"default",
"""If present, an IPropertyDefaultHolder object returning an object
to be used as the default for the property""",
)
class IPropertyDefaultHolder(Interface):
"""Callable-object producing a default value for property"""
def __call__( property, client ):
"""Return the appropriate default value"""
class IProperty(Interface):
"""High-level functionality of BasicProperty-like objects"""
name = Attribute (
"name","""name of the property, used for storage and reporting""",
)
trueProperty = Attribute (
"trueProperty",
"""if true, this property really does describe a
property, that is, a descriptor for an attribute which is
accessed using object.x notation.
if false, this property is used to interact with the
property system, but is not actually a property of an
object (for instance when the object is an old-style class
which cannot support properties, you can define virtual
properties for use with the class) The property system
can examine the value of trueProperty to determine whether
to use setattr(object,name,value) or call
property.__set__(object, value) to use the property.""",
)
def __get__( client, klass=None ):
"""Retrieve the current value of the property for the client
Performs coercion and retrieval of the property value
from the client, including potential default-value
retrieval.
"""
def __set__( client, value ):
"""Set the current value of the property for the client
Perform coercion and storage of the property value
for the client. Returns the actual value set (which
may be different than the passed value).
"""
def __delete__( client ):
"""Delete the current value of the property for the client
"""
class IPropertyPickle (Interface):
"""Provide pickle support to retrieve/set property values"""
def getState( client ):
"""Helper for client.__getstate__, gets storable value for this property"""
def setState( client, value ):
"""Helper for client.__setstate__, sets storable value"""
class IPropertyMethodStore(Interface):
"""Objects using client methods for data storage/retrieval"""
setMethod = Attribute (
"setMethod","""Method name used to set the data value on client""",
)
getMethod = Attribute (
"getMethod","""Method name used to get the data value from the client""",
)
delMethod = Attribute (
"delMethod","""Method name used to delete the data value from the client""",
)
class IPropertyReadOnly(Interface):
"""Read-only flags for Property-like objects"""
readOnly = Attribute(
'readOnly', """If true, disallow editing of the property through the property editors (doesn't change the underlying property's capabilities, just prevents changes through the property editors)""",
)

View file

@ -0,0 +1,68 @@
"""Module providing "late-binding" for type specifiers
This is a generically useful utility module, it is
provided here just because this is where it is used
the most.
"""
import sys
def bind( specifier ):
"""Find the class(es) specified by specifier
Allows you to pass a type specifier, one of:
string -> fully-qualified string specifying an importable class
tuple/list -> list of specifiers
class/other -> a class or other type (untouched)
and get back either a single class, or a tuple of
classes.
This allows you to specify types like so:
"wxPython.wx.wxFramePtr"
(str,unicode,"my.special.StringClass")
in many places within the basicproperty and wxoo packages
for use as a sequence of classes without getting messed up
by mutual-import problems.
Note: the only time you get back a single class (as opposed to
a tuple with a single class as it's only item) is when you
specify a string or class as the root specifier.
"""
if isinstance( specifier, unicode ):
specifier = str(specifier)
if isinstance( specifier, str):
return importByName( specifier )
elif isinstance( specifier, (tuple,list)):
return tuple(flatten([
bind(spec)
for spec in specifier
]))
else:
return specifier
def importByName( fullName ):
"""Import a class by name"""
name = fullName.split(".")
moduleName = name[:-1]
className = name[-1]
module = __import__( ".".join(moduleName), {}, {}, moduleName)
return getattr( module, className )
def flatten(inlist, type=type, ltype=(list,tuple), maxint= sys.maxint):
"""Flatten out a list, code developed by myself and modified by Tim Peters, then by me again :)"""
try:
# for every possible index
for ind in xrange( maxint):
# while that index currently holds a list
while isinstance( inlist[ind], ltype):
# expand that list into the index (and subsequent indicies)
inlist[ind:ind+1] = list(inlist[ind])
#ind = ind+1
except IndexError:
pass
return inlist

View file

@ -0,0 +1,35 @@
BasicTypes for Python
Copyright (c) 2002-2003, Michael C. Fletcher
All rights reserved.
THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY
SITUATION ENDANGERING HUMAN LIFE OR PROPERTY.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials
provided with the distribution.
The name of Michael C. Fletcher may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,227 @@
"""Restricted lists with base-datatype meta-type support
This module is normally used to easily create a baseType
definition for a property like so:
listof_Blarghs = list_types.listof(
name = "listof_Blarghs",
baseType = Blargh,
dataType = "list.Blargh",
)
class X(propertied.Propertied):
blarghs = common.ListProperty(
'blarghs', "A sequence of Blarghs",
baseType = listof_Blarghs,
)
The listof_Blarghs type (a sub-class of list) will
have coercion, data-type checking, and factories support,
and a dataType attribute which allows the wxoo system
to service instances in a number of useful ways.
The module also includes a set of "basic" list-of-types
mirroring those in the basic_types module.
"""
from basictypes import basic_types, rlist, latebind
import types, inspect, os
class ListOf( rlist.rlist ):
"""List of a particular object-type
This is the default base class for the classes
generated by the listof meta-class. It is a
simple restrictive list (see the superclass)
which calls its class'es coerce_single method
on every value added.
"""
def beforeAdd( self, value ):
"""Called before all attempts to add an item"""
return type(self).coerce_single( value )
class listof( type ):
"""Meta-class which creates a new typed list-class
The listof meta-class is normally called like so:
classname = listof(
"classname",
baseType,
dataType = "list.something",
)
with baseType being the basictypes specifier for the
base object type, or a simple data-class.
Note: there is a horrible hack that tries to figure
out the correct __module__ name for the resulting class,
this is very annoyingly set to list_types by default,
rather than the module which calls listof() :( .
"""
def __new__( cls, baseType, name= "", bases=(ListOf,), **named):
if named.has_key( 'dataType'):
dataType = named.get('dataType')
elif hasattr( baseType, 'dataType'):
dataType = baseType.dataType
elif hasattr( baseType, '__name__'):
dataType = baseType.__name__
elif isinstance( baseType, str):
# probably a string-specified data-type
dataType = (baseType.split('.')[-1])
else:
raise ValueError( """listof couldn't determine dataType specifier for %r"""%(baseType))
if not name:
name = "list_%s"%(dataType.replace(".",'_'))
if not named.has_key( 'dataType'):
# we're auto-calculating a data-type, add list. to the front
dataType = 'list.' + dataType + 's'
named ["dataType"] = dataType
baseObject = super( listof, cls).__new__( cls, name, bases,{} )
baseObject.baseType = baseType
## Following is the code to try and figure out the
## module in which the class should reside, this is
## fragile, as there might be some code that doesn't
## setup the class in the root of the module. Like
## any other class, that class will be unpickle-able.
stack = inspect.stack(1)
module = stack[1][0].f_globals['__name__']
baseObject.__module__ = module
from basicproperty import linearise
linearise.Lineariser.registerHelper( baseObject, linearise.SequenceLin() )
return baseObject
def __init__( self, *args, **named ):
"""Dummy init to avoid conflicts in Python 2.6"""
def coerce(cls, value ):
"""Attempt to coerce the value using one of our baseType"""
if isinstance( value, (str,unicode)):
value = [ value ]
if cls.check_single( value ):
value = [ value ]
assert issubclass( cls, rlist.rlist )
assert issubclass( cls, ListOf )
newValue = cls()
if isinstance( value, (str,unicode)):
value = [value]
newValue[:] = value
return newValue
def factories(cls):
"""Retrieve the list of factories for this class"""
base = cls.baseType
if base:
if hasattr( base, 'factories'):
return base.factories()
else:
return base
return ()
def coerce_single( cls, value ):
"""coerce a single value to an acceptable type"""
if cls.check_single( value ):
return value
# needs actual coercion
base = cls.baseType
if base and hasattr( base, 'coerce'):
return base.coerce( value )
elif base:
return base(value)
return value
def check( cls, value ):
"""Check the whole set (debugging checks)"""
if not isinstance( value, list ):
return 0
base = cls.baseType
if base:
for item in value:
if base and hasattr( base, 'check'):
if not base.check( item ):
return 0
elif base:
if not isinstance( item, base ):
return 0
return 1
def check_single( cls, value ):
"""Check whether a value is an instance of an acceptable type"""
base = cls.baseType
if base and hasattr( base, 'check'):
return base.check( value )
elif base:
return isinstance( value, base )
return 1
def _get_name( self ):
if named.has_key( 'dataType'):
dataType = named.get('dataType')
elif hasattr( baseType, 'dataType'):
dataType = baseType.dataType
elif hasattr( baseType, '__name__'):
dataType = baseType.__name__
elif isinstance( baseType, str):
# probably a string-specified data-type
dataType = baseType.split('.')[-1]
else:
raise ValueError( """listof couldn't determine dataType specifier for %r"""%(baseType))
def _get_baseType( self ):
"""Get the baseType as an object/class"""
base = getattr( self, '_baseType')
if isinstance( base, type):
return base
if isinstance( base, unicode):
base = str(base)
if isinstance( base, (str,tuple)):
new = latebind.bind( base )
if isinstance( new, (tuple,list) ):
base = type(base)( new )
else:
base = new
assert (
isinstance( base, (type,types.ClassType)) or
(
hasattr( base,'coerce') and
hasattr( base, 'check')
)
), """Specified base type %r for listof is not a class/type, and doesn't define both coerce and check methods"""%(
base,
)
setattr( self, '_baseType', base)
return base
def _set_baseType( self, value ):
setattr( self, '_baseType', value)
def _del_baseType( self, ):
delattr( self, '_baseType')
baseType = property(
_get_baseType, _set_baseType, _del_baseType,
doc="""The base-type specifier for the listof type"""
)
listof_strings = listof(
basic_types.String_DT,
"listof_strings",
)
listof_ints = listof(
basic_types.Int_DT,
"listof_ints",
)
listof_bools = listof(
basic_types.Boolean_DT,
"listof_bools",
)
listof_longs = listof(
basic_types.Long_DT,
"listof_longs",
)
listof_floats = listof(
basic_types.Float_DT,
"listof_floats",
)
listof_classnames = listof(
basic_types.ClassName_DT,
"listof_classnames",
)
listof_classes = listof(
basic_types.Class_DT,
"listof_classes",
)

View file

@ -0,0 +1,893 @@
"""Core-python interface definitions"""
from protocols import Interface, Attribute, declareImplementation
import sys
### Python-object protocols
class IObjectPyDoc(Interface):
"""Object which contains python doc string
"""
__doc__ = Attribute(
"__doc__","""Python documentation string for the object""",
)
class IObjectPyDict(Interface):
"""Object with a Python __dict__ attribute
"""
__dict__ = Attribute(
"__dict__","""Python object dictionary""",
)
class IObjectContains(Interface):
"""Object which can determine whether it contains other
"""
def __contains__( other ):
"""Determine whether object contains other"""
class IObjectEq(Interface):
"""Object which can compute whether other is equal
XXX should have adapters both ways for eq and ne
"""
def __eq__( other ):
"""Determine whether object == other"""
class IObjectNe(Interface):
"""Object which can compute whether other is not equal
XXX should have adapters both ways for eq and ne
"""
def __ne__( other ):
"""Determine whether object != other"""
class IObjectPyName(Interface):
"""Object with a __name__ attribute"""
__name__ = Attribute(
"__name__","""Integral name for the object""",
)
class IObjectComparable(Interface):
"""Object which can be compared to other objects using cmp
There's a considerable number of methods, or a
very large number of interfaces. Not sure what
should be done, so for now it's just a marker.
"""
class IObjectGetItem( Interface):
"""Object which retrieves a sub-item by key (random access)"""
def __getitem__( key ):
"""Get sub-item by key or index"""
class IObjectSetItem( Interface):
"""Object which sets a sub-item by key (random access)"""
def __setitem__( key, other ):
"""Set sub-item by key or index"""
class IObjectDelItem( Interface):
"""Object which deletes a sub-item by key (random access)"""
def __delitem__( key ):
"""Delete sub-item by key or index"""
class IObjectLength( Interface ):
"""Objects which can report a total length (number of sub-elements)"""
def __len__( ):
"""Return the length as an integer or long"""
class IObjectNonZero( Interface ):
"""Objects which can determine their Boolean truth value directly
XXX Should be an adapter for IObjectLength?
"""
def __nonzero__( ):
"""Determine if the object is True (not False)"""
class IObjectHash( Interface ):
"""Objects which can calculate a hash/key value"""
def __hash__( ):
"""Return the hash as an integer"""
class IObjectIter(Interface):
"""Object which provides explicit iteration support"""
def __iter__():
"""Return an iterator for the items of this object"""
### Pickle (and copy) protocols
## The code for these mechanisms includes "fallback" interfaces
## such as "has a dictionary", along with the ability to exclude
## 0,1,2 or even all 3 sub-interface. An object is pickleable/copyable
## even without any of these interfaces, they are merely extra
## functionality interfaces that can be used.
class IPickleable(Interface):
"""Marker interface declaring that object is pickleable"""
class IPickleGetState(Interface):
"""Object which allows retrieval of current state"""
def __getstate__():
"""Retrieve an object representing the current state"""
class IPickleSetState(Interface):
"""Object which allows initialization from an archive of current state"""
def __setstate__(state):
"""Initialize the object from the given state"""
class IPickleGetInitArgs(Interface):
"""Object which allows retrieval of "recreation" arguments"""
def __getinitargs__():
"""Retrieve initialization arguments to re-create current state"""
# XXX should have the __reduce__ interface as well, but I've
# never actually used that to document it.
# I don't like the names for these interfaces, suggestions? -- mcf
class ICopyable(Interface):
"""Marker interface declaring that object is copyable"""
class ICopyCopy(ICopyable):
"""Object which defines explicit shallow __copy__ method"""
def __copy__():
"""Return a shallow copy of the object"""
class IDeepCopyable(Interface):
"""Marker interface declaring that object is copyable"""
class ICopyDeepCopy(IDeepCopyable):
"""Object which defines explicit __deepcopy__ method"""
def __deepcopy__(memory):
"""Return a deep copy of the object
memory -- dictionary of already-copied elements
"""
### Sequence protocols
class ISequenceGetItem( Interface):
"""Sequence version of get-item, integer keys
This is the "random access" integer key retrieval
interface, whereas the IIterator interface gives
the more limited sequential interface.
"""
def __getitem__( index ):
"""Get sub-item by index"""
class ISequenceSetItem( Interface):
"""Sequence version of set-item, integer keys"""
def __setitem__( index, other ):
"""Set sub-item by index"""
class ISequenceDelItem( Interface):
"""Sequence version of get-item, integer keys"""
def __delitem__( index ):
"""Delete sub-item by index"""
class ISequenceGetSlice( Interface ):
"""Sequence which can retrieve a "slice" of sub-objects by index"""
def __getslice__( start, stop):
"""Get sub-items by index-range"""
class ISequenceSetSlice( Interface ):
"""Sequence which can set a "slice" of sub-objects by index"""
def __setslice__( start, stop, other):
"""Set sub-items by index-range"""
class ISequenceDelSlice( Interface ):
"""Sequence which can delete a "slice" of sub-objects by index"""
def __delslice__( start, stop):
"""Delete sub-items by index-range"""
class ISequenceAppend(Interface):
"""Sequence object to which items may be appended"""
def append( item):
"""Append the particular item to the sequence"""
class ISequenceCount(Interface):
"""Sequence object which can count instances of items"""
def count(item):
"""Return the number of occurrences of the item in the sequence"""
class ISequenceExtend(Interface):
"""Sequence object which can be extended with another sequence"""
def extend(other):
"""Extend this sequence with the other sequence"""
class ISequenceIndex(Interface):
"""Sequence object which can determine index of a sub-item"""
def index(item):
"""Return integer index of first occurrence of item in sequence"""
class ISequenceInsert(Interface):
"""Sequence object which can insert item at a given index
XXX Should have adapters for ISequenceAppend and ISequenceExtend
"""
def insert(index, item):
"""Insert the given item at the given index
(sequence[index] should be item afterward)
"""
class ISequenceRemove(Interface):
"""Sequence object which can remove an instance of an item"""
def remove(item):
"""Remove the first instance of the given item from sequence"""
class ISequenceReverse(Interface):
"""Sequence whose order can be reversed in-place"""
def reverse():
"""Reverse the sequence's order in-place"""
class ISequenceSort(Interface):
"""Sequence whose order can be sorted in-place"""
def sort(function = None):
"""Sort the sequence in-place
function -- if specified, the comparison function
to use for sorting, otherwise cmp
"""
class ISequencePop(Interface):
"""Sequence which can "pop" last item"""
def pop():
"""Remove and return the last item of the sequence"""
class ISequencePopAny(Interface):
"""Sequence which can "pop" any item"""
def pop(index =-1):
"""Remove and return the given item from the sequence"""
### The rather simple iterator protocol
class IIterator(Interface):
"""Object which can operate as an iterator"""
def __iter__():
"""Return the object itself to allow for x in Iterator operation"""
def next():
"""Return the next item in the sequence"""
### Text-type objects (strings and Unicode)
## Sub-API: joining and splitting
class ITextJoin( Interface):
"""Text which can join sequences of text objects"""
def join(sequence):
"""Return texts within sequence joined by this text"""
class ITextSplit( Interface):
"""Text which can create sequences by splitting on a sub-string"""
def split(substring, maximum=None):
"""Return text intervals between instances of substring in text"""
class ITextReplace(Interface):
"""Text which can generate copies with replaced sub-strings"""
def replace(old, new, maximum = None):
"""Return text with instances of old substring replaced by new substring
maximum -- if specified, limits total number of substitutions
"""
class ITextSplitLines(Interface):
"""Text which can split itself on line breaks"""
def splitlines(keepends= 0):
"""Return text intervals between newline characters
keepends -- if true, retain the newline characters
as part of the intervals (lines)
"""
## Sub-API: Create new versions of objects with different formatting/encoding
class ITextCapitalize(Interface):
"""Text which can generate word-capitalized copies"""
def capitalize():
"""Return copy of text with the first characters capitalized"""
class ITextCenterAlign(Interface):
"""Text which can generate center-aligned copy of a given width"""
def center(width):
"""Return copy of text centered in string of given width"""
class ITextRightAlign(Interface):
"""Text which can generate right-aligned copy of a given width"""
def rjust(width):
"""Return copy of text right-aligned in string of given width"""
class ITextLeftAlign(Interface):
"""Text which can generate left-aligned copy of a given width"""
def ljust(width):
"""Return copy of text left-aligned in string of given width"""
class ITextZeroFill(Interface):
"""Text which can generate left-zero-padded copies of itself"""
def zfill(width):
"""Return copy of text left-zero-padded in string of given width"""
class ITextTranslate(Interface):
"""Text which can generate translation-table modified copies of itself"""
def translate(table, toDelete=""):
"""Return copy of text translated via table with to toDelete characters removed"""
class ITextExpandTabs( Interface):
"""Text which can generate tab-expanded copies"""
def expandtabs( tabsize = 8 ):
"""Return copy of text with tabs expanded to tabsize spaces"""
## Case manipulation
class ITextCaseUpper(Interface):
"""Text which can generate upper case copies"""
def upper():
"""Return a copy of text in all uppercase"""
class ITextCaseLower(Interface):
"""Text which can generate lower case copies"""
def lower():
"""Return a copy of text in all lowercase"""
class ITextCaseSwap(Interface):
"""Text which can generate case-swapped copies"""
def swapcase():
"""Return a copy of text with case of all characters swapped"""
class ITextCaseTitle(Interface):
"""Text which can generate title-cased copies"""
def title():
"""Return a copy of text in title-case"""
class ITextStripLeft(Interface):
"""Text which can generate copies with leftmost whitespace trimmed"""
def lstrip(whitespace = None):
"""Return copy of text with leftmost whitespace trimmed
whitespace -- None, indicating regular whitespace, otherwise
set of characters which should be trimmed.
"""
class ITextStripRight(Interface):
"""Text which can generate copies with rightmost whitespace trimmed"""
def rstrip(whitespace = None):
"""Return copy of text with rightmost whitespace trimmed
whitespace -- None, indicating regular whitespace, otherwise
set of characters which should be trimmed.
"""
class ITextStrip(Interface):
"""Text which can generate copies with leading and trailing whitespace trimmed"""
def strip(whitespace = None):
"""Return copy of text with leading and trailing whitespace trimmed
whitespace -- None, indicating regular whitespace, otherwise
set of characters which should be trimmed.
"""
class ITextDecode(Interface):
"""Text which can be decoded using a particular codec"""
def decode(encoding = None, errors = "strict"):
"""Decode text using the codec registered for encoding.
encoding defaults to the default encoding. errors may be given
to set a different error handling scheme. Default is 'strict'
meaning that encoding errors raise a ValueError. Other possible
values are 'ignore' and 'replace'.
"""
class ITextEncode(Interface):
"""Text which can be encoded using a particular codec"""
def encode(encoding = None, errors = "strict"):
"""Encode text using the codec registered for encoding.
encoding defaults to the default encoding. errors may be given
to set a different error handling scheme. Default is 'strict'
meaning that encoding errors raise a ValueError. Other possible
values are 'ignore' and 'replace'.
"""
## Sub-API: Query for contents based on sub-strings
class ITextStartsWith( Interface):
"""Text which can determine whether it starts with a particular sub-string"""
def startswith( prefix, start = 0, end = sys.maxint):
"""Return true if the text (in the given slice) starts with the given suffix """
class ITextEndsWith( Interface):
"""Text which can determine whether it ends with a particular sub-string"""
def endswith( suffix, start = 0, end = sys.maxint):
"""Return true if the text (in the given slice) ends with the given suffix """
class ITextCount(ISequenceCount):
"""Text which can count substring occurrences in a given range"""
def count( sub, start= 0, end=sys.maxint):
"""Count the number of occurrences of substring in given slice"""
class ITextFind(Interface):
"""Text which can find start of contained sub-strings"""
def find( sub, start = 0, end =sys.maxint):
"""Return lowest index where substring found in slice, -1 if not found"""
class ITextIndex( ISequenceIndex ):
"""Text providing sequence-style index method with extra arguments"""
def index(sub, start=0, end= sys.maxint):
"""Return lowest index where substring found in slice, ValueError if not found"""
class ITextFindRight(Interface):
"""Text which can find start of contained sub-strings from end of text"""
def rfind( sub, start = 0, end =sys.maxint):
"""Return highest index where substring found in slice, -1 if not found"""
class ITextIndexRight( ISequenceIndex ):
"""Text which can find start of contained sub-strings from end of text, sequence style"""
def rindex(sub, start=0, end= sys.maxint):
"""Return highest index where substring found in slice, ValueError if not found"""
class ITextIsAlphaNumeric( Interface ):
"""Text providing test whether the text is all-alphanumeric (and non-null)"""
def isalnum():
"""Return whether len(text) > 0 and text is entirely alphanumeric"""
class ITextIsAlpha( Interface ):
"""Text providing test whether the text is all-alphabetic (and non-null)"""
def isalpha():
"""Return whether len(text) > 0 and text is entirely alphabetic"""
class ITextIsDigit( Interface ):
"""Text providing test whether the text is all-digits"""
def isdigit():
"""Return whether text is entirely composed of digit characters"""
class ITextIsNumeric( Interface ):
"""Text providing test whether the text is all-numeric characters"""
def isdigit():
"""Return whether text is entirely composed of numeric characters"""
class ITextIsLower( Interface ):
"""Text providing test whether the text is all-lowercase (and non-null)"""
def islower():
"""Return whether len(text) > 0 and text is entirely lowercase"""
class ITextIsSpace( Interface ):
"""Text providing test whether the text is all-whitespace (and non-null)"""
def isspace():
"""Return whether len(text) > 0 and text is entirely whitespace"""
class ITextIsTitleCased( Interface):
"""Text providing test whether text is in title case format"""
def istitle():
"""Return whether text is entirely formatted in title case"""
class ITextIsUpperCased( Interface):
"""Text providing test whether text is in upper case format"""
def isupper():
"""Return whether text is entirely formatted in upper case"""
### Dictionary/mapping protocol
class IMappingClear(Interface):
"""Mapping object able to clear all subelements"""
def clear():
"""Remove all subelements from this object"""
class IMappingCopy(Interface):
"""Mapping object able to create a shallow copy of itself"""
def copy():
"""Return a shallow copy of the mapping"""
class IMappingUpdate(Interface):
"""Mapping object able to update from another mapping object"""
def update(other):
"""Add all keys from other, overriding local key-value combinations"""
class IMappingGet(Interface):
"""Mapping object providing call to retrieve an item or return default"""
def get(key, default = None):
"""Return the item for the giving key, or default"""
class IMappingPopItem(Interface):
"""Mapping object providing method to retrieve and remove random value"""
def popitem():
"""Return some (key,value) pair from the dictionary, KeyError if empty"""
class IMappingSetDefault(Interface):
"""Mapping object providing method to retrieve or set-default key value"""
def setdefault(key, default = None):
"""Retrieve current value, or set default value and return that"""
class IMappingHasKey(Interface):
"""Mapping object providing call to determine whether key is defined"""
def has_key(key):
"""Determine whether the key exists in the mapping"""
class IMappingItems(Interface):
"""Mapping object able to return all items as a (key, value) list"""
def items():
"""Return all items in mapping as a (key, value) list"""
class IMappingIterItems(Interface):
"""Mapping object able to return all items as a (key, value) iterable"""
def iteritems():
"""Return all items in mapping as a (key, value) iterable"""
class IMappingKeys(Interface):
"""Mapping object able to return all keys as a list"""
def keys():
"""Return all keys in mapping as a list"""
class IMappingIterKeys(Interface):
"""Mapping object able to return all keys as an iterable"""
def iterkeys():
"""Return all keys in mapping as an iterable"""
class IMappingValues(Interface):
"""Mapping object able to return all values as a list"""
def values():
"""Return all values in mapping as a list"""
class IMappingIterValues(Interface):
"""Mapping object able to return all values as an iterable"""
def itervalues():
"""Return all values in mapping as an iterable"""
### Stream and file protocols
class IStreamClose(Interface):
"""Stream providing a close method to release resources"""
closed = Attribute (
"closed","""Boolean displaying the current open/closed status""",
)
def close():
"""flush internal buffers, close the stream, and release resources"""
class IStreamFlush(Interface):
"""Stream providing a flush method to flush internal buffers"""
def flush():
"""Flush (write) internal buffers to the stream"""
class IStreamIsTTY(Interface):
"""Stream allowing query for whether it is a TTY-like device"""
def isatty():
"""Return Boolean representing whether is a TTY-like device"""
class IStreamRead(Interface):
"""Stream providing basic read method"""
def read(size = None):
"""read available bytes from stream, limit to size if specified"""
class IStreamWrite(Interface):
"""Stream providing basic write method"""
def write(string):
"""Write the string to the stream"""
class IStreamReadLine(Interface):
"""Stream providing line-reading method"""
def readline(size = None):
"""read a line from stream, limit bytes read to ~ size if specified"""
class IStreamReadLines(Interface):
"""Stream providing multiple-line-reading method"""
def readlines(size = None):
"""read lines from stream, limit bytes read to ~ size if specified"""
class IStreamXReadLines(Interface):
"""Stream providing optimized multiple-line-reading method
XXX This probably shouldn't be an interface unto itself,
or at least it should be a child of IStreamReadLines
"""
def xreadlines(size = None):
"""read lines from stream, limit bytes read to ~ size if specified"""
class IStreamWriteLines(Interface):
"""Stream providing multiple-line-writing method"""
def writelines(iterable):
"""Iterate over the iterable writing each resulting string to stream"""
class IStreamSeek(Interface):
"""Stream providing random-access seeking to position"""
def seek(offset, whence):
"""seek(offset[, whence]) -> None. Move to new stream position
Argument offset is a byte count. Optional argument whence defaults to
0 (offset from start of file, offset should be >= 0); other values are 1
(move relative to current position, positive or negative), and 2 (move
relative to end of file, usually negative, although many platforms allow
seeking beyond the end of a file).
"""
class IStreamTell(Interface):
"""Stream providing feedback regarding current position"""
def tell():
"""return current file position (integer or long)"""
class IStreamTruncate(Interface):
"""Stream providing feedback regarding current position
XXX Documentation seems to suggest that this interface requires
IStreamTell, though only in cases where size is not specified
"""
def truncate(size = None):
"""Truncated stream to given size, or current position if not specified"""
class IStreamMode(Interface):
"""Stream having a mode attribute"""
mode = Attribute (
"mode", """The I/O mode for the file""",
)
class IStreamName(Interface):
"""Stream having a name attribute"""
name = Attribute (
"name", """Filename or data-source description""",
)
class IStringIOGetValue(Interface):
"""Provides access to current value of StringIO buffer"""
def getvalue ():
"""Retrieve the current value of the string buffer"""
### DBM database protocol
class IDBMDatabase(
IObjectContains,
IObjectIter,
IObjectGetItem,
IObjectSetItem,
IObjectDelItem,
IMappingIterKeys,
IMappingKeys,
IStreamClose,
IMappingHasKey,
):
"""(Dumb/G)DBM Interface
XXX Note this interface is derived from the DumbDBM
runtime class, there may be items which are not
officially considered part of interface.
"""
class IBSDIteration(Interface):
"""BSDDB iteration interface
This is the interface provided by the dbhash module's
"database" objects (in addition to the IDBMDatabase
interface).
"""
def first():
"""return the first key in the database"""
def next(key):
"""return the key in the database after given key"""
def last():
"""return the last key in the database"""
def previous(key):
"""return the key in the database before given key"""
def sync():
"""synchronize in-memory database with on-disk database"""
class IBSDIterationSetLocation(IBSDIteration):
"""Adds ability to set database cursor location by key
"""
def set_location( key ):
"""Set cursor to item indicated by key, return (key,value)"""
### Array-specific
class IArrayIOString(Interface):
"""Array-object to/from string method interfaces"""
def tostring():
"""Convert to array of machine values and return as string"""
def fromstring( string ):
"""Appends items from string (sequence of machine values)"""
class IArrayIOList(Interface):
"""Array-object to/from list method interfaces"""
def tolist():
"""Convert to list of elements"""
def fromlist( string ):
"""Appends items from list"""
class IArrayIOFile(Interface):
"""Array-object to/from file method interfaces"""
def tofile():
"""Write to file as array of machine values"""
def fromfile( string ):
"""Appends from file/stream as array of machine values"""
class IArrayMetadata( Interface ):
"""Array-object providing item-type metadata"""
typecode = Attribute(
"typecode", """The typecode character used to create the array""",
)
itemsize = Attribute(
"itemsize", """The length in bytes of one array item in the internal representation""",
)
class IArrayByteswap( Interface ):
"""Mutable-array interface"""
def byteswap( ):
"""Byte-swap the array data"""
list__implements__ = [
IObjectContains,
IObjectEq,
IObjectNe,
IObjectComparable,
IObjectLength,
#IObjectIter, # list doesn't have __iter__
IPickleable,
ICopyable,
IDeepCopyable,
ISequenceGetItem,
ISequenceSetItem,
ISequenceDelItem,
ISequenceGetSlice,
ISequenceSetSlice,
ISequenceDelSlice,
ISequenceAppend,
ISequenceCount,
ISequenceExtend,
ISequenceIndex,
ISequenceInsert,
ISequenceRemove,
ISequenceReverse,
ISequenceSort,
ISequencePopAny,
]
str__implements__= [
IObjectContains,
IObjectEq,
IObjectNe,
IObjectComparable,
IObjectLength,
IObjectHash,
#IObjectIter, # str doesn't have __iter__ :(
IPickleable,
ICopyable,
IDeepCopyable,
ISequenceGetItem,
ISequenceGetSlice,
ITextCount,
ITextJoin,
ITextSplit,
ITextReplace,
ITextSplitLines,
ITextCapitalize,
ITextCenterAlign,
ITextRightAlign,
ITextLeftAlign,
ITextZeroFill,
ITextTranslate,
ITextExpandTabs,
ITextCaseUpper,
ITextCaseLower,
ITextCaseSwap,
ITextCaseTitle,
ITextStripLeft,
ITextStripRight,
ITextStrip,
ITextDecode,
ITextEncode,
ITextStartsWith,
ITextEndsWith,
ITextCount,
ITextFind,
ITextIndex,
ITextFindRight,
ITextIndexRight,
ITextIsAlphaNumeric,
ITextIsAlpha,
ITextIsDigit,
ITextIsNumeric,
ITextIsLower,
ITextIsSpace,
ITextIsTitleCased,
ITextIsUpperCased,
]
unicode__implements__= [
IObjectContains,
#IObjectEq, # doesn't implement it :(
#IObjectNe, # doesn't implement it :(
IObjectComparable,
IObjectLength,
IObjectHash,
#IObjectIter, # unicode doesn't have __iter__ :(
IPickleable,
ICopyable,
IDeepCopyable,
ISequenceGetItem,
ISequenceGetSlice,
ITextCount,
ITextJoin,
ITextSplit,
ITextReplace,
ITextSplitLines,
ITextCapitalize,
ITextCenterAlign,
ITextRightAlign,
ITextLeftAlign,
ITextZeroFill,
ITextTranslate,
ITextExpandTabs,
ITextCaseUpper,
ITextCaseLower,
ITextCaseSwap,
ITextCaseTitle,
ITextStripLeft,
ITextStripRight,
ITextStrip,
#ITextDecode,
ITextEncode,
ITextStartsWith,
ITextEndsWith,
ITextCount,
ITextFind,
ITextIndex,
ITextFindRight,
ITextIndexRight,
ITextIsAlphaNumeric,
ITextIsAlpha,
ITextIsDigit,
ITextIsNumeric,
ITextIsLower,
ITextIsSpace,
ITextIsTitleCased,
ITextIsUpperCased,
]
tuple__implements__= [
IObjectContains,
IObjectEq,
IObjectNe,
IObjectComparable,
IObjectLength,
IObjectHash,
IPickleable,
ICopyable,
IDeepCopyable,
ISequenceGetItem,
ISequenceGetSlice,
]
dict__implements__= [
IObjectContains,
IObjectEq,
IObjectNe,
IObjectComparable,
IObjectGetItem,
IObjectSetItem,
IObjectDelItem,
IObjectLength,
IObjectIter,
IPickleable,
ICopyable,
IDeepCopyable,
IMappingClear,
IMappingCopy,
IMappingUpdate,
IMappingGet,
IMappingPopItem,
IMappingSetDefault,
IMappingHasKey,
IMappingItems,
IMappingIterItems,
IMappingKeys,
IMappingIterKeys,
IMappingValues,
IMappingIterValues,
]
array_ArrayType__implements__= [
### The array object in Python 2.2.2, although it works
## as though it had the commented out interfaces doesn't
## actually provide the interface's signature :(
## The interface signatures even show up in the ArrayType
## class, they just aren't showing up in the objects :(
#IObjectContains,
#IObjectEq,
#IObjectNe,
IObjectComparable,
#IObjectLength,
#IObjectIter, # list doesn't have __iter__
# not sure arrays really are all three of these
IPickleable,
ICopyable,
IDeepCopyable,
#ISequenceGetItem,
#ISequenceSetItem,
#ISequenceDelItem,
#ISequenceGetSlice,
#ISequenceSetSlice,
#ISequenceDelSlice,
ISequenceAppend,
ISequenceCount,
ISequenceExtend,
ISequenceIndex,
ISequenceInsert,
ISequenceRemove,
ISequenceReverse,
ISequenceSort,
ISequencePopAny,
IArrayIOString,
IArrayIOList,
IArrayIOFile,
IArrayMetadata,
IArrayByteswap,
]
bsddb__implements__= [
IDBMDatabase,
IBSDIterationSetLocation,
]
dbhash__implements__= [
IDBMDatabase,
IBSDIteration,
]
dumbdbm__implements__= [
IDBMDatabase,
]
file__implements__ = [
IObjectIter,
IObjectHash,
IStreamClose,
IStreamFlush,
IStreamIsTTY,
IStreamRead,
IStreamWrite,
IStreamReadLine,
IStreamReadLines,
IStreamXReadLines,
IStreamWriteLines,
IStreamSeek,
IStreamTell,
IStreamTruncate,
IStreamMode,
IStreamName,
]
StringIO_StringIO__implements__= file__implements__ + [
# XXX does it actually support everything the file does?
IStringIOGetValue,
]
baseTypeImplements = [
(list, list__implements__),
(tuple, tuple__implements__),
(str, str__implements__),
(unicode, unicode__implements__),
(dict, dict__implements__),
## (array.ArrayType, array_ArrayType__implements__),
]
def register():
for classObject, interfaceList in baseTypeImplements:
declareImplementation(
classObject,
interfaceList,
)
register()

View file

@ -0,0 +1,16 @@
"""Mapping from core types/classes to stand-in DataTypeDefinitions"""
REGISTRY = {
}
def registerDT( base, DT ):
"""Register a DataTypeDefinition for a given base-class"""
REGISTRY[ base ] = DT
def getDT( base ):
"""Return the appropriate DT for the given base-class
This looks up the base in the registry, returning
either a registered stand-alone data-type-definition
or the base itself.
"""
return REGISTRY.get( base, base )

View file

@ -0,0 +1,48 @@
"""Restrictive-List base class"""
class rlist( list ):
"""Sub-class of list that calls a method before insertion allowed
"""
def __init__(self, value= None):
"""Initialize the restricted list object"""
if value is not None:
value = self.beforeMultipleAdd([ self.beforeAdd(item) for item in value ])
else:
value = []
super (rlist, self).__init__(value)
def __setslice__( self, start, stop, value ):
"""__setslice__ with value checking"""
value = self.beforeMultipleAdd([ self.beforeAdd(item) for item in value ])
return super(rlist,self).__setslice__( start, stop, value )
def extend( self, value ):
"""extend with value checking"""
value = self.beforeMultipleAdd([ self.beforeAdd(item) for item in value ])
return super(rlist,self).extend( value )
__iadd__ = extend
def append( self, value ):
"""append with value checking"""
value = self.beforeAdd( value )
return super(rlist,self).append( value )
def insert( self, index, value ):
"""insert with value checking"""
value = self.beforeAdd( value )
return super(rlist,self).insert( index, value )
def __setitem__( self, index, value ):
"""__setitem__ with value checking"""
value = self.beforeAdd( value )
return super(rlist,self).__setitem__( index, value )
def beforeAdd( self, value ):
"""Called before all attempts to add an item"""
return value
def beforeMultipleAdd( self, value ):
"""Called before attempts to add more than one item (beforeAdd has already be called for each item)"""
return value
if __name__ == "__main__":
a = rlist( [1,2,3,4,5] )
print a
print a[:].__class__
m = rlist( [1,2,3,4,5] )
m.append( 's' )

View file

@ -0,0 +1,67 @@
"""This belongs over in basicproperty/basictypes, obviously"""
class RestrictedString( unicode ):
"""Data-type definition for a string with restricted contents
ACCEPTED_CHARACTERS -- set of (unicode) characters accepted by
the string-type
REPLACE_CHARACTER -- character with which to replace unaccepted
characters (*must* be in ACCEPTED_CHARACTERS!)
"""
ACCEPTED_CHARACTERS = u''
REPLACE_CHARACTER = u''
def __new__( cls, value ):
"""Initialise the restricted string/unicode value"""
if isinstance( value, unicode ):
cleaned = cls.clean( value )
return super(RestrictedString,cls).__new__( cls, cleaned )
else:
return cls.coerce( value )
def coerce( cls, value ):
"""Coerce the value to the correct data-type"""
# first get a unicode value...
if value is None:
value = u""
if isinstance( value, str ):
value = value.decode( ) # default encoding must be set...
if isinstance( value, (int,float,long)):
value = unicode( value )
if not isinstance( value, unicode ):
raise TypeError( """Don't know how to convert a %r instance (%r) to a restricted unicode value"""%(
type(value), value,
))
return cls( cls.clean( value ))
coerce = classmethod( coerce )
def clean( cls, value ):
"""Return only those characters in value in ACCEPTED_CHARACTERS"""
result = []
for x in value:
if x in cls.ACCEPTED_CHARACTERS:
result.append( x )
else:
result.append( cls.REPLACE_CHARACTER )
return u"".join( result )
clean = classmethod( clean )
if __name__ == "__main__":
from basicproperty import common, propertied, basic
class Test( RestrictedString ):
ACCEPTED_CHARACTERS = (
'tis' +
"' ."
).decode( 'latin-1' )
class Test2( Test ):
REPLACE_CHARACTER = ' '
class PTest( propertied.Propertied ):
value = basic.BasicProperty(
"value", """Testing value""",
baseType = Test2,
)
t = Test( 'this\tand that' )
print t
t = Test2( 'this\tand that' )
print t
c = PTest( value = 'this\tand that' )
print c.value
c = PTest( value = '2322.5' )
print c.value

View file

@ -0,0 +1,87 @@
"""Object representing functional union of two types
(wrt. the basictypes/basicproperty type interfaces)
"""
class TypeUnion( tuple ):
"""An object providing, as much as possible, the union of two types
The TypeUnion is intended to allow specifying
baseTypes which are actually 2 or more sub-
types. The TypeUnion is responsible for
mediating between the sub-types (for instance
making sure that items which are instances of
one type are not arbitrarily converted to
instances of another).
"""
def __new__( cls, *arguments, **named ):
"""Create a new TypeUnion object
A class-name will be calculated by cls.createName
"""
base = super( TypeUnion, cls).__new__( cls, *arguments, **named )
base.__name__ = cls.createName( base )
return base
def __repr__( self ):
return self.__name__
def createName( cls, base ):
"""Try to create a type-name from base (tuple of sub-types)"""
set = []
for typ in base:
if hasattr( typ, '__name__'):
set.append( typ.__name__.split('.')[-1] )
else:
set.append( str(typ).split('.')[-1])
return "".join( set )
createName = classmethod( createName )
def check( self, value ):
"""Is the value acceptable as one of our types"""
for typ in self:
if hasattr( typ, 'check'):
if typ.check( value ):
return 1
elif isinstance( value, typ ):
return 1
return 0
def coerce( self, value ):
"""Coerce the value to one of our types"""
if self.check( value ):
return value
best = self.bestMatch( value )
if best is not None:
return best.coerce( value )
else:
err = None
for typ in self:
try:
return typ.coerce( value )
except (ValueError,TypeError), err:
pass
raise TypeError( """Couldn't convert %r value to any of %r (%s)"""%(
value, tuple(self), err
))
def factories( self ):
"""Get the default set of factory objects"""
result = []
for item in self:
if hasattr( item, 'factories'):
result.extend( list(item.factories()))
elif callable( item ):
result.append( item )
return result
def bestMatch( self, value ):
"""Find the closest item to value's type
Defaults to the first item
"""
# is value an instance of item or item.baseType?
for typ in self:
if isinstance(value,typ) or (
hasattr( typ,'baseType') and
isinstance(value, typ.baseType)
):
return typ
# XXX should have meta-data on the types to allow better
# recovery here
return None

View file

@ -0,0 +1,18 @@
"""URL manipulation and object-oriented class hierarchy"""
def path( value, target = None, force=0 ):
"""Convert value to the target path-type
XXX this becomes a hook-point for creating
virtual file-systems, with force true requiring
the particular target and false allowing
substitutes
"""
if target is None:
from basictypes.vfs import filepath
target = filepath.FilePath
if not isinstance( value, target ):
value = target( value )
return value

View file

@ -0,0 +1,154 @@
"""Base-class for file-path strings
"""
import os
class BasePath (str):
"""Representation of a path in a virtual file system
Interact with the paths in an object-oriented way
(following are ideas for what to do)
* Support for standard os and os.path queries
* listdir
* isfile/dir/symlink
* create sub-directory
* join-with-string to get sub-path
* absolute-path -> Path object
* support .. and . directory resolution (and elimination)
* Mime-types
* assoc values
* comparisons
* parent, child -> boolean
* shared path -> fragments
* shared root -> boolean
* open( *, ** )
* for directories, create/open file based on standard file() call
* for zipfile-embedded paths, use zipfile transparently to create/open sub-file
* for directories, create the directory (possibly recursively)
* for files, error normally, possibly useful for things
like zipfiles and db-interface files which want to provide
directory-like interface
* file( name ) -> Path
* create a sub-file path
* sub( name ) -> Path
* create a sub-directory path
* Eventually support zipfiles as Directory paths
XXX Need to raise appropriate exceptions and look at ways to
reduce overhead beyond the naive implementation of many of the
methods.
XXX Need to deal with upcoming switch to unicode values for path
names on OSes which support them.
"""
__slots__ = ()
def check( cls, value ):
"""Is the value an instance of this class?"""
return isinstance( value, BasePath )
check = classmethod( check )
def coerce( cls, value ):
"""Coerce value to an instance of this class"""
if cls.check( value ):
return value
elif isinstance( value, BasePath ):
return value
elif isinstance( value, (str, unicode)):
return cls( value )
elif isinstance( value, file ) or hasattr( value, 'name'):
return cls( value.name )
else:
raise TypeError( """Unable to coerce value %r to a %s object"""%(
value, cls.__name__
))
coerce = classmethod( coerce )
def __repr__( self ):
return '%s(%s)'%(self.__class__.__name__, super(BasePath,self).__repr__( ))
def __eq__( self, other ):
"""Attempt to determine if we are equal to other__"""
other = self.__class__.coerce( other )
return self.canonical() == other.canonical()
def isParent (self, other):
"""Return true if we are the parent of the other path
Other can be a string specifier or a Path object
"""
other = self.__class__.coerce( other )
return other.parent() == self
def isChild (self, other):
"""Return true if we are a child of the other path"""
other = self.__class__.coerce( other )
return self.parent() == other
def isAncestor( self, other ):
"""Return true if we are an ancestor of the other path"""
other = self.__class__.coerce( other )
if self.shareRoot( other ):
for item in self.__class__.coerce(other).parents():
if item == self:
return 1
return 0
def isDescendent( self, other ):
"""Return true if we are a descendent of the other path"""
other = self.__class__.coerce( other )
return other.isAncestor( self )
def shareRoot( self, other):
"""Return true if we are descended from the same root in the file system"""
other = self.__class__.coerce( other )
return other.root() == self.root()
def walk( self, file=None, pre=None, post=None ):
"""Simple walking method
For directories:
pre(path) is called before starting to process each directory
submember.walk(file,pre,post) is called on each sub-member
post(path) is called after processing all sub-members of the directory
For files:
file( path ) is called for each file in each directory
"""
if not file and not pre and not post:
return
if self.isFile():
# what to do about file+directory types?
if file:
file(self)
if self.isDir():
# is a directory
if pre:
pre( self )
children = self.list()
for child in children:
child.walk( file, pre, post )
if post:
post( self )
### virtual methods...
def isFile( self ):
"""Return true if we are a file path"""
def isAbsolute (self):
"""Return true if this path is an absolute path (i.e. fully specified from a root)"""
def isRoot( self ):
"""True iff this object is the root of its filesystem"""
def baseOnly( self ):
"""Is this path reachable using only the base file system"""
def sharedRoot(self, other):
"""Return the path of the longest shared prefix for ourselves and other"""
def canonical(self):
"""Get a canonical version of this path
The new path will be absolute, expanded,
case-normalized, normalized, and converted
to a path of the same type as this one.
It will include, where required, a pointer
to the parent-filesystem-path which points
to this path's root.
"""
def join( self, name ):
"""Create a new Path from this path plus name"""
def split( self ):
"""Return our parent path (if available) and our name"""
# make string split and join available via aliases
sjoin = str.join
ssplit = str.split

View file

@ -0,0 +1,360 @@
"""Spike test for filesystem-path objects
## Create a path representing an existing directory
>>> from filepath import path
>>> p = path( 'c:\\temp' )
>>> p
FileSystemPath('c:\\temp')
## Lists the current contents of the directory
>>> p.list()
[FileSystemPath('c:\\temp\\abook.mab'), FileSystemPath('c:\\temp\\impab.mab'), FileSystemPath('c:\\temp\\test.dat')]
## Create a new path pointing to a non-existent directory
>>> sd = p+'somewhere'
>>> sd.exists()
0
## Create the directory pointed to by our path
>>> sd.createDirectory()
>>> sd.exists()
1
## Get the parent of the path
>>> sd.parent()
FileSystemPath('c:\\temp')
## Compare two different versions of the same path
>>> sd.parent() == p
1
## Explore the hierarchy of parents a little
## note that sd.parent().parent().parent() returns None
## so isn't really fun to look at in an interactive session
>>> sd.parent().parent()
FileSystemPath('c:\\')
>>> sd.root()
FileSystemPath('c:')
## Create a deeply nested directory
>>> deep = sd.join( 'this', 'that', 'those' )
>>> deep.createDirectory()
>>> deep.exists()
1
## Create a path for a file (deep + 'test.txt') also works
>>> f = deep.join( 'test.txt' )
>>> f.open('w').write( 'some text' )
>>> f
FileSystemPath('c:\\temp\\somewhere\\this\\that\\those\\test.txt')
>>> f.exists()
1
>>> f.size()
9L
>>> f.remove()
>>> f.exists()
0
>>> f.open('w').write( 'some text' )
## Remove the entire deeply nested directory
## including files
>>> sd.remove()
>>> f.exists()
0
## Demonstrate walking a path hierarchy with simple callbacks
>>> newDirectories = ["why", "not", "me"]
>>> for directory in newDirectories:
... sub = sd+directory
... sub.createDirectory ()
... (sub + 'test.txt').open('w').write( "hello world!")
...
>>> fileList = []
>>> sd.walk( file = fileList.append )
>>> fileList
[FileSystemPath('c:\\temp\\somewhere\\me\\test.txt'), FileSystemPath('c:\\temp\\somewhere\\not\\test.txt'), FileSystemPath('c:\\temp\\somewhere\\why\\test.txt')]
>>> directoryList = []
>>> sd.walk( pre = directoryList.append )
>>> directoryList
[FileSystemPath('c:\\temp\\somewhere'), FileSystemPath('c:\\temp\\somewhere\\me'), FileSystemPath('c:\\temp\\somewhere\\not'), FileSystemPath('c:\\temp\\somewhere\\why')]
>>>
"""
import os
from basictypes.vfs import path, basepath
class FilePath( basepath.BasePath ):
"""Representation of a path in the FileSystem
XXX More documentation
XXX Need better support for unc names
"""
root = None
fragments = None
def isFile( self ):
"""Return true if we are a file path"""
return os.path.isfile( self )
def isDir( self ):
"""Return true if we are a directory path"""
return os.path.isdir( self )
def isRoot( self ):
"""True if this object is the root of its filesystem"""
unc = self.unc()
if not unc:
return os.path.ismount( self )
else:
return unc == self.canonical()
def isAbsolute (self):
"""Return true if this path is an absolute path"""
return os.path.isabs( self )
def baseOnly( self ):
"""Is this path reachable using only the base file system"""
return 1
def parent( self ):
"""Get the parent of this path as a Path"""
# rather than using canonical, we should instead
# first try splitting ourselves, only forcing the
# canonical conversion if we find that doesn't tell
# us whether we have parents... alternately, raise
# an error saying we can't tell what the parent is
# at the moment...
if self.isRoot():
return None
parent, rest = os.path.split( self.canonical() )
if rest and parent:
# managed to split something off
return path(parent, self.__class__)
else:
return None
def baseName (self):
"""Get the final fragment of the path as a string"""
return os.path.basename( self )
def exists( self ):
"""Return true if this path exists
XXX Should catch primitive errors and raise more useful ones
"""
if os.path.exists( self ):
return 1
else:
return 0
def root( self ):
"""Get the root of this path"""
full = path(self.canonical(), self.__class__)
unc = full.unc()
if unc:
return unc
drive = full.drive()
if drive:
return drive
else:
return None
def unc( self, ):
"""Get the root UNC Path, or None if it doesn't exist
XXX This appears to always return the unc name as lowercase?
"""
unc,rest = os.path.splitunc( self.canonical())
if unc:
return path( unc, self.__class__ )
else:
return None
def drive (self,):
"""Get the root drive Path, or None if it doesn't exist"""
drive,rest = os.path.splitdrive( self.canonical())
if drive:
return path( drive+os.sep, self.__class__ )
else:
return None
def fragments (self):
"""Get the path as a set of string fragments"""
fragments = []
fragment = 1
full = path(self.canonical(), self.__class__ )
while full and fragment:
full, fragment = full.split()
if fragment:
fragments.append( fragment )
else:
fragments.append( full )
fragments.reverse()
return fragments
def parents( self ):
"""Return all of our parents up to our root"""
fragments = self.fragments()
if not fragments:
# XXX fix this
raise ValueError( "parents of a NULL path?" )
current = None
result = []
for value in fragments[:-1]:
if current is None:
current = value
else:
current = current + value
result.append( current )
return result
def canonical(self):
"""Get a canonical version of this path
The new path will be absolute, expanded,
case-normalized, and normalized.
"""
return os.path.normcase(
os.path.normpath(
os.path.expanduser(
os.path.expandvars(
os.path.realpath(self)
)
)
)
)
def join( self, * name ):
"""Create a new Path from this path plus name"""
return path(
os.path.join(str(self), *name),
self.__class__
)
__add__ = join
def split( self ):
"""Return our parent path (if available) and our name"""
head, tail = os.path.split( self )
return path(head, self.__class__), path(tail, self.__class__)
def extension (self):
"""Return the file extension, or "" if there is no extension"""
return os.path.splitext(self)[1]
splitext = extension
def stat( self ):
"""Attempt to run a stat on the object"""
return os.stat( self )
def size( self ):
"""Attempt to get the (byte) size of the object on disk
Note: calling this on directories does a recursive call
adding up the sizes up the files in the directory, which can
be rather expensive.
"""
if self.exists():
if self.isFile():
return os.stat( self).st_size
else:
class collect(object):
total = 0
def __call__( self, filePath ):
self.total = self.total + filePath.size()
c = collect()
self.walk( file = c )
return c.total
raise NotImplementedError( """Couldn't get size for path %s"""%(repr(self)))
def permissions (self, mode=None ):
"""Attempt to update and/or read permissions for the path
if mode is None --> attempt to read permissions, return None if couldn't succeed
if mode is anything else --> call chmod with the value
XXX Eventually, this should support platform-specific permission
specifications, such as Secure Linux or WinNT permissions,
though I'm not sure how
"""
if mode is None:
# get
if self.exists():
if self.isFile():
return os.stat( self).st_mode
else:
return None
raise NotImplementedError( """Couldn't get permissions for path %s"""%(repr(self)))
else:
if self.exists():
os.chmod( self, mode )
def remove( self ):
"""(Recursively) remove this object from the filesystem
XXX Should provide options to change permissions if they are
incorrectly set (e.g. read-only), and maybe clobber
locks/holds if they exist (not even sure that's possible)
"""
self.walk(
file=os.remove,
post= os.rmdir
)
### APIs that assume directory/file status...
def list( self, glob="" ):
"""Return a list of Path objects representing the current contents of this directory"""
return [
self.join( name )
for name in os.listdir( self )
]
def createDirectory(self,*arguments,**namedarguments):
"""Ensure that a directory exists, if possible
Note: will not work with zipfile directories as they
don't really exist, zipfiles should probably recurse
down to the ZIP file, create the ZIP file and then,
if there is an embedded filesystem (such as an embedded
zipfile) create that, otherwise ignore the remainder
of the path.
"""
return os.makedirs( self, *arguments, **namedarguments )
def file (self, name ):
"""Create a new file path within this directory"""
if self.isDir():
return self.join( name )
else:
raise ValueError( """Can't currently create a file in a file""" )
def subDir(self, name,*arguments,**namedarguments):
"""Create a new subdirectory path within this directory"""
if self.isDir():
return self.join( name )
else:
raise ValueError( """Can't currently create a directory in a file""" )
def open(self, *arguments,**namedarguments):
"""Attempt to open a file path for reading/writing/appending
returns file( self, *arguments, **namedarguments) for the moment
might return a file sub-class eventually
"""
return file( self, *arguments, **namedarguments)
### "Somday" functionality
def mimeType( self ):
"""Attempt to determine the platform-specific mime type mapping for this path
XXX Only source I know with this info is wxPython
"""
def association (self):
"""Attempt to determine the platform-specific application association for the path
XXX This is going to be a guess on most platforms I'd assume
Windows -- assoc + ftype, or a registry access
Mac -- resource fork of the file
"""
def start (self):
"""Attempt to start the system's default application for this file
This is a serious security consideration, but it's something people
are wanting to do all the time, not sure where to stand on it.
"""
def touch( self ):
"""Attempt to update times on the path"""

View file

@ -0,0 +1,116 @@
import unittest, os
from basictypes.vfs import path
class NTFilePathTests(unittest.TestCase):
"""Note: these tests are Windows-specific"""
def testRepr( self ):
"""Test representation is as expected"""
value = path( "test" )
result = repr( value )
expected = "%s(%s)"%( value.__class__.__name__, repr(str(value)))
self.failUnlessEqual( result, expected, """Got:\n%s\t\nExpected:\n\t%s"""% (result, expected))
def testSimpleRelative( self ):
"""Test a simple relative path for basic operations"""
fileName = "test.tmp"
testFile = path(fileName)
assert testFile == fileName,"""Path did not match an equal string"""
assert str(testFile) == fileName,"""str(Path) did not match an equal string"""
assert not testFile.isAbsolute(),"""relative path declared itself absolute"""
assert not testFile.isRoot(),"""non-root path declared itself root"""
assert testFile.baseOnly(),"""base file system path declared itself non-base"""
open( fileName,'w' ).write( 'ha' )
assert testFile.exists(),"""could not create file in current working directory, or exists method failure"""
assert testFile.parent() == os.getcwd(),"""file created in current working directory does not report current working directory as parent"""
assert testFile.size() == 2,"""file with two bytes written reports size other than 2, possible bug in size (or data writing)"""
open( testFile, 'w').write( 'ham' )
assert testFile.exists(),"""could not create file in current working directory, or exists method failure"""
assert testFile.parent() == os.getcwd(),"""file created in current working directory does not report current working directory as parent"""
assert testFile.size() == 3,"""file with 3 bytes written reports size other than 3, possible bug in size (or data writing)"""
def testFullySpecified (self):
"""Test a fully specified file path"""
# now a fully-specified path
fileName = "c:\\test.tmp"
testFile = path(fileName)
assert testFile == fileName,"""Path did not match an equal string"""
assert str(testFile) == fileName,"""str(Path) did not match an equal string"""
assert testFile.isAbsolute(),"""absolute path declared itself relative"""
assert not testFile.isRoot(),"""root path declared itself non-root"""
assert testFile.baseOnly(),"""base file system path declared itself non-base"""
result = testFile.parent()
assert result == "c:\\","""parent reported as %s"""% (result)
assert result == path( "c:\\" ),"""parent reported as %s"""% (result)
assert testFile.isChild( "c:\\" )
assert testFile.drive() == 'c:\\', "got %s"%( repr(testFile.drive()))
assert testFile.root() == 'c:\\'
assert path( "c:\\" ).isParent( testFile )
def testRoot(self):
"""Test a root for the file system"""
# test a real root
roots = [path("c:\\"), path( r"\\Raistlin\c")]
for root in roots:
assert root.isRoot()
assert root.parent() == None
assert root.root() == root
assert path("c:\\").unc() == None
assert path("c:\\").drive() == "c:\\"
assert path("c:\\temp").drive() == "c:\\"
assert path("c:\\temp").root() == "c:\\"
assert path("c:\\temp").drive()[-1] == os.sep
assert path(r"\\Raistlin\c").drive() == None
assert path(r"\\Raistlin\c").unc() == r"\\raistlin\c"
assert path(r"\\Raistlin\c").parent() == None
assert path(r"\\Raistlin\c").root() == r"\\raistlin\c"
def testFragments (self):
"""Test ability to break paths into fragments"""
assert path(
"p:\\temp\\this\\that\\thos\\test.tmp"
).fragments() == [ 'p:\\', 'temp','this','that','thos','test.tmp']
assert path(
"c:"
).fragments() == [ 'c:\\']
assert path(
"p:\\temp\\this\\that\\thos\\test.tmp"
).parents() == [
'p:\\',
'p:\\temp',
'p:\\temp\\this',
'p:\\temp\\this\\that',
'p:\\temp\\this\\that\\thos',
], "Got: %s"%( path(
"p:\\temp\\this\\that\\thos\\test.tmp"
).parents() )
def testWalk (self):
"""Test three-method walking functions"""
dir = path(
"p:\\temp"
)
assert dir.join('test') == "p:\\temp\\test"
result = []
# need a testing directory for this to be automated...
dir.walk( result.append )
def testFileDirectoryOperations (self):
"""Test file and/or directory-specific operations"""
dir = path(
"p:\\temp\\this\\that"
)
try:
dir.walk( file=os.remove, post= os.rmdir)
except OSError, err:
print 'failed to remove', err
dir.createDirectory( mode = 0777 )
f = dir+'test.txt'
f.open('w').write( 'testing' )
assert f.exists()
assert f.open('r').read() == 'testing'
def getSuite():
return unittest.makeSuite(NTFilePathTests,'test')
if __name__ == "__main__":
unittest.main(defaultTest="getSuite")

View file

@ -0,0 +1 @@
"""wxPython data-type models"""

View file

@ -0,0 +1,59 @@
"""wxPython colour data-type definition
"""
import wx
from basictypes import datatypedefinition, registry
#from basictypes.wxtypes import wxcopyreg
from wxPython.lib import colourdb
COLOUR_DB_INITIALISED = 0
__all__ = ( "wxColour_DT", )
class wxColour_DT( datatypedefinition.BaseType_DT ):
"""Colour data-modelling type stand-in"""
dataType = "wx.colour"
baseType = wx.Colour
def coerce(cls, value):
"""Attempt to convert the given value to a wx.Colour
Accepted Values:
wx.Colour(Ptr)
'#FFFFFF' style strings
'black' string colour names
3-tuple or 3-list of 0-255 integers
None -- (gives black)
"""
if cls.check( value ):
return value
elif isinstance( value, (str,unicode) ):
if value and value[0] == '#':
rest = value[1:]
if rest:
value = int( rest, 16)
return wx.Colour( value >> 16 & 255, value >> 8 & 255, value & 255 )
else:
return wx.Colour( 0,0,0)
else:
try:
obj = wx.Colour( value )
except (ValueError,TypeError):
global COLOUR_DB_INITIALISED
if not COLOUR_DB_INITIALISED:
COLOUR_DB_INITIALISED = 1
colourdb.updateColourDB()
obj = wx.NamedColour( value )
if not obj.Ok():
raise ValueError( """Unrecognised string value %r for Colour value"""%(value))
elif isinstance( value, (tuple,list) ):
if len(value) == 3:
obj = wx.Colour( *value )
else:
raise ValueError( """Unable to create wx.Colour from %r, incorrect length"""%( value ))
elif value is None:
return wx.Colour( 0,0,0)
else:
raise TypeError( """Unable to convert value %r (type %s) to wx.Colour object"""%( value, type(value)))
return obj
coerce = classmethod( coerce )
registry.registerDT( wx.Colour, wxColour_DT)
registry.registerDT( wx.ColourPtr, wxColour_DT)

View file

@ -0,0 +1,13 @@
"""Data-type definition for wxPython font class"""
from wxPython.wx import *
from basictypes import datatypedefinition, registry
##from basictypes.wx import wxcopyreg
__all__ = ( "wxFont_DT", )
class wxFont_DT( datatypedefinition.BaseType_DT ):
"""Data-type definition for wxPython font class"""
dataType = "wx.font"
baseType = wxFontPtr
registry.registerDT( wxFontPtr, wxFont_DT)
registry.registerDT( wxFont, wxFont_DT)

View file

@ -0,0 +1,212 @@
"""wxPython colour data-type definition
"""
import wx
from basictypes import datatypedefinition, enumeration, registry
##from basictypes.wx import wxcopyreg
from basicproperty import basic
__all__ = ( "wxPen_DT", "PenStyleProperty", "PenCapProperty")
class wxPen(wx.Pen):
"""A somewhat easier-to-use Pen class for use with basictypes"""
dataType = "wx.pen"
coreAttributes = ('colour','width','style','cap','join',)
extraAttributes = ('stipple','dashes')
def __init__(
self,
colour="BLACK",
width=1,
style=wx.SOLID,
cap=wx.CAP_ROUND,
join=wx.JOIN_ROUND,
stipple=None,
dashes=None,
):
"""Initialize the wxPen object
colour -- wxColour specifier, a wxColour, a named colour
or a #ffffff formatted string value
width -- pen-width in pixels
style -- one of the wxPen style constants
wxSOLID
wxVERTICAL_HATCH
wxUSER_DASH
wxCROSSDIAG_HATCH
wxHORIZONTAL_HATCH
wxSTIPPLE
wxBDIAGONAL_HATCH
wxFDIAGONAL_HATCH
wxDOT_DASH
wxSHORT_DASH
wxLONG_DASH
wxCROSS_HATCH
wxTRANSPARENT
cap -- one of the wxPen cap constants
wxCAP_ROUND
wxCAP_BUTT
wxCAP_PROJECTING
join -- one of the wxPen join constants
wxJOIN_BEVEL
wxJOIN_MITER
wxJOIN_ROUND
stipple -- when style == wxSTIPPLE, a bitmap used to
control the drawing style
dashes -- when style == wxUSER_DASH, an array used to
control the drawing style
XXX what is the array of? lengths? I assume it's
just a python list of something, but I don't
know what.
"""
if isinstance( style, enumeration.Enumeration ):
style = style.value()
wx.Pen.__init__( self, colour, width, style )
if isinstance( join, enumeration.Enumeration ):
join = join.value()
self.SetJoin( join )
if isinstance( cap, enumeration.Enumeration ):
cap = cap.value()
self.SetCap( cap )
if style == wx.STIPPLE and stipple is not None:
self.SetStipple( stipple )
elif style == wx.USER_DASH and dashes is not None:
self.SetDashes( dashes )
def coreValues( self ):
"""Get the core values for this instance"""
return dict([
(attr,getattr(self,'Get%s'%attr.capitalize())())
for attr in self.coreAttributes
])
def __repr__( self ):
"""Get a nice debugging representation of this object"""
v = self.coreValues()
v = ", ".join([
'%s=%r'%(attr,v.get(attr))
for attr in self.coreAttributes
if v.get(attr) is not None
])
return "%s(%s)"%( self.__class__.__name__, v)
def __eq__( self, other ):
"""Compare our core values to pen defined in other"""
if not isinstance( other, wx.Pen):
other = self.__class__.coerce( other )
a,b = self.coreValues(), other.coreValues()
if a != b:
return 0
# possibility of a stipple or dashes type diff
if a['style'] == wx.STIPPLE:
return self.GetStipple() == other.GetStipple()
elif a['style'] == wx.USER_DASH:
return self.GetDashes() == other.GetDashes()
else:
return 1
def check( cls, value ):
"""Check that value is a wxPen instance"""
return isinstance( value, cls )
check = classmethod( check )
def coerce( cls, value ):
"""Coerce value to an instance of baseType
Accepted:
object: w/ style, colour and width props (cap, join, and stipple optional)
tuple (colour,width,style,cap,join,stipple)
dict w/ names
"""
if cls.check( value ):
return value
if isinstance( value, (tuple,list)):
return cls( *value )
elif isinstance( value, dict ):
return cls( **value )
else:
set = {}
for attribute in cls.coreAttributes+cls.extraAttributes:
method = 'Get%s'%(attribute.capitalize())
if hasattr( value, attribute ):
set[attribute] = getattr(value,attribute)
elif hasattr( value, method):
set[attribute] = getattr(value,method)()
return cls( **set )
coerce = classmethod( coerce )
registry.registerDT( wx.PenPtr, wxPen)
def defaultPen( ):
return wx.BLACK_PEN
PenStyleSet = enumeration.EnumerationSet()
PenStyleSet.new(name='wxSHORT_DASH',value=wx.SHORT_DASH,friendlyName='Short Dash')
PenStyleSet.new(name='wxSOLID',value=wx.SOLID,friendlyName='Solid ')
PenStyleSet.new(name='wxCROSS_HATCH',value=wx.CROSS_HATCH,friendlyName='Cross-Hatching')
PenStyleSet.new(name='wxVERTICAL_HATCH',value=wx.VERTICAL_HATCH,friendlyName='Vertical Hatching')
PenStyleSet.new(name='wxFDIAGONAL_HATCH',value=wx.FDIAGONAL_HATCH,friendlyName='Forward Diagonal Hatching')
PenStyleSet.new(name='wxLONG_DASH',value=wx.LONG_DASH,friendlyName='Long Dash')
PenStyleSet.new(name='wxUSER_DASH',value=wx.USER_DASH,friendlyName='User Defined Dash')
PenStyleSet.new(name='wxCROSSDIAG_HATCH',value=wx.CROSSDIAG_HATCH,friendlyName='Cross-Diagonal Hatching')
PenStyleSet.new(name='wxHORIZONTAL_HATCH',value=wx.HORIZONTAL_HATCH,friendlyName='Horizontal Hatching')
PenStyleSet.new(name='wxSTIPPLE',value=wx.STIPPLE,friendlyName='Stippled')
PenStyleSet.new(name='wxBDIAGONAL_HATCH',value=wx.BDIAGONAL_HATCH,friendlyName='Diagonal Hatching')
PenStyleSet.new(name='wxTRANSPARENT',value=wx.TRANSPARENT,friendlyName='Transparent')
PenStyleSet.new(name='wxDOT_DASH',value=wx.DOT_DASH,friendlyName='Dot Dash')
PenCapSet = enumeration.EnumerationSet()
PenCapSet.new(name='wxCAP_BUTT',value=wx.CAP_BUTT,friendlyName='Flat')
PenCapSet.new(name='wxCAP_PROJECTING',value=wx.CAP_PROJECTING,friendlyName='Projecting')
PenCapSet.new(name='wxCAP_ROUND',value=wx.CAP_ROUND,friendlyName='Rounded')
PenJoinSet = enumeration.EnumerationSet()
PenJoinSet.new(name='wxJOIN_BEVEL',value=wx.JOIN_BEVEL,friendlyName='Bevel')
PenJoinSet.new(name='wxJOIN_MITER',value=wx.JOIN_MITER,friendlyName='Miter')
PenJoinSet.new(name='wxJOIN_ROUND',value=wx.JOIN_ROUND,friendlyName='Round')
class PenStyle(enumeration.Enumeration):
"""Enumeration representing a pen-drawing style"""
set = PenStyleSet
dataType = enumeration.Enumeration.dataType+'.penstyle'
class PenCap(enumeration.Enumeration):
"""Enumeration representing a pen-cap style"""
set = PenCapSet
dataType = enumeration.Enumeration.dataType+'.pencap'
class PenJoin(enumeration.Enumeration):
"""Enumeration representing a pen-join style"""
set = PenJoinSet
dataType = enumeration.Enumeration.dataType+'.penjoin'
##class PenStandIn( propertied.Propertied ):
## """Stand-in object for editing (immutable) wxPen values"""
## style = basic.BasicProperty(
## 'style', """The line style for the pen""",
## friendlyName = """Line Style""",
## baseType = PenStyle,
## defaultValue = 'wxSOLID',
## )
## cap = basic.BasicProperty(
## 'cap', """The cap (end-of-line) style for the pen""",
## friendlyName = """Cap Style""",
## baseType = PenCap,
## defaultValue = 'wxCAP_ROUND',
## )
## join = basic.BasicProperty(
## 'join', """The cap (end-of-line) style for the pen""",
## friendlyName = """Join Style""",
## baseType = PenJoin,
## defaultValue = 'wxJOIN_ROUND',
## )
## colour = common.ColourProperty(
## "colour", """The pen colour""",
## friendlyName = "Colour",
## defaultValue = (0,0,0),
## )
## width = common.IntegerProperty(
## "width", """The line width of the pen""",
## defaultValue = 1,
## )

View file

@ -0,0 +1,131 @@
"""wxcopyreg -- functions for storing/restoring simple wxPython data types to pickle-friendly formats
importing this module installs the functions automatically!
"""
import pickle, zlib
from wxPython.wx import *
##
def bind( classObject, outFunction, inFunction ):
"""Bind get and set state for the classObject"""
classObject.__getstate__ = outFunction
classObject.__setstate__ = inFunction
def wxColourOut( value ):
return value.Red(), value.Green(), value.Blue()
def wxColourIn( self, args ):
self.this = apply(gdic.new_wxColour,args)
self.thisown = 1
bind( wxColourPtr, wxColourOut, wxColourIn )
def wxFontOut( value ):
return (
value.GetPointSize(),
value.GetFamily(),
value.GetStyle(),
value.GetWeight(),
value.GetUnderlined(),
value.GetFaceName(),
# note that encoding is missing...
)
def wxFontIn( self, args ):
self.this = apply(fontsc.new_wxFont,args)
self.thisown = 1
bind( wxFontPtr, wxFontOut, wxFontIn )
def wxPenOut( value ):
colour = value.GetColour()
return (
(
colour.Red(),
colour.Green(),
colour.Blue()
),
(
value.GetWidth(),
value.GetStyle(),
),
(
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## value.GetStipple(),
value.GetJoin(),
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## value.GetDashes(),
value.GetCap(),
),
)
def wxPenIn( self, (colour, init, props) ):
colour = wxColour( *colour )
self.this = apply(gdic.new_wxPen,(colour,)+init)
self.thisown = 1
for prop, function in map( None, props, (
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## self.SetStipple,
self.SetJoin,
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## self.SetDashes,
self.SetCap
)):
function( prop )
def wxPyPenIn( self, (colour, init, props) ):
colour = wxColour( *colour )
self.this = apply(gdic.new_wxPyPen,(colour,)+init)
self.thisown = 1
for prop, function in map( None, props, (
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## self.SetStipple,
self.SetJoin,
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## self.SetDashes,
self.SetCap
)):
function( prop )
bind( wxPenPtr, wxPenOut, wxPenIn )
bind( wxPyPenPtr, wxPenOut, wxPyPenIn )
def wxImageOut( value ):
width,height = value.GetWidth(), value.GetHeight()
data = value.GetData()
data = zlib.compress( data )
return ( width, height, data )
def wxImageIn( self, (width, height, data) ):
self.this = apply(imagec.wxEmptyImage,(width,height))
self.thisown = 1
self.SetData( zlib.decompress( data) )
bind( wxImagePtr, wxImageOut, wxImageIn )
def test():
for o in [
wxColour( 23,23,23),
wxFont( 12, wxMODERN, wxNORMAL, wxNORMAL ),
wxPen(wxColour( 23,23,23),1,wxSOLID),
wxImage( 'test.jpg', wxBITMAP_TYPE_ANY ),
]:
o2 = pickle.loads(pickle.dumps(o))
print o2
if __name__ == "__main__":
wxInitAllImageHandlers()
test()

View file

@ -0,0 +1 @@
"""wxPython data-type models"""

View file

@ -0,0 +1,59 @@
"""wxPython colour data-type definition
"""
import wx
from basictypes import datatypedefinition, registry
#from basictypes.wxtypes import wxcopyreg
from wxPython.lib import colourdb
COLOUR_DB_INITIALISED = 0
__all__ = ( "wxColour_DT", )
class wxColour_DT( datatypedefinition.BaseType_DT ):
"""Colour data-modelling type stand-in"""
dataType = "wx.colour"
baseType = wx.Colour
def coerce(cls, value):
"""Attempt to convert the given value to a wx.Colour
Accepted Values:
wx.Colour(Ptr)
'#FFFFFF' style strings
'black' string colour names
3-tuple or 3-list of 0-255 integers
None -- (gives black)
"""
if cls.check( value ):
return value
elif isinstance( value, (str,unicode) ):
if value and value[0] == '#':
rest = value[1:]
if rest:
value = int( rest, 16)
return wx.Colour( value >> 16 & 255, value >> 8 & 255, value & 255 )
else:
return wx.Colour( 0,0,0)
else:
try:
obj = wx.Colour( value )
except (ValueError,TypeError):
global COLOUR_DB_INITIALISED
if not COLOUR_DB_INITIALISED:
COLOUR_DB_INITIALISED = 1
colourdb.updateColourDB()
obj = wx.NamedColour( value )
if not obj.Ok():
raise ValueError( """Unrecognised string value %r for Colour value"""%(value))
elif isinstance( value, (tuple,list) ):
if len(value) == 3:
obj = wx.Colour( *value )
else:
raise ValueError( """Unable to create wx.Colour from %r, incorrect length"""%( value ))
elif value is None:
return wx.Colour( 0,0,0)
else:
raise TypeError( """Unable to convert value %r (type %s) to wx.Colour object"""%( value, type(value)))
return obj
coerce = classmethod( coerce )
registry.registerDT( wx.Colour, wxColour_DT)
registry.registerDT( wx.ColourPtr, wxColour_DT)

View file

@ -0,0 +1,13 @@
"""Data-type definition for wxPython font class"""
from wxPython.wx import *
from basictypes import datatypedefinition, registry
##from basictypes.wx import wxcopyreg
__all__ = ( "wxFont_DT", )
class wxFont_DT( datatypedefinition.BaseType_DT ):
"""Data-type definition for wxPython font class"""
dataType = "wx.font"
baseType = wxFontPtr
registry.registerDT( wxFontPtr, wxFont_DT)
registry.registerDT( wxFont, wxFont_DT)

View file

@ -0,0 +1,212 @@
"""wxPython colour data-type definition
"""
import wx
from basictypes import datatypedefinition, enumeration, registry
##from basictypes.wx import wxcopyreg
from basicproperty import basic
__all__ = ( "wxPen_DT", "PenStyleProperty", "PenCapProperty")
class wxPen(wx.Pen):
"""A somewhat easier-to-use Pen class for use with basictypes"""
dataType = "wx.pen"
coreAttributes = ('colour','width','style','cap','join',)
extraAttributes = ('stipple','dashes')
def __init__(
self,
colour="BLACK",
width=1,
style=wx.SOLID,
cap=wx.CAP_ROUND,
join=wx.JOIN_ROUND,
stipple=None,
dashes=None,
):
"""Initialize the wxPen object
colour -- wxColour specifier, a wxColour, a named colour
or a #ffffff formatted string value
width -- pen-width in pixels
style -- one of the wxPen style constants
wxSOLID
wxVERTICAL_HATCH
wxUSER_DASH
wxCROSSDIAG_HATCH
wxHORIZONTAL_HATCH
wxSTIPPLE
wxBDIAGONAL_HATCH
wxFDIAGONAL_HATCH
wxDOT_DASH
wxSHORT_DASH
wxLONG_DASH
wxCROSS_HATCH
wxTRANSPARENT
cap -- one of the wxPen cap constants
wxCAP_ROUND
wxCAP_BUTT
wxCAP_PROJECTING
join -- one of the wxPen join constants
wxJOIN_BEVEL
wxJOIN_MITER
wxJOIN_ROUND
stipple -- when style == wxSTIPPLE, a bitmap used to
control the drawing style
dashes -- when style == wxUSER_DASH, an array used to
control the drawing style
XXX what is the array of? lengths? I assume it's
just a python list of something, but I don't
know what.
"""
if isinstance( style, enumeration.Enumeration ):
style = style.value()
wx.Pen.__init__( self, colour, width, style )
if isinstance( join, enumeration.Enumeration ):
join = join.value()
self.SetJoin( join )
if isinstance( cap, enumeration.Enumeration ):
cap = cap.value()
self.SetCap( cap )
if style == wx.STIPPLE and stipple is not None:
self.SetStipple( stipple )
elif style == wx.USER_DASH and dashes is not None:
self.SetDashes( dashes )
def coreValues( self ):
"""Get the core values for this instance"""
return dict([
(attr,getattr(self,'Get%s'%attr.capitalize())())
for attr in self.coreAttributes
])
def __repr__( self ):
"""Get a nice debugging representation of this object"""
v = self.coreValues()
v = ", ".join([
'%s=%r'%(attr,v.get(attr))
for attr in self.coreAttributes
if v.get(attr) is not None
])
return "%s(%s)"%( self.__class__.__name__, v)
def __eq__( self, other ):
"""Compare our core values to pen defined in other"""
if not isinstance( other, wx.Pen):
other = self.__class__.coerce( other )
a,b = self.coreValues(), other.coreValues()
if a != b:
return 0
# possibility of a stipple or dashes type diff
if a['style'] == wx.STIPPLE:
return self.GetStipple() == other.GetStipple()
elif a['style'] == wx.USER_DASH:
return self.GetDashes() == other.GetDashes()
else:
return 1
def check( cls, value ):
"""Check that value is a wxPen instance"""
return isinstance( value, cls )
check = classmethod( check )
def coerce( cls, value ):
"""Coerce value to an instance of baseType
Accepted:
object: w/ style, colour and width props (cap, join, and stipple optional)
tuple (colour,width,style,cap,join,stipple)
dict w/ names
"""
if cls.check( value ):
return value
if isinstance( value, (tuple,list)):
return cls( *value )
elif isinstance( value, dict ):
return cls( **value )
else:
set = {}
for attribute in cls.coreAttributes+cls.extraAttributes:
method = 'Get%s'%(attribute.capitalize())
if hasattr( value, attribute ):
set[attribute] = getattr(value,attribute)
elif hasattr( value, method):
set[attribute] = getattr(value,method)()
return cls( **set )
coerce = classmethod( coerce )
registry.registerDT( wx.PenPtr, wxPen)
def defaultPen( ):
return wx.BLACK_PEN
PenStyleSet = enumeration.EnumerationSet()
PenStyleSet.new(name='wxSHORT_DASH',value=wx.SHORT_DASH,friendlyName='Short Dash')
PenStyleSet.new(name='wxSOLID',value=wx.SOLID,friendlyName='Solid ')
PenStyleSet.new(name='wxCROSS_HATCH',value=wx.CROSS_HATCH,friendlyName='Cross-Hatching')
PenStyleSet.new(name='wxVERTICAL_HATCH',value=wx.VERTICAL_HATCH,friendlyName='Vertical Hatching')
PenStyleSet.new(name='wxFDIAGONAL_HATCH',value=wx.FDIAGONAL_HATCH,friendlyName='Forward Diagonal Hatching')
PenStyleSet.new(name='wxLONG_DASH',value=wx.LONG_DASH,friendlyName='Long Dash')
PenStyleSet.new(name='wxUSER_DASH',value=wx.USER_DASH,friendlyName='User Defined Dash')
PenStyleSet.new(name='wxCROSSDIAG_HATCH',value=wx.CROSSDIAG_HATCH,friendlyName='Cross-Diagonal Hatching')
PenStyleSet.new(name='wxHORIZONTAL_HATCH',value=wx.HORIZONTAL_HATCH,friendlyName='Horizontal Hatching')
PenStyleSet.new(name='wxSTIPPLE',value=wx.STIPPLE,friendlyName='Stippled')
PenStyleSet.new(name='wxBDIAGONAL_HATCH',value=wx.BDIAGONAL_HATCH,friendlyName='Diagonal Hatching')
PenStyleSet.new(name='wxTRANSPARENT',value=wx.TRANSPARENT,friendlyName='Transparent')
PenStyleSet.new(name='wxDOT_DASH',value=wx.DOT_DASH,friendlyName='Dot Dash')
PenCapSet = enumeration.EnumerationSet()
PenCapSet.new(name='wxCAP_BUTT',value=wx.CAP_BUTT,friendlyName='Flat')
PenCapSet.new(name='wxCAP_PROJECTING',value=wx.CAP_PROJECTING,friendlyName='Projecting')
PenCapSet.new(name='wxCAP_ROUND',value=wx.CAP_ROUND,friendlyName='Rounded')
PenJoinSet = enumeration.EnumerationSet()
PenJoinSet.new(name='wxJOIN_BEVEL',value=wx.JOIN_BEVEL,friendlyName='Bevel')
PenJoinSet.new(name='wxJOIN_MITER',value=wx.JOIN_MITER,friendlyName='Miter')
PenJoinSet.new(name='wxJOIN_ROUND',value=wx.JOIN_ROUND,friendlyName='Round')
class PenStyle(enumeration.Enumeration):
"""Enumeration representing a pen-drawing style"""
set = PenStyleSet
dataType = enumeration.Enumeration.dataType+'.penstyle'
class PenCap(enumeration.Enumeration):
"""Enumeration representing a pen-cap style"""
set = PenCapSet
dataType = enumeration.Enumeration.dataType+'.pencap'
class PenJoin(enumeration.Enumeration):
"""Enumeration representing a pen-join style"""
set = PenJoinSet
dataType = enumeration.Enumeration.dataType+'.penjoin'
##class PenStandIn( propertied.Propertied ):
## """Stand-in object for editing (immutable) wxPen values"""
## style = basic.BasicProperty(
## 'style', """The line style for the pen""",
## friendlyName = """Line Style""",
## baseType = PenStyle,
## defaultValue = 'wxSOLID',
## )
## cap = basic.BasicProperty(
## 'cap', """The cap (end-of-line) style for the pen""",
## friendlyName = """Cap Style""",
## baseType = PenCap,
## defaultValue = 'wxCAP_ROUND',
## )
## join = basic.BasicProperty(
## 'join', """The cap (end-of-line) style for the pen""",
## friendlyName = """Join Style""",
## baseType = PenJoin,
## defaultValue = 'wxJOIN_ROUND',
## )
## colour = common.ColourProperty(
## "colour", """The pen colour""",
## friendlyName = "Colour",
## defaultValue = (0,0,0),
## )
## width = common.IntegerProperty(
## "width", """The line width of the pen""",
## defaultValue = 1,
## )

View file

@ -0,0 +1,131 @@
"""wxcopyreg -- functions for storing/restoring simple wxPython data types to pickle-friendly formats
importing this module installs the functions automatically!
"""
import pickle, zlib
from wxPython.wx import *
##
def bind( classObject, outFunction, inFunction ):
"""Bind get and set state for the classObject"""
classObject.__getstate__ = outFunction
classObject.__setstate__ = inFunction
def wxColourOut( value ):
return value.Red(), value.Green(), value.Blue()
def wxColourIn( self, args ):
self.this = apply(gdic.new_wxColour,args)
self.thisown = 1
bind( wxColourPtr, wxColourOut, wxColourIn )
def wxFontOut( value ):
return (
value.GetPointSize(),
value.GetFamily(),
value.GetStyle(),
value.GetWeight(),
value.GetUnderlined(),
value.GetFaceName(),
# note that encoding is missing...
)
def wxFontIn( self, args ):
self.this = apply(fontsc.new_wxFont,args)
self.thisown = 1
bind( wxFontPtr, wxFontOut, wxFontIn )
def wxPenOut( value ):
colour = value.GetColour()
return (
(
colour.Red(),
colour.Green(),
colour.Blue()
),
(
value.GetWidth(),
value.GetStyle(),
),
(
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## value.GetStipple(),
value.GetJoin(),
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## value.GetDashes(),
value.GetCap(),
),
)
def wxPenIn( self, (colour, init, props) ):
colour = wxColour( *colour )
self.this = apply(gdic.new_wxPen,(colour,)+init)
self.thisown = 1
for prop, function in map( None, props, (
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## self.SetStipple,
self.SetJoin,
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## self.SetDashes,
self.SetCap
)):
function( prop )
def wxPyPenIn( self, (colour, init, props) ):
colour = wxColour( *colour )
self.this = apply(gdic.new_wxPyPen,(colour,)+init)
self.thisown = 1
for prop, function in map( None, props, (
#stipple is a bitmap, we don't currently have
#mechanisms for storing/restoring it, so ignore it
## self.SetStipple,
self.SetJoin,
# missing in the current wxPython pre-release
# should be available in wxPython 2.3.3 final
## self.SetDashes,
self.SetCap
)):
function( prop )
bind( wxPenPtr, wxPenOut, wxPenIn )
bind( wxPyPenPtr, wxPenOut, wxPyPenIn )
def wxImageOut( value ):
width,height = value.GetWidth(), value.GetHeight()
data = value.GetData()
data = zlib.compress( data )
return ( width, height, data )
def wxImageIn( self, (width, height, data) ):
self.this = apply(imagec.wxEmptyImage,(width,height))
self.thisown = 1
self.SetData( zlib.decompress( data) )
bind( wxImagePtr, wxImageOut, wxImageIn )
def test():
for o in [
wxColour( 23,23,23),
wxFont( 12, wxMODERN, wxNORMAL, wxNORMAL ),
wxPen(wxColour( 23,23,23),1,wxSOLID),
wxImage( 'test.jpg', wxBITMAP_TYPE_ANY ),
]:
o2 = pickle.loads(pickle.dumps(o))
print o2
if __name__ == "__main__":
wxInitAllImageHandlers()
test()

View file

@ -0,0 +1,55 @@
from xml.sax import saxutils
import locale
defaultEncoding = locale.getdefaultlocale()[-1]
class Generator( saxutils.XMLGenerator ):
"""Friendly generator for XML code"""
def __init__( self, out=None, encoding="utf-8"):
"""Initialise the generator
Just overrides the default encoding of the base-class
"""
super( self, Generator ).__init__( file, encoding )
def startElement( self, name, attributes=None ):
"""Start a new element with given attributes"""
super(Generator,self).startElement( name, self._fixAttributes(attributes) )
def _fixAttributes( self, attributes=None ):
"""Fix an attribute-set to be all unicode strings"""
if attributes is None:
attributes = {}
for key,value in attributes.items():
if not isinstance( value, (str,unicode)):
attributes[key] = unicode( value )
elif isinstance( value, str ):
attributes[key] = value.decode( defaultEncoding )
class Store( Generator ):
"""Store a set of objects to an XML representation"""
def __init__( self, *arguments, **named ):
"""Initialise the store"""
super( Store, self ).__init__( *arguments, **named )
self.classMapping = {
}
self.rClassMapping = {
}
self.todo = []
self.alreadyDone = {}
def classToElementName( self, classObject ):
"""Get the element name for a given object"""
name = classObject.__name__
if self.rClassMapping.has_key( name ):
return self.rClassMapping.get( name )
short = name.split('.')[-1]
count = 2
while self.classMapping.has_key( short ):
short = short + str(count)
count += 1
self.classMapping[ short ] = name
self.rClassMapping[ name ] = short
return short
def encodeInAttributes( self, property, client ):
"""Determine whether to encode this property as an element attribute"""
def handleObject( self, object ):
"""Produce code for a single object"""

138
resources/lib/gui.py Normal file
View file

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
import os
import sys
import base64
import xbmc
import xbmcgui
import transmissionrpc
from basictypes.bytes import Bytes
from repeater import Repeater
_ = sys.modules[ "__main__" ].__language__
__settings__ = xbmc.Settings(path=os.getcwd())
KEY_BUTTON_BACK = 275
KEY_KEYBOARD_ESC = 61467
class TransmissionGUI(xbmcgui.WindowXMLDialog):
def __init__(self, strXMLname, strFallbackPath, strDefaultName, bforeFallback=0):
params = {
'address': __settings__.getSetting('rpc_host'),
'port': __settings__.getSetting('rpc_port'),
'user': __settings__.getSetting('rpc_user'),
'password': __settings__.getSetting('rpc_password')
}
self.transmission = transmissionrpc.transmission.Client(**params)
self.list = {}
self.torrents = {}
def onInit(self):
self.updateTorrents()
self.repeater = Repeater(1.0, self.updateTorrents)
self.repeater.start()
def shutDown(self):
print "terminating repeater"
self.repeater.stop()
print "closing transmission gui"
self.close()
def updateTorrents(self):
list = self.getControl(20)
torrents = self.transmission.info()
for i, torrent in torrents.iteritems():
statusline = "[%(status)s] %(down)s down (%(pct).2f%%), %(up)s up (Ratio: %(ratio).2f)" % \
{'down': Bytes.format(torrent.downloadedEver), 'pct': torrent.progress, \
'up': Bytes.format(torrent.uploadedEver), 'ratio': torrent.ratio, \
'status': torrent.status}
if torrent.status is 'downloading':
statusline += " ETA: %(eta)s" % \
{'eta': torrent.eta}
if i not in self.list:
# Create a new list item
l = xbmcgui.ListItem(label=torrent.name, label2=statusline)
list.addItem(l)
self.list[i] = l
else:
# Update existing list item
l = self.list[i]
self.torrents = torrents
l.setLabel(torrent.name)
l.setLabel2(statusline)
l.setProperty('TorrentID', str(i))
l.setProperty('TorrentProgress', "%.2ff" % torrent.progress)
l.setInfo('torrent', torrent.fields)
l.setInfo('video', {'episode': int(torrent.progress)})
removed = [id for id in self.list.keys() if id not in torrents.keys()]
if len(removed) > 0:
# Clear torrents from the list that have been removed
for id in removed:
del self.list[id]
list.reset()
for id, item in self.list.iteritems():
list.addItem(item)
def onAction(self, action):
buttonCode = action.getButtonCode()
actionID = action.getId()
if (buttonCode == KEY_BUTTON_BACK or buttonCode == KEY_KEYBOARD_ESC):
self.shutDown()
def onClick(self, controlID):
list = self.getControl(20)
if (controlID == 11):
# Add torrent
d = xbmcgui.Dialog()
f = d.browse(1, _(0), 'files', '.torrent')
self.transmission.add_url(f)
if (controlID == 12):
# Remove selected torrent
item = list.getSelectedItem()
if item and xbmcgui.Dialog().yesno(_(0), 'Remove \'%s\'?' % self.torrents[int(item.getProperty('TorrentID'))].name):
remove_data = xbmcgui.Dialog().yesno(_(0), 'Remove data as well?')
self.transmission.remove(int(item.getProperty('TorrentID')), remove_data)
if (controlID == 13):
# Stop selected torrent
item = list.getSelectedItem()
if item:
self.transmission.stop(int(item.getProperty('TorrentID')))
if (controlID == 14):
# Start selected torrent
item = list.getSelectedItem()
if item:
t = int(item.getProperty('TorrentID'))
self.transmission.start(int(item.getProperty('TorrentID')))
if (controlID == 15):
# Stop all torrents
self.transmission.stop(self.torrents.keys())
if (controlID == 16):
# Start all torrents
self.transmission.start(self.torrents.keys())
if (controlID == 17):
# Exit button
self.shutDown()
if (controlID == 20):
return
# A torrent was chosen, show details
item = list.getSelectedItem()
w = TorrentInfoGUI("script-Transmission-main.xml",os.getcwd() ,"default")
w.setTorrent(int(item.getProperty('TorrentID')))
w.doModal()
del w
def onFocus(self, controlID):
pass
class TorrentInfoGUI(xbmcgui.WindowXMLDialog):
def __init__(self, strXMLname, strFallbackPath, strDefaultName, bforeFallback=0):
self.torrent_id = None
pass
def setTorrent(t_id):
self.torrent_id = t_id
def onInit(self):
pass
def onAction(self, action):
buttonCode = action.getButtonCode()
actionID = action.getId()
if (buttonCode == KEY_BUTTON_BACK or buttonCode == KEY_KEYBOARD_ESC):
self.close()
def onClick(self, controlID):
pass
def onFocus(self, controlID):
pass

41
resources/lib/repeater.py Normal file
View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import threading
import time
class Repeater:
def __init__(self, interval, action, arguments = []):
self.interval = interval
self.action = action
self.arguments = arguments
self.event = None
def start(self):
if self.event:
return
self.event = threading.Event()
self.thread = threading.Thread(target=Repeater.repeat, args=(self.event, self.interval, self.action, self.arguments))
self.thread.start()
def stop(self):
if not self.event:
return
self.event.set()
self.thread.join()
self.event = None
def repeat(cls, event, interval, action, arguments = []):
while True:
event.wait(interval)
if event.isSet():
break;
action(*arguments)
repeat = classmethod(repeat)
if __name__ == '__main__':
def foo(a, b):
print a, b
r = Repeater(1.0, foo, ['foo', 'bar'])
r.start()
time.sleep(10)
r.stop()

View file

@ -0,0 +1,318 @@
r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of
JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data
interchange format.
:mod:`simplejson` exposes an API familiar to users of the standard library
:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained
version of the :mod:`json` library contained in Python 2.6, but maintains
compatibility with Python 2.4 and Python 2.5 and (currently) has
significant performance advantages, even without using the optional C
extension for speedups.
Encoding basic Python object hierarchies::
>>> import simplejson as json
>>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}])
'["foo", {"bar": ["baz", null, 1.0, 2]}]'
>>> print json.dumps("\"foo\bar")
"\"foo\bar"
>>> print json.dumps(u'\u1234')
"\u1234"
>>> print json.dumps('\\')
"\\"
>>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)
{"a": 0, "b": 0, "c": 0}
>>> from StringIO import StringIO
>>> io = StringIO()
>>> json.dump(['streaming API'], io)
>>> io.getvalue()
'["streaming API"]'
Compact encoding::
>>> import simplejson as json
>>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':'))
'[1,2,3,{"4":5,"6":7}]'
Pretty printing::
>>> import simplejson as json
>>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4)
>>> print '\n'.join([l.rstrip() for l in s.splitlines()])
{
"4": 5,
"6": 7
}
Decoding JSON::
>>> import simplejson as json
>>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}]
>>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj
True
>>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar'
True
>>> from StringIO import StringIO
>>> io = StringIO('["streaming API"]')
>>> json.load(io)[0] == 'streaming API'
True
Specializing JSON object decoding::
>>> import simplejson as json
>>> def as_complex(dct):
... if '__complex__' in dct:
... return complex(dct['real'], dct['imag'])
... return dct
...
>>> json.loads('{"__complex__": true, "real": 1, "imag": 2}',
... object_hook=as_complex)
(1+2j)
>>> import decimal
>>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1')
True
Specializing JSON object encoding::
>>> import simplejson as json
>>> def encode_complex(obj):
... if isinstance(obj, complex):
... return [obj.real, obj.imag]
... raise TypeError(repr(o) + " is not JSON serializable")
...
>>> json.dumps(2 + 1j, default=encode_complex)
'[2.0, 1.0]'
>>> json.JSONEncoder(default=encode_complex).encode(2 + 1j)
'[2.0, 1.0]'
>>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j))
'[2.0, 1.0]'
Using simplejson.tool from the shell to validate and pretty-print::
$ echo '{"json":"obj"}' | python -m simplejson.tool
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m simplejson.tool
Expecting property name: line 1 column 2 (char 2)
"""
__version__ = '2.0.9'
__all__ = [
'dump', 'dumps', 'load', 'loads',
'JSONDecoder', 'JSONEncoder',
]
__author__ = 'Bob Ippolito <bob@redivi.com>'
from decoder import JSONDecoder
from encoder import JSONEncoder
_default_encoder = JSONEncoder(
skipkeys=False,
ensure_ascii=True,
check_circular=True,
allow_nan=True,
indent=None,
separators=None,
encoding='utf-8',
default=None,
)
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=None, indent=None, separators=None,
encoding='utf-8', default=None, **kw):
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
``.write()``-supporting file-like object).
If ``skipkeys`` is true then ``dict`` keys that are not basic types
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
will be skipped instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the some chunks written to ``fp``
may be ``unicode`` instances, subject to normal Python ``str`` to
``unicode`` coercion rules. Unless ``fp.write()`` explicitly
understands ``unicode`` (as in ``codecs.getwriter()``) this is likely
to cause an error.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``OverflowError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``)
in strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and object
members will be pretty-printed with that indent level. An indent level
of 0 will only insert newlines. ``None`` is the most compact representation.
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
then it will be used instead of the default ``(', ', ': ')`` separators.
``(',', ':')`` is the most compact JSON representation.
``encoding`` is the character encoding for str instances, default is UTF-8.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
``.default()`` method to serialize additional types), specify it with
the ``cls`` kwarg.
"""
# cached encoder
if (not skipkeys and ensure_ascii and
check_circular and allow_nan and
cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and not kw):
iterable = _default_encoder.iterencode(obj)
else:
if cls is None:
cls = JSONEncoder
iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii,
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
separators=separators, encoding=encoding,
default=default, **kw).iterencode(obj)
# could accelerate with writelines in some versions of Python, at
# a debuggability cost
for chunk in iterable:
fp.write(chunk)
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
allow_nan=True, cls=None, indent=None, separators=None,
encoding='utf-8', default=None, **kw):
"""Serialize ``obj`` to a JSON formatted ``str``.
If ``skipkeys`` is false then ``dict`` keys that are not basic types
(``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``)
will be skipped instead of raising a ``TypeError``.
If ``ensure_ascii`` is false, then the return value will be a
``unicode`` instance subject to normal Python ``str`` to ``unicode``
coercion rules instead of being escaped to an ASCII ``str``.
If ``check_circular`` is false, then the circular reference check
for container types will be skipped and a circular reference will
result in an ``OverflowError`` (or worse).
If ``allow_nan`` is false, then it will be a ``ValueError`` to
serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in
strict compliance of the JSON specification, instead of using the
JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``).
If ``indent`` is a non-negative integer, then JSON array elements and
object members will be pretty-printed with that indent level. An indent
level of 0 will only insert newlines. ``None`` is the most compact
representation.
If ``separators`` is an ``(item_separator, dict_separator)`` tuple
then it will be used instead of the default ``(', ', ': ')`` separators.
``(',', ':')`` is the most compact JSON representation.
``encoding`` is the character encoding for str instances, default is UTF-8.
``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.
To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the
``.default()`` method to serialize additional types), specify it with
the ``cls`` kwarg.
"""
# cached encoder
if (not skipkeys and ensure_ascii and
check_circular and allow_nan and
cls is None and indent is None and separators is None and
encoding == 'utf-8' and default is None and not kw):
return _default_encoder.encode(obj)
if cls is None:
cls = JSONEncoder
return cls(
skipkeys=skipkeys, ensure_ascii=ensure_ascii,
check_circular=check_circular, allow_nan=allow_nan, indent=indent,
separators=separators, encoding=encoding, default=default,
**kw).encode(obj)
_default_decoder = JSONDecoder(encoding=None, object_hook=None)
def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, **kw):
"""Deserialize ``fp`` (a ``.read()``-supporting file-like object containing
a JSON document) to a Python object.
If the contents of ``fp`` is encoded with an ASCII based encoding other
than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must
be specified. Encodings that are not ASCII based (such as UCS-2) are
not allowed, and should be wrapped with
``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode``
object and passed to ``loads()``
``object_hook`` is an optional function that will be called with the
result of any object literal decode (a ``dict``). The return value of
``object_hook`` will be used instead of the ``dict``. This feature
can be used to implement custom decoders (e.g. JSON-RPC class hinting).
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
kwarg.
"""
return loads(fp.read(),
encoding=encoding, cls=cls, object_hook=object_hook,
parse_float=parse_float, parse_int=parse_int,
parse_constant=parse_constant, **kw)
def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, **kw):
"""Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON
document) to a Python object.
If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding
other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name
must be specified. Encodings that are not ASCII based (such as UCS-2)
are not allowed and should be decoded to ``unicode`` first.
``object_hook`` is an optional function that will be called with the
result of any object literal decode (a ``dict``). The return value of
``object_hook`` will be used instead of the ``dict``. This feature
can be used to implement custom decoders (e.g. JSON-RPC class hinting).
``parse_float``, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
``parse_int``, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
``parse_constant``, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN, null, true, false.
This can be used to raise an exception if invalid JSON numbers
are encountered.
To use a custom ``JSONDecoder`` subclass, specify it with the ``cls``
kwarg.
"""
if (cls is None and encoding is None and object_hook is None and
parse_int is None and parse_float is None and
parse_constant is None and not kw):
return _default_decoder.decode(s)
if cls is None:
cls = JSONDecoder
if object_hook is not None:
kw['object_hook'] = object_hook
if parse_float is not None:
kw['parse_float'] = parse_float
if parse_int is not None:
kw['parse_int'] = parse_int
if parse_constant is not None:
kw['parse_constant'] = parse_constant
return cls(encoding=encoding, **kw).decode(s)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,354 @@
"""Implementation of JSONDecoder
"""
import re
import sys
import struct
from simplejson.scanner import make_scanner
try:
from simplejson._speedups import scanstring as c_scanstring
except ImportError:
c_scanstring = None
__all__ = ['JSONDecoder']
FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL
def _floatconstants():
_BYTES = '7FF80000000000007FF0000000000000'.decode('hex')
if sys.byteorder != 'big':
_BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1]
nan, inf = struct.unpack('dd', _BYTES)
return nan, inf, -inf
NaN, PosInf, NegInf = _floatconstants()
def linecol(doc, pos):
lineno = doc.count('\n', 0, pos) + 1
if lineno == 1:
colno = pos
else:
colno = pos - doc.rindex('\n', 0, pos)
return lineno, colno
def errmsg(msg, doc, pos, end=None):
# Note that this function is called from _speedups
lineno, colno = linecol(doc, pos)
if end is None:
#fmt = '{0}: line {1} column {2} (char {3})'
#return fmt.format(msg, lineno, colno, pos)
fmt = '%s: line %d column %d (char %d)'
return fmt % (msg, lineno, colno, pos)
endlineno, endcolno = linecol(doc, end)
#fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})'
#return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end)
fmt = '%s: line %d column %d - line %d column %d (char %d - %d)'
return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end)
_CONSTANTS = {
'-Infinity': NegInf,
'Infinity': PosInf,
'NaN': NaN,
}
STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS)
BACKSLASH = {
'"': u'"', '\\': u'\\', '/': u'/',
'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t',
}
DEFAULT_ENCODING = "utf-8"
def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match):
"""Scan the string s for a JSON string. End is the index of the
character in s after the quote that started the JSON string.
Unescapes all valid JSON string escape sequences and raises ValueError
on attempt to decode an invalid string. If strict is False then literal
control characters are allowed in the string.
Returns a tuple of the decoded string and the index of the character in s
after the end quote."""
if encoding is None:
encoding = DEFAULT_ENCODING
chunks = []
_append = chunks.append
begin = end - 1
while 1:
chunk = _m(s, end)
if chunk is None:
raise ValueError(
errmsg("Unterminated string starting at", s, begin))
end = chunk.end()
content, terminator = chunk.groups()
# Content is contains zero or more unescaped string characters
if content:
if not isinstance(content, unicode):
content = unicode(content, encoding)
_append(content)
# Terminator is the end of string, a literal control character,
# or a backslash denoting that an escape sequence follows
if terminator == '"':
break
elif terminator != '\\':
if strict:
msg = "Invalid control character %r at" % (terminator,)
#msg = "Invalid control character {0!r} at".format(terminator)
raise ValueError(errmsg(msg, s, end))
else:
_append(terminator)
continue
try:
esc = s[end]
except IndexError:
raise ValueError(
errmsg("Unterminated string starting at", s, begin))
# If not a unicode escape sequence, must be in the lookup table
if esc != 'u':
try:
char = _b[esc]
except KeyError:
msg = "Invalid \\escape: " + repr(esc)
raise ValueError(errmsg(msg, s, end))
end += 1
else:
# Unicode escape sequence
esc = s[end + 1:end + 5]
next_end = end + 5
if len(esc) != 4:
msg = "Invalid \\uXXXX escape"
raise ValueError(errmsg(msg, s, end))
uni = int(esc, 16)
# Check for surrogate pair on UCS-4 systems
if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535:
msg = "Invalid \\uXXXX\\uXXXX surrogate pair"
if not s[end + 5:end + 7] == '\\u':
raise ValueError(errmsg(msg, s, end))
esc2 = s[end + 7:end + 11]
if len(esc2) != 4:
raise ValueError(errmsg(msg, s, end))
uni2 = int(esc2, 16)
uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00))
next_end += 6
char = unichr(uni)
end = next_end
# Append the unescaped character
_append(char)
return u''.join(chunks), end
# Use speedup if available
scanstring = c_scanstring or py_scanstring
WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS)
WHITESPACE_STR = ' \t\n\r'
def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
pairs = {}
# Use a slice to prevent IndexError from being raised, the following
# check will raise a more specific ValueError if the string is empty
nextchar = s[end:end + 1]
# Normally we expect nextchar == '"'
if nextchar != '"':
if nextchar in _ws:
end = _w(s, end).end()
nextchar = s[end:end + 1]
# Trivial empty object
if nextchar == '}':
return pairs, end + 1
elif nextchar != '"':
raise ValueError(errmsg("Expecting property name", s, end))
end += 1
while True:
key, end = scanstring(s, end, encoding, strict)
# To skip some function call overhead we optimize the fast paths where
# the JSON key separator is ": " or just ":".
if s[end:end + 1] != ':':
end = _w(s, end).end()
if s[end:end + 1] != ':':
raise ValueError(errmsg("Expecting : delimiter", s, end))
end += 1
try:
if s[end] in _ws:
end += 1
if s[end] in _ws:
end = _w(s, end + 1).end()
except IndexError:
pass
try:
value, end = scan_once(s, end)
except StopIteration:
raise ValueError(errmsg("Expecting object", s, end))
pairs[key] = value
try:
nextchar = s[end]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end]
except IndexError:
nextchar = ''
end += 1
if nextchar == '}':
break
elif nextchar != ',':
raise ValueError(errmsg("Expecting , delimiter", s, end - 1))
try:
nextchar = s[end]
if nextchar in _ws:
end += 1
nextchar = s[end]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end]
except IndexError:
nextchar = ''
end += 1
if nextchar != '"':
raise ValueError(errmsg("Expecting property name", s, end - 1))
if object_hook is not None:
pairs = object_hook(pairs)
return pairs, end
def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR):
values = []
nextchar = s[end:end + 1]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end:end + 1]
# Look-ahead for trivial empty array
if nextchar == ']':
return values, end + 1
_append = values.append
while True:
try:
value, end = scan_once(s, end)
except StopIteration:
raise ValueError(errmsg("Expecting object", s, end))
_append(value)
nextchar = s[end:end + 1]
if nextchar in _ws:
end = _w(s, end + 1).end()
nextchar = s[end:end + 1]
end += 1
if nextchar == ']':
break
elif nextchar != ',':
raise ValueError(errmsg("Expecting , delimiter", s, end))
try:
if s[end] in _ws:
end += 1
if s[end] in _ws:
end = _w(s, end + 1).end()
except IndexError:
pass
return values, end
class JSONDecoder(object):
"""Simple JSON <http://json.org> decoder
Performs the following translations in decoding by default:
+---------------+-------------------+
| JSON | Python |
+===============+===================+
| object | dict |
+---------------+-------------------+
| array | list |
+---------------+-------------------+
| string | unicode |
+---------------+-------------------+
| number (int) | int, long |
+---------------+-------------------+
| number (real) | float |
+---------------+-------------------+
| true | True |
+---------------+-------------------+
| false | False |
+---------------+-------------------+
| null | None |
+---------------+-------------------+
It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as
their corresponding ``float`` values, which is outside the JSON spec.
"""
def __init__(self, encoding=None, object_hook=None, parse_float=None,
parse_int=None, parse_constant=None, strict=True):
"""``encoding`` determines the encoding used to interpret any ``str``
objects decoded by this instance (utf-8 by default). It has no
effect when decoding ``unicode`` objects.
Note that currently only encodings that are a superset of ASCII work,
strings of other encodings should be passed in as ``unicode``.
``object_hook``, if specified, will be called with the result
of every JSON object decoded and its return value will be used in
place of the given ``dict``. This can be used to provide custom
deserializations (e.g. to support JSON-RPC class hinting).
``parse_float``, if specified, will be called with the string
of every JSON float to be decoded. By default this is equivalent to
float(num_str). This can be used to use another datatype or parser
for JSON floats (e.g. decimal.Decimal).
``parse_int``, if specified, will be called with the string
of every JSON int to be decoded. By default this is equivalent to
int(num_str). This can be used to use another datatype or parser
for JSON integers (e.g. float).
``parse_constant``, if specified, will be called with one of the
following strings: -Infinity, Infinity, NaN.
This can be used to raise an exception if invalid JSON numbers
are encountered.
"""
self.encoding = encoding
self.object_hook = object_hook
self.parse_float = parse_float or float
self.parse_int = parse_int or int
self.parse_constant = parse_constant or _CONSTANTS.__getitem__
self.strict = strict
self.parse_object = JSONObject
self.parse_array = JSONArray
self.parse_string = scanstring
self.scan_once = make_scanner(self)
def decode(self, s, _w=WHITESPACE.match):
"""Return the Python representation of ``s`` (a ``str`` or ``unicode``
instance containing a JSON document)
"""
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
end = _w(s, end).end()
if end != len(s):
raise ValueError(errmsg("Extra data", s, end, len(s)))
return obj
def raw_decode(self, s, idx=0):
"""Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning
with a JSON document) and return a 2-tuple of the Python
representation and the index in ``s`` where the document ended.
This can be used to decode a JSON document from a string that may
have extraneous data at the end.
"""
try:
obj, end = self.scan_once(s, idx)
except StopIteration:
raise ValueError("No JSON object could be decoded")
return obj, end

View file

@ -0,0 +1,440 @@
"""Implementation of JSONEncoder
"""
import re
try:
from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii
except ImportError:
c_encode_basestring_ascii = None
try:
from simplejson._speedups import make_encoder as c_make_encoder
except ImportError:
c_make_encoder = None
ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]')
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
HAS_UTF8 = re.compile(r'[\x80-\xff]')
ESCAPE_DCT = {
'\\': '\\\\',
'"': '\\"',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
}
for i in range(0x20):
#ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i))
ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,))
# Assume this produces an infinity on all machines (probably not guaranteed)
INFINITY = float('1e66666')
FLOAT_REPR = repr
def encode_basestring(s):
"""Return a JSON representation of a Python string
"""
def replace(match):
return ESCAPE_DCT[match.group(0)]
return '"' + ESCAPE.sub(replace, s) + '"'
def py_encode_basestring_ascii(s):
"""Return an ASCII-only JSON representation of a Python string
"""
if isinstance(s, str) and HAS_UTF8.search(s) is not None:
s = s.decode('utf-8')
def replace(match):
s = match.group(0)
try:
return ESCAPE_DCT[s]
except KeyError:
n = ord(s)
if n < 0x10000:
#return '\\u{0:04x}'.format(n)
return '\\u%04x' % (n,)
else:
# surrogate pair
n -= 0x10000
s1 = 0xd800 | ((n >> 10) & 0x3ff)
s2 = 0xdc00 | (n & 0x3ff)
#return '\\u{0:04x}\\u{1:04x}'.format(s1, s2)
return '\\u%04x\\u%04x' % (s1, s2)
return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"'
encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii
class JSONEncoder(object):
"""Extensible JSON <http://json.org> encoder for Python data structures.
Supports the following objects and types by default:
+-------------------+---------------+
| Python | JSON |
+===================+===============+
| dict | object |
+-------------------+---------------+
| list, tuple | array |
+-------------------+---------------+
| str, unicode | string |
+-------------------+---------------+
| int, long, float | number |
+-------------------+---------------+
| True | true |
+-------------------+---------------+
| False | false |
+-------------------+---------------+
| None | null |
+-------------------+---------------+
To extend this to recognize other objects, subclass and implement a
``.default()`` method with another method that returns a serializable
object for ``o`` if possible, otherwise it should call the superclass
implementation (to raise ``TypeError``).
"""
item_separator = ', '
key_separator = ': '
def __init__(self, skipkeys=False, ensure_ascii=True,
check_circular=True, allow_nan=True, sort_keys=False,
indent=None, separators=None, encoding='utf-8', default=None):
"""Constructor for JSONEncoder, with sensible defaults.
If skipkeys is false, then it is a TypeError to attempt
encoding of keys that are not str, int, long, float or None. If
skipkeys is True, such items are simply skipped.
If ensure_ascii is true, the output is guaranteed to be str
objects with all incoming unicode characters escaped. If
ensure_ascii is false, the output will be unicode object.
If check_circular is true, then lists, dicts, and custom encoded
objects will be checked for circular references during encoding to
prevent an infinite recursion (which would cause an OverflowError).
Otherwise, no such check takes place.
If allow_nan is true, then NaN, Infinity, and -Infinity will be
encoded as such. This behavior is not JSON specification compliant,
but is consistent with most JavaScript based encoders and decoders.
Otherwise, it will be a ValueError to encode such floats.
If sort_keys is true, then the output of dictionaries will be
sorted by key; this is useful for regression tests to ensure
that JSON serializations can be compared on a day-to-day basis.
If indent is a non-negative integer, then JSON array
elements and object members will be pretty-printed with that
indent level. An indent level of 0 will only insert newlines.
None is the most compact representation.
If specified, separators should be a (item_separator, key_separator)
tuple. The default is (', ', ': '). To get the most compact JSON
representation you should specify (',', ':') to eliminate whitespace.
If specified, default is a function that gets called for objects
that can't otherwise be serialized. It should return a JSON encodable
version of the object or raise a ``TypeError``.
If encoding is not None, then all input strings will be
transformed into unicode using that encoding prior to JSON-encoding.
The default is UTF-8.
"""
self.skipkeys = skipkeys
self.ensure_ascii = ensure_ascii
self.check_circular = check_circular
self.allow_nan = allow_nan
self.sort_keys = sort_keys
self.indent = indent
if separators is not None:
self.item_separator, self.key_separator = separators
if default is not None:
self.default = default
self.encoding = encoding
def default(self, o):
"""Implement this method in a subclass such that it returns
a serializable object for ``o``, or calls the base implementation
(to raise a ``TypeError``).
For example, to support arbitrary iterators, you could
implement default like this::
def default(self, o):
try:
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
return JSONEncoder.default(self, o)
"""
raise TypeError(repr(o) + " is not JSON serializable")
def encode(self, o):
"""Return a JSON string representation of a Python data structure.
>>> JSONEncoder().encode({"foo": ["bar", "baz"]})
'{"foo": ["bar", "baz"]}'
"""
# This is for extremely simple cases and benchmarks.
if isinstance(o, basestring):
if isinstance(o, str):
_encoding = self.encoding
if (_encoding is not None
and not (_encoding == 'utf-8')):
o = o.decode(_encoding)
if self.ensure_ascii:
return encode_basestring_ascii(o)
else:
return encode_basestring(o)
# This doesn't pass the iterator directly to ''.join() because the
# exceptions aren't as detailed. The list call should be roughly
# equivalent to the PySequence_Fast that ''.join() would do.
chunks = self.iterencode(o, _one_shot=True)
if not isinstance(chunks, (list, tuple)):
chunks = list(chunks)
return ''.join(chunks)
def iterencode(self, o, _one_shot=False):
"""Encode the given object and yield each string
representation as available.
For example::
for chunk in JSONEncoder().iterencode(bigobject):
mysocket.write(chunk)
"""
if self.check_circular:
markers = {}
else:
markers = None
if self.ensure_ascii:
_encoder = encode_basestring_ascii
else:
_encoder = encode_basestring
if self.encoding != 'utf-8':
def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding):
if isinstance(o, str):
o = o.decode(_encoding)
return _orig_encoder(o)
def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY):
# Check for specials. Note that this type of test is processor- and/or
# platform-specific, so do tests which don't depend on the internals.
if o != o:
text = 'NaN'
elif o == _inf:
text = 'Infinity'
elif o == _neginf:
text = '-Infinity'
else:
return _repr(o)
if not allow_nan:
raise ValueError(
"Out of range float values are not JSON compliant: " +
repr(o))
return text
if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys:
_iterencode = c_make_encoder(
markers, self.default, _encoder, self.indent,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, self.allow_nan)
else:
_iterencode = _make_iterencode(
markers, self.default, _encoder, self.indent, floatstr,
self.key_separator, self.item_separator, self.sort_keys,
self.skipkeys, _one_shot)
return _iterencode(o, 0)
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
## HACK: hand-optimized bytecode; turn globals into locals
False=False,
True=True,
ValueError=ValueError,
basestring=basestring,
dict=dict,
float=float,
id=id,
int=int,
isinstance=isinstance,
list=list,
long=long,
str=str,
tuple=tuple,
):
def _iterencode_list(lst, _current_indent_level):
if not lst:
yield '[]'
return
if markers is not None:
markerid = id(lst)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = lst
buf = '['
if _indent is not None:
_current_indent_level += 1
newline_indent = '\n' + (' ' * (_indent * _current_indent_level))
separator = _item_separator + newline_indent
buf += newline_indent
else:
newline_indent = None
separator = _item_separator
first = True
for value in lst:
if first:
first = False
else:
buf = separator
if isinstance(value, basestring):
yield buf + _encoder(value)
elif value is None:
yield buf + 'null'
elif value is True:
yield buf + 'true'
elif value is False:
yield buf + 'false'
elif isinstance(value, (int, long)):
yield buf + str(value)
elif isinstance(value, float):
yield buf + _floatstr(value)
else:
yield buf
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
for chunk in chunks:
yield chunk
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + (' ' * (_indent * _current_indent_level))
yield ']'
if markers is not None:
del markers[markerid]
def _iterencode_dict(dct, _current_indent_level):
if not dct:
yield '{}'
return
if markers is not None:
markerid = id(dct)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = dct
yield '{'
if _indent is not None:
_current_indent_level += 1
newline_indent = '\n' + (' ' * (_indent * _current_indent_level))
item_separator = _item_separator + newline_indent
yield newline_indent
else:
newline_indent = None
item_separator = _item_separator
first = True
if _sort_keys:
items = dct.items()
items.sort(key=lambda kv: kv[0])
else:
items = dct.iteritems()
for key, value in items:
if isinstance(key, basestring):
pass
# JavaScript is weakly typed for these, so it makes sense to
# also allow them. Many encoders seem to do something like this.
elif isinstance(key, float):
key = _floatstr(key)
elif key is True:
key = 'true'
elif key is False:
key = 'false'
elif key is None:
key = 'null'
elif isinstance(key, (int, long)):
key = str(key)
elif _skipkeys:
continue
else:
raise TypeError("key " + repr(key) + " is not a string")
if first:
first = False
else:
yield item_separator
yield _encoder(key)
yield _key_separator
if isinstance(value, basestring):
yield _encoder(value)
elif value is None:
yield 'null'
elif value is True:
yield 'true'
elif value is False:
yield 'false'
elif isinstance(value, (int, long)):
yield str(value)
elif isinstance(value, float):
yield _floatstr(value)
else:
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
for chunk in chunks:
yield chunk
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + (' ' * (_indent * _current_indent_level))
yield '}'
if markers is not None:
del markers[markerid]
def _iterencode(o, _current_indent_level):
if isinstance(o, basestring):
yield _encoder(o)
elif o is None:
yield 'null'
elif o is True:
yield 'true'
elif o is False:
yield 'false'
elif isinstance(o, (int, long)):
yield str(o)
elif isinstance(o, float):
yield _floatstr(o)
elif isinstance(o, (list, tuple)):
for chunk in _iterencode_list(o, _current_indent_level):
yield chunk
elif isinstance(o, dict):
for chunk in _iterencode_dict(o, _current_indent_level):
yield chunk
else:
if markers is not None:
markerid = id(o)
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
for chunk in _iterencode(o, _current_indent_level):
yield chunk
if markers is not None:
del markers[markerid]
return _iterencode

View file

@ -0,0 +1,65 @@
"""JSON token scanner
"""
import re
try:
from simplejson._speedups import make_scanner as c_make_scanner
except ImportError:
c_make_scanner = None
__all__ = ['make_scanner']
NUMBER_RE = re.compile(
r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?',
(re.VERBOSE | re.MULTILINE | re.DOTALL))
def py_make_scanner(context):
parse_object = context.parse_object
parse_array = context.parse_array
parse_string = context.parse_string
match_number = NUMBER_RE.match
encoding = context.encoding
strict = context.strict
parse_float = context.parse_float
parse_int = context.parse_int
parse_constant = context.parse_constant
object_hook = context.object_hook
def _scan_once(string, idx):
try:
nextchar = string[idx]
except IndexError:
raise StopIteration
if nextchar == '"':
return parse_string(string, idx + 1, encoding, strict)
elif nextchar == '{':
return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook)
elif nextchar == '[':
return parse_array((string, idx + 1), _scan_once)
elif nextchar == 'n' and string[idx:idx + 4] == 'null':
return None, idx + 4
elif nextchar == 't' and string[idx:idx + 4] == 'true':
return True, idx + 4
elif nextchar == 'f' and string[idx:idx + 5] == 'false':
return False, idx + 5
m = match_number(string, idx)
if m is not None:
integer, frac, exp = m.groups()
if frac or exp:
res = parse_float(integer + (frac or '') + (exp or ''))
else:
res = parse_int(integer)
return res, m.end()
elif nextchar == 'N' and string[idx:idx + 3] == 'NaN':
return parse_constant('NaN'), idx + 3
elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity':
return parse_constant('Infinity'), idx + 8
elif nextchar == '-' and string[idx:idx + 9] == '-Infinity':
return parse_constant('-Infinity'), idx + 9
else:
raise StopIteration
return _scan_once
make_scanner = c_make_scanner or py_make_scanner

View file

@ -0,0 +1,37 @@
r"""Command-line tool to validate and pretty-print JSON
Usage::
$ echo '{"json":"obj"}' | python -m simplejson.tool
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m simplejson.tool
Expecting property name: line 1 column 2 (char 2)
"""
import sys
import simplejson
def main():
if len(sys.argv) == 1:
infile = sys.stdin
outfile = sys.stdout
elif len(sys.argv) == 2:
infile = open(sys.argv[1], 'rb')
outfile = sys.stdout
elif len(sys.argv) == 3:
infile = open(sys.argv[1], 'rb')
outfile = open(sys.argv[2], 'wb')
else:
raise SystemExit(sys.argv[0] + " [infile [outfile]]")
try:
obj = simplejson.load(infile)
except ValueError, e:
raise SystemExit(e)
simplejson.dump(obj, outfile, sort_keys=True, indent=4)
outfile.write('\n')
if __name__ == '__main__':
main()

View file

@ -0,0 +1,80 @@
diff -rupN transmissionrpc-0.3/transmission.py transmissionrpc/transmission.py
--- transmissionrpc-0.3/transmission.py 2009-12-10 16:39:33.134130829 -0500
+++ transmissionrpc/transmission.py 2009-12-10 16:45:37.385258836 -0500
@@ -69,7 +69,7 @@ class Torrent(object):
wanted = self.fields['wanted']
index = 1
for item in zip(indicies, files, priorities, wanted):
- selected = True if item[3] else False
+ selected = bool(item[3])
priority = PRIORITY[item[2]]
result[item[0]] = {
'selected': selected,
@@ -252,29 +252,30 @@ class Client(object):
while True:
error_data = ""
try:
- self._debug_request(request)
- socket.setdefaulttimeout(10)
- if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
- response = urllib2.urlopen(request, timeout=60)
- else:
- response = urllib2.urlopen(request)
- break
- except urllib2.HTTPError, error:
- error_data = error.read()
- if error.code == 409:
- logger.info('Server responded with 409, trying to set session-id.')
- if request_count > 1:
- raise TransmissionError('Session ID negotiation failed.', error)
- if 'X-Transmission-Session-Id' in error.headers:
- self.sessionid = error.headers['X-Transmission-Session-Id']
- request.add_header('X-Transmission-Session-Id', self.sessionid)
+ try:
+ self._debug_request(request)
+ socket.setdefaulttimeout(10)
+ if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
+ response = urllib2.urlopen(request, timeout=60)
else:
- raise TransmissionError('Unknown conflict.', error)
- except urllib2.URLError, error:
- raise TransmissionError('Failed to connect to daemon.', error)
- except httplib.BadStatusLine, error:
- if (request_count > 1):
- raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
+ response = urllib2.urlopen(request)
+ break
+ except urllib2.HTTPError, error:
+ error_data = error.read()
+ if error.code == 409:
+ logger.info('Server responded with 409, trying to set session-id.')
+ if request_count > 1:
+ raise TransmissionError('Session ID negotiation failed.', error)
+ if 'X-Transmission-Session-Id' in error.headers:
+ self.sessionid = error.headers['X-Transmission-Session-Id']
+ request.add_header('X-Transmission-Session-Id', self.sessionid)
+ else:
+ raise TransmissionError('Unknown conflict.', error)
+ except urllib2.URLError, error:
+ raise TransmissionError('Failed to connect to daemon.', error)
+ except httplib.BadStatusLine, error:
+ if (request_count > 1):
+ raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
finally:
if error_data:
self._debug_response(error, error_data)
diff -rupN transmissionrpc-0.3/utils.py transmissionrpc/utils.py
--- transmissionrpc-0.3/utils.py 2009-12-10 16:39:33.134130829 -0500
+++ transmissionrpc/utils.py 2009-12-10 16:45:37.386133947 -0500
@@ -64,7 +64,10 @@ def rpc_bool(arg):
arg = bool(int(arg))
except:
arg = arg.lower() in [u'true', u'yes']
- return 1 if bool(arg) else 0
+ if bool(arg):
+ return 1
+ else:
+ return 0
TR_TYPE_MAP = {
'number' : int,

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# 2008-08, Erik Svensson <erik.public@gmail.com>
from constants import *
from transmission import TransmissionError, Torrent, Session, Client
__author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.3'
__copyright__ = u'Copyright (c) 2008 Erik Svensson'
__license__ = u'MIT'

View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import logging
logger = logging.getLogger('transmissionrpc')
logger.setLevel(logging.ERROR)
def mirror_dict(d):
d.update(dict((v, k) for k, v in d.iteritems()))
return d
DEFAULT_PORT = 9091
TR_STATUS_CHECK_WAIT = (1<<0)
TR_STATUS_CHECK = (1<<1)
TR_STATUS_DOWNLOAD = (1<<2)
TR_STATUS_SEED = (1<<3)
TR_STATUS_STOPPED = (1<<4)
STATUS = mirror_dict({
'check pending' : TR_STATUS_CHECK_WAIT,
'checking' : TR_STATUS_CHECK,
'downloading' : TR_STATUS_DOWNLOAD,
'seeding' : TR_STATUS_SEED,
'stopped' : TR_STATUS_STOPPED,
})
TR_PRI_LOW = -1
TR_PRI_NORMAL = 0
TR_PRI_HIGH = 1
PRIORITY = mirror_dict({
'low' : TR_PRI_LOW,
'normal' : TR_PRI_NORMAL,
'high' : TR_PRI_HIGH
})
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
RATIO_LIMIT = mirror_dict({
'global' : TR_RATIOLIMIT_GLOBAL,
'single' : TR_RATIOLIMIT_SINGLE,
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
})
# A note on argument maps
# These maps are used to verify *-set methods. The information is structured in
# a tree.
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# |
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# Arguments for torrent methods
TORRENT_ARGS = {
'get' : {
'activityDate': ('number', 1, None, None, None),
'addedDate': ('number', 1, None, None, None),
'announceResponse': ('string', 1, None, None, None),
'announceURL': ('string', 1, None, None, None),
'bandwidthPriority': ('number', 5, None, None, None),
'comment': ('string', 1, None, None, None),
'corruptEver': ('number', 1, None, None, None),
'creator': ('string', 1, None, None, None),
'dateCreated': ('number', 1, None, None, None),
'desiredAvailable': ('number', 1, None, None, None),
'doneDate': ('number', 1, None, None, None),
'downloadDir': ('string', 4, None, None, None),
'downloadedEver': ('number', 1, None, None, None),
'downloaders': ('number', 4, None, None, None),
'downloadLimit': ('number', 1, None, None, None),
'downloadLimited': ('boolean', 5, None, None, None),
'downloadLimitMode': ('number', 1, 5, None, None),
'error': ('number', 1, None, None, None),
'errorString': ('number', 1, None, None, None),
'eta': ('number', 1, None, None, None),
'files': ('array', 1, None, None, None),
'fileStats': ('array', 5, None, None, None),
'hashString': ('string', 1, None, None, None),
'haveUnchecked': ('number', 1, None, None, None),
'haveValid': ('number', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'id': ('number', 1, None, None, None),
'isPrivate': ('boolean', 1, None, None, None),
'lastAnnounceTime': ('number', 1, None, None, None),
'lastScrapeTime': ('number', 1, None, None, None),
'leechers': ('number', 1, None, None, None),
'leftUntilDone': ('number', 1, None, None, None),
'manualAnnounceTime': ('number', 1, None, None, None),
'maxConnectedPeers': ('number', 1, None, None, None),
'name': ('string', 1, None, None, None),
'nextAnnounceTime': ('number', 1, None, None, None),
'nextScrapeTime': ('number', 1, None, None, None),
'peer-limit': ('number', 5, None, None, None),
'peers': ('array', 2, None, None, None),
'peersConnected': ('number', 1, None, None, None),
'peersFrom': ('object', 1, None, None, None),
'peersGettingFromUs': ('number', 1, None, None, None),
'peersKnown': ('number', 1, None, None, None),
'peersSendingToUs': ('number', 1, None, None, None),
'percentDone': ('double', 5, None, None, None),
'pieces': ('string', 5, None, None, None),
'pieceCount': ('number', 1, None, None, None),
'pieceSize': ('number', 1, None, None, None),
'priorities': ('array', 1, None, None, None),
'rateDownload': ('number', 1, None, None, None),
'rateUpload': ('number', 1, None, None, None),
'recheckProgress': ('double', 1, None, None, None),
'scrapeResponse': ('string', 1, None, None, None),
'scrapeURL': ('string', 1, None, None, None),
'seeders': ('number', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'sizeWhenDone': ('number', 1, None, None, None),
'startDate': ('number', 1, None, None, None),
'status': ('number', 1, None, None, None),
'swarmSpeed': ('number', 1, None, None, None),
'timesCompleted': ('number', 1, None, None, None),
'trackers': ('array', 1, None, None, None),
'totalSize': ('number', 1, None, None, None),
'torrentFile': ('string', 5, None, None, None),
'uploadedEver': ('number', 1, None, None, None),
'uploadLimit': ('number', 1, None, None, None),
'uploadLimitMode': ('number', 1, 5, None, None),
'uploadLimited': ('boolean', 5, None, None, None),
'uploadRatio': ('double', 1, None, None, None),
'wanted': ('array', 1, None, None, None),
'webseeds': ('array', 1, None, None, None),
'webseedsSendingToUs': ('number', 1, None, None, None),
},
'set': {
'bandwidthPriority': ('number', 5, None, None, None),
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'ids': ('array', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
},
'add': {
'download-dir': ('string', 1, None, None, None),
'filename': ('string', 1, None, None, None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'metainfo': ('string', 1, None, None, None),
'paused': ('boolean', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
}
}
# Arguments for session methods
SESSION_ARGS = {
'get': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"blocklist-size": ('number', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, None),
"peer-limit-global": ('number', 5, None, None, None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, None),
"pex-enabled": ('boolean', 5, None, None, None),
"port": ('number', 1, 5, None, None),
"peer-port": ('number', 5, None, None, None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"rpc-version": ('number', 4, None, None, None),
"rpc-version-minimum": ('number', 4, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
"version": ('string', 3, None, None, None),
},
'set': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
"port": ('number', 1, 5, None, 'peer-port'),
"peer-port": ('number', 5, None, 'port', None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
},
}

View file

@ -0,0 +1,606 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import sys, os, time, datetime
import re
import httplib, urllib2, base64, socket
try:
import json
except ImportError:
import simplejson as json
from constants import *
from utils import *
class TransmissionError(Exception):
def __init__(self, message='', original=None):
Exception.__init__(self, message)
self.message = message
self.original = original
def __str__(self):
if self.original:
original_name = type(self.original).__name__
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
else:
return self.args
class Torrent(object):
"""
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
All fetched torrent fields are accessable through this class using attributes.
This class has a few convenience properties using the torrent data.
"""
def __init__(self, fields):
if 'id' not in fields:
raise ValueError('Torrent requires an id')
self.fields = {}
self.update(fields)
def __repr__(self):
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
def __str__(self):
return 'torrent %s' % self.fields['name']
def update(self, other):
"""Update the torrent data from a Transmission arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Torrent):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def files(self):
"""
Get list of files for this torrent. This function returns a dictionary with file information for each file.
"""
result = {}
if 'files' in self.fields:
indicies = xrange(len(self.fields['files']))
files = self.fields['files']
priorities = self.fields['priorities']
wanted = self.fields['wanted']
index = 1
for item in zip(indicies, files, priorities, wanted):
selected = bool(item[3])
priority = PRIORITY[item[2]]
result[item[0]] = {
'selected': selected,
'priority': priority,
'size': item[1]['length'],
'name': item[1]['name'],
'completed': item[1]['bytesCompleted']}
return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
@property
def status(self):
"""Get the status as string."""
return STATUS[self.fields['status']]
@property
def progress(self):
"""Get the download progress in percent as float."""
try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
except ZeroDivisionError:
return 0.0
@property
def ratio(self):
"""Get the upload/download ratio."""
try:
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
except ZeroDivisionError:
return 0.0
@property
def eta(self):
"""Get the "eta" as datetime.timedelta."""
eta = self.fields['eta']
if eta >= 0:
return datetime.timedelta(seconds=eta)
else:
ValueError('eta not valid')
@property
def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
@property
def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
@property
def date_started(self):
"""Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate'])
@property
def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
def format_eta(self):
"""Returns the attribute "eta" formatted as a string."""
eta = self.fields['eta']
if eta == -1:
return 'not available'
elif eta == -2:
return 'unknown'
else:
return format_timedelta(self.eta)
class Session(object):
"""
Session is a class holding the session data for a Transmission daemon.
Access the session field can be done through attributes.
The attributes available are the same as the session arguments in the
Transmission RPC specification, but with underscore instead of hypen.
``download-dir`` -> ``download_dir``.
"""
def __init__(self, fields={}):
self.fields = {}
self.update(fields)
def update(self, other):
"""Update the session data from a session arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Session):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
def __str__(self):
text = ''
for k, v in self.fields.iteritems():
text += "% 32s: %s\n" % (k[-32:], v)
return text
class Client(object):
"""
This is it. This class implements the json-RPC protocol to communicate with Transmission.
"""
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
base_url = 'http://' + address + ':' + str(port)
self.url = base_url + '/transmission/rpc'
if user and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
opener = urllib2.build_opener(
urllib2.HTTPBasicAuthHandler(password_manager)
, urllib2.HTTPDigestAuthHandler(password_manager)
)
urllib2.install_opener(opener)
elif user or password:
logger.warning('Either user or password missing, not using authentication.')
self._sequence = 0
self.session = Session()
self.sessionid = 0
self.protocol_version = None
self.get_session()
self.torrent_get_arguments = get_arguments('torrent-get'
, self.rpc_version)
def _debug_request(self, request):
logger.debug(
json.dumps(
{
'request': {
'url': request.get_full_url(),
'request-headers': dict(request.header_items()),
'request-data': json.loads(request.data),
}
},
indent=2
)
)
def _debug_response(self, response, response_data):
try:
response_data = json.loads(response_data)
except:
pass
logger.debug(
json.dumps(
{
'response': {
'url': response.url,
'code': response.code,
'msg': response.msg,
'headers': dict(response.headers),
'data': response_data,
}
},
indent=2
)
)
def _http_query(self, query):
headers = {'X-Transmission-Session-Id': self.sessionid}
request = urllib2.Request(self.url, query, headers)
request_count = 0
while True:
error_data = ""
try:
try:
self._debug_request(request)
socket.setdefaulttimeout(10)
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
response = urllib2.urlopen(request, timeout=60)
else:
response = urllib2.urlopen(request)
break
except urllib2.HTTPError, error:
error_data = error.read()
if error.code == 409:
logger.info('Server responded with 409, trying to set session-id.')
if request_count > 1:
raise TransmissionError('Session ID negotiation failed.', error)
if 'X-Transmission-Session-Id' in error.headers:
self.sessionid = error.headers['X-Transmission-Session-Id']
request.add_header('X-Transmission-Session-Id', self.sessionid)
else:
raise TransmissionError('Unknown conflict.', error)
except urllib2.URLError, error:
raise TransmissionError('Failed to connect to daemon.', error)
except httplib.BadStatusLine, error:
if (request_count > 1):
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
finally:
if error_data:
self._debug_response(error, error_data)
request_count = request_count + 1
result = response.read()
self._debug_response(response, result)
return result
def _request(self, method, arguments={}, ids=[], require_ids = False):
"""Send json-rpc request to Transmission using http POST"""
if not isinstance(method, (str, unicode)):
raise ValueError('request takes method as string')
if not isinstance(arguments, dict):
raise ValueError('request takes arguments as dict')
ids = self._format_ids(ids)
if len(ids) > 0:
arguments['ids'] = ids
elif require_ids:
raise ValueError('request require ids')
query = json.dumps({'tag': self._sequence, 'method': method
, 'arguments': arguments})
logger.info(query)
self._sequence += 1
start = time.time()
http_data = self._http_query(query)
elapsed = time.time() - start
logger.info('http request took %.3f s' % (elapsed))
try:
data = json.loads(http_data)
except ValueError, e:
logger.error('Error: ' + str(e))
logger.error('Request: \"%s\"' % (query))
logger.error('HTTP data: \"%s\"' % (http_data))
raise
logger.info(json.dumps(data, indent=2))
if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\"'
% data['result'])
results = {}
if method == 'torrent-get':
for item in data['arguments']['torrents']:
results[item['id']] = Torrent(item)
if self.protocol_version == 2 and 'peers' not in item:
self.protocol_version = 1
elif method == 'torrent-add':
item = data['arguments']['torrent-added']
results[item['id']] = Torrent(item)
elif method == 'session-get':
self._update_session(data['arguments'])
elif method == 'session-stats':
# older versions of T has the return data in "session-stats"
if 'session-stats' in data['arguments']:
self._update_session(data['arguments']['session-stats'])
else:
self._update_session(data['arguments'])
elif method in ('port-test', 'blocklist-update'):
results = data['arguments']
else:
return None
return results
def _format_ids(self, args):
"""Take things and make them valid torrent identifiers"""
ids = []
if isinstance(args, (int, long)):
ids.append(args)
elif isinstance(args, (str, unicode)):
for item in re.split(u'[ ,]+', args):
if len(item) == 0:
continue
addition = None
try:
# handle index
addition = [int(item)]
except ValueError:
pass
if not addition:
# handle hashes
try:
int(item, 16)
addition = [item]
except:
pass
if not addition:
# handle index ranges i.e. 5:10
match = re.match(u'^(\d+):(\d+)$', item)
if match:
try:
idx_from = int(match.group(1))
idx_to = int(match.group(2))
addition = range(idx_from, idx_to + 1)
except:
pass
if not addition:
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
ids.extend(addition)
elif isinstance(args, (list)):
for item in args:
ids.extend(self._format_ids(item))
else:
raise ValueError(u'Invalid torrent id')
return ids
def _update_session(self, data):
self.session.update(data)
@property
def rpc_version(self):
if self.protocol_version == None:
if hasattr(self.session, 'rpc_version'):
self.protocol_version = self.session.rpc_version
elif hasattr(self.session, 'version'):
self.protocol_version = 3
else:
self.protocol_version = 2
return self.protocol_version
def _rpc_version_warning(self, version):
if self.rpc_version < version:
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
def add(self, data, **kwargs):
"""
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
args = {'metainfo': data}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add',
argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args)
def add_url(self, torrent_url, **kwargs):
"""
Add torrent to transfers list. Takes a url to a .torrent file.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
torrent_file = None
if os.path.exists(torrent_url):
torrent_file = open(torrent_url, 'r')
else:
try:
torrent_file = urllib2.urlopen(torrent_url)
except:
torrent_file = None
if not torrent_file:
raise TransmissionError('File does not exist.')
torrent_data = base64.b64encode(torrent_file.read())
return self.add(torrent_data, **kwargs)
def remove(self, ids, delete_data=False):
"""
remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not.
"""
self._rpc_version_warning(3)
self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True)
def start(self, ids):
"""start torrent(s) with provided id(s)"""
self._request('torrent-start', {}, ids, True)
def stop(self, ids):
"""stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True)
def verify(self, ids):
"""verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True)
def reannounce(self, ids):
"""reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True)
def info(self, ids=[], arguments={}):
"""Get detailed information for torrent(s) with provided id(s)."""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids)
def get_files(self, ids=[]):
"""
Get list of files for provided torrent id(s).
This function returns a dictonary for each requested torrent id holding
the information about the files.
"""
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids)
result = {}
for id, torrent in request_result.iteritems():
result[id] = torrent.files()
return result
def set_files(self, items):
"""
Set file properties. Takes a dictonary with similar contents as the
result of get_files.
"""
if not isinstance(items, dict):
raise ValueError('Invalid file description')
for tid, files in items.iteritems():
if not isinstance(files, dict):
continue
wanted = []
unwanted = []
priority_high = []
priority_normal = []
priority_low = []
for fid, file in files.iteritems():
if not isinstance(file, dict):
continue
if 'selected' in file and file['selected']:
wanted.append(fid)
else:
unwanted.append(fid)
if 'priority' in file:
if file['priority'] == 'high':
priority_high.append(fid)
elif file['priority'] == 'normal':
priority_normal.append(fid)
elif file['priority'] == 'low':
priority_low.append(fid)
self.change([tid], files_wanted = wanted
, files_unwanted = unwanted
, priority_high = priority_high
, priority_normal = priority_normal
, priority_low = priority_low)
def list(self):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver']
return self._request('torrent-get', {'fields': fields})
def change(self, ids, **kwargs):
"""
Change torrent parameters. This is the list of parameters that.
"""
args = {}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('torrent-set', args, ids, True)
else:
ValueError("No arguments to set")
def get_session(self):
"""Get session parameters"""
self._request('session-get')
return self.session
def set_session(self, **kwargs):
"""Set session parameters"""
args = {}
for key, value in kwargs.iteritems():
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value')
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('session-set', args)
def blocklist_update(self):
"""Update block list. Returns the size of the block list."""
self._rpc_version_warning(5)
result = self._request('blocklist-update')
if 'blocklist-size' in result:
return result['blocklist-size']
return None
def port_test(self):
"""
Tests to see if your incoming peer port is accessible from the
outside world.
"""
self._rpc_version_warning(5)
result = self._request('port-test')
if 'port-is-open' in result:
return result['port-is-open']
return None
def session_stats(self):
"""Get session statistics"""
self._request('session-stats')
return self.session

View file

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import socket, datetime
import constants
from constants import logger
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
def format_size(size):
s = float(size)
i = 0
while size >= 1024.0 and i < len(UNITS):
i += 1
size /= 1024.0
return (size, UNITS[i])
def format_speed(size):
(size, unit) = format_size(size)
return (size, unit + '/s')
def format_timedelta(delta):
minutes, seconds = divmod(delta.seconds, 60)
hours, minutes = divmod(minutes, 60)
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
def format_timestamp(timestamp):
if timestamp > 0:
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.isoformat(' ')
else:
return '-'
class INetAddressError(Exception):
pass
def inet_address(address, default_port, default_address='localhost'):
addr = address.split(':')
if len(addr) == 1:
try:
port = int(addr[0])
addr = default_address
except:
addr = addr[0]
port = default_port
elif len(addr) == 2:
port = int(addr[1])
if len(addr[0]) == 0:
addr = default_address
else:
addr = addr[0]
else:
addr = default_address
port = default_port
try:
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror, e:
raise INetAddressError('Cannot look up address "%s".' % address)
return (addr, port)
def rpc_bool(arg):
if isinstance(arg, (str, unicode)):
try:
arg = bool(int(arg))
except:
arg = arg.lower() in [u'true', u'yes']
if bool(arg):
return 1
else:
return 0
TR_TYPE_MAP = {
'number' : int,
'string' : str,
'double': float,
'boolean' : rpc_bool,
'array': list,
'object': dict
}
def make_python_name(name):
return name.replace('-', '_')
def make_rpc_name(name):
return name.replace('_', '-')
def argument_value_convert(method, argument, value, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
if argument in args:
info = args[argument]
invalid_version = True
while invalid_version:
invalid_version = False
replacement = None
if rpc_version < info[1]:
invalid_version = True
replacement = info[3]
if info[2] and info[2] <= rpc_version:
invalid_version = True
replacement = info[4]
if invalid_version:
if replacement:
logger.warning(
'Replacing requested argument "%s" with "%s".'
% (argument, replacement))
argument = replacement
info = args[argument]
else:
raise ValueError(
'Method "%s" Argument "%s" does not exist in version %d.'
% (method, argument, rpc_version))
return (argument, TR_TYPE_MAP[info[0]](value))
else:
raise ValueError('Argument "%s" does not exists for method "%s".',
(argument, method))
def get_arguments(method, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
accessible = []
for argument, info in args.iteritems():
valid_version = True
if rpc_version < info[1]:
valid_version = False
if info[2] and info[2] <= rpc_version:
valid_version = False
if valid_version:
accessible.append(argument)
return accessible

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# 2008-08, Erik Svensson <erik.public@gmail.com>
from constants import *
from transmission import TransmissionError, Torrent, Session, Client
__author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.3'
__copyright__ = u'Copyright (c) 2008 Erik Svensson'
__license__ = u'MIT'

View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import logging
logger = logging.getLogger('transmissionrpc')
logger.setLevel(logging.ERROR)
def mirror_dict(d):
d.update(dict((v, k) for k, v in d.iteritems()))
return d
DEFAULT_PORT = 9091
TR_STATUS_CHECK_WAIT = (1<<0)
TR_STATUS_CHECK = (1<<1)
TR_STATUS_DOWNLOAD = (1<<2)
TR_STATUS_SEED = (1<<3)
TR_STATUS_STOPPED = (1<<4)
STATUS = mirror_dict({
'check pending' : TR_STATUS_CHECK_WAIT,
'checking' : TR_STATUS_CHECK,
'downloading' : TR_STATUS_DOWNLOAD,
'seeding' : TR_STATUS_SEED,
'stopped' : TR_STATUS_STOPPED,
})
TR_PRI_LOW = -1
TR_PRI_NORMAL = 0
TR_PRI_HIGH = 1
PRIORITY = mirror_dict({
'low' : TR_PRI_LOW,
'normal' : TR_PRI_NORMAL,
'high' : TR_PRI_HIGH
})
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
RATIO_LIMIT = mirror_dict({
'global' : TR_RATIOLIMIT_GLOBAL,
'single' : TR_RATIOLIMIT_SINGLE,
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
})
# A note on argument maps
# These maps are used to verify *-set methods. The information is structured in
# a tree.
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# |
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# Arguments for torrent methods
TORRENT_ARGS = {
'get' : {
'activityDate': ('number', 1, None, None, None),
'addedDate': ('number', 1, None, None, None),
'announceResponse': ('string', 1, None, None, None),
'announceURL': ('string', 1, None, None, None),
'bandwidthPriority': ('number', 5, None, None, None),
'comment': ('string', 1, None, None, None),
'corruptEver': ('number', 1, None, None, None),
'creator': ('string', 1, None, None, None),
'dateCreated': ('number', 1, None, None, None),
'desiredAvailable': ('number', 1, None, None, None),
'doneDate': ('number', 1, None, None, None),
'downloadDir': ('string', 4, None, None, None),
'downloadedEver': ('number', 1, None, None, None),
'downloaders': ('number', 4, None, None, None),
'downloadLimit': ('number', 1, None, None, None),
'downloadLimited': ('boolean', 5, None, None, None),
'downloadLimitMode': ('number', 1, 5, None, None),
'error': ('number', 1, None, None, None),
'errorString': ('number', 1, None, None, None),
'eta': ('number', 1, None, None, None),
'files': ('array', 1, None, None, None),
'fileStats': ('array', 5, None, None, None),
'hashString': ('string', 1, None, None, None),
'haveUnchecked': ('number', 1, None, None, None),
'haveValid': ('number', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'id': ('number', 1, None, None, None),
'isPrivate': ('boolean', 1, None, None, None),
'lastAnnounceTime': ('number', 1, None, None, None),
'lastScrapeTime': ('number', 1, None, None, None),
'leechers': ('number', 1, None, None, None),
'leftUntilDone': ('number', 1, None, None, None),
'manualAnnounceTime': ('number', 1, None, None, None),
'maxConnectedPeers': ('number', 1, None, None, None),
'name': ('string', 1, None, None, None),
'nextAnnounceTime': ('number', 1, None, None, None),
'nextScrapeTime': ('number', 1, None, None, None),
'peer-limit': ('number', 5, None, None, None),
'peers': ('array', 2, None, None, None),
'peersConnected': ('number', 1, None, None, None),
'peersFrom': ('object', 1, None, None, None),
'peersGettingFromUs': ('number', 1, None, None, None),
'peersKnown': ('number', 1, None, None, None),
'peersSendingToUs': ('number', 1, None, None, None),
'percentDone': ('double', 5, None, None, None),
'pieces': ('string', 5, None, None, None),
'pieceCount': ('number', 1, None, None, None),
'pieceSize': ('number', 1, None, None, None),
'priorities': ('array', 1, None, None, None),
'rateDownload': ('number', 1, None, None, None),
'rateUpload': ('number', 1, None, None, None),
'recheckProgress': ('double', 1, None, None, None),
'scrapeResponse': ('string', 1, None, None, None),
'scrapeURL': ('string', 1, None, None, None),
'seeders': ('number', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'sizeWhenDone': ('number', 1, None, None, None),
'startDate': ('number', 1, None, None, None),
'status': ('number', 1, None, None, None),
'swarmSpeed': ('number', 1, None, None, None),
'timesCompleted': ('number', 1, None, None, None),
'trackers': ('array', 1, None, None, None),
'totalSize': ('number', 1, None, None, None),
'torrentFile': ('string', 5, None, None, None),
'uploadedEver': ('number', 1, None, None, None),
'uploadLimit': ('number', 1, None, None, None),
'uploadLimitMode': ('number', 1, 5, None, None),
'uploadLimited': ('boolean', 5, None, None, None),
'uploadRatio': ('double', 1, None, None, None),
'wanted': ('array', 1, None, None, None),
'webseeds': ('array', 1, None, None, None),
'webseedsSendingToUs': ('number', 1, None, None, None),
},
'set': {
'bandwidthPriority': ('number', 5, None, None, None),
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'ids': ('array', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
},
'add': {
'download-dir': ('string', 1, None, None, None),
'filename': ('string', 1, None, None, None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'metainfo': ('string', 1, None, None, None),
'paused': ('boolean', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
}
}
# Arguments for session methods
SESSION_ARGS = {
'get': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"blocklist-size": ('number', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, None),
"peer-limit-global": ('number', 5, None, None, None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, None),
"pex-enabled": ('boolean', 5, None, None, None),
"port": ('number', 1, 5, None, None),
"peer-port": ('number', 5, None, None, None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"rpc-version": ('number', 4, None, None, None),
"rpc-version-minimum": ('number', 4, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
"version": ('string', 3, None, None, None),
},
'set': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
"port": ('number', 1, 5, None, 'peer-port'),
"peer-port": ('number', 5, None, 'port', None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
},
}

View file

@ -0,0 +1,605 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import sys, os, time, datetime
import re
import httplib, urllib2, base64, socket
try:
import json
except ImportError:
import simplejson as json
from constants import *
from utils import *
class TransmissionError(Exception):
def __init__(self, message='', original=None):
Exception.__init__(self, message)
self.message = message
self.original = original
def __str__(self):
if self.original:
original_name = type(self.original).__name__
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
else:
return self.args
class Torrent(object):
"""
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
All fetched torrent fields are accessable through this class using attributes.
This class has a few convenience properties using the torrent data.
"""
def __init__(self, fields):
if 'id' not in fields:
raise ValueError('Torrent requires an id')
self.fields = {}
self.update(fields)
def __repr__(self):
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
def __str__(self):
return 'torrent %s' % self.fields['name']
def update(self, other):
"""Update the torrent data from a Transmission arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Torrent):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def files(self):
"""
Get list of files for this torrent. This function returns a dictionary with file information for each file.
"""
result = {}
if 'files' in self.fields:
indicies = xrange(len(self.fields['files']))
files = self.fields['files']
priorities = self.fields['priorities']
wanted = self.fields['wanted']
index = 1
for item in zip(indicies, files, priorities, wanted):
selected = True if item[3] else False
priority = PRIORITY[item[2]]
result[item[0]] = {
'selected': selected,
'priority': priority,
'size': item[1]['length'],
'name': item[1]['name'],
'completed': item[1]['bytesCompleted']}
return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
@property
def status(self):
"""Get the status as string."""
return STATUS[self.fields['status']]
@property
def progress(self):
"""Get the download progress in percent as float."""
try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
except ZeroDivisionError:
return 0.0
@property
def ratio(self):
"""Get the upload/download ratio."""
try:
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
except ZeroDivisionError:
return 0.0
@property
def eta(self):
"""Get the "eta" as datetime.timedelta."""
eta = self.fields['eta']
if eta >= 0:
return datetime.timedelta(seconds=eta)
else:
ValueError('eta not valid')
@property
def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
@property
def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
@property
def date_started(self):
"""Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate'])
@property
def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
def format_eta(self):
"""Returns the attribute "eta" formatted as a string."""
eta = self.fields['eta']
if eta == -1:
return 'not available'
elif eta == -2:
return 'unknown'
else:
return format_timedelta(self.eta)
class Session(object):
"""
Session is a class holding the session data for a Transmission daemon.
Access the session field can be done through attributes.
The attributes available are the same as the session arguments in the
Transmission RPC specification, but with underscore instead of hypen.
``download-dir`` -> ``download_dir``.
"""
def __init__(self, fields={}):
self.fields = {}
self.update(fields)
def update(self, other):
"""Update the session data from a session arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Session):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
def __str__(self):
text = ''
for k, v in self.fields.iteritems():
text += "% 32s: %s\n" % (k[-32:], v)
return text
class Client(object):
"""
This is it. This class implements the json-RPC protocol to communicate with Transmission.
"""
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
base_url = 'http://' + address + ':' + str(port)
self.url = base_url + '/transmission/rpc'
if user and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
opener = urllib2.build_opener(
urllib2.HTTPBasicAuthHandler(password_manager)
, urllib2.HTTPDigestAuthHandler(password_manager)
)
urllib2.install_opener(opener)
elif user or password:
logger.warning('Either user or password missing, not using authentication.')
self._sequence = 0
self.session = Session()
self.sessionid = 0
self.protocol_version = None
self.get_session()
self.torrent_get_arguments = get_arguments('torrent-get'
, self.rpc_version)
def _debug_request(self, request):
logger.debug(
json.dumps(
{
'request': {
'url': request.get_full_url(),
'request-headers': dict(request.header_items()),
'request-data': json.loads(request.data),
}
},
indent=2
)
)
def _debug_response(self, response, response_data):
try:
response_data = json.loads(response_data)
except:
pass
logger.debug(
json.dumps(
{
'response': {
'url': response.url,
'code': response.code,
'msg': response.msg,
'headers': dict(response.headers),
'data': response_data,
}
},
indent=2
)
)
def _http_query(self, query):
headers = {'X-Transmission-Session-Id': self.sessionid}
request = urllib2.Request(self.url, query, headers)
request_count = 0
while True:
error_data = ""
try:
self._debug_request(request)
socket.setdefaulttimeout(10)
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
response = urllib2.urlopen(request, timeout=60)
else:
response = urllib2.urlopen(request)
break
except urllib2.HTTPError, error:
error_data = error.read()
if error.code == 409:
logger.info('Server responded with 409, trying to set session-id.')
if request_count > 1:
raise TransmissionError('Session ID negotiation failed.', error)
if 'X-Transmission-Session-Id' in error.headers:
self.sessionid = error.headers['X-Transmission-Session-Id']
request.add_header('X-Transmission-Session-Id', self.sessionid)
else:
raise TransmissionError('Unknown conflict.', error)
except urllib2.URLError, error:
raise TransmissionError('Failed to connect to daemon.', error)
except httplib.BadStatusLine, error:
if (request_count > 1):
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
finally:
if error_data:
self._debug_response(error, error_data)
request_count = request_count + 1
result = response.read()
self._debug_response(response, result)
return result
def _request(self, method, arguments={}, ids=[], require_ids = False):
"""Send json-rpc request to Transmission using http POST"""
if not isinstance(method, (str, unicode)):
raise ValueError('request takes method as string')
if not isinstance(arguments, dict):
raise ValueError('request takes arguments as dict')
ids = self._format_ids(ids)
if len(ids) > 0:
arguments['ids'] = ids
elif require_ids:
raise ValueError('request require ids')
query = json.dumps({'tag': self._sequence, 'method': method
, 'arguments': arguments})
logger.info(query)
self._sequence += 1
start = time.time()
http_data = self._http_query(query)
elapsed = time.time() - start
logger.info('http request took %.3f s' % (elapsed))
try:
data = json.loads(http_data)
except ValueError, e:
logger.error('Error: ' + str(e))
logger.error('Request: \"%s\"' % (query))
logger.error('HTTP data: \"%s\"' % (http_data))
raise
logger.info(json.dumps(data, indent=2))
if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\"'
% data['result'])
results = {}
if method == 'torrent-get':
for item in data['arguments']['torrents']:
results[item['id']] = Torrent(item)
if self.protocol_version == 2 and 'peers' not in item:
self.protocol_version = 1
elif method == 'torrent-add':
item = data['arguments']['torrent-added']
results[item['id']] = Torrent(item)
elif method == 'session-get':
self._update_session(data['arguments'])
elif method == 'session-stats':
# older versions of T has the return data in "session-stats"
if 'session-stats' in data['arguments']:
self._update_session(data['arguments']['session-stats'])
else:
self._update_session(data['arguments'])
elif method in ('port-test', 'blocklist-update'):
results = data['arguments']
else:
return None
return results
def _format_ids(self, args):
"""Take things and make them valid torrent identifiers"""
ids = []
if isinstance(args, (int, long)):
ids.append(args)
elif isinstance(args, (str, unicode)):
for item in re.split(u'[ ,]+', args):
if len(item) == 0:
continue
addition = None
try:
# handle index
addition = [int(item)]
except ValueError:
pass
if not addition:
# handle hashes
try:
int(item, 16)
addition = [item]
except:
pass
if not addition:
# handle index ranges i.e. 5:10
match = re.match(u'^(\d+):(\d+)$', item)
if match:
try:
idx_from = int(match.group(1))
idx_to = int(match.group(2))
addition = range(idx_from, idx_to + 1)
except:
pass
if not addition:
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
ids.extend(addition)
elif isinstance(args, (list)):
for item in args:
ids.extend(self._format_ids(item))
else:
raise ValueError(u'Invalid torrent id')
return ids
def _update_session(self, data):
self.session.update(data)
@property
def rpc_version(self):
if self.protocol_version == None:
if hasattr(self.session, 'rpc_version'):
self.protocol_version = self.session.rpc_version
elif hasattr(self.session, 'version'):
self.protocol_version = 3
else:
self.protocol_version = 2
return self.protocol_version
def _rpc_version_warning(self, version):
if self.rpc_version < version:
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
def add(self, data, **kwargs):
"""
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
args = {'metainfo': data}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add',
argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args)
def add_url(self, torrent_url, **kwargs):
"""
Add torrent to transfers list. Takes a url to a .torrent file.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
torrent_file = None
if os.path.exists(torrent_url):
torrent_file = open(torrent_url, 'r')
else:
try:
torrent_file = urllib2.urlopen(torrent_url)
except:
torrent_file = None
if not torrent_file:
raise TransmissionError('File does not exist.')
torrent_data = base64.b64encode(torrent_file.read())
return self.add(torrent_data, **kwargs)
def remove(self, ids, delete_data=False):
"""
remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not.
"""
self._rpc_version_warning(3)
self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True)
def start(self, ids):
"""start torrent(s) with provided id(s)"""
self._request('torrent-start', {}, ids, True)
def stop(self, ids):
"""stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True)
def verify(self, ids):
"""verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True)
def reannounce(self, ids):
"""reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True)
def info(self, ids=[], arguments={}):
"""Get detailed information for torrent(s) with provided id(s)."""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids)
def get_files(self, ids=[]):
"""
Get list of files for provided torrent id(s).
This function returns a dictonary for each requested torrent id holding
the information about the files.
"""
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids)
result = {}
for id, torrent in request_result.iteritems():
result[id] = torrent.files()
return result
def set_files(self, items):
"""
Set file properties. Takes a dictonary with similar contents as the
result of get_files.
"""
if not isinstance(items, dict):
raise ValueError('Invalid file description')
for tid, files in items.iteritems():
if not isinstance(files, dict):
continue
wanted = []
unwanted = []
priority_high = []
priority_normal = []
priority_low = []
for fid, file in files.iteritems():
if not isinstance(file, dict):
continue
if 'selected' in file and file['selected']:
wanted.append(fid)
else:
unwanted.append(fid)
if 'priority' in file:
if file['priority'] == 'high':
priority_high.append(fid)
elif file['priority'] == 'normal':
priority_normal.append(fid)
elif file['priority'] == 'low':
priority_low.append(fid)
self.change([tid], files_wanted = wanted
, files_unwanted = unwanted
, priority_high = priority_high
, priority_normal = priority_normal
, priority_low = priority_low)
def list(self):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver']
return self._request('torrent-get', {'fields': fields})
def change(self, ids, **kwargs):
"""
Change torrent parameters. This is the list of parameters that.
"""
args = {}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('torrent-set', args, ids, True)
else:
ValueError("No arguments to set")
def get_session(self):
"""Get session parameters"""
self._request('session-get')
return self.session
def set_session(self, **kwargs):
"""Set session parameters"""
args = {}
for key, value in kwargs.iteritems():
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value')
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('session-set', args)
def blocklist_update(self):
"""Update block list. Returns the size of the block list."""
self._rpc_version_warning(5)
result = self._request('blocklist-update')
if 'blocklist-size' in result:
return result['blocklist-size']
return None
def port_test(self):
"""
Tests to see if your incoming peer port is accessible from the
outside world.
"""
self._rpc_version_warning(5)
result = self._request('port-test')
if 'port-is-open' in result:
return result['port-is-open']
return None
def session_stats(self):
"""Get session statistics"""
self._request('session-stats')
return self.session

View file

@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import socket, datetime
import constants
from constants import logger
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
def format_size(size):
s = float(size)
i = 0
while size >= 1024.0 and i < len(UNITS):
i += 1
size /= 1024.0
return (size, UNITS[i])
def format_speed(size):
(size, unit) = format_size(size)
return (size, unit + '/s')
def format_timedelta(delta):
minutes, seconds = divmod(delta.seconds, 60)
hours, minutes = divmod(minutes, 60)
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
def format_timestamp(timestamp):
if timestamp > 0:
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.isoformat(' ')
else:
return '-'
class INetAddressError(Exception):
pass
def inet_address(address, default_port, default_address='localhost'):
addr = address.split(':')
if len(addr) == 1:
try:
port = int(addr[0])
addr = default_address
except:
addr = addr[0]
port = default_port
elif len(addr) == 2:
port = int(addr[1])
if len(addr[0]) == 0:
addr = default_address
else:
addr = addr[0]
else:
addr = default_address
port = default_port
try:
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror, e:
raise INetAddressError('Cannot look up address "%s".' % address)
return (addr, port)
def rpc_bool(arg):
if isinstance(arg, (str, unicode)):
try:
arg = bool(int(arg))
except:
arg = arg.lower() in [u'true', u'yes']
return 1 if bool(arg) else 0
TR_TYPE_MAP = {
'number' : int,
'string' : str,
'double': float,
'boolean' : rpc_bool,
'array': list,
'object': dict
}
def make_python_name(name):
return name.replace('-', '_')
def make_rpc_name(name):
return name.replace('_', '-')
def argument_value_convert(method, argument, value, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
if argument in args:
info = args[argument]
invalid_version = True
while invalid_version:
invalid_version = False
replacement = None
if rpc_version < info[1]:
invalid_version = True
replacement = info[3]
if info[2] and info[2] <= rpc_version:
invalid_version = True
replacement = info[4]
if invalid_version:
if replacement:
logger.warning(
'Replacing requested argument "%s" with "%s".'
% (argument, replacement))
argument = replacement
info = args[argument]
else:
raise ValueError(
'Method "%s" Argument "%s" does not exist in version %d.'
% (method, argument, rpc_version))
return (argument, TR_TYPE_MAP[info[0]](value))
else:
raise ValueError('Argument "%s" does not exists for method "%s".',
(argument, method))
def get_arguments(method, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
accessible = []
for argument, info in args.iteritems():
valid_version = True
if rpc_version < info[1]:
valid_version = False
if info[2] and info[2] <= rpc_version:
valid_version = False
if valid_version:
accessible.append(argument)
return accessible

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# 2008-08, Erik Svensson <erik.public@gmail.com>
from constants import *
from transmission import TransmissionError, Torrent, Session, Client
__author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.4'
__copyright__ = u'Copyright (c) 2009 Erik Svensson'
__license__ = u'MIT'

View file

@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import logging
logger = logging.getLogger('transmissionrpc')
logger.setLevel(logging.ERROR)
def mirror_dict(d):
d.update(dict((v, k) for k, v in d.iteritems()))
return d
DEFAULT_PORT = 9091
DEFAULT_TIMEOUT = 30
TR_STATUS_CHECK_WAIT = (1<<0)
TR_STATUS_CHECK = (1<<1)
TR_STATUS_DOWNLOAD = (1<<2)
TR_STATUS_SEED = (1<<3)
TR_STATUS_STOPPED = (1<<4)
STATUS = mirror_dict({
'check pending' : TR_STATUS_CHECK_WAIT,
'checking' : TR_STATUS_CHECK,
'downloading' : TR_STATUS_DOWNLOAD,
'seeding' : TR_STATUS_SEED,
'stopped' : TR_STATUS_STOPPED,
})
TR_PRI_LOW = -1
TR_PRI_NORMAL = 0
TR_PRI_HIGH = 1
PRIORITY = mirror_dict({
'low' : TR_PRI_LOW,
'normal' : TR_PRI_NORMAL,
'high' : TR_PRI_HIGH
})
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
RATIO_LIMIT = mirror_dict({
'global' : TR_RATIOLIMIT_GLOBAL,
'single' : TR_RATIOLIMIT_SINGLE,
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
})
# A note on argument maps
# These maps are used to verify *-set methods. The information is structured in
# a tree.
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# |
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# Arguments for torrent methods
TORRENT_ARGS = {
'get' : {
'activityDate': ('number', 1, None, None, None),
'addedDate': ('number', 1, None, None, None),
'announceResponse': ('string', 1, None, None, None),
'announceURL': ('string', 1, None, None, None),
'bandwidthPriority': ('number', 5, None, None, None),
'comment': ('string', 1, None, None, None),
'corruptEver': ('number', 1, None, None, None),
'creator': ('string', 1, None, None, None),
'dateCreated': ('number', 1, None, None, None),
'desiredAvailable': ('number', 1, None, None, None),
'doneDate': ('number', 1, None, None, None),
'downloadDir': ('string', 4, None, None, None),
'downloadedEver': ('number', 1, None, None, None),
'downloaders': ('number', 4, None, None, None),
'downloadLimit': ('number', 1, None, None, None),
'downloadLimited': ('boolean', 5, None, None, None),
'downloadLimitMode': ('number', 1, 5, None, None),
'error': ('number', 1, None, None, None),
'errorString': ('number', 1, None, None, None),
'eta': ('number', 1, None, None, None),
'files': ('array', 1, None, None, None),
'fileStats': ('array', 5, None, None, None),
'hashString': ('string', 1, None, None, None),
'haveUnchecked': ('number', 1, None, None, None),
'haveValid': ('number', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'id': ('number', 1, None, None, None),
'isPrivate': ('boolean', 1, None, None, None),
'lastAnnounceTime': ('number', 1, None, None, None),
'lastScrapeTime': ('number', 1, None, None, None),
'leechers': ('number', 1, None, None, None),
'leftUntilDone': ('number', 1, None, None, None),
'manualAnnounceTime': ('number', 1, None, None, None),
'maxConnectedPeers': ('number', 1, None, None, None),
'name': ('string', 1, None, None, None),
'nextAnnounceTime': ('number', 1, None, None, None),
'nextScrapeTime': ('number', 1, None, None, None),
'peer-limit': ('number', 5, None, None, None),
'peers': ('array', 2, None, None, None),
'peersConnected': ('number', 1, None, None, None),
'peersFrom': ('object', 1, None, None, None),
'peersGettingFromUs': ('number', 1, None, None, None),
'peersKnown': ('number', 1, None, None, None),
'peersSendingToUs': ('number', 1, None, None, None),
'percentDone': ('double', 5, None, None, None),
'pieces': ('string', 5, None, None, None),
'pieceCount': ('number', 1, None, None, None),
'pieceSize': ('number', 1, None, None, None),
'priorities': ('array', 1, None, None, None),
'rateDownload': ('number', 1, None, None, None),
'rateUpload': ('number', 1, None, None, None),
'recheckProgress': ('double', 1, None, None, None),
'scrapeResponse': ('string', 1, None, None, None),
'scrapeURL': ('string', 1, None, None, None),
'seeders': ('number', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'sizeWhenDone': ('number', 1, None, None, None),
'startDate': ('number', 1, None, None, None),
'status': ('number', 1, None, None, None),
'swarmSpeed': ('number', 1, None, None, None),
'timesCompleted': ('number', 1, None, None, None),
'trackers': ('array', 1, None, None, None),
'totalSize': ('number', 1, None, None, None),
'torrentFile': ('string', 5, None, None, None),
'uploadedEver': ('number', 1, None, None, None),
'uploadLimit': ('number', 1, None, None, None),
'uploadLimitMode': ('number', 1, 5, None, None),
'uploadLimited': ('boolean', 5, None, None, None),
'uploadRatio': ('double', 1, None, None, None),
'wanted': ('array', 1, None, None, None),
'webseeds': ('array', 1, None, None, None),
'webseedsSendingToUs': ('number', 1, None, None, None),
},
'set': {
'bandwidthPriority': ('number', 5, None, None, None),
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'ids': ('array', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
},
'add': {
'download-dir': ('string', 1, None, None, None),
'filename': ('string', 1, None, None, None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'metainfo': ('string', 1, None, None, None),
'paused': ('boolean', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
}
}
# Arguments for session methods
SESSION_ARGS = {
'get': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"blocklist-size": ('number', 5, None, None, None),
"dht-enabled": ('boolean', 6, None, None, None),
"download-dir": ('string', 1, None, None, None),
"encryption": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, None),
"peer-limit-global": ('number', 5, None, None, None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, None),
"pex-enabled": ('boolean', 5, None, None, None),
"port": ('number', 1, 5, None, None),
"peer-port": ('number', 5, None, None, None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"rpc-version": ('number', 4, None, None, None),
"rpc-version-minimum": ('number', 4, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
"version": ('string', 3, None, None, None),
},
'set': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"dht-enabled": ('boolean', 6, None, None, None),
"download-dir": ('string', 1, None, None, None),
"encryption": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
"port": ('number', 1, 5, None, 'peer-port'),
"peer-port": ('number', 5, None, 'port', None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
},
}

View file

@ -0,0 +1,628 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import sys, os, time, datetime
import re
import httplib, urllib2, base64, socket
try:
import json
except ImportError:
import simplejson as json
from constants import *
from utils import *
class TransmissionError(Exception):
def __init__(self, message='', original=None):
Exception.__init__(self, message)
self.message = message
self.original = original
def __str__(self):
if self.original:
original_name = type(self.original).__name__
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
else:
return self.args
class Torrent(object):
"""
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
All fetched torrent fields are accessable through this class using attributes.
This class has a few convenience properties using the torrent data.
"""
def __init__(self, fields):
if 'id' not in fields:
raise ValueError('Torrent requires an id')
self.fields = {}
self.update(fields)
def __repr__(self):
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
def __str__(self):
return 'torrent %s' % self.fields['name']
def update(self, other):
"""Update the torrent data from a Transmission arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Torrent):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def files(self):
"""
Get list of files for this torrent. This function returns a dictionary with file information for each file.
"""
result = {}
if 'files' in self.fields:
indicies = xrange(len(self.fields['files']))
files = self.fields['files']
priorities = self.fields['priorities']
wanted = self.fields['wanted']
index = 1
for item in zip(indicies, files, priorities, wanted):
selected = bool(item[3])
priority = PRIORITY[item[2]]
result[item[0]] = {
'selected': selected,
'priority': priority,
'size': item[1]['length'],
'name': item[1]['name'],
'completed': item[1]['bytesCompleted']}
return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
@property
def status(self):
"""Get the status as string."""
return STATUS[self.fields['status']]
@property
def progress(self):
"""Get the download progress in percent as float."""
try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
except ZeroDivisionError:
return 0.0
@property
def ratio(self):
"""Get the upload/download ratio."""
try:
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
except ZeroDivisionError:
return 0.0
@property
def eta(self):
"""Get the "eta" as datetime.timedelta."""
eta = self.fields['eta']
if eta >= 0:
return datetime.timedelta(seconds=eta)
else:
ValueError('eta not valid')
@property
def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
@property
def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
@property
def date_started(self):
"""Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate'])
@property
def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
def format_eta(self):
"""Returns the attribute "eta" formatted as a string."""
eta = self.fields['eta']
if eta == -1:
return 'not available'
elif eta == -2:
return 'unknown'
else:
return format_timedelta(self.eta)
class Session(object):
"""
Session is a class holding the session data for a Transmission daemon.
Access the session field can be done through attributes.
The attributes available are the same as the session arguments in the
Transmission RPC specification, but with underscore instead of hypen.
``download-dir`` -> ``download_dir``.
"""
def __init__(self, fields={}):
self.fields = {}
self.update(fields)
def update(self, other):
"""Update the session data from a session arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Session):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
def __str__(self):
text = ''
for k, v in self.fields.iteritems():
text += "% 32s: %s\n" % (k[-32:], v)
return text
class Client(object):
"""
This is it. This class implements the json-RPC protocol to communicate with Transmission.
"""
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
base_url = 'http://' + address + ':' + str(port)
self.url = base_url + '/transmission/rpc'
if user and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
opener = urllib2.build_opener(
urllib2.HTTPBasicAuthHandler(password_manager)
, urllib2.HTTPDigestAuthHandler(password_manager)
)
urllib2.install_opener(opener)
elif user or password:
logger.warning('Either user or password missing, not using authentication.')
self._sequence = 0
self.session = Session()
self.sessionid = 0
self.protocol_version = None
self.get_session()
self.torrent_get_arguments = get_arguments('torrent-get'
, self.rpc_version)
def _debug_request(self, request):
logger.debug(
json.dumps(
{
'request': {
'url': request.get_full_url(),
'request-headers': dict(request.header_items()),
'request-data': json.loads(request.data),
}
},
indent=2
)
)
def _debug_response(self, response, response_data):
try:
response_data = json.loads(response_data)
except:
pass
logger.debug(
json.dumps(
{
'response': {
'url': response.url,
'code': response.code,
'msg': response.msg,
'headers': dict(response.headers),
'data': response_data,
}
},
indent=2
)
)
def _http_query(self, query, timeout=DEFAULT_TIMEOUT):
headers = {'X-Transmission-Session-Id': self.sessionid}
request = urllib2.Request(self.url, query, headers)
request_count = 0
while True:
error_data = ""
try:
try:
self._debug_request(request)
socket.setdefaulttimeout(timeout) # 30 seconds
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
response = urllib2.urlopen(request, timeout=timeout)
else:
response = urllib2.urlopen(request)
break
except urllib2.HTTPError, error:
error_data = error.read()
if error.code == 409:
logger.info('Server responded with 409, trying to set session-id.')
if request_count > 1:
raise TransmissionError('Session ID negotiation failed.', error)
if 'X-Transmission-Session-Id' in error.headers:
self.sessionid = error.headers['X-Transmission-Session-Id']
request.add_header('X-Transmission-Session-Id', self.sessionid)
else:
raise TransmissionError('Unknown conflict.', error)
except urllib2.URLError, error:
raise TransmissionError('Failed to connect to daemon.', error)
except httplib.BadStatusLine, error:
if (request_count > 1):
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
finally:
if error_data:
self._debug_response(error, error_data)
request_count = request_count + 1
result = response.read()
self._debug_response(response, result)
return result
def _request(self, method, arguments={}, ids=[], require_ids=False, timeout=DEFAULT_TIMEOUT):
"""Send json-rpc request to Transmission using http POST"""
if not isinstance(method, (str, unicode)):
raise ValueError('request takes method as string')
if not isinstance(arguments, dict):
raise ValueError('request takes arguments as dict')
ids = self._format_ids(ids)
if len(ids) > 0:
arguments['ids'] = ids
elif require_ids:
raise ValueError('request require ids')
query = json.dumps({'tag': self._sequence, 'method': method
, 'arguments': arguments})
logger.info(query)
self._sequence += 1
start = time.time()
http_data = self._http_query(query, timeout)
elapsed = time.time() - start
logger.info('http request took %.3f s' % (elapsed))
try:
data = json.loads(http_data)
except ValueError, e:
logger.error('Error: ' + str(e))
logger.error('Request: \"%s\"' % (query))
logger.error('HTTP data: \"%s\"' % (http_data))
raise
logger.info(json.dumps(data, indent=2))
if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\"'
% data['result'])
results = {}
if method == 'torrent-get':
for item in data['arguments']['torrents']:
results[item['id']] = Torrent(item)
if self.protocol_version == 2 and 'peers' not in item:
self.protocol_version = 1
elif method == 'torrent-add':
item = data['arguments']['torrent-added']
results[item['id']] = Torrent(item)
elif method == 'session-get':
self._update_session(data['arguments'])
elif method == 'session-stats':
# older versions of T has the return data in "session-stats"
if 'session-stats' in data['arguments']:
self._update_session(data['arguments']['session-stats'])
else:
self._update_session(data['arguments'])
elif method in ('port-test', 'blocklist-update'):
results = data['arguments']
else:
return None
return results
def _format_ids(self, args):
"""Take things and make them valid torrent identifiers"""
ids = []
if isinstance(args, (int, long)):
ids.append(args)
elif isinstance(args, (str, unicode)):
for item in re.split(u'[ ,]+', args):
if len(item) == 0:
continue
addition = None
try:
# handle index
addition = [int(item)]
except ValueError:
pass
if not addition:
# handle hashes
try:
int(item, 16)
addition = [item]
except:
pass
if not addition:
# handle index ranges i.e. 5:10
match = re.match(u'^(\d+):(\d+)$', item)
if match:
try:
idx_from = int(match.group(1))
idx_to = int(match.group(2))
addition = range(idx_from, idx_to + 1)
except:
pass
if not addition:
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
ids.extend(addition)
elif isinstance(args, (list)):
for item in args:
ids.extend(self._format_ids(item))
else:
raise ValueError(u'Invalid torrent id')
return ids
def _update_session(self, data):
self.session.update(data)
@property
def rpc_version(self):
if self.protocol_version == None:
if hasattr(self.session, 'rpc_version'):
self.protocol_version = self.session.rpc_version
elif hasattr(self.session, 'version'):
self.protocol_version = 3
else:
self.protocol_version = 2
return self.protocol_version
def _rpc_version_warning(self, version):
if self.rpc_version < version:
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
def add(self, data, timeout=DEFAULT_TIMEOUT, **kwargs):
"""
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
Additional arguments are:
* `metainfo`, string, alternate way to pass base64 encoded torrent data
* `filename`, path or url, provide torrent data as a file path or URL.
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
args = {}
if data:
args = {'metainfo': data}
if 'metainfo' in kwargs:
pass
if 'filename' in kwargs:
pass
else:
raise ValueError('No torrent data or torrent url.')
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add',
argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args, timeout=timeout)
def add_url(self, torrent_url, **kwargs):
"""
Add torrent to transfers list. Takes a url to a .torrent file.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
torrent_file = None
if os.path.exists(torrent_url):
torrent_file = open(torrent_url, 'r')
else:
try:
torrent_file = urllib2.urlopen(torrent_url)
except:
torrent_file = None
if not torrent_file:
raise TransmissionError('File does not exist.')
torrent_data = base64.b64encode(torrent_file.read())
return self.add(torrent_data, **kwargs)
def remove(self, ids, delete_data=False, timeout=DEFAULT_TIMEOUT):
"""
remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not.
"""
self._rpc_version_warning(3)
self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True, timeout=timeout)
def start(self, ids, timeout=DEFAULT_TIMEOUT):
"""start torrent(s) with provided id(s)"""
self._request('torrent-start', {}, ids, True, timeout=timeout)
def stop(self, ids, timeout=DEFAULT_TIMEOUT):
"""stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True, timeout=timeout)
def verify(self, ids, timeout=DEFAULT_TIMEOUT):
"""verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True, timeout=timeout)
def reannounce(self, ids, timeout=DEFAULT_TIMEOUT):
"""reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True, timeout=timeout)
def info(self, ids=[], arguments={}, timeout=DEFAULT_TIMEOUT):
"""Get detailed information for torrent(s) with provided id(s)."""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids, timeout=timeout)
def get_files(self, ids=[], timeout=DEFAULT_TIMEOUT):
"""
Get list of files for provided torrent id(s).
This function returns a dictonary for each requested torrent id holding
the information about the files.
"""
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout)
result = {}
for id, torrent in request_result.iteritems():
result[id] = torrent.files()
return result
def set_files(self, items, timeout=DEFAULT_TIMEOUT):
"""
Set file properties. Takes a dictonary with similar contents as the
result of get_files.
"""
if not isinstance(items, dict):
raise ValueError('Invalid file description')
for tid, files in items.iteritems():
if not isinstance(files, dict):
continue
wanted = []
unwanted = []
priority_high = []
priority_normal = []
priority_low = []
for fid, file in files.iteritems():
if not isinstance(file, dict):
continue
if 'selected' in file and file['selected']:
wanted.append(fid)
else:
unwanted.append(fid)
if 'priority' in file:
if file['priority'] == 'high':
priority_high.append(fid)
elif file['priority'] == 'normal':
priority_normal.append(fid)
elif file['priority'] == 'low':
priority_low.append(fid)
self.change([tid], files_wanted = wanted
, files_unwanted = unwanted
, priority_high = priority_high
, priority_normal = priority_normal
, priority_low = priority_low, timeout=timeout)
def list(self, timeout=DEFAULT_TIMEOUT):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver']
return self._request('torrent-get', {'fields': fields}, timeout=timeout)
def change(self, ids, timeout=DEFAULT_TIMEOUT, **kwargs):
"""
Change torrent parameters. This is the list of parameters that.
"""
args = {}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('torrent-set', args, ids, True, timeout=timeout)
else:
ValueError("No arguments to set")
def move(self, ids, location, timeout=DEFAULT_TIMEOUT):
"""Move torrent data to the new location."""
self._rpc_version_warning(6)
args = {'location': location, 'move': True}
self._request('torrent-set-location', args, ids, True, timeout=timeout);
def locate(self, ids, location, timeout=DEFAULT_TIMEOUT):
"""Locate torrent data at the location."""
self._rpc_version_warning(6)
args = {'location': location, 'move': False}
self._request('torrent-set-location', args, ids, True, timeout=timeout);
def get_session(self, timeout=DEFAULT_TIMEOUT):
"""Get session parameters"""
self._request('session-get', timeout=timeout)
return self.session
def set_session(self, timeout=DEFAULT_TIMEOUT, **kwargs):
"""Set session parameters"""
args = {}
for key, value in kwargs.iteritems():
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value')
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('session-set', args, timeout=timeout)
def blocklist_update(self, timeout=DEFAULT_TIMEOUT):
"""Update block list. Returns the size of the block list."""
self._rpc_version_warning(5)
result = self._request('blocklist-update', timeout=timeout)
if 'blocklist-size' in result:
return result['blocklist-size']
return None
def port_test(self, timeout=DEFAULT_TIMEOUT):
"""
Tests to see if your incoming peer port is accessible from the
outside world.
"""
self._rpc_version_warning(5)
result = self._request('port-test', timeout=timeout)
if 'port-is-open' in result:
return result['port-is-open']
return None
def session_stats(self, timeout=DEFAULT_TIMEOUT):
"""Get session statistics"""
self._request('session-stats', timeout=timeout)
return self.session

View file

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import socket, datetime
import constants
from constants import logger
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
def format_size(size):
s = float(size)
i = 0
while size >= 1024.0 and i < len(UNITS):
i += 1
size /= 1024.0
return (size, UNITS[i])
def format_speed(size):
(size, unit) = format_size(size)
return (size, unit + '/s')
def format_timedelta(delta):
minutes, seconds = divmod(delta.seconds, 60)
hours, minutes = divmod(minutes, 60)
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
def format_timestamp(timestamp):
if timestamp > 0:
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.isoformat(' ')
else:
return '-'
class INetAddressError(Exception):
pass
def inet_address(address, default_port, default_address='localhost'):
addr = address.split(':')
if len(addr) == 1:
try:
port = int(addr[0])
addr = default_address
except:
addr = addr[0]
port = default_port
elif len(addr) == 2:
port = int(addr[1])
if len(addr[0]) == 0:
addr = default_address
else:
addr = addr[0]
else:
addr = default_address
port = default_port
try:
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror, e:
raise INetAddressError('Cannot look up address "%s".' % address)
return (addr, port)
def rpc_bool(arg):
if isinstance(arg, (str, unicode)):
try:
arg = bool(int(arg))
except:
arg = arg.lower() in [u'true', u'yes']
if bool(arg):
return 1
else:
return 0
TR_TYPE_MAP = {
'number' : int,
'string' : str,
'double': float,
'boolean' : rpc_bool,
'array': list,
'object': dict
}
def make_python_name(name):
return name.replace('-', '_')
def make_rpc_name(name):
return name.replace('_', '-')
def argument_value_convert(method, argument, value, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
if argument in args:
info = args[argument]
invalid_version = True
while invalid_version:
invalid_version = False
replacement = None
if rpc_version < info[1]:
invalid_version = True
replacement = info[3]
if info[2] and info[2] <= rpc_version:
invalid_version = True
replacement = info[4]
if invalid_version:
if replacement:
logger.warning(
'Replacing requested argument "%s" with "%s".'
% (argument, replacement))
argument = replacement
info = args[argument]
else:
raise ValueError(
'Method "%s" Argument "%s" does not exist in version %d.'
% (method, argument, rpc_version))
return (argument, TR_TYPE_MAP[info[0]](value))
else:
raise ValueError('Argument "%s" does not exists for method "%s".',
(argument, method))
def get_arguments(method, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
accessible = []
for argument, info in args.iteritems():
valid_version = True
if rpc_version < info[1]:
valid_version = False
if info[2] and info[2] <= rpc_version:
valid_version = False
if valid_version:
accessible.append(argument)
return accessible

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# 2008-08, Erik Svensson <erik.public@gmail.com>
from constants import *
from transmission import TransmissionError, Torrent, Session, Client
__author__ = u'Erik Svensson <erik.public@gmail.com>'
__version__ = u'0.3'
__copyright__ = u'Copyright (c) 2008 Erik Svensson'
__license__ = u'MIT'

View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import logging
logger = logging.getLogger('transmissionrpc')
logger.setLevel(logging.ERROR)
def mirror_dict(d):
d.update(dict((v, k) for k, v in d.iteritems()))
return d
DEFAULT_PORT = 9091
TR_STATUS_CHECK_WAIT = (1<<0)
TR_STATUS_CHECK = (1<<1)
TR_STATUS_DOWNLOAD = (1<<2)
TR_STATUS_SEED = (1<<3)
TR_STATUS_STOPPED = (1<<4)
STATUS = mirror_dict({
'check pending' : TR_STATUS_CHECK_WAIT,
'checking' : TR_STATUS_CHECK,
'downloading' : TR_STATUS_DOWNLOAD,
'seeding' : TR_STATUS_SEED,
'stopped' : TR_STATUS_STOPPED,
})
TR_PRI_LOW = -1
TR_PRI_NORMAL = 0
TR_PRI_HIGH = 1
PRIORITY = mirror_dict({
'low' : TR_PRI_LOW,
'normal' : TR_PRI_NORMAL,
'high' : TR_PRI_HIGH
})
TR_RATIOLIMIT_GLOBAL = 0 # follow the global settings
TR_RATIOLIMIT_SINGLE = 1 # override the global settings, seeding until a certain ratio
TR_RATIOLIMIT_UNLIMITED = 2 # override the global settings, seeding regardless of ratio
RATIO_LIMIT = mirror_dict({
'global' : TR_RATIOLIMIT_GLOBAL,
'single' : TR_RATIOLIMIT_SINGLE,
'unlimeted' : TR_RATIOLIMIT_UNLIMITED
})
# A note on argument maps
# These maps are used to verify *-set methods. The information is structured in
# a tree.
# set +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# | +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# |
# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>]
# Arguments for torrent methods
TORRENT_ARGS = {
'get' : {
'activityDate': ('number', 1, None, None, None),
'addedDate': ('number', 1, None, None, None),
'announceResponse': ('string', 1, None, None, None),
'announceURL': ('string', 1, None, None, None),
'bandwidthPriority': ('number', 5, None, None, None),
'comment': ('string', 1, None, None, None),
'corruptEver': ('number', 1, None, None, None),
'creator': ('string', 1, None, None, None),
'dateCreated': ('number', 1, None, None, None),
'desiredAvailable': ('number', 1, None, None, None),
'doneDate': ('number', 1, None, None, None),
'downloadDir': ('string', 4, None, None, None),
'downloadedEver': ('number', 1, None, None, None),
'downloaders': ('number', 4, None, None, None),
'downloadLimit': ('number', 1, None, None, None),
'downloadLimited': ('boolean', 5, None, None, None),
'downloadLimitMode': ('number', 1, 5, None, None),
'error': ('number', 1, None, None, None),
'errorString': ('number', 1, None, None, None),
'eta': ('number', 1, None, None, None),
'files': ('array', 1, None, None, None),
'fileStats': ('array', 5, None, None, None),
'hashString': ('string', 1, None, None, None),
'haveUnchecked': ('number', 1, None, None, None),
'haveValid': ('number', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'id': ('number', 1, None, None, None),
'isPrivate': ('boolean', 1, None, None, None),
'lastAnnounceTime': ('number', 1, None, None, None),
'lastScrapeTime': ('number', 1, None, None, None),
'leechers': ('number', 1, None, None, None),
'leftUntilDone': ('number', 1, None, None, None),
'manualAnnounceTime': ('number', 1, None, None, None),
'maxConnectedPeers': ('number', 1, None, None, None),
'name': ('string', 1, None, None, None),
'nextAnnounceTime': ('number', 1, None, None, None),
'nextScrapeTime': ('number', 1, None, None, None),
'peer-limit': ('number', 5, None, None, None),
'peers': ('array', 2, None, None, None),
'peersConnected': ('number', 1, None, None, None),
'peersFrom': ('object', 1, None, None, None),
'peersGettingFromUs': ('number', 1, None, None, None),
'peersKnown': ('number', 1, None, None, None),
'peersSendingToUs': ('number', 1, None, None, None),
'percentDone': ('double', 5, None, None, None),
'pieces': ('string', 5, None, None, None),
'pieceCount': ('number', 1, None, None, None),
'pieceSize': ('number', 1, None, None, None),
'priorities': ('array', 1, None, None, None),
'rateDownload': ('number', 1, None, None, None),
'rateUpload': ('number', 1, None, None, None),
'recheckProgress': ('double', 1, None, None, None),
'scrapeResponse': ('string', 1, None, None, None),
'scrapeURL': ('string', 1, None, None, None),
'seeders': ('number', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'sizeWhenDone': ('number', 1, None, None, None),
'startDate': ('number', 1, None, None, None),
'status': ('number', 1, None, None, None),
'swarmSpeed': ('number', 1, None, None, None),
'timesCompleted': ('number', 1, None, None, None),
'trackers': ('array', 1, None, None, None),
'totalSize': ('number', 1, None, None, None),
'torrentFile': ('string', 5, None, None, None),
'uploadedEver': ('number', 1, None, None, None),
'uploadLimit': ('number', 1, None, None, None),
'uploadLimitMode': ('number', 1, 5, None, None),
'uploadLimited': ('boolean', 5, None, None, None),
'uploadRatio': ('double', 1, None, None, None),
'wanted': ('array', 1, None, None, None),
'webseeds': ('array', 1, None, None, None),
'webseedsSendingToUs': ('number', 1, None, None, None),
},
'set': {
'bandwidthPriority': ('number', 5, None, None, None),
'downloadLimit': ('number', 5, None, 'speed-limit-down', None),
'downloadLimited': ('boolean', 5, None, 'speed-limit-down-enabled', None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'honorsSessionLimits': ('boolean', 5, None, None, None),
'ids': ('array', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
'seedRatioLimit': ('double', 5, None, None, None),
'seedRatioMode': ('number', 5, None, None, None),
'speed-limit-down': ('number', 1, 5, None, 'downloadLimit'),
'speed-limit-down-enabled': ('boolean', 1, 5, None, 'downloadLimited'),
'speed-limit-up': ('number', 1, 5, None, 'uploadLimit'),
'speed-limit-up-enabled': ('boolean', 1, 5, None, 'uploadLimited'),
'uploadLimit': ('number', 5, None, 'speed-limit-up', None),
'uploadLimited': ('boolean', 5, None, 'speed-limit-up-enabled', None),
},
'add': {
'download-dir': ('string', 1, None, None, None),
'filename': ('string', 1, None, None, None),
'files-wanted': ('array', 1, None, None, None),
'files-unwanted': ('array', 1, None, None, None),
'metainfo': ('string', 1, None, None, None),
'paused': ('boolean', 1, None, None, None),
'peer-limit': ('number', 1, None, None, None),
'priority-high': ('array', 1, None, None, None),
'priority-low': ('array', 1, None, None, None),
'priority-normal': ('array', 1, None, None, None),
}
}
# Arguments for session methods
SESSION_ARGS = {
'get': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"blocklist-size": ('number', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, None),
"peer-limit-global": ('number', 5, None, None, None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, None),
"pex-enabled": ('boolean', 5, None, None, None),
"port": ('number', 1, 5, None, None),
"peer-port": ('number', 5, None, None, None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"rpc-version": ('number', 4, None, None, None),
"rpc-version-minimum": ('number', 4, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
"version": ('string', 3, None, None, None),
},
'set': {
"alt-speed-down": ('number', 5, None, None, None),
"alt-speed-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-begin": ('number', 5, None, None, None),
"alt-speed-time-enabled": ('boolean', 5, None, None, None),
"alt-speed-time-end": ('number', 5, None, None, None),
"alt-speed-time-day": ('number', 5, None, None, None),
"alt-speed-up": ('number', 5, None, None, None),
"blocklist-enabled": ('boolean', 5, None, None, None),
"encryption": ('string', 1, None, None, None),
"download-dir": ('string', 1, None, None, None),
"peer-limit": ('number', 1, 5, None, 'peer-limit-global'),
"peer-limit-global": ('number', 5, None, 'peer-limit', None),
"peer-limit-per-torrent": ('number', 5, None, None, None),
"pex-allowed": ('boolean', 1, 5, None, 'pex-enabled'),
"pex-enabled": ('boolean', 5, None, 'pex-allowed', None),
"port": ('number', 1, 5, None, 'peer-port'),
"peer-port": ('number', 5, None, 'port', None),
"peer-port-random-on-start": ('boolean', 5, None, None, None),
"port-forwarding-enabled": ('boolean', 1, None, None, None),
"seedRatioLimit": ('double', 5, None, None, None),
"seedRatioLimited": ('boolean', 5, None, None, None),
"speed-limit-down": ('number', 1, None, None, None),
"speed-limit-down-enabled": ('boolean', 1, None, None, None),
"speed-limit-up": ('number', 1, None, None, None),
"speed-limit-up-enabled": ('boolean', 1, None, None, None),
},
}

View file

@ -0,0 +1,606 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import sys, os, time, datetime
import re
import httplib, urllib2, base64, socket
try:
import json
except ImportError:
import simplejson as json
from constants import *
from utils import *
class TransmissionError(Exception):
def __init__(self, message='', original=None):
Exception.__init__(self, message)
self.message = message
self.original = original
def __str__(self):
if self.original:
original_name = type(self.original).__name__
return '%s Original exception: %s, "%s"' % (self.message, original_name, self.original.args)
else:
return self.args
class Torrent(object):
"""
Torrent is a class holding the data raceived from Transmission regarding a bittorrent transfer.
All fetched torrent fields are accessable through this class using attributes.
This class has a few convenience properties using the torrent data.
"""
def __init__(self, fields):
if 'id' not in fields:
raise ValueError('Torrent requires an id')
self.fields = {}
self.update(fields)
def __repr__(self):
return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
def __str__(self):
return 'torrent %s' % self.fields['name']
def update(self, other):
"""Update the torrent data from a Transmission arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Torrent):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def files(self):
"""
Get list of files for this torrent. This function returns a dictionary with file information for each file.
"""
result = {}
if 'files' in self.fields:
indicies = xrange(len(self.fields['files']))
files = self.fields['files']
priorities = self.fields['priorities']
wanted = self.fields['wanted']
index = 1
for item in zip(indicies, files, priorities, wanted):
selected = bool(item[3])
priority = PRIORITY[item[2]]
result[item[0]] = {
'selected': selected,
'priority': priority,
'size': item[1]['length'],
'name': item[1]['name'],
'completed': item[1]['bytesCompleted']}
return result
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
@property
def status(self):
"""Get the status as string."""
return STATUS[self.fields['status']]
@property
def progress(self):
"""Get the download progress in percent as float."""
try:
return 100.0 * (self.fields['sizeWhenDone'] - self.fields['leftUntilDone']) / float(self.fields['sizeWhenDone'])
except ZeroDivisionError:
return 0.0
@property
def ratio(self):
"""Get the upload/download ratio."""
try:
return self.fields['uploadedEver'] / float(self.fields['downloadedEver'])
except ZeroDivisionError:
return 0.0
@property
def eta(self):
"""Get the "eta" as datetime.timedelta."""
eta = self.fields['eta']
if eta >= 0:
return datetime.timedelta(seconds=eta)
else:
ValueError('eta not valid')
@property
def date_active(self):
"""Get the attribute "activityDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['activityDate'])
@property
def date_added(self):
"""Get the attribute "addedDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['addedDate'])
@property
def date_started(self):
"""Get the attribute "startDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['startDate'])
@property
def date_done(self):
"""Get the attribute "doneDate" as datetime.datetime."""
return datetime.datetime.fromtimestamp(self.fields['doneDate'])
def format_eta(self):
"""Returns the attribute "eta" formatted as a string."""
eta = self.fields['eta']
if eta == -1:
return 'not available'
elif eta == -2:
return 'unknown'
else:
return format_timedelta(self.eta)
class Session(object):
"""
Session is a class holding the session data for a Transmission daemon.
Access the session field can be done through attributes.
The attributes available are the same as the session arguments in the
Transmission RPC specification, but with underscore instead of hypen.
``download-dir`` -> ``download_dir``.
"""
def __init__(self, fields={}):
self.fields = {}
self.update(fields)
def update(self, other):
"""Update the session data from a session arguments dictinary"""
fields = None
if isinstance(other, dict):
fields = other
elif isinstance(other, Session):
fields = other.fields
else:
raise ValueError('Cannot update with supplied data')
for k, v in fields.iteritems():
self.fields[k.replace('-', '_')] = v
def __getattr__(self, name):
try:
return self.fields[name]
except KeyError, e:
raise AttributeError('No attribute %s' % name)
def __str__(self):
text = ''
for k, v in self.fields.iteritems():
text += "% 32s: %s\n" % (k[-32:], v)
return text
class Client(object):
"""
This is it. This class implements the json-RPC protocol to communicate with Transmission.
"""
def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None):
base_url = 'http://' + address + ':' + str(port)
self.url = base_url + '/transmission/rpc'
if user and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm=None, uri=self.url, user=user, passwd=password)
opener = urllib2.build_opener(
urllib2.HTTPBasicAuthHandler(password_manager)
, urllib2.HTTPDigestAuthHandler(password_manager)
)
urllib2.install_opener(opener)
elif user or password:
logger.warning('Either user or password missing, not using authentication.')
self._sequence = 0
self.session = Session()
self.sessionid = 0
self.protocol_version = None
self.get_session()
self.torrent_get_arguments = get_arguments('torrent-get'
, self.rpc_version)
def _debug_request(self, request):
logger.debug(
json.dumps(
{
'request': {
'url': request.get_full_url(),
'request-headers': dict(request.header_items()),
'request-data': json.loads(request.data),
}
},
indent=2
)
)
def _debug_response(self, response, response_data):
try:
response_data = json.loads(response_data)
except:
pass
logger.debug(
json.dumps(
{
'response': {
'url': response.url,
'code': response.code,
'msg': response.msg,
'headers': dict(response.headers),
'data': response_data,
}
},
indent=2
)
)
def _http_query(self, query):
headers = {'X-Transmission-Session-Id': self.sessionid}
request = urllib2.Request(self.url, query, headers)
request_count = 0
while True:
error_data = ""
try:
try:
self._debug_request(request)
socket.setdefaulttimeout(10)
if (sys.version_info[0] == 2 and sys.version_info[1] > 5) or sys.version_info[0] > 2:
response = urllib2.urlopen(request, timeout=60)
else:
response = urllib2.urlopen(request)
break
except urllib2.HTTPError, error:
error_data = error.read()
if error.code == 409:
logger.info('Server responded with 409, trying to set session-id.')
if request_count > 1:
raise TransmissionError('Session ID negotiation failed.', error)
if 'X-Transmission-Session-Id' in error.headers:
self.sessionid = error.headers['X-Transmission-Session-Id']
request.add_header('X-Transmission-Session-Id', self.sessionid)
else:
raise TransmissionError('Unknown conflict.', error)
except urllib2.URLError, error:
raise TransmissionError('Failed to connect to daemon.', error)
except httplib.BadStatusLine, error:
if (request_count > 1):
raise TransmissionError('Failed to request %s "%s".' % (self.url, query), error)
finally:
if error_data:
self._debug_response(error, error_data)
request_count = request_count + 1
result = response.read()
self._debug_response(response, result)
return result
def _request(self, method, arguments={}, ids=[], require_ids = False):
"""Send json-rpc request to Transmission using http POST"""
if not isinstance(method, (str, unicode)):
raise ValueError('request takes method as string')
if not isinstance(arguments, dict):
raise ValueError('request takes arguments as dict')
ids = self._format_ids(ids)
if len(ids) > 0:
arguments['ids'] = ids
elif require_ids:
raise ValueError('request require ids')
query = json.dumps({'tag': self._sequence, 'method': method
, 'arguments': arguments})
logger.info(query)
self._sequence += 1
start = time.time()
http_data = self._http_query(query)
elapsed = time.time() - start
logger.info('http request took %.3f s' % (elapsed))
try:
data = json.loads(http_data)
except ValueError, e:
logger.error('Error: ' + str(e))
logger.error('Request: \"%s\"' % (query))
logger.error('HTTP data: \"%s\"' % (http_data))
raise
logger.info(json.dumps(data, indent=2))
if data['result'] != 'success':
raise TransmissionError('Query failed with result \"%s\"'
% data['result'])
results = {}
if method == 'torrent-get':
for item in data['arguments']['torrents']:
results[item['id']] = Torrent(item)
if self.protocol_version == 2 and 'peers' not in item:
self.protocol_version = 1
elif method == 'torrent-add':
item = data['arguments']['torrent-added']
results[item['id']] = Torrent(item)
elif method == 'session-get':
self._update_session(data['arguments'])
elif method == 'session-stats':
# older versions of T has the return data in "session-stats"
if 'session-stats' in data['arguments']:
self._update_session(data['arguments']['session-stats'])
else:
self._update_session(data['arguments'])
elif method in ('port-test', 'blocklist-update'):
results = data['arguments']
else:
return None
return results
def _format_ids(self, args):
"""Take things and make them valid torrent identifiers"""
ids = []
if isinstance(args, (int, long)):
ids.append(args)
elif isinstance(args, (str, unicode)):
for item in re.split(u'[ ,]+', args):
if len(item) == 0:
continue
addition = None
try:
# handle index
addition = [int(item)]
except ValueError:
pass
if not addition:
# handle hashes
try:
int(item, 16)
addition = [item]
except:
pass
if not addition:
# handle index ranges i.e. 5:10
match = re.match(u'^(\d+):(\d+)$', item)
if match:
try:
idx_from = int(match.group(1))
idx_to = int(match.group(2))
addition = range(idx_from, idx_to + 1)
except:
pass
if not addition:
raise ValueError(u'Invalid torrent id, \"%s\"' % item)
ids.extend(addition)
elif isinstance(args, (list)):
for item in args:
ids.extend(self._format_ids(item))
else:
raise ValueError(u'Invalid torrent id')
return ids
def _update_session(self, data):
self.session.update(data)
@property
def rpc_version(self):
if self.protocol_version == None:
if hasattr(self.session, 'rpc_version'):
self.protocol_version = self.session.rpc_version
elif hasattr(self.session, 'version'):
self.protocol_version = 3
else:
self.protocol_version = 2
return self.protocol_version
def _rpc_version_warning(self, version):
if self.rpc_version < version:
logger.warning('Using feature not supported by server. RPC version for server %d, feature introduced in %d.' % (self.rpc_version, version))
def add(self, data, **kwargs):
"""
Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
args = {'metainfo': data}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-add',
argument, value, self.rpc_version)
args[arg] = val
return self._request('torrent-add', args)
def add_url(self, torrent_url, **kwargs):
"""
Add torrent to transfers list. Takes a url to a .torrent file.
Additional arguments are:
* `paused`, boolean, Whether to pause the transfer on add.
* `download_dir`, path, The directory where the downloaded
contents will be saved in.
* `peer_limit`, number, Limits the number of peers for this
transfer.
* `files_unwanted`,
* `files_wanted`,
* `priority_high`,
* `priority_low`,
* `priority_normal`,
"""
torrent_file = None
if os.path.exists(torrent_url):
torrent_file = open(torrent_url, 'r')
else:
try:
torrent_file = urllib2.urlopen(torrent_url)
except:
torrent_file = None
if not torrent_file:
raise TransmissionError('File does not exist.')
torrent_data = base64.b64encode(torrent_file.read())
return self.add(torrent_data, **kwargs)
def remove(self, ids, delete_data=False):
"""
remove torrent(s) with provided id(s). Local data is removed if
delete_data is True, otherwise not.
"""
self._rpc_version_warning(3)
self._request('torrent-remove',
{'delete-local-data':rpc_bool(delete_data)}, ids, True)
def start(self, ids):
"""start torrent(s) with provided id(s)"""
self._request('torrent-start', {}, ids, True)
def stop(self, ids):
"""stop torrent(s) with provided id(s)"""
self._request('torrent-stop', {}, ids, True)
def verify(self, ids):
"""verify torrent(s) with provided id(s)"""
self._request('torrent-verify', {}, ids, True)
def reannounce(self, ids):
"""reannounce torrent(s) with provided id(s)"""
self._rpc_version_warning(5)
self._request('torrent-reannounce', {}, ids, True)
def info(self, ids=[], arguments={}):
"""Get detailed information for torrent(s) with provided id(s)."""
if not arguments:
arguments = self.torrent_get_arguments
return self._request('torrent-get', {'fields': arguments}, ids)
def get_files(self, ids=[]):
"""
Get list of files for provided torrent id(s).
This function returns a dictonary for each requested torrent id holding
the information about the files.
"""
fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
request_result = self._request('torrent-get', {'fields': fields}, ids)
result = {}
for id, torrent in request_result.iteritems():
result[id] = torrent.files()
return result
def set_files(self, items):
"""
Set file properties. Takes a dictonary with similar contents as the
result of get_files.
"""
if not isinstance(items, dict):
raise ValueError('Invalid file description')
for tid, files in items.iteritems():
if not isinstance(files, dict):
continue
wanted = []
unwanted = []
priority_high = []
priority_normal = []
priority_low = []
for fid, file in files.iteritems():
if not isinstance(file, dict):
continue
if 'selected' in file and file['selected']:
wanted.append(fid)
else:
unwanted.append(fid)
if 'priority' in file:
if file['priority'] == 'high':
priority_high.append(fid)
elif file['priority'] == 'normal':
priority_normal.append(fid)
elif file['priority'] == 'low':
priority_low.append(fid)
self.change([tid], files_wanted = wanted
, files_unwanted = unwanted
, priority_high = priority_high
, priority_normal = priority_normal
, priority_low = priority_low)
def list(self):
"""list all torrents"""
fields = ['id', 'hashString', 'name', 'sizeWhenDone', 'leftUntilDone'
, 'eta', 'status', 'rateUpload', 'rateDownload', 'uploadedEver'
, 'downloadedEver']
return self._request('torrent-get', {'fields': fields})
def change(self, ids, **kwargs):
"""
Change torrent parameters. This is the list of parameters that.
"""
args = {}
for key, value in kwargs.iteritems():
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('torrent-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('torrent-set', args, ids, True)
else:
ValueError("No arguments to set")
def get_session(self):
"""Get session parameters"""
self._request('session-get')
return self.session
def set_session(self, **kwargs):
"""Set session parameters"""
args = {}
for key, value in kwargs.iteritems():
if key == 'encryption' and value not in ['required', 'preferred', 'tolerated']:
raise ValueError('Invalid encryption value')
argument = make_rpc_name(key)
(arg, val) = argument_value_convert('session-set'
, argument, value, self.rpc_version)
args[arg] = val
if len(args) > 0:
self._request('session-set', args)
def blocklist_update(self):
"""Update block list. Returns the size of the block list."""
self._rpc_version_warning(5)
result = self._request('blocklist-update')
if 'blocklist-size' in result:
return result['blocklist-size']
return None
def port_test(self):
"""
Tests to see if your incoming peer port is accessible from the
outside world.
"""
self._rpc_version_warning(5)
result = self._request('port-test')
if 'port-is-open' in result:
return result['port-is-open']
return None
def session_stats(self):
"""Get session statistics"""
self._request('session-stats')
return self.session

View file

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# 2008-07, Erik Svensson <erik.public@gmail.com>
import socket, datetime
import constants
from constants import logger
UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
def format_size(size):
s = float(size)
i = 0
while size >= 1024.0 and i < len(UNITS):
i += 1
size /= 1024.0
return (size, UNITS[i])
def format_speed(size):
(size, unit) = format_size(size)
return (size, unit + '/s')
def format_timedelta(delta):
minutes, seconds = divmod(delta.seconds, 60)
hours, minutes = divmod(minutes, 60)
return '%d %02d:%02d:%02d' % (delta.days, hours, minutes, seconds)
def format_timestamp(timestamp):
if timestamp > 0:
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.isoformat(' ')
else:
return '-'
class INetAddressError(Exception):
pass
def inet_address(address, default_port, default_address='localhost'):
addr = address.split(':')
if len(addr) == 1:
try:
port = int(addr[0])
addr = default_address
except:
addr = addr[0]
port = default_port
elif len(addr) == 2:
port = int(addr[1])
if len(addr[0]) == 0:
addr = default_address
else:
addr = addr[0]
else:
addr = default_address
port = default_port
try:
socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
except socket.gaierror, e:
raise INetAddressError('Cannot look up address "%s".' % address)
return (addr, port)
def rpc_bool(arg):
if isinstance(arg, (str, unicode)):
try:
arg = bool(int(arg))
except:
arg = arg.lower() in [u'true', u'yes']
if bool(arg):
return 1
else:
return 0
TR_TYPE_MAP = {
'number' : int,
'string' : str,
'double': float,
'boolean' : rpc_bool,
'array': list,
'object': dict
}
def make_python_name(name):
return name.replace('-', '_')
def make_rpc_name(name):
return name.replace('_', '-')
def argument_value_convert(method, argument, value, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
if argument in args:
info = args[argument]
invalid_version = True
while invalid_version:
invalid_version = False
replacement = None
if rpc_version < info[1]:
invalid_version = True
replacement = info[3]
if info[2] and info[2] <= rpc_version:
invalid_version = True
replacement = info[4]
if invalid_version:
if replacement:
logger.warning(
'Replacing requested argument "%s" with "%s".'
% (argument, replacement))
argument = replacement
info = args[argument]
else:
raise ValueError(
'Method "%s" Argument "%s" does not exist in version %d.'
% (method, argument, rpc_version))
return (argument, TR_TYPE_MAP[info[0]](value))
else:
raise ValueError('Argument "%s" does not exists for method "%s".',
(argument, method))
def get_arguments(method, rpc_version):
if method in ('torrent-add', 'torrent-get', 'torrent-set'):
args = constants.TORRENT_ARGS[method[-3:]]
elif method in ('session-get', 'session-set'):
args = constants.SESSION_ARGS[method[-3:]]
else:
return ValueError('Method "%s" not supported' % (method))
accessible = []
for argument, info in args.iteritems():
valid_version = True
if rpc_version < info[1]:
valid_version = False
if info[2] and info[2] <= rpc_version:
valid_version = False
if valid_version:
accessible.append(argument)
return accessible

8
resources/settings.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings>
<setting type="lsep" label="1000" />
<setting id="rpc_host" type="text" label="1001" default="localhost" />
<setting id="rpc_port" type="integer" label="1002" default="9091" />
<setting id="rpc_user" type="text" label="1003" default="" />
<setting id="rpc_password" type="text" option="hidden" label="1004" default="" />
</settings>

View file

@ -0,0 +1,20 @@
<window>
<defaultcontrol>20</defaultcontrol>
<allowoverlay>yes</allowoverlay>
<controls>
<control type="label">
<description>textarea</description>
<posx>0</posx>
<posy>0</posy>
<width>720</width>
<height>40</height>
<visible>true</visible>
<label>Transmission XBMC - Details</label>
<font>font11</font>
<textcolor>white</textcolor>
<align>center</align>
</control>
<control type="group">
</control>
</controls>
</window>

View file

@ -0,0 +1,216 @@
<window>
<defaultcontrol>20</defaultcontrol>
<coordinates>
<system>1</system>
<posx>20</posx>
<posy>20</posy>
</coordinates>
<controls>
<control type="image">
<description>backdrop</description>
<posx>-20</posx>
<posy>-20</posy>
<width>720</width>
<height>576</height>
<texture>black.png</texture>
</control>
<control type="image">
<posx>-10</posx>
<posy>-8</posy>
<width>700</width>
<height>560</height>
<texture>transmission-main.png</texture>
<animation effect="fade" time="200">WindowOpen</animation>
<animation effect="fade" time="200">WindowClose</animation>
</control>
<control type="label">
<description>textarea</description>
<posx>0</posx>
<posy>0</posy>
<width>700</width>
<visible>true</visible>
<label>Transmission</label>
<font>font11</font>
<textcolor>white</textcolor>
<align>center</align>
</control>
<control type="group">
<posx>0</posx>
<posy>10</posy>
<!-- Button Group -->
<control type="button" id="11">
<description>Add torrent</description>
<posx>0</posx>
<posy>0</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT101</label>
<onright>20</onright>
<ondown>12</ondown>
</control>
<control type="button" id="12">
<description>Remove torrent</description>
<posx>0</posx>
<posy>70</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT102</label>
<onright>20</onright>
<onup>11</onup>
<ondown>13</ondown>
</control>
<control type="button" id="13">
<description>Stop torrent</description>
<posx>0</posx>
<posy>150</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT103</label>
<onright>20</onright>
<onup>12</onup>
<ondown>14</ondown>
</control>
<control type="button" id="14">
<description>Start torrent</description>
<posx>0</posx>
<posy>220</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT104</label>
<onright>20</onright>
<onup>13</onup>
<ondown>15</ondown>
</control>
<control type="button" id="15">
<description>Stop all torrents</description>
<posx>0</posx>
<posy>300</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT105</label>
<onright>20</onright>
<onup>14</onup>
<ondown>16</ondown>
</control>
<control type="button" id="16">
<description>Start all torrents</description>
<posx>0</posx>
<posy>370</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT106</label>
<onright>20</onright>
<onup>15</onup>
<ondown>17</ondown>
</control>
<control type="button" id="17">
<description>Exit</description>
<posx>0</posx>
<posy>450</posy>
<height>70</height>
<width>70</width>
<align>center</align>
<aligny>top</aligny>
<textoffsety>45</textoffsety>
<label>SCRIPT107</label>
<onright>20</onright>
<onup>16</onup>
</control>
</control>
<!--
<control type="image">
<posx>90</posx>
<posy>35</posy>
<width>560</width>
<height>500</height>
<texture>blue.png</texture>
</control>
-->
<control type="list" id="20">
<description>Torrent list</description>
<posx>90</posx>
<posy>35</posy>
<width>560</width>
<height>500</height>
<viewtype label="Torrent List">list</viewtype>
<orientation>vertical</orientation>
<visible>true</visible>
<onleft>11</onleft>
<itemlayout width="560" height="70">
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>610</width>
<height>70</height>
<texture>list-bg.png</texture>
</control>
<control type="label">
<posx>10</posx>
<posy>0</posy>
<width>640</width>
<height>20</height>
<info>ListItem.label</info>
</control>
<control type="label">
<posx>15</posx>
<posy>20</posy>
<width>635</width>
<height>20</height>
<info>ListItem.label2</info>
</control>
</itemlayout>
<focusedlayout width="560" height="70">
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>610</width>
<height>70</height>
<visible>Control.HasFocus(20)</visible>
<texture border="5">list-bg-selected.png</texture>
</control>
<control type="image">
<posx>0</posx>
<posy>0</posy>
<width>610</width>
<height>70</height>
<visible>!Control.HasFocus(20)</visible>
<texture border="5">list-bg-selected-nofocus.png</texture>
</control>
<control type="label">
<posx>10</posx>
<posy>0</posy>
<width>640</width>
<height>20</height>
<info>ListItem.label</info>
<scroll>true</scroll>
</control>
<control type="label">
<posx>15</posx>
<posy>20</posy>
<width>635</width>
<height>20</height>
<info>ListItem.label2</info>
</control>
</focusedlayout>
</control>
</controls>
</window>

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

View file

@ -0,0 +1,11 @@
<skin>
<defaultresolution>pal</defaultresolution>
<defaultresolutionwide>pal</defaultresolutionwide>
<version>2.1</version>
<zoom>0</zoom>
<credits>
<skinname>default</skinname>
<name>correl</name>
</credits>
</skin>

View file

@ -0,0 +1,3 @@
[Dolphin]
Timestamp=2009,12,9,0,31,14
ViewMode=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB