Commits

anothergene committed 84b5559

Added 0.70 version of transmisionrpc lib

Comments (0)

Files changed (8)

transmissionrpc/__init__.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT, STATUS, PRIORITY, RATIO_LIMIT, LOGGER
+from transmissionrpc.error import TransmissionError, HTTPHandlerError
+from transmissionrpc.httphandler import HTTPHandler, DefaultHTTPHandler
+from transmissionrpc.torrent import Torrent
+from transmissionrpc.session import Session
+from transmissionrpc.client import Client
+from transmissionrpc.utils import add_stdout_logger
+
+__author__    = u'Erik Svensson <erik.public@gmail.com>'
+__version__   = u'0.7'
+__copyright__ = u'Copyright (c) 2008-2010 Erik Svensson'
+__license__   = u'MIT'

transmissionrpc/client.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+import os, re, time
+import warnings
+import httplib, urllib2, urlparse, base64
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+from transmissionrpc.constants import DEFAULT_PORT, DEFAULT_TIMEOUT
+from transmissionrpc.error import TransmissionError, HTTPHandlerError
+from transmissionrpc.utils import LOGGER, get_arguments, make_rpc_name, argument_value_convert, rpc_bool
+from transmissionrpc.httphandler import DefaultHTTPHandler
+from transmissionrpc.torrent import Torrent
+from transmissionrpc.session import Session
+
+def debug_httperror(error):
+    """
+    Log the Transmission RPC HTTP error. 
+    """
+    try:
+        data = json.loads(error.data)
+    except ValueError:
+        data = error.data
+    LOGGER.debug(
+        json.dumps(
+            {
+                'response': {
+                    'url': error.url,
+                    'code': error.code,
+                    'msg': error.message,
+                    'headers': error.headers,
+                    'data': data,
+                }
+            },
+            indent=2
+        )
+    )
+
+"""
+Torrent ids
+
+Many functions in Client takes torrent id. A torrent id can either be id or
+hashString. When suppling multiple id's it is possible to use a list mixed
+with both id and hashString.
+
+Timeouts
+
+Since most methods results in HTTP requests against Transmission, it is
+possible to provide a argument called ``timeout``. Timeout is only effective
+when using Python 2.6 or later and the default timeout is 30 seconds.
+"""
+
+class Client(object):
+    """
+    Client is the class handling the Transmission JSON-RPC client protocol.
+    """
+
+    def __init__(self, address='localhost', port=DEFAULT_PORT, user=None, password=None, http_handler=None, timeout=None):
+        if isinstance(timeout, (int, long, float)):
+            self._query_timeout = float(timeout)
+        else:
+            self._query_timeout = DEFAULT_TIMEOUT
+        urlo = urlparse.urlparse(address)
+        if urlo.scheme == '':
+            base_url = 'http://' + address + ':' + str(port)
+            self.url = base_url + '/transmission/rpc'
+        else:
+            if urlo.port:
+                self.url = urlo.scheme + '://' + urlo.hostname + ':' + str(urlo.port) + urlo.path
+            else:
+                self.url = urlo.scheme + '://' + urlo.hostname + urlo.path
+            LOGGER.info('Using custom URL "' + self.url + '".')
+            if urlo.username and urlo.password:
+                user = urlo.username
+                password = urlo.password
+            elif urlo.username or urlo.password:
+                LOGGER.warning('Either user or password missing, not using authentication.')
+        if http_handler == None:
+            self.http_handler = DefaultHTTPHandler()
+        else:
+            if hasattr(http_handler, 'set_authentication') and hasattr(http_handler, 'request'):
+                self.http_handler = http_handler
+            else:
+                raise ValueError('Invalid HTTP handler.')
+        if user and password:
+            self.http_handler.set_authentication(self.url, user, password)
+        elif user or password:
+            LOGGER.warning('Either user or password missing, not using authentication.')
+        self._sequence = 0
+        self.session = Session()
+        self.session_id = 0
+        self.server_version = None
+        self.protocol_version = None
+        self.get_session()
+        self.torrent_get_arguments = get_arguments('torrent-get'
+                                                   , self.rpc_version)
+
+    def _get_timeout(self):
+        """
+        Get current timeout for HTTP queries.
+        """
+        return self._query_timeout
+    
+    def _set_timeout(self, value):
+        """
+        Set timeout for HTTP queries.
+        """
+        self._query_timeout = float(value)
+    
+    def _del_timeout(self):
+        """
+        Reset the HTTP query timeout to the default.
+        """
+        self._query_timeout = DEFAULT_TIMEOUT
+    
+    timeout = property(_get_timeout, _set_timeout, _del_timeout, doc="HTTP query timeout.")
+
+    def _http_query(self, query, timeout=None):
+        """
+        Query Transmission through HTTP.
+        """
+        headers = {'x-transmission-session-id': str(self.session_id)}
+        request_count = 0
+        if timeout == None:
+            timeout = self._query_timeout
+        while True:
+            LOGGER.debug(json.dumps({'url': self.url, 'headers': headers, 'query': query, 'timeout': timeout}, indent=2))
+            try:
+                result = self.http_handler.request(self.url, query, headers, timeout)
+                break
+            except HTTPHandlerError, error:
+                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.session_id = error.headers['x-transmission-session-id']
+                        headers = {'x-transmission-session-id': str(self.session_id)}
+                    else:
+                        debug_httperror(error)
+                        raise TransmissionError('Unknown conflict.', error)
+                else:
+                    debug_httperror(error)
+                    raise TransmissionError('Request failed.', error)
+            request_count = request_count + 1
+        return result
+
+    def _request(self, method, arguments=None, ids=None, require_ids=False, timeout=None):
+        """
+        Send json-rpc request to Transmission using http POST
+        """
+        if not isinstance(method, (str, unicode)):
+            raise ValueError('request takes method as string')
+        if arguments == None:
+            arguments = {}
+        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})
+        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, error:
+            LOGGER.error('Error: ' + str(error))
+            LOGGER.error('Request: \"%s\"' % (query))
+            LOGGER.error('HTTP data: \"%s\"' % (http_data))
+            raise
+
+        LOGGER.debug(json.dumps(data, indent=2))
+        if 'result' in data:
+            if data['result'] != 'success':
+                raise TransmissionError('Query failed with result \"%s\".' % (data['result']))
+        else:
+            raise TransmissionError('Query failed without result.')
+
+        results = {}
+        if method == 'torrent-get':
+            for item in data['arguments']['torrents']:
+                results[item['id']] = Torrent(self, 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(self, 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 args == None:
+            pass
+        elif 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 ValueError:
+                        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 ValueError:
+                            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):
+        """
+        Update session data.
+        """
+        self.session.update(data)
+
+    def _update_server_version(self):
+        if self.server_version == None:
+            version_major = 1
+            version_minor = 30
+            version_changeset = 0
+            version_parser = re.compile('(\d).(\d+) \((\d+)\)')
+            if hasattr(self.session, 'version'):
+                match = version_parser.match(self.session.version)
+                if (match):
+                    version_major = int(match.group(1))
+                    version_minor = int(match.group(2))
+                    version_changeset = match.group(3)
+            self.server_version = (version_major, version_minor, version_changeset)
+
+    @property
+    def rpc_version(self):
+        """
+        Get the Transmission RPC version. Trying to deduct if the server don't have a version value.
+        """
+        if self.protocol_version == None:
+            # Ugly fix for 2.12 reporting rpc-version 10, but having new arguments
+            if (self.server_version and (self.server_version[0] == 2 and self.server_version[1] == 12)):
+                self.protocol_version = 11
+            elif 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):
+        """
+        Add a warning to the log if the Transmission RPC version is lower then the provided 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=None, **kwargs):
+        """
+        Add torrent to transfers list. Takes a base64 encoded .torrent file in data.
+        Additional arguments are:
+
+        ===================== ==== =============================================================
+        Argument              RPC  Description                                                  
+        ===================== ==== =============================================================
+        ``bandwidthPriority`` 8 -  Priority for this transfer.                                  
+        ``download_dir``      1 -  The directory where the downloaded contents will be saved in.
+        ``filename``          1 -  A filepath or URL to a torrent file or a magnet link.        
+        ``files_unwanted``    1 -  A list of file id's that shouldn't be downloaded.            
+        ``files_wanted``      1 -  A list of file id's that should be downloaded.               
+        ``metainfo``          1 -  The content of a torrent file, base64 encoded.               
+        ``paused``            1 -  If True, does not start the transfer when added.             
+        ``peer_limit``        1 -  Maximum number of peers allowed.                             
+        ``priority_high``     1 -  A list of file id's that should have high priority.          
+        ``priority_low``      1 -  A list of file id's that should have low priority.           
+        ``priority_normal``   1 -  A list of file id's that should have normal priority.        
+        ===================== ==== =============================================================
+        """
+        args = {}
+        if data:
+            args = {'metainfo': data}
+        elif 'metainfo' not in kwargs and 'filename' not in kwargs:
+            raise ValueError('No torrent data or torrent uri.')
+        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_uri(self, uri, **kwargs):
+        """
+        Add torrent to transfers list. Takes a uri to a torrent, supporting
+        all uri's supported by Transmissions torrent-add 'filename'
+        argument. Additional arguments are:
+
+        ===================== ==== =============================================================
+        Argument              RPC  Description                                                  
+        ===================== ==== =============================================================
+        ``bandwidthPriority`` 8 -  Priority for this transfer.                                  
+        ``download_dir``      1 -  The directory where the downloaded contents will be saved in.
+        ``files_unwanted``    1 -  A list of file id's that shouldn't be downloaded.            
+        ``files_wanted``      1 -  A list of file id's that should be downloaded.               
+        ``paused``            1 -  If True, does not start the transfer when added.             
+        ``peer_limit``        1 -  Maximum number of peers allowed.                             
+        ``priority_high``     1 -  A list of file id's that should have high priority.          
+        ``priority_low``      1 -  A list of file id's that should have low priority.           
+        ``priority_normal``   1 -  A list of file id's that should have normal priority.        
+        ===================== ==== =============================================================
+        """
+        if uri == None:
+            raise ValueError('add_uri requires a URI.')
+        # there has been some problem with T's built in torrent fetcher,
+        # use a python one instead
+        parseduri = urlparse.urlparse(uri)
+        torrent_data = None
+        if parseduri.scheme in ['file', 'ftp', 'ftps', 'http', 'https']:
+            torrent_file = urllib2.urlopen(uri)
+            torrent_data = base64.b64encode(torrent_file.read())
+        if torrent_data:
+            return self.add(torrent_data, **kwargs)
+        else:
+            return self.add(None, filename=uri, **kwargs)
+    
+    def remove(self, ids, delete_data=False, timeout=None):
+        """
+        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=None):
+        """start torrent(s) with provided id(s)"""
+        self._request('torrent-start', {}, ids, True, timeout=timeout)
+
+    def stop(self, ids, timeout=None):
+        """stop torrent(s) with provided id(s)"""
+        self._request('torrent-stop', {}, ids, True, timeout=timeout)
+
+    def verify(self, ids, timeout=None):
+        """verify torrent(s) with provided id(s)"""
+        self._request('torrent-verify', {}, ids, True, timeout=timeout)
+
+    def reannounce(self, ids, timeout=None):
+        """Reannounce torrent(s) with provided id(s)"""
+        self._rpc_version_warning(5)
+        self._request('torrent-reannounce', {}, ids, True, timeout=timeout)
+
+    def info(self, ids=None, arguments=None, timeout=None):
+        """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=None, timeout=None):
+        """
+    	Get list of files for provided torrent id(s). If ids is empty,
+    	information for all torrents are fetched. This function returns a dictonary
+    	for each requested torrent id holding the information about the files.
+
+    	::
+
+    		{
+    			<torrent id>: {
+    				<file id>: {
+    					'name': <file name>,
+    					'size': <file size in bytes>,
+    					'completed': <bytes completed>,
+    					'priority': <priority ('high'|'normal'|'low')>,
+    					'selected': <selected for download (True|False)>
+    				}
+
+    				...
+    			}
+
+    			...
+    		}
+        """
+        fields = ['id', 'name', 'hashString', 'files', 'priorities', 'wanted']
+        request_result = self._request('torrent-get', {'fields': fields}, ids, timeout=timeout)
+        result = {}
+        for tid, torrent in request_result.iteritems():
+            result[tid] = torrent.files()
+        return result
+
+    def set_files(self, items, timeout=None):
+        """
+        Set file properties. Takes a dictonary with similar contents as the result
+    	of `get_files`.
+
+    	::
+
+    		{
+    			<torrent id>: {
+    				<file id>: {
+    					'priority': <priority ('high'|'normal'|'low')>,
+    					'selected': <selected for download (True|False)>
+    				}
+
+    				...
+    			}
+
+    			...
+    		}
+        """
+        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_desc in files.iteritems():
+                if not isinstance(file_desc, dict):
+                    continue
+                if 'selected' in file_desc and file_desc['selected']:
+                    wanted.append(fid)
+                else:
+                    unwanted.append(fid)
+                if 'priority' in file_desc:
+                    if file_desc['priority'] == 'high':
+                        priority_high.append(fid)
+                    elif file_desc['priority'] == 'normal':
+                        priority_normal.append(fid)
+                    elif file_desc['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=None):
+        """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=None, **kwargs):
+        """
+    	Change torrent parameters for the torrent(s) with the supplied id's. The
+    	parameters are:
+
+        ============================ ===== =============== =======================================================================================
+        Argument                     RPC   Replaced by     Description                                                                            
+        ============================ ===== =============== =======================================================================================
+        ``bandwidthPriority``        5 -                   Priority for this transfer.                                                            
+        ``downloadLimit``            5 -                   Set the speed limit for download in Kib/s.                                             
+        ``downloadLimited``          5 -                   Enable download speed limiter.                                                         
+        ``files_unwanted``           1 -                   A list of file id's that shouldn't be downloaded.                                      
+        ``files_wanted``             1 -                   A list of file id's that should be downloaded.                                         
+        ``honorsSessionLimits``      5 -                   Enables or disables the transfer to honour the upload limit set in the session.        
+        ``ids``                      1 -                   Local download location.                                                               
+        ``peer_limit``               1 -                   The peer limit for the torrents.                                                       
+        ``priority_high``            1 -                   A list of file id's that should have high priority.                                    
+        ``priority_low``             1 -                   A list of file id's that should have normal priority.                                  
+        ``priority_normal``          1 -                   A list of file id's that should have low priority.                                     
+        ``seedIdleLimit``            10 -                  Seed inactivity limit in minutes.                                                      
+        ``seedIdleMode``             10 -                  Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.
+        ``seedRatioLimit``           5 -                   Seeding ratio.                                                                         
+        ``seedRatioMode``            5 -                   Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.  
+        ``speed_limit_down``         1 - 5 downloadLimit   Set the speed limit for download in Kib/s.                                             
+        ``speed_limit_down_enabled`` 1 - 5 downloadLimited Enable download speed limiter.                                                         
+        ``speed_limit_up``           1 - 5 uploadLimit     Set the speed limit for upload in Kib/s.                                               
+        ``speed_limit_up_enabled``   1 - 5 uploadLimited   Enable upload speed limiter.                                                           
+        ``trackerAdd``               10 -                  Array of string with announce URLs to add.                                             
+        ``trackerRemove``            10 -                  Array of ids of trackers to remove.                                                    
+        ``trackerReplace``           10 -                  Array of (id, url) tuples where the announce URL should be replaced.                   
+        ``uploadLimit``              5 -                   Set the speed limit for upload in Kib/s.                                               
+        ``uploadLimited``            5 -                   Enable upload speed limiter.                                                           
+        ============================ ===== =============== =======================================================================================
+
+    	.. NOTE::
+    	   transmissionrpc will try to automatically fix argument errors.
+        """
+        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=None):
+        """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=None):
+        """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=None):
+        """Get session parameters"""
+        self._request('session-get', timeout=timeout)
+        self._update_server_version()
+        return self.session
+
+    def set_session(self, timeout=None, **kwargs):
+        """
+        Set session parameters. The parameters are:
+        
+        ================================ ===== ================= ==========================================================================================================================
+        Argument                         RPC   Replaced by       Description                                                                                                               
+        ================================ ===== ================= ==========================================================================================================================
+        ``alt_speed_down``               5 -                     Alternate session download speed limit (in Kib/s).                                                                        
+        ``alt_speed_enabled``            5 -                     Enables alternate global download speed limiter.                                                                          
+        ``alt_speed_time_begin``         5 -                     Time when alternate speeds should be enabled. Minutes after midnight.                                                     
+        ``alt_speed_time_day``           5 -                     Enables alternate speeds scheduling these days.                                                                           
+        ``alt_speed_time_enabled``       5 -                     Enables alternate speeds scheduling.                                                                                      
+        ``alt_speed_time_end``           5 -                     Time when alternate speeds should be disabled. Minutes after midnight.                                                    
+        ``alt_speed_up``                 5 -                     Alternate session upload speed limit (in Kib/s).                                                                          
+        ``blocklist_enabled``            5 -                     Enables the block list                                                                                                    
+        ``cache_size_mb``                10 -                    The maximum size of the disk cache in MB                                                                                  
+        ``dht_enabled``                  6 -                     Enables DHT.                                                                                                              
+        ``download_dir``                 1 -                     Set the session download directory.                                                                                       
+        ``encryption``                   1 -                     Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``.                                     
+        ``idle_seeding_limit``           10 -                    The default seed inactivity limit in minutes.                                                                             
+        ``idle_seeding_limit_enabled``   10 -                    Enables the default seed inactivity limit                                                                                 
+        ``incomplete_dir``               7 -                     The path to the directory of incomplete transfer data.                                                                    
+        ``incomplete_dir_enabled``       7 -                     Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target.
+        ``lpd_enabled``                  9 -                     Enables local peer discovery for public torrents.                                                                         
+        ``peer_limit``                   1 - 5 peer-limit-global Maximum number of peers                                                                                                   
+        ``peer_limit_global``            5 -                     Maximum number of peers                                                                                                   
+        ``peer_limit_per_torrent``       5 -                     Maximum number of peers per transfer                                                                                      
+        ``peer_port``                    5 -                     Peer port.                                                                                                                
+        ``peer_port_random_on_start``    5 -                     Enables randomized peer port on start of Transmission.                                                                    
+        ``pex_allowed``                  1 - 5 pex-enabled       Allowing PEX in public torrents.                                                                                          
+        ``pex_enabled``                  5 -                     Allowing PEX in public torrents.                                                                                          
+        ``port``                         1 - 5 peer-port         Peer port.                                                                                                                
+        ``port_forwarding_enabled``      1 -                     Enables port forwarding.                                                                                                  
+        ``rename_partial_files``         8 -                     Appends ".part" to incomplete files                                                                                       
+        ``script_torrent_done_enabled``  9 -                     Whether or not to call the "done" script.                                                                                 
+        ``script_torrent_done_filename`` 9 -                     Filename of the script to run when the transfer is done.                                                                  
+        ``seedRatioLimit``               5 -                     Seed ratio limit. 1.0 means 1:1 download and upload ratio.                                                                
+        ``seedRatioLimited``             5 -                     Enables seed ration limit.                                                                                                
+        ``speed_limit_down``             1 -                     Download speed limit (in Kib/s).                                                                                          
+        ``speed_limit_down_enabled``     1 -                     Enables download speed limiting.                                                                                          
+        ``speed_limit_up``               1 -                     Upload speed limit (in Kib/s).                                                                                            
+        ``speed_limit_up_enabled``       1 -                     Enables upload speed limiting.                                                                                            
+        ``start_added_torrents``         9 -                     Added torrents will be started right away.                                                                                
+        ``trash_original_torrent_files`` 9 -                     The .torrent file of added torrents will be deleted.                                                                      
+        ================================ ===== ================= ==========================================================================================================================
+
+        .. NOTE::
+    	   transmissionrpc will try to automatically fix argument errors.
+        """
+        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=None):
+        """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=None):
+        """
+        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=None):
+        """Get session statistics"""
+        self._request('session-stats', timeout=timeout)
+        return self.session

