Initial commit
27
default.py
Normal 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
After Width: | Height: | Size: 43 KiB |
20
resources/language/English/strings.xml
Normal 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>
|
8
resources/lib/basictypes/__init__.py
Normal 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.
|
||||
"""
|
274
resources/lib/basictypes/basic_types.py
Normal 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 )
|
||||
|
66
resources/lib/basictypes/booleanfix.py
Normal 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
|
||||
|
303
resources/lib/basictypes/boundary.py
Normal 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.
|
||||
"""
|
||||
|
76
resources/lib/basictypes/bytes.py
Normal 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
|
309
resources/lib/basictypes/callable.py
Normal 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,
|
||||
## )
|
||||
##
|
91
resources/lib/basictypes/datatypedefinition.py
Normal 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 )
|
108
resources/lib/basictypes/date_types.py
Normal 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
|
196
resources/lib/basictypes/datedatetime_types.py
Normal 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 )
|
||||
|
383
resources/lib/basictypes/datemx_types.py
Normal 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 )
|
57
resources/lib/basictypes/debug.py
Normal 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 )
|
||||
|
||||
|
21
resources/lib/basictypes/decimaldt.py
Normal 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 )
|
||||
|
37
resources/lib/basictypes/domainname.py
Normal 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 )
|
282
resources/lib/basictypes/enumeration.py
Normal 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())
|
||||
##
|
||||
|
20
resources/lib/basictypes/factory.py
Normal 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',
|
||||
)
|
||||
|
257
resources/lib/basictypes/interfaces.py
Normal 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)""",
|
||||
)
|
68
resources/lib/basictypes/latebind.py
Normal 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
|
||||
|
||||
|
35
resources/lib/basictypes/license.txt
Normal 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.
|
227
resources/lib/basictypes/list_types.py
Normal 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",
|
||||
)
|
893
resources/lib/basictypes/pythoninterfaces.py
Normal 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()
|
16
resources/lib/basictypes/registry.py
Normal 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 )
|
48
resources/lib/basictypes/rlist.py
Normal 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' )
|
||||
|
||||
|
67
resources/lib/basictypes/rstring.py
Normal 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
|
87
resources/lib/basictypes/typeunion.py
Normal 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
|
||||
|
18
resources/lib/basictypes/vfs/__init__.py
Normal 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
|
||||
|
154
resources/lib/basictypes/vfs/basepath.py
Normal 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
|
360
resources/lib/basictypes/vfs/filepath.py
Normal 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"""
|
||||
|
116
resources/lib/basictypes/vfs/test_filepath.py
Normal 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")
|
1
resources/lib/basictypes/wx/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""wxPython data-type models"""
|
59
resources/lib/basictypes/wx/colour.py
Normal 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)
|
13
resources/lib/basictypes/wx/font.py
Normal 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)
|
212
resources/lib/basictypes/wx/pen.py
Normal 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,
|
||||
## )
|
||||
|
||||
|
||||
|
||||
|
131
resources/lib/basictypes/wx/wxcopyreg.py
Normal 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()
|
1
resources/lib/basictypes/wxtypes/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""wxPython data-type models"""
|
59
resources/lib/basictypes/wxtypes/colour.py
Normal 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)
|
13
resources/lib/basictypes/wxtypes/font.py
Normal 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)
|
212
resources/lib/basictypes/wxtypes/pen.py
Normal 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,
|
||||
## )
|
||||
|
||||
|
||||
|
||||
|
131
resources/lib/basictypes/wxtypes/wxcopyreg.py
Normal 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()
|
55
resources/lib/basictypes/xmlgenerator.py
Normal 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
|
@ -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
|
@ -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()
|
318
resources/lib/simplejson/__init__.py
Normal 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)
|
2329
resources/lib/simplejson/_speedups.c
Normal file
354
resources/lib/simplejson/decoder.py
Normal 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
|
440
resources/lib/simplejson/encoder.py
Normal 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
|
65
resources/lib/simplejson/scanner.py
Normal 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
|
37
resources/lib/simplejson/tool.py
Normal 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()
|
80
resources/lib/transmissionrpc-0.3-python2.4.patch
Normal 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,
|
10
resources/lib/transmissionrpc-0.3-python2.4/__init__.py
Normal 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'
|
230
resources/lib/transmissionrpc-0.3-python2.4/constants.py
Executable 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),
|
||||
},
|
||||
}
|
606
resources/lib/transmissionrpc-0.3-python2.4/transmission.py
Executable 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
|
138
resources/lib/transmissionrpc-0.3-python2.4/utils.py
Normal 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
|
10
resources/lib/transmissionrpc-0.3/__init__.py
Normal 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'
|
230
resources/lib/transmissionrpc-0.3/constants.py
Executable 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),
|
||||
},
|
||||
}
|
605
resources/lib/transmissionrpc-0.3/transmission.py
Executable 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
|
135
resources/lib/transmissionrpc-0.3/utils.py
Normal 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
|
10
resources/lib/transmissionrpc-0.4/__init__.py
Executable 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'
|
234
resources/lib/transmissionrpc-0.4/constants.py
Executable 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),
|
||||
},
|
||||
}
|
628
resources/lib/transmissionrpc-0.4/transmission.py
Executable 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
|
138
resources/lib/transmissionrpc-0.4/utils.py
Normal 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
|
10
resources/lib/transmissionrpc/__init__.py
Normal 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'
|
230
resources/lib/transmissionrpc/constants.py
Executable 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),
|
||||
},
|
||||
}
|
606
resources/lib/transmissionrpc/transmission.py
Executable 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
|
138
resources/lib/transmissionrpc/utils.py
Normal 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
|
@ -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>
|
20
resources/skins/default/PAL/script-Transmission-details.xml
Normal 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>
|
216
resources/skins/default/PAL/script-Transmission-main.xml
Normal 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>
|
BIN
resources/skins/default/media/black.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
resources/skins/default/media/blue.png
Normal file
After Width: | Height: | Size: 227 B |
BIN
resources/skins/default/media/list-bg-selected-nofocus.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
resources/skins/default/media/list-bg-selected.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
resources/skins/default/media/list-bg.png
Normal file
After Width: | Height: | Size: 554 B |
BIN
resources/skins/default/media/transmission-main.png
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
resources/skins/default/media/transmission-main.xcf
Normal file
11
resources/skins/default/skin.xml
Normal 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>
|
3
resources/skins/media/.directory
Normal file
|
@ -0,0 +1,3 @@
|
|||
[Dolphin]
|
||||
Timestamp=2009,12,9,0,31,14
|
||||
ViewMode=1
|
BIN
resources/skins/media/transmission-512.png
Normal file
After Width: | Height: | Size: 43 KiB |