sintonia/python_apps/soundcloud-api/scapi/__init__.py

1013 lines
36 KiB
Python

## SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful
## API
##
## Copyright (C) 2008 Diez B. Roggisch
## Contact mailto:deets@soundcloud.com
##
## This library is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser General Public
## License as published by the Free Software Foundation; either
## version 2.1 of the License, or (at your option) any later version.
##
## This library is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
## Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public
## License along with this library; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import urllib
import urllib2
import re
import logging
import simplejson
import cgi
from scapi.MultipartPostHandler import MultipartPostHandler
from inspect import isclass
import urlparse
from scapi.authentication import BasicAuthenticator
from scapi.util import (
escape,
MultiDict,
)
logging.basicConfig()
logger = logging.getLogger(__name__)
USE_PROXY = False
"""
Something like http://127.0.0.1:10000/
"""
PROXY = ''
"""
Endpoints, for reference:
The url Soundcould offers to obtain request-tokens: 'http://api.soundcloud.com/oauth/request_token'
The url Soundcould offers to exchange access-tokens for request-tokens: 'http://api.soundcloud.com/oauth/access_token'
The url Soundcould offers to make users authorize a concrete request token: 'http://api.soundcloud.com/oauth/authorize'
"""
__all__ = ['SoundCloudAPI', 'USE_PROXY', 'PROXY']
class NoResultFromRequest(Exception):
pass
class InvalidMethodException(Exception):
def __init__(self, message):
self._message = message
Exception.__init__(self)
def __repr__(self):
res = Exception.__repr__(self)
res += "\n"
res += "-" * 10
res += "\nmessage:\n\n"
res += self._message
return res
class UnknownContentType(Exception):
def __init__(self, msg):
Exception.__init__(self)
self._msg = msg
def __repr__(self):
return self.__class__.__name__ + ":" + self._msg
def __str__(self):
return str(self)
class PartitionCollectionGenerator():
def __init__(self, scope, method, Gen, NextPartition):
self.NextPartition = NextPartition
self.Generator = Gen
self.Scope = scope
self.Method = method
def __iter__(self):
return self.Generator
def next(self):
return self.Generator.next()
def __call__(self, someParam):
self.someParam = someParam
for line in self.content:
if line == someParam:
yield line
def GetNextPartition(self):
if self.NextPartition != None:
method = re.search('(^[a-z]+)', self.Method).group(0)
params = re.search('\?.+', self.NextPartition).group(0)
params = params.replace('u0026', '&')
return self.Scope._call(method, params)
else:
return None
class ApiConnector(object):
"""
The ApiConnector holds all the data necessary to authenticate against
the soundcloud-api. You can instantiate several connectors if you like, but usually one
should be sufficient.
"""
"""
SoundClound imposes a maximum on the number of returned items. This value is that
maximum.
"""
LIST_LIMIT = 50
"""
The query-parameter that is used to request results beginning from a certain offset.
"""
LIST_OFFSET_PARAMETER = 'offset'
"""
The query-parameter that is used to request results being limited to a certain amount.
Currently this is of no use and just for completeness sake.
"""
LIST_LIMIT_PARAMETER = 'limit'
def __init__(self, host, user=None, password=None, authenticator=None, base="", collapse_scope=True):
"""
Constructor for the API-Singleton. Use it once with parameters, and then the
subsequent calls internal to the API will work.
@type host: str
@param host: the host to connect to, e.g. "api.soundcloud.com". If a port is needed, use
"api.soundcloud.com:1234"
@type user: str
@param user: if given, the username for basic HTTP authentication
@type password: str
@param password: if the user is given, you have to give a password as well
@type authenticator: OAuthAuthenticator | BasicAuthenticator
@param authenticator: the authenticator to use, see L{scapi.authentication}
"""
self.host = host
self.host = self.host.replace("http://", "")
if self.host[-1] == '/': # Remove a trailing slash, but leave other slashes alone
self.host = self.host[0:-1]
if authenticator is not None:
self.authenticator = authenticator
elif user is not None and password is not None:
self.authenticator = BasicAuthenticator(user, password)
self._base = base
self.collapse_scope = collapse_scope
def normalize_method(self, method):
"""
This method will take a method that has been part of a redirect of some sort
and see if it's valid, which means that it's located beneath our base.
If yes, we return it normalized without that very base.
"""
_, _, path, _, _, _ = urlparse.urlparse(method)
if path.startswith("/"):
path = path[1:]
# if the base is "", we return the whole path,
# otherwise normalize it away
if self._base == "":
return path
if path.startswith(self._base):
return path[len(self._base)-1:]
raise InvalidMethodException("Not a valid API method: %s" % method)
def fetch_request_token(self, url=None, oauth_callback="oob", oauth_verifier=None):
"""
Helper-function for a registered consumer to obtain a request token, as
used by oauth.
Use it like this:
>>> oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
CONSUMER_SECRET,
None,
None)
>>> sca = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)
>>> token, secret = sca.fetch_request_token()
>>> authorization_url = sca.get_request_token_authorization_url(token)
Please note the None passed as token & secret to the authenticator.
"""
request_url = "http://" + self.host + "/oauth/request_token"
if url is None:
url = request_url
req = urllib2.Request(url)
self.authenticator.augment_request(req, None, oauth_callback=oauth_callback, oauth_verifier=oauth_verifier)
handlers = []
if USE_PROXY:
handlers.append(urllib2.ProxyHandler({'http' : PROXY}))
opener = urllib2.build_opener(*handlers)
handle = opener.open(req, None)
info = handle.info()
content = handle.read()
params = cgi.parse_qs(content, keep_blank_values=False)
key = params['oauth_token'][0]
secret = params['oauth_token_secret'][0]
return key, secret
def fetch_access_token(self, oauth_verifier):
"""
Helper-function for a registered consumer to exchange an access token for
a request token.
Use it like this:
>>> oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER,
CONSUMER_SECRET,
request_token,
request_token_secret)
>>> sca = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)
>>> token, secret = sca.fetch_access_token()
Please note the values passed as token & secret to the authenticator.
"""
access_token_url = "http://" + self.host + "/oauth/access_token"
return self.fetch_request_token(access_token_url, oauth_verifier=oauth_verifier)
def get_request_token_authorization_url(self, token):
"""
Simple helper function to generate the url needed
to ask a user for request token authorization.
See also L{fetch_request_token}.
Possible usage:
>>> import webbrowser
>>> sca = scapi.ApiConnector()
>>> authorization_url = sca.get_request_token_authorization_url(token)
>>> webbrowser.open(authorization_url)
"""
auth_url = self.host.split("/")[0]
auth_url = "http://" + auth_url + "/oauth/authorize"
auth_url = auth_url.replace("api.", "")
return "%s?oauth_token=%s" % (auth_url, token)
class SCRedirectHandler(urllib2.HTTPRedirectHandler):
"""
A urllib2-Handler to deal with the redirects the RESTful API of SC uses.
"""
alternate_method = None
def http_error_303(self, req, fp, code, msg, hdrs):
"""
In case of return-code 303 (See-other), we have to store the location we got
because that will determine the actual type of resource returned.
"""
self.alternate_method = hdrs['location']
# for oauth, we need to re-create the whole header-shizzle. This
# does it - it recreates a full url and signs the request
new_url = self.alternate_method
# if USE_PROXY:
# import pdb; pdb.set_trace()
# old_url = req.get_full_url()
# protocol, host, _, _, _, _ = urlparse.urlparse(old_url)
# new_url = urlparse.urlunparse((protocol, host, self.alternate_method, None, None, None))
req = req.recreate_request(new_url)
return urllib2.HTTPRedirectHandler.http_error_303(self, req, fp, code, msg, hdrs)
def http_error_201(self, req, fp, code, msg, hdrs):
"""
We fake a 201 being a 303 so that our redirection-scheme takes place
for the 201 the API throws in case we created something. If the location is
not available though, that means that whatever we created has succeded - without
being a named resource. Assigning an asset to a track is an example of such
case.
"""
if 'location' not in hdrs:
raise NoResultFromRequest()
return self.http_error_303(req, fp, 303, msg, hdrs)
class Scope(object):
"""
The basic means to query and create resources. The Scope uses the L{ApiConnector} to
create the proper URIs for querying or creating resources.
For accessing resources from the root level, you explcitly create a Scope and pass it
an L{ApiConnector}-instance. Then you can query it
or create new resources like this:
>>> connector = scapi.ApiConnector(host='host', user='user', password='password') # initialize the API
>>> scope = scapi.Scope(connector) # get the root scope
>>> users = list(scope.users())
[<scapi.User object at 0x12345>, ...]
Please not that all resources that are lists are returned as B{generator}. So you need
to either iterate over them, or call list(resources) on them.
When accessing resources that belong to another resource, like contacts of a user, you access
the parent's resource scope implicitly through the resource instance like this:
>>> user = scope.users().next()
>>> list(user.contacts())
[<scapi.Contact object at 0x12345>, ...]
"""
def __init__(self, connector, scope=None, parent=None):
"""
Create the Scope. It can have a resource as scope, and possibly a parent-scope.
@param connector: The connector to use.
@type connector: ApiConnector
@type scope: scapi.RESTBase
@param scope: the resource to make this scope belong to
@type parent: scapi.Scope
@param parent: the parent scope of this scope
"""
if scope is None:
scope = ()
else:
scope = scope,
if parent is not None:
scope = parent._scope + scope
self._scope = scope
self._connector = connector
def _get_connector(self):
return self._connector
def oauth_sign_get_request(self, url):
"""
This method will take an arbitrary url, and rewrite it
so that the current authenticator's oauth-headers are appended
as query-parameters.
This is used in streaming and downloading, because those content
isn't served from the SoundCloud servers themselves.
A usage example would look like this:
>>> sca = scapi.Scope(connector)
>>> track = sca.tracks(params={
"filter" : "downloadable",
}).next()
>>> download_url = track.download_url
>>> signed_url = track.oauth_sign_get_request(download_url)
>>> data = urllib2.urlopen(signed_url).read()
"""
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
req = urllib2.Request(url)
all_params = {}
if query:
all_params.update(cgi.parse_qs(query))
if not all_params:
all_params = None
self._connector.authenticator.augment_request(req, all_params, False)
auth_header = req.get_header("Authorization")
auth_header = auth_header[len("OAuth "):]
query_params = []
if query:
query_params.append(query)
for part in auth_header.split(","):
key, value = part.split("=")
assert key.startswith("oauth")
value = value[1:-1]
query_params.append("%s=%s" % (key, value))
query = "&".join(query_params)
url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
return url
def _create_request(self, url, connector, parameters, queryparams, alternate_http_method=None, use_multipart=False):
"""
This method returnes the urllib2.Request to perform the actual HTTP-request.
We return a subclass that overload the get_method-method to return a custom method like "PUT".
Additionally, the request is enhanced with the current authenticators authorization scheme
headers.
@param url: the destination url
@param connector: our connector-instance
@param parameters: the POST-parameters to use.
@type parameters: None|dict<str, basestring|list<basestring>>
@param queryparams: the queryparams to use
@type queryparams: None|dict<str, basestring|list<basestring>>
@param alternate_http_method: an alternate HTTP-method to use
@type alternate_http_method: str
@return: the fully equipped request
@rtype: urllib2.Request
"""
class MyRequest(urllib2.Request):
def get_method(self):
if alternate_http_method is not None:
return alternate_http_method
return urllib2.Request.get_method(self)
def has_data(self):
return parameters is not None
def augment_request(self, params, use_multipart=False):
connector.authenticator.augment_request(self, params, use_multipart)
@classmethod
def recreate_request(cls, location):
return self._create_request(location, connector, None, None)
req = MyRequest(url)
all_params = {}
if parameters is not None:
all_params.update(parameters)
if queryparams is not None:
all_params.update(queryparams)
if not all_params:
all_params = None
req.augment_request(all_params, use_multipart)
req.add_header("Accept", "application/json")
return req
def _create_query_string(self, queryparams):
"""
Small helpermethod to create the querystring from a dict.
@type queryparams: None|dict<str, basestring|list<basestring>>
@param queryparams: the queryparameters.
@return: either the empty string, or a "?" followed by the parameters joined by "&"
@rtype: str
"""
if not queryparams:
return ""
h = []
for key, values in queryparams.iteritems():
if isinstance(values, (int, long, float)):
values = str(values)
if isinstance(values, basestring):
values = [values]
for v in values:
v = v.encode("utf-8")
h.append("%s=%s" % (key, escape(v)))
return "?" + "&".join(h)
def _call(self, method, *args, **kwargs):
"""
The workhorse. It's complicated, convoluted and beyond understanding of a mortal being.
You have been warned.
"""
queryparams = {}
__offset__ = ApiConnector.LIST_LIMIT
if "__offset__" in kwargs:
offset = kwargs.pop("__offset__")
queryparams['offset'] = offset
__offset__ = offset + ApiConnector.LIST_LIMIT
if "params" in kwargs:
queryparams.update(kwargs.pop("params"))
# create a closure to invoke this method again with a greater offset
_cl_method = method
_cl_args = tuple(args)
_cl_kwargs = {}
_cl_kwargs.update(kwargs)
_cl_kwargs["__offset__"] = __offset__
def continue_list_fetching():
return self._call(method, *_cl_args, **_cl_kwargs)
connector = self._get_connector()
def filelike(v):
if isinstance(v, file):
return True
if hasattr(v, "read"):
return True
return False
alternate_http_method = None
if "_alternate_http_method" in kwargs:
alternate_http_method = kwargs.pop("_alternate_http_method")
urlparams = kwargs if kwargs else None
use_multipart = False
if urlparams is not None:
fileargs = dict((key, value) for key, value in urlparams.iteritems() if filelike(value))
use_multipart = bool(fileargs)
# ensure the method has a trailing /
if method[-1] != "/":
method = method + "/"
if args:
method = "%s%s" % (method, "/".join(str(a) for a in args))
scope = ''
if self._scope:
scopes = self._scope
if connector.collapse_scope:
scopes = scopes[-1:]
scope = "/".join([sc._scope() for sc in scopes]) + "/"
url = "http://%(host)s/%(base)s%(scope)s%(method)s%(queryparams)s" % dict(host=connector.host, method=method, base=connector._base, scope=scope, queryparams=self._create_query_string(queryparams))
# we need to install SCRedirectHandler
# to gather possible See-Other redirects
# so that we can exchange our method
redirect_handler = SCRedirectHandler()
handlers = [redirect_handler]
if USE_PROXY:
handlers.append(urllib2.ProxyHandler({'http' : PROXY}))
req = self._create_request(url, connector, urlparams, queryparams, alternate_http_method, use_multipart)
http_method = req.get_method()
if urlparams is not None:
logger.debug("Posting url: %s, method: %s", url, http_method)
else:
logger.debug("Fetching url: %s, method: %s", url, http_method)
if use_multipart:
handlers.extend([MultipartPostHandler])
else:
if urlparams is not None:
urlparams = urllib.urlencode(urlparams.items(), True)
opener = urllib2.build_opener(*handlers)
try:
handle = opener.open(req, urlparams)
except NoResultFromRequest:
return None
except urllib2.HTTPError, e:
if http_method == "GET" and e.code == 404:
return None
raise
info = handle.info()
ct = info['Content-Type']
content = handle.read()
logger.debug("Content-type:%s", ct)
logger.debug("Request Content:\n%s", content)
if redirect_handler.alternate_method is not None:
method = connector.normalize_method(redirect_handler.alternate_method)
logger.debug("Method changed through redirect to: <%s>", method)
try:
if "application/json" in ct:
content = content.strip()
#If linked partitioning is on, extract the URL to the next collection:
partition_url = None
if method.find('linked_partitioning=1') != -1:
pattern = re.compile('(next_partition_href":")(.*?)(")')
if pattern.search(content):
partition_url = pattern.search(content).group(2)
if not content:
content = "{}"
try:
res = simplejson.loads(content)
except:
logger.error("Couldn't decode returned json")
logger.error(content)
raise
res = self._map(res, method, continue_list_fetching, partition_url)
return res
elif len(content) <= 1:
# this might be the famous SeeOtherSpecialCase which means that
# all that matters is just the method
pass
raise UnknownContentType("%s, returned:\n%s" % (ct, content))
finally:
handle.close()
def _map(self, res, method, continue_list_fetching, next_partition = None):
"""
This method will take the JSON-result of a HTTP-call and return our domain-objects.
It's also deep magic, don't look.
"""
pathparts = reversed(method.split("/"))
stack = []
for part in pathparts:
stack.append(part)
if part in RESTBase.REGISTRY:
cls = RESTBase.REGISTRY[part]
# multiple objects, without linked partitioning
if isinstance(res, list):
def result_gen():
count = 0
for item in res:
yield cls(item, self, stack)
count += 1
if count == ApiConnector.LIST_LIMIT:
for item in continue_list_fetching():
yield item
logger.debug(res)
return PartitionCollectionGenerator(self, method, result_gen(), next_partition)
# multiple objects, with linked partitioning
elif isinstance(res, dict) and res.has_key('next_partition_href'):
def result_gen():
count = 0
for item in res['collection']:
yield cls(item, self, stack)
count += 1
if count == ApiConnector.LIST_LIMIT:
for item in continue_list_fetching():
yield item
logger.debug(res)
return PartitionCollectionGenerator(self, method, result_gen(), next_partition)
else:
return cls(res, self, stack)
logger.debug("don't know how to handle result")
logger.debug(res)
return res
def __getattr__(self, _name):
"""
Retrieve an API-method or a scoped domain-class.
If the former, result is a callable that supports the following invocations:
- calling (...), with possible arguments (positional/keyword), return the resulting resource or list of resources.
When calling, you can pass a keyword-argument B{params}. This must be a dict or L{MultiDict} and will be used to add additional query-get-parameters.
- invoking append(resource) on it will PUT the resource, making it part of the current resource. Makes
sense only if it's a collection of course.
- invoking remove(resource) on it will DELETE the resource from it's container. Also only usable on collections.
TODO: describe the latter
"""
scope = self
class api_call(object):
def __call__(selfish, *args, **kwargs):
return self._call(_name, *args, **kwargs)
def new(self, **kwargs):
"""
Will invoke the new method on the named resource _name, with
self as scope.
"""
cls = RESTBase.REGISTRY[_name]
return cls.new(scope, **kwargs)
def append(selfish, resource):
"""
If the current scope is
"""
try:
self._call(_name, str(resource.id), _alternate_http_method="PUT")
except AttributeError:
self._call(_name, str(resource), _alternate_http_method="PUT")
def remove(selfish, resource):
try:
self._call(_name, str(resource.id), _alternate_http_method="DELETE")
except AttributeError:
self._call(_name, str(resource), _alternate_http_method="DELETE")
if _name in RESTBase.ALL_DOMAIN_CLASSES:
cls = RESTBase.ALL_DOMAIN_CLASSES[_name]
class ScopeBinder(object):
def new(self, *args, **data):
d = MultiDict()
name = cls._singleton()
def unfold_value(key, value):
if isinstance(value, (basestring, file)):
d.add(key, value)
elif isinstance(value, dict):
for sub_key, sub_value in value.iteritems():
unfold_value("%s[%s]" % (key, sub_key), sub_value)
else:
# assume iteration else
for sub_value in value:
unfold_value(key + "[]", sub_value)
for key, value in data.iteritems():
unfold_value("%s[%s]" % (name, key), value)
return scope._call(cls.KIND, **d)
def create(self, **data):
return cls.create(scope, **data)
def get(self, id):
return cls.get(scope, id)
return ScopeBinder()
return api_call()
def __repr__(self):
return str(self)
def __str__(self):
scopes = self._scope
base = ""
if len(scopes) > 1:
base = str(scopes[-2])
return base + "/" + str(scopes[-1])
# maybe someday I'll make that work.
# class RESTBaseMeta(type):
# def __new__(self, name, bases, d):
# clazz = type(name, bases, d)
# if 'KIND' in d:
# kind = d['KIND']
# RESTBase.REGISTRY[kind] = clazz
# return clazz
class RESTBase(object):
"""
The baseclass for all our domain-objects/resources.
"""
REGISTRY = {}
ALL_DOMAIN_CLASSES = {}
ALIASES = []
KIND = None
def __init__(self, data, scope, path_stack=None):
self.__data = data
self.__scope = scope
# try and see if we can/must create an id out of our path
logger.debug("path_stack: %r", path_stack)
if path_stack:
try:
id = int(path_stack[0])
self.__data['id'] = id
except ValueError:
pass
def __getattr__(self, name):
if name in self.__data:
obj = self.__data[name]
if name in RESTBase.REGISTRY:
if isinstance(obj, dict):
obj = RESTBase.REGISTRY[name](obj, self.__scope)
elif isinstance(obj, list):
obj = [RESTBase.REGISTRY[name](o, self.__scope) for o in obj]
else:
logger.warning("Found %s in our registry, but don't know what to do with"\
"the object.")
return obj
scope = Scope(self.__scope._get_connector(), scope=self, parent=self.__scope)
return getattr(scope, name)
def __setattr__(self, name, value):
"""
This method is used to set a property, a resource or a list of resources as property of the resource the
method is invoked on.
For example, to set a comment on a track, do
>>> sca = scapi.Scope(connector)
>>> track = scapi.Track.new(title='bar', sharing="private")
>>> comment = scapi.Comment.create(body="This is the body of my comment", timestamp=10)
>>> track.comments = comment
To set a list of users as permissions, do
>>> sca = scapi.Scope(connector)
>>> me = sca.me()
>>> track = scapi.Track.new(title='bar', sharing="private")
>>> users = sca.users()
>>> users_to_set = [user for user in users[:10] if user != me]
>>> track.permissions = users_to_set
And finally, to simply change the title of a track, do
>>> sca = scapi.Scope(connector)
>>> track = sca.Track.get(track_id)
>>> track.title = "new_title"
@param name: the property name
@type name: str
@param value: the property, resource or resources to set
@type value: RESTBase | list<RESTBase> | basestring | long | int | float
@return: None
"""
# update "private" data, such as __data
if "_RESTBase__" in name:
self.__dict__[name] = value
else:
if isinstance(value, list) and len(value):
# the parametername is something like
# permissions[user_id][]
# so we try to infer that.
parameter_name = "%s[%s_id][]" % (name, value[0]._singleton())
values = [o.id for o in value]
kwargs = {"_alternate_http_method" : "PUT",
parameter_name : values}
self.__scope._call(self.KIND, self.id, name, **kwargs)
elif isinstance(value, RESTBase):
# we got a single instance, so make that an argument
self.__scope._call(self.KIND, self.id, name, **value._as_arguments())
else:
# we have a simple property
parameter_name = "%s[%s]" % (self._singleton(), name)
kwargs = {"_alternate_http_method" : "PUT",
parameter_name : self._convert_value(value)}
self.__scope._call(self.KIND, self.id, **kwargs)
def _as_arguments(self):
"""
Converts a resource to a argument-string the way Rails expects it.
"""
res = {}
for key, value in self.__data.items():
value = self._convert_value(value)
res["%s[%s]" % (self._singleton(), key)] = value
return res
def _convert_value(self, value):
if isinstance(value, unicode):
value = value.encode("utf-8")
elif isinstance(value, file):
pass
else:
value = str(value)
return value
@classmethod
def create(cls, scope, **data):
"""
This is a convenience-method for creating an object that will be passed
as parameter - e.g. a comment. A usage would look like this:
>>> sca = scapi.Scope(connector)
>>> track = sca.Track.new(title='bar', sharing="private")
>>> comment = sca.Comment.create(body="This is the body of my comment", timestamp=10)
>>> track.comments = comment
"""
return cls(data, scope)
@classmethod
def new(cls, scope, **data):
"""
Create a new resource inside a given Scope. The actual values are in data.
So for creating new resources, you have two options:
- create an instance directly using the class:
>>> scope = scapi.Scope(connector)
>>> scope.User.new(...)
<scapi.User object at 0x1234>
- create a instance in a certain scope:
>>> scope = scapi.Scope(connector)
>>> user = scapi.User("1")
>>> track = user.tracks.new()
<scapi.Track object at 0x1234>
@param scope: if not empty, a one-element tuple containing the Scope
@type scope: tuple<Scope>[1]
@param data: the data
@type data: dict
@return: new instance of the resource
"""
return getattr(scope, cls.__name__).new(**data)
@classmethod
def get(cls, scope, id):
"""
Fetch a resource by id.
Simply pass a known id as argument. For example
>>> sca = scapi.Scope(connector)
>>> track = sca.Track.get(id)
"""
return getattr(scope, cls.KIND)(id)
def _scope(self):
"""
Return the scope this resource lives in, which is the KIND and id
@return: "<KIND>/<id>"
"""
return "%s/%s" % (self.KIND, str(self.id))
@classmethod
def _singleton(cls):
"""
This method will take a resource name like "users" and
return the single-case, in the example "user".
Currently, it's not very sophisticated, only strips a trailing s.
"""
name = cls.KIND
if name[-1] == 's':
return name[:-1]
raise ValueError("Can't make %s to a singleton" % name)
def __repr__(self):
res = []
res.append("\n\n******\n%s:" % self.__class__.__name__)
res.append("")
for key, v in self.__data.iteritems():
key = str(key)
if isinstance(v, unicode):
v = v.encode('utf-8')
else:
v = str(v)
res.append("%s=%s" % (key, v))
return "\n".join(res)
def __hash__(self):
return hash("%s%i" % (self.KIND, self.id))
def __eq__(self, other):
"""
Test for equality.
Resources are considered equal if the have the same kind and id.
"""
if not isinstance(other, RESTBase):
return False
res = self.KIND == other.KIND and self.id == other.id
return res
def __ne__(self, other):
return not self == other
class User(RESTBase):
"""
A user domain object/resource.
"""
KIND = 'users'
ALIASES = ['me', 'permissions', 'contacts', 'user']
class Track(RESTBase):
"""
A track domain object/resource.
"""
KIND = 'tracks'
ALIASES = ['favorites']
class Comment(RESTBase):
"""
A comment domain object/resource.
"""
KIND = 'comments'
class Event(RESTBase):
"""
A event domain object/resource.
"""
KIND = 'events'
class Playlist(RESTBase):
"""
A playlist/set domain object/resource
"""
KIND = 'playlists'
class Group(RESTBase):
"""
A group domain object/resource
"""
KIND = 'groups'
# this registers all the RESTBase subclasses.
# One day using a metaclass will make this a tad
# less ugly.
def register_classes():
g = {}
g.update(globals())
for name, cls in [(k, v) for k, v in g.iteritems() if isclass(v) and issubclass(v, RESTBase) and not v == RESTBase]:
RESTBase.REGISTRY[cls.KIND] = cls
RESTBase.ALL_DOMAIN_CLASSES[cls.__name__] = cls
for alias in cls.ALIASES:
RESTBase.REGISTRY[alias] = cls
__all__.append(name)
register_classes()