Package scapi
[hide private]
[frames] | no frames]

Source Code for Package scapi

  1  ##    SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful 
  2  ##    API 
  3  ## 
  4  ##    Copyright (C) 2008  Diez B. Roggisch 
  5  ##    Contact mailto:deets@soundcloud.com 
  6  ## 
  7  ##    This library is free software; you can redistribute it and/or 
  8  ##    modify it under the terms of the GNU Lesser General Public 
  9  ##    License as published by the Free Software Foundation; either 
 10  ##    version 2.1 of the License, or (at your option) any later version. 
 11  ## 
 12  ##    This library is distributed in the hope that it will be useful, 
 13  ##    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  ##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 15  ##    Lesser General Public License for more details. 
 16  ## 
 17  ##    You should have received a copy of the GNU Lesser General Public 
 18  ##    License along with this library; if not, write to the Free Software 
 19  ##    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 20   
 21  import urllib 
 22  import urllib2 
 23   
 24  import logging 
 25  import simplejson 
 26  import cgi 
 27  from scapi.MultipartPostHandler import MultipartPostHandler 
 28  from inspect import isclass 
 29  import urlparse 
 30  from scapi.authentication import BasicAuthenticator 
 31  from scapi.util import ( 
 32      escape, 
 33      MultiDict, 
 34      ) 
 35   
 36  logging.basicConfig() 
 37  logger = logging.getLogger(__name__) 
 38   
 39  USE_PROXY = False 
 40  """ 
 41  Something like http://127.0.0.1:10000/ 
 42  """ 
 43  PROXY = '' 
 44   
 45   
 46   
 47  """ 
 48  The url Soundcould offers to obtain request-tokens 
 49  """ 
 50  REQUEST_TOKEN_URL = 'http://api.soundcloud.com/oauth/request_token' 
 51  """ 
 52  The url Soundcould offers to exchange access-tokens for request-tokens. 
 53  """ 
 54  ACCESS_TOKEN_URL = 'http://api.soundcloud.com/oauth/access_token' 
 55  """ 
 56  The url Soundcould offers to make users authorize a concrete request token. 
 57  """ 
 58  AUTHORIZATION_URL = 'http://api.soundcloud.com/oauth/authorize' 
 59   
 60  __all__ = ['SoundCloudAPI', 'USE_PROXY', 'PROXY', 'REQUEST_TOKEN_URL', 'ACCESS_TOKEN_URL', 'AUTHORIZATION_URL'] 