transmissionrpc/constants.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+import logging
+
+LOGGER = logging.getLogger('transmissionrpc')
+LOGGER.setLevel(logging.ERROR)
+
+def mirror_dict(source):
+    """
+    Creates a dictionary with all values as keys and all keys as values.
+    """
+    source.update(dict((value, key) for key, value in source.iteritems()))
+    return source
+
+DEFAULT_PORT = 9091
+
+DEFAULT_TIMEOUT = 30.0
+
+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,
+    'unlimited' : TR_RATIOLIMIT_UNLIMITED
+})
+
+TR_IDLELIMIT_GLOBAL     = 0 # follow the global settings
+TR_IDLELIMIT_SINGLE     = 1 # override the global settings, seeding until a certain idle time
+TR_IDLELIMIT_UNLIMITED  = 2 # override the global settings, seeding regardless of activity
+
+IDLE_LIMIT = mirror_dict({
+    'global'    : TR_RATIOLIMIT_GLOBAL,
+    'single'    : TR_RATIOLIMIT_SINGLE,
+    'unlimited' : 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>, <description>]
+#  |  +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>, <description>]
+#  |
+# get +- <argument1> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>, <description>]
+#     +- <argument2> - [<type>, <added version>, <removed version>, <previous argument name>, <next argument name>, <description>]
+
+# Arguments for torrent methods
+TORRENT_ARGS = {
+    'get' : {
+        'activityDate':                 ('number', 1, None, None, None, ''),
+        'addedDate':                    ('number', 1, None, None, None, ''),
+        'announceResponse':             ('string', 1, 7, None, None, ''),
+        'announceURL':                  ('string', 1, 7, 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, 7, 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, ''),
+        'isFinished':                   ('boolean', 9, None, None, None, ''),
+        'isPrivate':                    ('boolean', 1, None, None, None, ''),
+        'lastAnnounceTime':             ('number', 1, 7, None, None, ''),
+        'lastScrapeTime':               ('number', 1, 7, None, None, ''),
+        'leechers':                     ('number', 1, 7, None, None, ''),
+        'leftUntilDone':                ('number', 1, None, None, None, ''),
+        'magnetLink':                   ('string', 7, None, None, None, ''),
+        'manualAnnounceTime':           ('number', 1, None, None, None, ''),
+        'maxConnectedPeers':            ('number', 1, None, None, None, ''),
+        'metadataPercentComplete':      ('number', 7, None, None, None, ''),
+        'name':                         ('string', 1, None, None, None, ''),
+        'nextAnnounceTime':             ('number', 1, 7, None, None, ''),
+        'nextScrapeTime':               ('number', 1, 7, 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, 7, None, None, ''),
+        'scrapeURL':                    ('string', 1, 7, None, None, ''),
+        'seeders':                      ('number', 1, 7, None, None, ''),
+        'seedIdleLimit':                ('number', 10, None, None, None, ''),
+        'seedIdleMode':                 ('number', 10, 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, 7, None, None, ''),
+        'timesCompleted':               ('number', 1, 7, None, None, ''),
+        'trackers':                     ('array', 1, None, None, None, ''),
+        'trackerStats':                 ('object', 7, 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, 'Priority for this transfer.'),
+        'downloadLimit':                ('number', 5, None, 'speed-limit-down', None, 'Set the speed limit for download in Kib/s.'),
+        'downloadLimited':              ('boolean', 5, None, 'speed-limit-down-enabled', None, 'Enable download speed limiter.'),
+        'files-wanted':                 ('array', 1, None, None, None, "A list of file id's that should be downloaded."),
+        'files-unwanted':               ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."),
+        'honorsSessionLimits':          ('boolean', 5, None, None, None, "Enables or disables the transfer to honour the upload limit set in the session."),
+        'ids':                          ('array', 1, None, None, None, 'Local download location.'),
+        'peer-limit':                   ('number', 1, None, None, None, 'The peer limit for the torrents.'),
+        'priority-high':                ('array', 1, None, None, None, "A list of file id's that should have high priority."),
+        'priority-low':                 ('array', 1, None, None, None, "A list of file id's that should have normal priority."),
+        'priority-normal':              ('array', 1, None, None, None, "A list of file id's that should have low priority."),
+        'seedIdleLimit':                ('number', 10, None, None, None, 'Seed inactivity limit in minutes.'),
+        'seedIdleMode':                 ('number', 10, None, None, None, 'Seed inactivity mode. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'),
+        'seedRatioLimit':               ('double', 5, None, None, None, 'Seeding ratio.'),
+        'seedRatioMode':                ('number', 5, None, None, None, 'Which ratio to use. 0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.'),
+        'speed-limit-down':             ('number', 1, 5, None, 'downloadLimit', 'Set the speed limit for download in Kib/s.'),
+        'speed-limit-down-enabled':     ('boolean', 1, 5, None, 'downloadLimited', 'Enable download speed limiter.'),
+        'speed-limit-up':               ('number', 1, 5, None, 'uploadLimit', 'Set the speed limit for upload in Kib/s.'),
+        'speed-limit-up-enabled':       ('boolean', 1, 5, None, 'uploadLimited', 'Enable upload speed limiter.'),
+        'trackerAdd':                   ('array', 10, None, None, None, 'Array of string with announce URLs to add.'),
+        'trackerRemove':                ('array', 10, None, None, None, 'Array of ids of trackers to remove.'),
+        'trackerReplace':               ('array', 10, None, None, None, 'Array of (id, url) tuples where the announce URL should be replaced.'),
+        'uploadLimit':                  ('number', 5, None, 'speed-limit-up', None, 'Set the speed limit for upload in Kib/s.'),
+        'uploadLimited':                ('boolean', 5, None, 'speed-limit-up-enabled', None, 'Enable upload speed limiter.'),
+    },
+    'add': {
+        'bandwidthPriority':            ('number', 8, None, None, None, 'Priority for this transfer.'),
+        'download-dir':                 ('string', 1, None, None, None, 'The directory where the downloaded contents will be saved in.'),
+        'filename':                     ('string', 1, None, None, None, "A filepath or URL to a torrent file or a magnet link."),
+        'files-wanted':                 ('array', 1, None, None, None, "A list of file id's that should be downloaded."),
+        'files-unwanted':               ('array', 1, None, None, None, "A list of file id's that shouldn't be downloaded."),
+        'metainfo':                     ('string', 1, None, None, None, 'The content of a torrent file, base64 encoded.'),
+        'paused':                       ('boolean', 1, None, None, None, 'If True, does not start the transfer when added.'),
+        'peer-limit':                   ('number', 1, None, None, None, 'Maximum number of peers allowed.'),
+        'priority-high':                ('array', 1, None, None, None, "A list of file id's that should have high priority."),
+        'priority-low':                 ('array', 1, None, None, None, "A list of file id's that should have low priority."),
+        'priority-normal':              ('array', 1, None, None, None, "A list of file id's that should have normal priority."),
+    }
+}
+
+# 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, ''),
+        "blocklist-url":                ('string', 11, None, None, None, ''),
+        "cache-size-mb":                ('number', 10, None, None, None, ''),
+        "config-dir":                   ('string', 8, None, None, None, ''),
+        "dht-enabled":                  ('boolean', 6, None, None, None, ''),
+        "download-dir":                 ('string', 1, None, None, None, ''),
+        "encryption":                   ('string', 1, None, None, None, ''),
+        "idle-seeding-limit":           ('number', 10, None, None, None, ''),
+        "idle-seeding-limit-enabled":   ('boolean', 10, None, None, None, ''),
+        "incomplete-dir":               ('string', 7, None, None, None, ''),
+        "incomplete-dir-enabled":       ('boolean', 7, None, None, None, ''),
+        "lpd-enabled":                  ('boolean', 9, 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, ''),
+        "rename-partial-files":         ('boolean', 8, None, None, None, ''),
+        "rpc-version":                  ('number', 4, None, None, None, ''),
+        "rpc-version-minimum":          ('number', 4, None, None, None, ''),
+        "script-torrent-done-enabled":  ('boolean', 9, None, None, None, ''),
+        "script-torrent-done-filename": ('string', 9, 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, ''),
+        "start-added-torrents":         ('boolean', 9, None, None, None, ''),
+        "trash-original-torrent-files": ('boolean', 9, None, None, None, ''),
+        'units':                        ('object', 10, None, None, None, ''),
+        "version":                      ('string', 3, None, None, None, ''),
+    },
+    'set': {
+        "alt-speed-down":               ('number', 5, None, None, None, 'Alternate session download speed limit (in Kib/s).'),
+        "alt-speed-enabled":            ('boolean', 5, None, None, None, 'Enables alternate global download speed limiter.'),
+        "alt-speed-time-begin":         ('number', 5, None, None, None, 'Time when alternate speeds should be enabled. Minutes after midnight.'),
+        "alt-speed-time-enabled":       ('boolean', 5, None, None, None, 'Enables alternate speeds scheduling.'),
+        "alt-speed-time-end":           ('number', 5, None, None, None, 'Time when alternate speeds should be disabled. Minutes after midnight.'),
+        "alt-speed-time-day":           ('number', 5, None, None, None, 'Enables alternate speeds scheduling these days.'),
+        "alt-speed-up":                 ('number', 5, None, None, None, 'Alternate session upload speed limit (in Kib/s).'),
+        "blocklist-enabled":            ('boolean', 5, None, None, None, 'Enables the block list'),
+        "blocklist-url":                ('string', 11, None, None, None, 'Location of the blocklist. Updated with blocklist-update.'),
+        "cache-size-mb":                ('number', 10, None, None, None, 'The maximum size of the disk cache in MB'),
+        "dht-enabled":                  ('boolean', 6, None, None, None, 'Enables DHT.'),
+        "download-dir":                 ('string', 1, None, None, None, 'Set the session download directory.'),
+        "encryption":                   ('string', 1, None, None, None, 'Set the session encryption mode, one of ``required``, ``preferred`` or ``tolerated``.'),
+        "idle-seeding-limit":           ('number', 10, None, None, None, 'The default seed inactivity limit in minutes.'),
+        "idle-seeding-limit-enabled":   ('boolean', 10, None, None, None, 'Enables the default seed inactivity limit'),
+        "incomplete-dir":               ('string', 7, None, None, None, 'The path to the directory of incomplete transfer data.'),
+        "incomplete-dir-enabled":       ('boolean', 7, None, None, None, 'Enables the incomplete transfer data directory. Otherwise data for incomplete transfers are stored in the download target.'),
+        "lpd-enabled":                  ('boolean', 9, None, None, None, 'Enables local peer discovery for public torrents.'),
+        "peer-limit":                   ('number', 1, 5, None, 'peer-limit-global', 'Maximum number of peers'),
+        "peer-limit-global":            ('number', 5, None, 'peer-limit', None, 'Maximum number of peers'),
+        "peer-limit-per-torrent":       ('number', 5, None, None, None, 'Maximum number of peers per transfer'),
+        "pex-allowed":                  ('boolean', 1, 5, None, 'pex-enabled', 'Allowing PEX in public torrents.'),
+        "pex-enabled":                  ('boolean', 5, None, 'pex-allowed', None, 'Allowing PEX in public torrents.'),
+        "port":                         ('number', 1, 5, None, 'peer-port', 'Peer port.'),
+        "peer-port":                    ('number', 5, None, 'port', None, 'Peer port.'),
+        "peer-port-random-on-start":    ('boolean', 5, None, None, None, 'Enables randomized peer port on start of Transmission.'),
+        "port-forwarding-enabled":      ('boolean', 1, None, None, None, 'Enables port forwarding.'),
+        "rename-partial-files":         ('boolean', 8, None, None, None, 'Appends ".part" to incomplete files'),
+        "script-torrent-done-enabled":  ('boolean', 9, None, None, None, 'Whether or not to call the "done" script.'),
+        "script-torrent-done-filename": ('string', 9, None, None, None, 'Filename of the script to run when the transfer is done.'),
+        "seedRatioLimit":               ('double', 5, None, None, None, 'Seed ratio limit. 1.0 means 1:1 download and upload ratio.'),
+        "seedRatioLimited":             ('boolean', 5, None, None, None, 'Enables seed ration limit.'),
+        "speed-limit-down":             ('number', 1, None, None, None, 'Download speed limit (in Kib/s).'),
+        "speed-limit-down-enabled":     ('boolean', 1, None, None, None, 'Enables download speed limiting.'),
+        "speed-limit-up":               ('number', 1, None, None, None, 'Upload speed limit (in Kib/s).'),
+        "speed-limit-up-enabled":       ('boolean', 1, None, None, None, 'Enables upload speed limiting.'),
+        "start-added-torrents":         ('boolean', 9, None, None, None, 'Added torrents will be started right away.'),
+        "trash-original-torrent-files": ('boolean', 9, None, None, None, 'The .torrent file of added torrents will be deleted.'),
+    },
+}