61 62 63 -class NoResultFromRequest(Exception):
64 pass
65
66 -class InvalidMethodException(Exception):
67
68 - def __init__(self, message):
69 self._message = message 70 Exception.__init__(self)
71
72 - def __repr__(self):
73 res = Exception.__repr__(self) 74 res += "\n" 75 res += "-" * 10 76 res += "\nmessage:\n\n" 77 res += self._message 78 return res
79
80 -class UnknownContentType(Exception):
81 - def __init__(self, msg):
82 Exception.__init__(self) 83 self._msg = msg
84
85 - def __repr__(self):
86 return self.__class__.__name__ + ":" + self._msg
87
88 - def __str__(self):
89 return str(self)
90
91 92 -class ApiConnector(object):
93 """ 94 The ApiConnector holds all the data necessary to authenticate against 95 the soundcloud-api. You can instantiate several connectors if you like, but usually one 96 should be sufficient. 97 """ 98 99 """ 100 SoundClound imposes a maximum on the number of returned items. This value is that 101 maximum. 102 """ 103 LIST_LIMIT = 50 104 105 """ 106 The query-parameter that is used to request results beginning from a certain offset. 107 """ 108 LIST_OFFSET_PARAMETER = 'offset' 109 """ 110 The query-parameter that is used to request results being limited to a certain amount. 111 112 Currently this is of no use and just for completeness sake. 113 """ 114 LIST_LIMIT_PARAMETER = 'limit' 115
116 - def __init__(self, host, user=None, password=None, authenticator=None, base="", collapse_scope=True):
117 """ 118 Constructor for the API-Singleton. Use it once with parameters, and then the 119 subsequent calls internal to the API will work. 120 121 @type host: str 122 @param host: the host to connect to, e.g. "api.soundcloud.com". If a port is needed, use 123 "api.soundcloud.com:1234" 124 @type user: str 125 @param user: if given, the username for basic HTTP authentication 126 @type password: str 127 @param password: if the user is given, you have to give a password as well 128 @type authenticator: OAuthAuthenticator | BasicAuthenticator 129 @param authenticator: the authenticator to use, see L{scapi.authentication} 130 """ 131 self.host = host 132 if authenticator is not None: 133 self.authenticator = authenticator 134 elif user is not None and password is not None: 135 self.authenticator = BasicAuthenticator(user, password) 136 self._base = base 137 self.collapse_scope = collapse_scope
138
139 - def normalize_method(self, method):
140 """ 141 This method will take a method that has been part of a redirect of some sort 142 and see if it's valid, which means that it's located beneath our base. 143 If yes, we return it normalized without that very base. 144 """ 145 _, _, path, _, _, _ = urlparse.urlparse(method) 146 if path.startswith("/"): 147 path = path[1:] 148 # if the base is "", we return the whole path, 149 # otherwise normalize it away 150 if self._base == "": 151 return path 152 if path.startswith(self._base): 153 return path[len(self._base)-1:] 154 raise InvalidMethodException("Not a valid API method: %s" % method)
155 156 157
158 - def fetch_request_token(self, url=None, oauth_callback="oob", oauth_verifier=None):
159 """ 160 Helper-function for a registered consumer to obtain a request token, as 161 used by oauth. 162 163 Use it like this: 164 165 >>> oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, 166 CONSUMER_SECRET, 167 None, 168 None) 169 170 >>> sca = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator) 171 >>> token, secret = sca.fetch_request_token() 172 >>> authorization_url = sca.get_request_token_authorization_url(token) 173 174 Please note the None passed as token & secret to the authenticator. 175 """ 176 if url is None: 177 url = REQUEST_TOKEN_URL 178 req = urllib2.Request(url) 179 self.authenticator.augment_request(req, None, oauth_callback=oauth_callback, oauth_verifier=oauth_verifier) 180 handlers = [] 181 if USE_PROXY: 182 handlers.append(urllib2.ProxyHandler({'http' : PROXY})) 183 opener = urllib2.build_opener(*handlers) 184 handle = opener.open(req, None) 185 info = handle.info() 186 content = handle.read() 187 params = cgi.parse_qs(content, keep_blank_values=False) 188 key = params['oauth_token'][0] 189 secret = params['oauth_token_secret'][0] 190 return key, secret
191 192
193 - def fetch_access_token(self, oauth_verifier):
194 """ 195 Helper-function for a registered consumer to exchange an access token for 196 a request token. 197 198 Use it like this: 199 200 >>> oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, 201 CONSUMER_SECRET, 202 request_token, 203 request_token_secret) 204 205 >>> sca = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator) 206 >>> token, secret = sca.fetch_access_token() 207 208 Please note the values passed as token & secret to the authenticator. 209 """ 210 return self.fetch_request_token(ACCESS_TOKEN_URL, oauth_verifier=oauth_verifier)
211 212
213 - def get_request_token_authorization_url(self, token):
214 """ 215 Simple helper function to generate the url needed 216 to ask a user for request token authorization. 217 218 See also L{fetch_request_token}. 219 220 Possible usage: 221 222 >>> import webbrowser 223 >>> sca = scapi.ApiConnector() 224 >>> authorization_url = sca.get_request_token_authorization_url(token) 225 >>> webbrowser.open(authorization_url) 226 """ 227 return "%s?oauth_token=%s" % (AUTHORIZATION_URL, token)
228
229 230 231 -class SCRedirectHandler(urllib2.HTTPRedirectHandler):
232 """ 233 A urllib2-Handler to deal with the redirects the RESTful API of SC uses. 234 """ 235 alternate_method = None 236
237 - def http_error_303(self, req, fp, code, msg, hdrs):
238 """ 239 In case of return-code 303 (See-other), we have to store the location we got 240 because that will determine the actual type of resource returned. 241 """ 242 self.alternate_method = hdrs['location'] 243 # for oauth, we need to re-create the whole header-shizzle. This 244 # does it - it recreates a full url and signs the request 245 new_url = self.alternate_method 246 # if USE_PROXY: 247 # import pdb; pdb.set_trace() 248 # old_url = req.get_full_url() 249 # protocol, host, _, _, _, _ = urlparse.urlparse(old_url) 250 # new_url = urlparse.urlunparse((protocol, host, self.alternate_method, None, None, None)) 251 req = req.recreate_request(new_url) 252 return urllib2.HTTPRedirectHandler.http_error_303(self, req, fp, code, msg, hdrs)
253
254 - def http_error_201(self, req, fp, code, msg, hdrs):
255 """ 256 We fake a 201 being a 303 so that our redirection-scheme takes place 257 for the 201 the API throws in case we created something. If the location is 258 not available though, that means that whatever we created has succeded - without 259 being a named resource. Assigning an asset to a track is an example of such 260 case. 261 """ 262 if 'location' not in hdrs: 263 raise NoResultFromRequest() 264 return self.http_error_303(req, fp, 303, msg, hdrs)
265
266 -class Scope(object):
267 """ 268 The basic means to query and create resources. The Scope uses the L{ApiConnector} to 269 create the proper URIs for querying or creating resources. 270 271 For accessing resources from the root level, you explcitly create a Scope and pass it 272 an L{ApiConnector}-instance. Then you can query it 273 or create new resources like this: 274 275 >>> connector = scapi.ApiConnector(host='host', user='user', password='password') # initialize the API 276 >>> scope = scapi.Scope(connector) # get the root scope 277 >>> users = list(scope.users()) 278 [<scapi.User object at 0x12345>, ...] 279 280 Please not that all resources that are lists are returned as B{generator}. So you need 281 to either iterate over them, or call list(resources) on them. 282 283 When accessing resources that belong to another resource, like contacts of a user, you access 284 the parent's resource scope implicitly through the resource instance like this: 285 286 >>> user = scope.users().next() 287 >>> list(user.contacts()) 288 [<scapi.Contact object at 0x12345>, ...] 289 290 """
291 - def __init__(self, connector, scope=None, parent=None):
292 """ 293 Create the Scope. It can have a resource as scope, and possibly a parent-scope. 294 295 @param connector: The connector to use. 296 @type connector: ApiConnector 297 @type scope: scapi.RESTBase 298 @param scope: the resource to make this scope belong to 299 @type parent: scapi.Scope 300 @param parent: the parent scope of this scope 301 """ 302 303 if scope is None: 304 scope = () 305 else: 306 scope = scope, 307 if parent is not None: 308 scope = parent._scope + scope 309 self._scope = scope 310 self._connector = connector
311
312 - def _get_connector(self):
313 return self._connector
314 315
316 - def oauth_sign_get_request(self, url):
317 """ 318 This method will take an arbitrary url, and rewrite it 319 so that the current authenticator's oauth-headers are appended 320 as query-parameters. 321 322 This is used in streaming and downloading, because those content 323 isn't served from the SoundCloud servers themselves. 324 325 A usage example would look like this: 326 327 >>> sca = scapi.Scope(connector) 328 >>> track = sca.tracks(params={ 329 "filter" : "downloadable", 330 }).next() 331 332 333 >>> download_url = track.download_url 334 >>> signed_url = track.oauth_sign_get_request(download_url) 335 >>> data = urllib2.urlopen(signed_url).read() 336 337 """ 338 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) 339 340 req = urllib2.Request(url) 341 342 all_params = {} 343 if query: 344 all_params.update(cgi.parse_qs(query)) 345 346 if not all_params: 347 all_params = None 348 349 self._connector.authenticator.augment_request(req, all_params, False) 350 351 auth_header = req.get_header("Authorization") 352 auth_header = auth_header[len("OAuth "):] 353 354 query_params = [] 355 if query: 356 query_params.append(query) 357 358 for part in auth_header.split(","): 359 key, value = part.split("=") 360 assert key.startswith("oauth") 361 value = value[1:-1] 362 query_params.append("%s=%s" % (key, value)) 363 364 query = "&".join(query_params) 365 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) 366 return url
367 368
369 - def _create_request(self, url, connector, parameters, queryparams, alternate_http_method=None, use_multipart=False):
370 """ 371 This method returnes the urllib2.Request to perform the actual HTTP-request. 372 373 We return a subclass that overload the get_method-method to return a custom method like "PUT". 374 Additionally, the request is enhanced with the current authenticators authorization scheme 375 headers. 376 377 @param url: the destination url 378 @param connector: our connector-instance 379 @param parameters: the POST-parameters to use. 380 @type parameters: None|dict<str, basestring|list<basestring>> 381 @param queryparams: the queryparams to use 382 @type queryparams: None|dict<str, basestring|list<basestring>> 383 @param alternate_http_method: an alternate HTTP-method to use 384 @type alternate_http_method: str 385 @return: the fully equipped request 386 @rtype: urllib2.Request 387 """ 388 class MyRequest(urllib2.Request): 389 def get_method(self): 390 if alternate_http_method is not None: 391 return alternate_http_method 392 return urllib2.Request.get_method(self)
393 394 def has_data(self): 395 return parameters is not None
396 397 def augment_request(self, params, use_multipart=False): 398 connector.authenticator.augment_request(self, params, use_multipart) 399 400 @classmethod 401 def recreate_request(cls, location): 402 return self._create_request(location, connector, None, None) 403 404 req = MyRequest(url) 405 all_params = {} 406 if parameters is not None: 407 all_params.update(parameters) 408 if queryparams is not None: 409 all_params.update(queryparams) 410 if not all_params: 411 all_params = None 412 req.augment_request(all_params, use_multipart) 413 req.add_header("Accept", "application/json") 414 return req 415 416
417 - def _create_query_string(self, queryparams):
418 """ 419 Small helpermethod to create the querystring from a dict. 420 421 @type queryparams: None|dict<str, basestring|list<basestring>> 422 @param queryparams: the queryparameters. 423 @return: either the empty string, or a "?" followed by the parameters joined by "&" 424 @rtype: str 425 """ 426 if not queryparams: 427 return "" 428 h = [] 429 for key, values in queryparams.iteritems(): 430 if isinstance(values, (int, long, float)): 431 values = str(values) 432 if isinstance(values, basestring): 433 values = [values] 434 for v in values: 435 v = v.encode("utf-8") 436 h.append("%s=%s" % (key, escape(v))) 437 return "?" + "&".join(h)
438 439
440 - def _call(self, method, *args, **kwargs):
441 """ 442 The workhorse. It's complicated, convoluted and beyond understanding of a mortal being. 443 444 You have been warned. 445 """ 446 447 queryparams = {} 448 __offset__ = ApiConnector.LIST_LIMIT 449 if "__offset__" in kwargs: 450 offset = kwargs.pop("__offset__") 451 queryparams['offset'] = offset 452 __offset__ = offset + ApiConnector.LIST_LIMIT 453 454 if "params" in kwargs: 455 queryparams.update(kwargs.pop("params")) 456 457 # create a closure to invoke this method again with a greater offset 458 _cl_method = method 459 _cl_args = tuple(args) 460 _cl_kwargs = {} 461 _cl_kwargs.update(kwargs) 462 _cl_kwargs["__offset__"] = __offset__ 463 def continue_list_fetching(): 464 return self._call(method, *_cl_args, **_cl_kwargs)
465 connector = self._get_connector() 466 def filelike(v): 467 if isinstance(v, file): 468 return True 469 if hasattr(v, "read"): 470 return True 471 return False 472 alternate_http_method = None 473 if "_alternate_http_method" in kwargs: 474 alternate_http_method = kwargs.pop("_alternate_http_method") 475 urlparams = kwargs if kwargs else None 476 use_multipart = False 477 if urlparams is not None: 478 fileargs = dict((key, value) for key, value in urlparams.iteritems() if filelike(value)) 479 use_multipart = bool(fileargs) 480 481 # ensure the method has a trailing / 482 if method[-1] != "/": 483 method = method + "/" 484 if args: 485 method = "%s%s" % (method, "/".join(str(a) for a in args)) 486 487 scope = '' 488 if self._scope: 489 scopes = self._scope 490 if connector.collapse_scope: 491 scopes = scopes[-1:] 492 scope = "/".join([sc._scope() for sc in scopes]) + "/" 493 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)) 494 495 # we need to install SCRedirectHandler 496 # to gather possible See-Other redirects 497 # so that we can exchange our method 498 redirect_handler = SCRedirectHandler() 499 handlers = [redirect_handler] 500 if USE_PROXY: 501 handlers.append(urllib2.ProxyHandler({'http' : PROXY})) 502 req = self._create_request(url, connector, urlparams, queryparams, alternate_http_method, use_multipart) 503 504 http_method = req.get_method() 505 if urlparams is not None: 506 logger.debug("Posting url: %s, method: %s", url, http_method) 507 else: 508 logger.debug("Fetching url: %s, method: %s", url, http_method) 509 510 511 if use_multipart: 512 handlers.extend([MultipartPostHandler]) 513 else: 514 if urlparams is not None: 515 urlparams = urllib.urlencode(urlparams.items(), True) 516 opener = urllib2.build_opener(*handlers) 517 try: 518 handle = opener.open(req, urlparams) 519 except NoResultFromRequest: 520 return None 521 except urllib2.HTTPError, e: 522 if http_method == "GET" and e.code == 404: 523 return None 524 raise 525 526 info = handle.info() 527 ct = info['Content-Type'] 528 content = handle.read() 529 logger.debug("Content-type:%s", ct) 530 logger.debug("Request Content:\n%s", content) 531 if redirect_handler.alternate_method is not None: 532 method = connector.normalize_method(redirect_handler.alternate_method) 533 logger.debug("Method changed through redirect to: <%s>", method) 534 535 try: 536 if "application/json" in ct: 537 content = content.strip() 538 if not content: 539 content = "{}" 540 try: 541 res = simplejson.loads(content) 542 except: 543 logger.error("Couldn't decode returned json") 544 logger.error(content) 545 raise 546 res = self._map(res, method, continue_list_fetching) 547 return res 548 elif len(content) <= 1: 549 # this might be the famous SeeOtherSpecialCase which means that 550 # all that matters is just the method 551 pass 552 raise UnknownContentType("%s, returned:\n%s" % (ct, content)) 553 finally: 554 handle.close() 555
556 - def _map(self, res, method, continue_list_fetching):
557 """ 558 This method will take the JSON-result of a HTTP-call and return our domain-objects. 559 560 It's also deep magic, don't look. 561 """ 562 pathparts = reversed(method.split("/")) 563 stack = [] 564 for part in pathparts: 565 stack.append(part) 566 if part in RESTBase.REGISTRY: 567 cls = RESTBase.REGISTRY[part] 568 # multiple objects 569 if isinstance(res, list): 570 def result_gen(): 571 count = 0 572 for item in res: 573 yield cls(item, self, stack) 574 count += 1 575 if count == ApiConnector.LIST_LIMIT: 576 for item in continue_list_fetching(): 577 yield item
578 return result_gen() 579 else: 580 return cls(res, self, stack) 581 logger.debug("don't know how to handle result") 582 logger.debug(res) 583 return res 584
585 - def __getattr__(self, _name):
586 """ 587 Retrieve an API-method or a scoped domain-class. 588 589 If the former, result is a callable that supports the following invocations: 590 591 - calling (...), with possible arguments (positional/keyword), return the resulting resource or list of resources. 592 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. 593 594 - invoking append(resource) on it will PUT the resource, making it part of the current resource. Makes 595 sense only if it's a collection of course. 596 597 - invoking remove(resource) on it will DELETE the resource from it's container. Also only usable on collections. 598 599 TODO: describe the latter 600 """ 601 scope = self 602 603 class api_call(object): 604 def __call__(selfish, *args, **kwargs): 605 return self._call(_name, *args, **kwargs)
606 607 def new(self, **kwargs): 608 """ 609 Will invoke the new method on the named resource _name, with 610 self as scope. 611 """ 612 cls = RESTBase.REGISTRY[_name] 613 return cls.new(scope, **kwargs) 614 615 def append(selfish, resource): 616 """ 617 If the current scope is 618 """ 619 self._call(_name, str(resource.id), _alternate_http_method="PUT") 620 621 def remove(selfish, resource): 622 self._call(_name, str(resource.id), _alternate_http_method="DELETE") 623 624 if _name in RESTBase.ALL_DOMAIN_CLASSES: 625 cls = RESTBase.ALL_DOMAIN_CLASSES[_name] 626 627 class ScopeBinder(object): 628 def new(self, *args, **data): 629 630 d = MultiDict() 631 name = cls._singleton() 632 633 def unfold_value(key, value): 634 if isinstance(value, (basestring, file)): 635 d.add(key, value) 636 elif isinstance(value, dict): 637 for sub_key, sub_value in value.iteritems(): 638 unfold_value("%s[%s]" % (key, sub_key), sub_value) 639 else: 640 # assume iteration else 641 for sub_value in value: 642 unfold_value(key + "[]", sub_value) 643 644 645 for key, value in data.iteritems(): 646 unfold_value("%s[%s]" % (name, key), value) 647 648 return scope._call(cls.KIND, **d) 649 650 def create(self, **data): 651 return cls.create(scope, **data) 652 653 def get(self, id): 654 return cls.get(scope, id) 655 656 657 return ScopeBinder() 658 return api_call() 659
660 - def __repr__(self):
661 return str(self)
662
663 - def __str__(self):
664 scopes = self._scope 665 base = "" 666 if len(scopes) > 1: 667 base = str(scopes[-2]) 668 return base + "/" + str(scopes[-1])
669
670 671 # maybe someday I'll make that work. 672 # class RESTBaseMeta(type): 673 # def __new__(self, name, bases, d): 674 # clazz = type(name, bases, d) 675 # if 'KIND' in d: 676 # kind = d['KIND'] 677 # RESTBase.REGISTRY[kind] = clazz 678 # return clazz 679 680 -class RESTBase(object):
681 """ 682 The baseclass for all our domain-objects/resources. 683 684 685 """ 686 REGISTRY = {} 687 688 ALL_DOMAIN_CLASSES = {} 689 690 ALIASES = [] 691 692 KIND = None 693
694 - def __init__(self, data, scope, path_stack=None):
695 self.__data = data 696 self.__scope = scope 697 # try and see if we can/must create an id out of our path 698 logger.debug("path_stack: %r", path_stack) 699 if path_stack: 700 try: 701 id = int(path_stack[0]) 702 self.__data['id'] = id 703 except ValueError: 704 pass
705
706 - def __getattr__(self, name):
707 if name in self.__data: 708 obj = self.__data[name] 709 if name in RESTBase.REGISTRY: 710 if isinstance(obj, dict): 711 obj = RESTBase.REGISTRY[name](obj, self.__scope) 712 elif isinstance(obj, list): 713 obj = [RESTBase.REGISTRY[name](o, self.__scope) for o in obj] 714 else: 715 logger.warning("Found %s in our registry, but don't know what to do with"\ 716 "the object.") 717 return obj 718 scope = Scope(self.__scope._get_connector(), scope=self, parent=self.__scope) 719 return getattr(scope, name)
720
721 - def __setattr__(self, name, value):
722 """ 723 This method is used to set a property, a resource or a list of resources as property of the resource the 724 method is invoked on. 725 726 For example, to set a comment on a track, do 727 728 >>> sca = scapi.Scope(connector) 729 >>> track = scapi.Track.new(title='bar', sharing="private") 730 >>> comment = scapi.Comment.create(body="This is the body of my comment", timestamp=10) 731 >>> track.comments = comment 732 733 To set a list of users as permissions, do 734 735 >>> sca = scapi.Scope(connector) 736 >>> me = sca.me() 737 >>> track = scapi.Track.new(title='bar', sharing="private") 738 >>> users = sca.users() 739 >>> users_to_set = [user for user in users[:10] if user != me] 740 >>> track.permissions = users_to_set 741 742 And finally, to simply change the title of a track, do 743 744 >>> sca = scapi.Scope(connector) 745 >>> track = sca.Track.get(track_id) 746 >>> track.title = "new_title" 747 748 @param name: the property name 749 @type name: str 750 @param value: the property, resource or resources to set 751 @type value: RESTBase | list<RESTBase> | basestring | long | int | float 752 @return: None 753 """ 754 755 # update "private" data, such as __data 756 if "_RESTBase__" in name: 757 self.__dict__[name] = value 758 else: 759 if isinstance(value, list) and len(value): 760 # the parametername is something like 761 # permissions[user_id][] 762 # so we try to infer that. 763 parameter_name = "%s[%s_id][]" % (name, value[0]._singleton()) 764 values = [o.id for o in value] 765 kwargs = {"_alternate_http_method" : "PUT", 766 parameter_name : values} 767 self.__scope._call(self.KIND, self.id, name, **kwargs) 768 elif isinstance(value, RESTBase): 769 # we got a single instance, so make that an argument 770 self.__scope._call(self.KIND, self.id, name, **value._as_arguments()) 771 else: 772 # we have a simple property 773 parameter_name = "%s[%s]" % (self._singleton(), name) 774 kwargs = {"_alternate_http_method" : "PUT", 775 parameter_name : self._convert_value(value)} 776 self.__scope._call(self.KIND, self.id, **kwargs)
777
778 - def _as_arguments(self):
779 """ 780 Converts a resource to a argument-string the way Rails expects it. 781 """ 782 res = {} 783 for key, value in self.__data.items(): 784 value = self._convert_value(value) 785 res["%s[%s]" % (self._singleton(), key)] = value 786 return res
787
788 - def _convert_value(self, value):
789 if isinstance(value, unicode): 790 value = value.encode("utf-8") 791 elif isinstance(value, file): 792 pass 793 else: 794 value = str(value) 795 return value
796 797 @classmethod
798 - def create(cls, scope, **data):
799 """ 800 This is a convenience-method for creating an object that will be passed 801 as parameter - e.g. a comment. A usage would look like this: 802 803 >>> sca = scapi.Scope(connector) 804 >>> track = sca.Track.new(title='bar', sharing="private") 805 >>> comment = sca.Comment.create(body="This is the body of my comment", timestamp=10) 806 >>> track.comments = comment 807 808 """ 809 return cls(data, scope)
810 811 @classmethod
812 - def new(cls, scope, **data):
813 """ 814 Create a new resource inside a given Scope. The actual values are in data. 815 816 So for creating new resources, you have two options: 817 818 - create an instance directly using the class: 819 820 >>> scope = scapi.Scope(connector) 821 >>> scope.User.new(...) 822 <scapi.User object at 0x1234> 823 824 - create a instance in a certain scope: 825 826 >>> scope = scapi.Scope(connector) 827 >>> user = scapi.User("1") 828 >>> track = user.tracks.new() 829 <scapi.Track object at 0x1234> 830 831 @param scope: if not empty, a one-element tuple containing the Scope 832 @type scope: tuple<Scope>[1] 833 @param data: the data 834 @type data: dict 835 @return: new instance of the resource 836 """ 837 return getattr(scope, cls.__name__).new(**data)
838 839 @classmethod
840 - def get(cls, scope, id):
841 """ 842 Fetch a resource by id. 843 844 Simply pass a known id as argument. For example 845 846 >>> sca = scapi.Scope(connector) 847 >>> track = sca.Track.get(id) 848 849 """ 850 return getattr(scope, cls.KIND)(id)
851 852
853 - def _scope(self):
854 """ 855 Return the scope this resource lives in, which is the KIND and id 856 857 @return: "<KIND>/<id>" 858 """ 859 return "%s/%s" % (self.KIND, str(self.id))
860 861 @classmethod
862 - def _singleton(cls):
863 """ 864 This method will take a resource name like "users" and 865 return the single-case, in the example "user". 866 867 Currently, it's not very sophisticated, only strips a trailing s. 868 """ 869 name = cls.KIND 870 if name[-1] == 's': 871 return name[:-1] 872 raise ValueError("Can't make %s to a singleton" % name)
873
874 - def __repr__(self):
875 res = [] 876 res.append("\n\n******\n%s:" % self.__class__.__name__) 877 res.append("") 878 for key, v in self.__data.iteritems(): 879 key = str(key) 880 if isinstance(v, unicode): 881 v = v.encode('utf-8') 882 else: 883 v = str(v) 884 res.append("%s=%s" % (key, v)) 885 return "\n".join(res)
886
887 - def __hash__(self):
888 return hash("%s%i" % (self.KIND, self.id))
889
890 - def __eq__(self, other):
891 """ 892 Test for equality. 893 894 Resources are considered equal if the have the same kind and id. 895 """ 896 if not isinstance(other, RESTBase): 897 return False 898 res = self.KIND == other.KIND and self.id == other.id 899 return res
900
901 - def __ne__(self, other):
902 return not self == other
903
904 -class User(RESTBase):
905 """ 906 A user domain object/resource. 907 """ 908 KIND = 'users' 909 ALIASES = ['me', 'permissions', 'contacts', 'user']
910
911 -class Track(RESTBase):
912 """ 913 A track domain object/resource. 914 """ 915 KIND = 'tracks' 916 ALIASES = ['favorites']
917
918 -class Comment(RESTBase):
919 """ 920 A comment domain object/resource. 921 """ 922 KIND = 'comments'
923
924 -class Event(RESTBase):
925 """ 926 A event domain object/resource. 927 """ 928 KIND = 'events'
929
930 -class Playlist(RESTBase):
931 """ 932 A playlist/set domain object/resource 933 """ 934 KIND = 'playlists'
935
936 -class Group(RESTBase):
937 """ 938 A group domain object/resource 939 """ 940 KIND = 'groups'
941
942 943 944 # this registers all the RESTBase subclasses. 945 # One day using a metaclass will make this a tad 946 # less ugly. 947 -def register_classes():
948 g = {} 949 g.update(globals()) 950 for name, cls in [(k, v) for k, v in g.iteritems() if isclass(v) and issubclass(v, RESTBase) and not v == RESTBase]: 951 RESTBase.REGISTRY[cls.KIND] = cls 952 RESTBase.ALL_DOMAIN_CLASSES[cls.__name__] = cls 953 for alias in cls.ALIASES: 954 RESTBase.REGISTRY[alias] = cls 955 __all__.append(name)
956 register_classes() 957