transmissionrpc/error.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+class TransmissionError(Exception):
+    """
+	This exception is raised when there has occured an error related to
+	communication with Transmission. It is a subclass of Exception.
+    """
+    def __init__(self, message='', original=None):
+        Exception.__init__(self)
+        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, str(self.original))
+        else:
+            return self.message
+
+class HTTPHandlerError(Exception):
+    """
+	This exception is raised when there has occured an error related to
+	the HTTP handler. It is a subclass of Exception.
+    """
+    def __init__(self, httpurl=None, httpcode=None, httpmsg=None, httpheaders=None, httpdata=None):
+        Exception.__init__(self)
+        self.url = ''
+        self.code = 600
+        self.message = ''
+        self.headers = {}
+        self.data = ''
+        if isinstance(httpurl, (str, unicode)):
+            self.url = httpurl
+        if isinstance(httpcode, (int, long)):
+            self.code = httpcode
+        if isinstance(httpmsg, (str, unicode)):
+            self.message = httpmsg
+        if isinstance(httpheaders, (dict)):
+            self.headers = httpheaders
+        if isinstance(httpdata, (str, unicode)):
+            self.data = httpdata
+
+    def __repr__(self):
+        return '<HTTPHandlerError %d, %s>' % (self.code, self.message)
+
+    def __str__(self):
+        return '<HTTPHandlerError %d, %s>' % (self.code, self.message)
+
+    def __unicode__(self):
+        return u'<HTTPHandlerError %d, %s>' % (self.code, self.message)

transmissionrpc/httphandler.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+import sys, httplib, urllib2
+
+from transmissionrpc.error import HTTPHandlerError
+
+class HTTPHandler(object):
+    """
+    Prototype for HTTP handling.
+    """
+    def set_authentication(self, uri, login, password):
+        """
+        Transmission use basic authentication in earlier versions and digest
+        authentication in later versions.
+        
+         * uri, the authentication realm URI.
+         * login, the authentication login.
+         * password, the authentication password.
+        """
+        raise NotImplementedError("Bad HTTPHandler, failed to implement set_authentication.")
+    
+    def request(self, url, query, headers, timeout):
+        """
+        Implement a HTTP POST request here.
+        
+         * url, The URL to request.
+         * query, The query data to send. This is a JSON data string.
+         * headers, a dictionary of headers to send.
+         * timeout, requested request timeout in seconds.
+        """
+        raise NotImplementedError("Bad HTTPHandler, failed to implement request.")
+
+class DefaultHTTPHandler(HTTPHandler):
+    """
+    The default HTTP handler provided with transmissionrpc.
+    """
+    def __init__(self):
+        HTTPHandler.__init__(self)
+
+    def set_authentication(self, uri, login, password):
+        password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+        password_manager.add_password(realm=None, uri=uri, user=login, passwd=password)
+        opener = urllib2.build_opener(
+            urllib2.HTTPBasicAuthHandler(password_manager)
+            , urllib2.HTTPDigestAuthHandler(password_manager)
+            )
+        urllib2.install_opener(opener)
+
+    def request(self, url, query, headers, timeout):
+        request = urllib2.Request(url, query, headers)
+        try:
+            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)
+        except urllib2.HTTPError, error:
+            if error.fp == None:
+                raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs))
+            else:
+                raise HTTPHandlerError(error.filename, error.code, error.msg, dict(error.hdrs), error.read())
+        except urllib2.URLError, error:
+            # urllib2.URLError documentation is absymal!
+            # Try to get the tuple arguments of URLError
+            if hasattr(error.reason, 'args') and isinstance(error.reason.args, tuple) and len(error.reason.args) == 2:
+                raise HTTPHandlerError(httpcode=error.reason.args[0], httpmsg=error.reason.args[1])
+            else:
+                raise HTTPHandlerError(httpmsg='urllib2.URLError: %s' % (error.reason))
+        except httplib.BadStatusLine, error:
+            raise HTTPHandlerError(httpmsg='httplib.BadStatusLine: %s' % (error.line))
+        return response.read()

transmissionrpc/session.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+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=None):
+        self.fields = {}
+        if fields != None:
+            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 key, value in fields.iteritems():
+            self.fields[key.replace('-', '_')] = value
+
+    def __getattr__(self, name):
+        try:
+            return self.fields[name]
+        except KeyError:
+            raise AttributeError('No attribute %s' % name)
+
+    def __str__(self):
+        text = ''
+        for key in sorted(self.fields.keys()):
+            text += "% 32s: %s\n" % (key[-32:], self.fields[key])
+        return text

transmissionrpc/torrent.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+import datetime
+
+from transmissionrpc.constants import STATUS, PRIORITY
+from transmissionrpc.utils import format_timedelta
+
+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, client, fields):
+        if 'id' not in fields:
+            raise ValueError('Torrent requires an id')
+        self.fields = {}
+        self.update(fields)
+        self.client = client
+
+    def __repr__(self):
+        return '<Torrent %d \"%s\">' % (self.fields['id'], self.fields['name'])
+
+    def __str__(self):
+        return 'torrent %s' % self.fields['name']
+    
+    def __copy__(self):
+        return Torrent(self.client, self.fields)
+
+    def update(self, other):
+        """
+        Update the torrent data from a Transmission JSON-RPC 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 key, value in fields.iteritems():
+            self.fields[key.replace('-', '_')] = value
+
+    def files(self):
+        """
+    	Get list of files for this torrent.
+
+    	This function returns a dictionary with file information for each file.
+    	The file information is has following fields:
+    	::
+
+    		{
+    			<file id>: {
+    				'name': <file name>,
+    				'size': <file size in bytes>,
+    				'completed': <bytes completed>,
+    				'priority': <priority ('high'|'normal'|'low')>,
+    				'selected': <selected for download>
+    			}
+
+    			...
+    		}
+        """
+        result = {}
+        if 'files' in self.fields:
+            indicies = xrange(len(self.fields['files']))
+            files = self.fields['files']
+            priorities = self.fields['priorities']
+            wanted = self.fields['wanted']
+            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:
+            raise AttributeError('No attribute %s' % name)
+
+    @property
+    def status(self):
+        """
+        Returns the torrent status. Is either one of 'check pending', 'checking',
+    	'downloading', 'seeding' or 'stopped'. The first two is related to
+    	verification.
+    	"""
+        return STATUS[self.fields['status']]
+
+    @property
+    def progress(self):
+        """Get the download progress in percent."""
+        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.
+
+    	* If eta is -1 the result is 'not available'
+    	* If eta is -2 the result is 'unknown'
+    	* Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>.
+    	"""
+        eta = self.fields['eta']
+        if eta == -1:
+            return 'not available'
+        elif eta == -2:
+            return 'unknown'
+        else:
+            return format_timedelta(self.eta)
+    
+    @property
+    def priority(self):
+        """
+        Get the priority as string.
+        Can be one of 'low', 'normal', 'high'.
+        """
+        return PRIORITY[self.fields['bandwidthPriority']]

transmissionrpc/utils.py

+# -*- coding: utf-8 -*-
+# Copyright (c) 2008-2010 Erik Svensson <erik.public@gmail.com>
+# Licensed under the MIT license.
+
+import socket, datetime, logging
+import transmissionrpc.constants as constants
+from transmissionrpc.constants import LOGGER
+
+UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']
+
+def format_size(size):
+    """
+    Format byte size into IEC prefixes, B, KiB, MiB ...
+    """
+    size = 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):
+    """
+    Format bytes per second speed into IEC prefixes, B/s, KiB/s, MiB/s ...
+    """
+    (size, unit) = format_size(size)
+    return (size, unit + '/s')
+
+def format_timedelta(delta):
+    """
+    Format datetime.timedelta into <days> <hours>:<mminutes>:<seconds>.
+    """
+    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):
+    """
+    Format unix timestamp into ISO date format.
+    """
+    if timestamp > 0:
+        dt_timestamp = datetime.datetime.fromtimestamp(timestamp)
+        return dt_timestamp.isoformat(' ')
+    else:
+        return '-'
+
+class INetAddressError(Exception):
+    """
+    Error parsing / generating a internet address.
+    """
+    pass
+
+def inet_address(address, default_port, default_address='localhost'):
+    """
+    Parse internet address.
+    """
+    addr = address.split(':')
+    if len(addr) == 1:
+        try:
+            port = int(addr[0])
+            addr = default_address
+        except ValueError:
+            addr = addr[0]
+            port = default_port
+    elif len(addr) == 2:
+        try:
+            port = int(addr[1])
+        except ValueError:
+            raise INetAddressError('Invalid address "%s".' % address)
+        if len(addr[0]) == 0:
+            addr = default_address
+        else:
+            addr = addr[0]
+    else:
+        raise INetAddressError('Invalid address "%s".' % address)
+    try:
+        socket.getaddrinfo(addr, port, socket.AF_INET, socket.SOCK_STREAM)
+    except socket.gaierror:
+        raise INetAddressError('Cannot look up address "%s".' % address)
+    return (addr, port)
+
+def rpc_bool(arg):
+    """
+    Convert between Python boolean and Transmission RPC boolean.
+    """
+    if isinstance(arg, (str, unicode)):
+        try:
+            arg = bool(int(arg))
+        except ValueError:
+            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):
+    """
+    Convert Transmission RPC name to python compatible name.
+    """
+    return name.replace('-', '_')
+
+def make_rpc_name(name):
+    """
+    Convert python compatible name to Transmission RPC name.
+    """
+    return name.replace('_', '-')
+
+def argument_value_convert(method, argument, value, rpc_version):
+    """
+    Check and fix Transmission RPC issues with regards to methods, arguments and values.
+    """
+    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):
+    """
+    Get arguments for method in specified Transmission 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
+
+def add_stdout_logger(level='debug'):
+    """
+    Add a stdout target for the transmissionrpc logging.
+    """
+    levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR}
+    
+    trpc_logger = logging.getLogger('transmissionrpc')
+    loghandler = logging.StreamHandler()
+    if level in levels.keys():
+        loglevel = levels[level]
+        trpc_logger.setLevel(loglevel)
+        loghandler.setLevel(loglevel)
+    trpc_logger.addHandler(loghandler)