From f68a8f67ea29bed0d4d5bd7ae28b49efb16e3be0 Mon Sep 17 00:00:00 2001 From: Naomi Date: Sat, 5 Mar 2011 11:53:29 -0500 Subject: [PATCH] soundcloud python/php apis, recorder python script so far. --- .zfproject.xml | 11 + LICENSE_3RD_PARTY | 4 + application/configs/ACL.php | 5 +- .../controllers/RecorderController.php | 32 + .../controllers/plugins/Acl_plugin.php | 9 +- application/models/Shows.php | 18 +- application/views/helpers/SoundCloudLink.php | 22 + .../scripts/airtime-recorder/index.phtml | 1 + .../scripts/recorder/get-show-schedule.phtml | 1 + .../views/scripts/recorder/index.phtml | 1 + library/soundcloud-api/README.md | 114 ++ .../soundcloud-api/Services/Soundcloud.php | 713 ++++++++++ .../Services/Soundcloud/Exception.php | 146 ++ .../Services/Soundcloud/Version.php | 22 + .../soundcloud-api/tests/Soundcloud_Test.php | 310 ++++ .../tests/Soundcloud_Test_Helper.php | 94 ++ python_apps/show-recorder/config.cfg | 8 + python_apps/show-recorder/testrecordscript.py | 118 ++ python_apps/show-recorder/testsoundcloud.py | 59 + python_apps/soundcloud-api/AUTHORS | 5 + python_apps/soundcloud-api/ChangeLog | 9 + python_apps/soundcloud-api/LICENSE | 458 ++++++ python_apps/soundcloud-api/README | 45 + python_apps/soundcloud-api/bootstrap.py | 58 + .../soundcloud-api/docs/api/api-objects.txt | 333 +++++ .../soundcloud-api/docs/api/class-tree.html | 216 +++ python_apps/soundcloud-api/docs/api/crarr.png | Bin 0 -> 340 bytes .../soundcloud-api/docs/api/epydoc.css | 322 +++++ python_apps/soundcloud-api/docs/api/epydoc.js | 293 ++++ .../api/exceptions.AssertionError-class.html | 299 ++++ .../soundcloud-api/docs/api/frames.html | 17 + python_apps/soundcloud-api/docs/api/help.html | 278 ++++ .../docs/api/identifier-index.html | 892 ++++++++++++ .../soundcloud-api/docs/api/index.html | 17 + .../soundcloud-api/docs/api/module-tree.html | 130 ++ .../soundcloud-api/docs/api/redirect.html | 38 + .../soundcloud-api/docs/api/scapi-module.html | 444 ++++++ .../soundcloud-api/docs/api/scapi-pysrc.html | 1263 +++++++++++++++++ .../docs/api/scapi.ApiConnector-class.html | 544 +++++++ .../docs/api/scapi.Asset-class.html | 258 ++++ .../docs/api/scapi.Comment-class.html | 258 ++++ .../docs/api/scapi.Event-class.html | 258 ++++ .../docs/api/scapi.Group-class.html | 258 ++++ .../scapi.InvalidMethodException-class.html | 297 ++++ .../api/scapi.NoResultFromRequest-class.html | 195 +++ .../docs/api/scapi.Playlist-class.html | 258 ++++ .../docs/api/scapi.RESTBase-class.html | 895 ++++++++++++ .../api/scapi.SCRedirectHandler-class.html | 319 +++++ .../docs/api/scapi.Scope-class.html | 682 +++++++++ .../docs/api/scapi.Track-class.html | 264 ++++ .../api/scapi.UnknownContentType-class.html | 337 +++++ .../docs/api/scapi.User-class.html | 264 ++++ .../docs/api/scapi.authentication-module.html | 228 +++ .../docs/api/scapi.authentication-pysrc.html | 348 +++++ ...thentication.BasicAuthenticator-class.html | 267 ++++ ...thentication.OAuthAuthenticator-class.html | 337 +++++ ....OAuthSignatureMethod_HMAC_SHA1-class.html | 294 ++++ .../docs/api/scapi.config-module.html | 114 ++ .../docs/api/scapi.config-pysrc.html | 122 ++ .../docs/api/scapi.json-module.html | 218 +++ .../docs/api/scapi.json-pysrc.html | 433 ++++++ .../docs/api/scapi.json.JsonReader-class.html | 544 +++++++ .../docs/api/scapi.json.JsonWriter-class.html | 233 +++ .../api/scapi.json.ReadException-class.html | 196 +++ .../api/scapi.json.WriteException-class.html | 196 +++ .../scapi.json._StringGenerator-class.html | 291 ++++ .../docs/api/scapi.tests-module.html | 140 ++ .../docs/api/scapi.tests-pysrc.html | 122 ++ .../api/scapi.tests.scapi_tests-module.html | 172 +++ .../api/scapi.tests.scapi_tests-pysrc.html | 760 ++++++++++ ...pi.tests.scapi_tests.SCAPITests-class.html | 1025 +++++++++++++ .../api/scapi.tests.test_connect-module.html | 586 ++++++++ .../api/scapi.tests.test_connect-pysrc.html | 627 ++++++++ .../api/scapi.tests.test_oauth-module.html | 225 +++ .../api/scapi.tests.test_oauth-pysrc.html | 182 +++ .../docs/api/scapi.util-module.html | 173 +++ .../docs/api/scapi.util-pysrc.html | 171 +++ .../docs/api/scapi.util.MultiDict-class.html | 247 ++++ .../docs/api/toc-everything.html | 151 ++ .../docs/api/toc-scapi-module.html | 70 + .../api/toc-scapi.authentication-module.html | 46 + .../docs/api/toc-scapi.config-module.html | 29 + .../docs/api/toc-scapi.json-module.html | 40 + .../docs/api/toc-scapi.multidict-module.html | 29 + .../docs/api/toc-scapi.tests-module.html | 29 + .../toc-scapi.tests.scapi_tests-module.html | 34 + .../toc-scapi.tests.test_connect-module.html | 68 + .../toc-scapi.tests.test_oauth-module.html | 41 + .../docs/api/toc-scapi.util-module.html | 37 + python_apps/soundcloud-api/docs/api/toc.html | 46 + python_apps/soundcloud-api/oauth/__init__.py | 0 .../soundcloud-api/oauth/example/client.py | 157 ++ .../soundcloud-api/oauth/example/server.py | 167 +++ python_apps/soundcloud-api/oauth/oauth.py | 505 +++++++ .../scapi/MultipartPostHandler.py | 135 ++ python_apps/soundcloud-api/scapi/__init__.py | 1012 +++++++++++++ .../soundcloud-api/scapi/authentication.py | 195 +++ python_apps/soundcloud-api/scapi/config.py | 2 + python_apps/soundcloud-api/scapi/json.py | 310 ++++ .../soundcloud-api/scapi/tests/__init__.py | 0 .../soundcloud-api/scapi/tests/knaster.mp3 | Bin 0 -> 80493 bytes .../soundcloud-api/scapi/tests/scapi_tests.py | 563 ++++++++ .../soundcloud-api/scapi/tests/spam.jpg | Bin 0 -> 85062 bytes .../scapi/tests/test_connect.py | 334 +++++ .../soundcloud-api/scapi/tests/test_oauth.py | 36 + python_apps/soundcloud-api/scapi/util.py | 53 + python_apps/soundcloud-api/setup.py | 22 + python_apps/soundcloud-api/test.ini | 33 + .../controllers/RecorderControllerTest.php | 20 + 109 files changed, 24297 insertions(+), 10 deletions(-) create mode 100644 application/controllers/RecorderController.php create mode 100644 application/views/helpers/SoundCloudLink.php create mode 100644 application/views/scripts/airtime-recorder/index.phtml create mode 100644 application/views/scripts/recorder/get-show-schedule.phtml create mode 100644 application/views/scripts/recorder/index.phtml create mode 100644 library/soundcloud-api/README.md create mode 100644 library/soundcloud-api/Services/Soundcloud.php create mode 100644 library/soundcloud-api/Services/Soundcloud/Exception.php create mode 100644 library/soundcloud-api/Services/Soundcloud/Version.php create mode 100644 library/soundcloud-api/tests/Soundcloud_Test.php create mode 100644 library/soundcloud-api/tests/Soundcloud_Test_Helper.php create mode 100644 python_apps/show-recorder/config.cfg create mode 100644 python_apps/show-recorder/testrecordscript.py create mode 100644 python_apps/show-recorder/testsoundcloud.py create mode 100644 python_apps/soundcloud-api/AUTHORS create mode 100644 python_apps/soundcloud-api/ChangeLog create mode 100644 python_apps/soundcloud-api/LICENSE create mode 100644 python_apps/soundcloud-api/README create mode 100644 python_apps/soundcloud-api/bootstrap.py create mode 100644 python_apps/soundcloud-api/docs/api/api-objects.txt create mode 100644 python_apps/soundcloud-api/docs/api/class-tree.html create mode 100644 python_apps/soundcloud-api/docs/api/crarr.png create mode 100644 python_apps/soundcloud-api/docs/api/epydoc.css create mode 100644 python_apps/soundcloud-api/docs/api/epydoc.js create mode 100644 python_apps/soundcloud-api/docs/api/exceptions.AssertionError-class.html create mode 100644 python_apps/soundcloud-api/docs/api/frames.html create mode 100644 python_apps/soundcloud-api/docs/api/help.html create mode 100644 python_apps/soundcloud-api/docs/api/identifier-index.html create mode 100644 python_apps/soundcloud-api/docs/api/index.html create mode 100644 python_apps/soundcloud-api/docs/api/module-tree.html create mode 100644 python_apps/soundcloud-api/docs/api/redirect.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.ApiConnector-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Asset-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Comment-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Event-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Group-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.InvalidMethodException-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.NoResultFromRequest-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Playlist-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.RESTBase-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.SCRedirectHandler-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Scope-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.Track-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.UnknownContentType-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.User-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.authentication-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.authentication-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.authentication.BasicAuthenticator-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthAuthenticator-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.config-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.config-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json.JsonReader-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json.JsonWriter-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json.ReadException-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json.WriteException-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.json._StringGenerator-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests.SCAPITests-class.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.util-module.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.util-pysrc.html create mode 100644 python_apps/soundcloud-api/docs/api/scapi.util.MultiDict-class.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-everything.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.authentication-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.config-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.json-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.multidict-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.tests-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.tests.scapi_tests-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_connect-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_oauth-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc-scapi.util-module.html create mode 100644 python_apps/soundcloud-api/docs/api/toc.html create mode 100644 python_apps/soundcloud-api/oauth/__init__.py create mode 100644 python_apps/soundcloud-api/oauth/example/client.py create mode 100644 python_apps/soundcloud-api/oauth/example/server.py create mode 100644 python_apps/soundcloud-api/oauth/oauth.py create mode 100644 python_apps/soundcloud-api/scapi/MultipartPostHandler.py create mode 100644 python_apps/soundcloud-api/scapi/__init__.py create mode 100644 python_apps/soundcloud-api/scapi/authentication.py create mode 100644 python_apps/soundcloud-api/scapi/config.py create mode 100644 python_apps/soundcloud-api/scapi/json.py create mode 100644 python_apps/soundcloud-api/scapi/tests/__init__.py create mode 100644 python_apps/soundcloud-api/scapi/tests/knaster.mp3 create mode 100644 python_apps/soundcloud-api/scapi/tests/scapi_tests.py create mode 100644 python_apps/soundcloud-api/scapi/tests/spam.jpg create mode 100644 python_apps/soundcloud-api/scapi/tests/test_connect.py create mode 100644 python_apps/soundcloud-api/scapi/tests/test_oauth.py create mode 100644 python_apps/soundcloud-api/scapi/util.py create mode 100644 python_apps/soundcloud-api/setup.py create mode 100644 python_apps/soundcloud-api/test.ini create mode 100644 tests/application/controllers/RecorderControllerTest.php diff --git a/.zfproject.xml b/.zfproject.xml index cffca41f9..c9da28565 100644 --- a/.zfproject.xml +++ b/.zfproject.xml @@ -100,6 +100,10 @@ + + + + @@ -325,6 +329,12 @@ + + + + + + @@ -370,6 +380,7 @@ + diff --git a/LICENSE_3RD_PARTY b/LICENSE_3RD_PARTY index e616578a1..9a89df2d2 100644 --- a/LICENSE_3RD_PARTY +++ b/LICENSE_3RD_PARTY @@ -28,6 +28,10 @@ Linked code: - Note: Only used for development, not needed to run Airtime. - License: LGPLv3 + * Soundcloud php api wrapper + - https://github.com/mptre/php-soundcloud/blob/master/Services/Soundcloud.php + - License: MIT + ---------------- Non-linked code: ---------------- diff --git a/application/configs/ACL.php b/application/configs/ACL.php index e6fd9c72e..26f1905b3 100644 --- a/application/configs/ACL.php +++ b/application/configs/ACL.php @@ -21,7 +21,8 @@ $ccAcl->add(new Zend_Acl_Resource('library')) ->add(new Zend_Acl_Resource('nowplaying')) ->add(new Zend_Acl_Resource('search')) ->add(new Zend_Acl_Resource('dashboard')) - ->add(new Zend_Acl_Resource('preference')); + ->add(new Zend_Acl_Resource('preference')) + ->add(new Zend_Acl_Resource('recorder')); /** Creating permissions */ $ccAcl->allow('G', 'index') @@ -29,13 +30,13 @@ $ccAcl->allow('G', 'index') ->allow('G', 'error') ->allow('G', 'nowplaying') ->allow('G', 'api') + ->allow('G', 'recorder') ->allow('G', 'schedule') ->allow('G', 'dashboard') ->allow('H', 'library') ->allow('H', 'search') ->allow('H', 'plupload') ->allow('H', 'playlist') - ->allow('H', 'sideplaylist') ->allow('A', 'user') ->allow('A', 'preference'); diff --git a/application/controllers/RecorderController.php b/application/controllers/RecorderController.php new file mode 100644 index 000000000..1f33beb93 --- /dev/null +++ b/application/controllers/RecorderController.php @@ -0,0 +1,32 @@ +_helper->getHelper('contextSwitch'); + $ajaxContext->addActionContext('get-show-schedule', 'json') + ->initContext(); + } + + public function indexAction() + { + // action body + } + + public function getShowScheduleAction() + { + //$from = $this->_getParam("from"); + //$to = $this->_getParam("to"); + + $today_timestamp = date("Y-m-d H:i:s"); + + $this->view->shows = Show::getShows($today_timestamp, null, $excludeInstance=NULL, $onlyRecord=TRUE); + } + + +} + + + diff --git a/application/controllers/plugins/Acl_plugin.php b/application/controllers/plugins/Acl_plugin.php index b0f0dd029..e3682ba11 100644 --- a/application/controllers/plugins/Acl_plugin.php +++ b/application/controllers/plugins/Acl_plugin.php @@ -110,10 +110,11 @@ class Zend_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract { $controller = strtolower($request->getControllerName()); - if ($controller == 'api'){ - $this->setRoleName("G"); - - } else if (!Zend_Auth::getInstance()->hasIdentity()){ + if ($controller == 'api' || $controller == 'recorder'){ + + $this->setRoleName("G"); + } + else if (!Zend_Auth::getInstance()->hasIdentity()){ if ($controller !== 'login') { diff --git a/application/models/Shows.php b/application/models/Shows.php index c6902fa80..9d62d77ec 100644 --- a/application/models/Shows.php +++ b/application/models/Shows.php @@ -175,15 +175,26 @@ class Show { Show::populateShowUntilLastGeneratedDate($showId); } - public static function getShows($start_timestamp, $end_timestamp, $excludeInstance=NULL) { + public static function getShows($start_timestamp, $end_timestamp, $excludeInstance=NULL, $onlyRecord=FALSE) { global $CC_DBC; $sql = "SELECT starts, ends, show_id, name, description, color, background_color, cc_show_instances.id AS instance_id FROM cc_show_instances - LEFT JOIN cc_show ON cc_show.id = cc_show_instances.show_id - WHERE ((starts >= '{$start_timestamp}' AND starts < '{$end_timestamp}') + LEFT JOIN cc_show ON cc_show.id = cc_show_instances.show_id"; + + //only want shows that are starting at the time or later. + if($onlyRecord) { + + $sql = $sql." WHERE (starts >= '{$start_timestamp}' AND starts < timestamp '{$start_timestamp}' + interval '2 hours')"; + $sql = $sql." AND (record = TRUE)"; + } + else { + + $sql = $sql." WHERE ((starts >= '{$start_timestamp}' AND starts < '{$end_timestamp}') OR (ends > '{$start_timestamp}' AND ends <= '{$end_timestamp}') OR (starts <= '{$start_timestamp}' AND ends >= '{$end_timestamp}'))"; + } + if(isset($excludeInstance)) { foreach($excludeInstance as $instance) { @@ -196,7 +207,6 @@ class Show { } //echo $sql; - return $CC_DBC->GetAll($sql); } diff --git a/application/views/helpers/SoundCloudLink.php b/application/views/helpers/SoundCloudLink.php new file mode 100644 index 000000000..9f4d321e5 --- /dev/null +++ b/application/views/helpers/SoundCloudLink.php @@ -0,0 +1,22 @@ +getRequest(); + $host = $request->getHttpHost(); + $controller = $request->getControllerName(); + $action = $request->getActionName(); + + $redirectUrl = "http://{$host}/{$controller}/{$action}"; + + $soundcloud = new Services_Soundcloud('2CLCxcSXYzx7QhhPVHN4A', 'pZ7beWmF06epXLHVUP1ufOg2oEnIt9XhE8l8xt0bBs', $redirectUrl); + $authorizeUrl = $soundcloud->getAuthorizeUrl(); + + return $authorizeUrl; + } +} + diff --git a/application/views/scripts/airtime-recorder/index.phtml b/application/views/scripts/airtime-recorder/index.phtml new file mode 100644 index 000000000..0a04fb79c --- /dev/null +++ b/application/views/scripts/airtime-recorder/index.phtml @@ -0,0 +1 @@ +

View script for controller AirtimeRecorder and script/action name index
\ No newline at end of file diff --git a/application/views/scripts/recorder/get-show-schedule.phtml b/application/views/scripts/recorder/get-show-schedule.phtml new file mode 100644 index 000000000..72a5d839b --- /dev/null +++ b/application/views/scripts/recorder/get-show-schedule.phtml @@ -0,0 +1 @@ +

View script for controller Recorder and script/action name getShowSchedule
\ No newline at end of file diff --git a/application/views/scripts/recorder/index.phtml b/application/views/scripts/recorder/index.phtml new file mode 100644 index 000000000..60ca18d3a --- /dev/null +++ b/application/views/scripts/recorder/index.phtml @@ -0,0 +1 @@ +

View script for controller Recorder and script/action name index
\ No newline at end of file diff --git a/library/soundcloud-api/README.md b/library/soundcloud-api/README.md new file mode 100644 index 000000000..68df3a21a --- /dev/null +++ b/library/soundcloud-api/README.md @@ -0,0 +1,114 @@ +# SoundCloud PHP API Wrapper + +## Introduction + +A wrapper for the SoundCloud API written in PHP with support for authentication using [OAuth 2.0](http://oauth.net/2/). + +The wrapper got a real overhaul with version 2.0. The current version was written with [PEAR](http://pear.php.net/) in mind and can easily by distributed as a PEAR package. + +## Getting started + +Check out the [getting started](https://github.com/mptre/php-soundcloud/wiki/OAuth-2) wiki entry for further reference on how to get started. Also make sure to check out the [demo application](https://github.com/mptre/ci-soundcloud) for some example code. + + +## Examples + +The wrapper includes convenient methods used to perform HTTP requests on behalf of the authenticated user. Below you'll find a few quick examples. + +Ofcourse you need to handle the authentication first before being able to request and modify protect resources as demonstrated below. Therefor I refer to the [demo application](https://github.com/mptre/ci-soundcloud) which got some example code on how to handle authentication. + +### GET + +
try {
+    $response = json_decode($soundcloud->get('me'), true);
+} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+    exit($e->getMessage());
+}
+ +### POST + +
$comment = <<<EOH
+<comment>
+    <body>Yeah!</body>
+</comment>
+EOH;
+
+try {
+    $response = json_decode(
+        $soundcloud->post(
+            'tracks/1/comments',
+            $comment,
+            array(CURLOPT_HTTPHEADER => array('Content-Type: application/xml'))
+        ),
+        true
+    );
+} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+    exit($e->getMessage());
+}
+ +### PUT + +
$track = <<<EOH
+<track>
+    <downloadable>true</downloadable>
+</track>
+EOH;
+
+try {
+    $response = json_decode(
+        $soundcloud->put(
+            'tracks/1',
+            $track,
+            array(CURLOPT_HTTPHEADER => array('Content-Type: application/xml'))
+        ),
+        true
+    );
+} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+    exit($e->getMessage());
+}
+ +### DELETE + +
try {
+    $response = json_decode($soundcloud->delete('tracks/1'), true);
+} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+    exit($e->getMessage());
+}
+ +### DOWNLOAD TRACK + +
try {
+    $track = $soundcloud->download(1337);
+} catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) {
+    exit($e->getMessage());
+}
+
+// do something clever with $track. Save to file perhaps?
+ +## Feedback and questions + +Found a bug or missing a feature? Don't hesitate to create a new issue here on GitHub. Or contact me [directly](https://github.com/mptre). + +Also make sure to check out the official [documentation](https://github.com/soundcloud/api/wiki/) and the join [Google Group](https://groups.google.com/group/soundcloudapi?pli=1) in order to stay updated. + +## License + +Copyright (c) 2011 Anton Lindqvist + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/soundcloud-api/Services/Soundcloud.php b/library/soundcloud-api/Services/Soundcloud.php new file mode 100644 index 000000000..531380151 --- /dev/null +++ b/library/soundcloud-api/Services/Soundcloud.php @@ -0,0 +1,713 @@ + + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud { + + /** + * Custom cURL option. + * + * @access public + * + * @var integer + */ + const CURLOPT_OAUTH_TOKEN = 173; + + /** + * Access token returned by the service provider after a successful authentication. + * + * @access private + * + * @var string + */ + private $_accessToken; + + /** + * Version of the API to use. + * + * @access private + * + * @var integer + */ + private static $_apiVersion = 1; + + /** + * Supported audio MIME types. + * + * @access private + * + * @var array + */ + private static $_audioMimeTypes = array( + 'aac' => 'video/mp4', + 'aiff' => 'audio/x-aiff', + 'flac' => 'audio/flac', + 'mp3' => 'audio/mpeg', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/x-wav' + ); + + /** + * OAuth client id. + * + * @access private + * + * @var string + */ + private $_clientId; + + /** + * OAuth client secret. + * + * @access private + * + * @var string + */ + private $_clientSecret; + + /** + * Development mode. + * + * @access private + * + * @var boolean + */ + private $_development; + + /** + * Available API domains. + * + * @access private + * + * @var array + */ + private static $_domains = array( + 'development' => 'sandbox-soundcloud.com', + 'production' => 'soundcloud.com' + ); + + /** + * HTTP response body from the last request. + * + * @access private + * + * @var string + */ + private $_lastHttpResponseBody; + + /** + * HTTP response code from the last request. + * + * @access private + * + * @var integer + */ + private $_lastHttpResponseCode; + + /** + * HTTP response headers from last request. + * + * @access private + * + * @var array + */ + private $_lastHttpResponseHeaders; + + /** + * OAuth paths. + * + * @access private + * + * @var array + */ + private static $_paths = array( + 'authorize' => 'connect', + 'access_token' => 'oauth2/token', + ); + + /** + * OAuth redirect uri. + * + * @access private + * + * @var string + */ + private $_redirectUri; + + /** + * API response format MIME type. + * + * @access private + * + * @var string + */ + private $_requestFormat; + + /** + * Available response formats. + * + * @access private + * + * @var array + */ + private static $_responseFormats = array( + '*' => '*/*', + 'json' => 'application/json', + 'xml' => 'application/xml' + ); + + /** + * HTTP user agent. + * + * @access private + * + * @var string + */ + private static $_userAgent = 'PHP-SoundCloud'; + + /** + * Class version. + * + * @var string + */ + public $version; + + /** + * Constructor. + * + * @param string $clientId OAuth client id + * @param string $clientSecret OAuth client secret + * @param string $redirectUri OAuth redirect uri + * @param boolean $development Sandbox mode + * + * @throws Services_Soundcloud_Missing_Client_Id_Exception when missing client id + * @return void + */ + function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) { + if (empty($clientId)) { + throw new Services_Soundcloud_Missing_Client_Id_Exception(); + } + + $this->_clientId = $clientId; + $this->_clientSecret = $clientSecret; + $this->_redirectUri = $redirectUri; + $this->_development = $development; + $this->_responseFormat = self::$_responseFormats['json']; + $this->version = Services_Soundcloud_Version::get(); + } + + /** + * Get authorization URL. + * + * @param array $params Optional query string parameters + * + * @return string + * @see Soundcloud::_buildUrl() + */ + function getAuthorizeUrl($params = array()) { + $defaultParams = array( + 'client_id' => $this->_clientId, + 'redirect_uri' => $this->_redirectUri, + 'response_type' => 'code' + ); + $params = array_merge($defaultParams, $params); + + return $this->_buildUrl(self::$_paths['authorize'], $params, false); + } + + /** + * Get access token URL. + * + * @param array $params Optional query string parameters + * + * @return string + * @see Soundcloud::_buildUrl() + */ + function getAccessTokenUrl($params = array()) { + return $this->_buildUrl(self::$_paths['access_token'], $params, false); + } + + /** + * Retrieve access token. + * + * @param string $code OAuth code returned from the service provider + * @param array $postData Optional post data + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_getAccessToken() + */ + function accessToken($code, $postData = array(), $curlOptions = array()) { + $defaultPostData = array( + 'code' => $code, + 'client_id' => $this->_clientId, + 'client_secret' => $this->_clientSecret, + 'redirect_uri' => $this->_redirectUri, + 'grant_type' => 'authorization_code' + ); + $postData = array_merge($defaultPostData, $postData); + + return $this->_getAccessToken($postData, $curlOptions); + } + + /** + * Refresh access token. + * + * @param string $refreshToken + * @param array $postData Optional post data + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_getAccessToken() + */ + function accessTokenRefresh($refreshToken, $postData = array(), $curlOptions = array()) { + $defaultPostData = array( + 'refresh_token' => $refreshToken, + 'client_id' => $this->_clientId, + 'client_secret' => $this->_clientSecret, + 'redirect_uri' => $this->_redirectUri, + 'grant_type' => 'refresh_token' + ); + $postData = array_merge($defaultPostData, $postData); + + return $this->_getAccessToken($postData, $curlOptions); + } + + /** + * Get access token. + * + * @return mixed + */ + function getAccessToken() { + return $this->_accessToken; + } + + /** + * Get API version. + * + * @return integer + */ + function getApiVersion() { + return self::$_apiVersion; + } + + /** + * Get the corresponding MIME type for a given file extension. + * + * @param string $extension + * + * @return string + * @throws Services_Soundcloud_Unsupported_Audio_Format_Exception if the format is unsupported + */ + function getAudioMimeType($extension) { + if (array_key_exists($extension, self::$_audioMimeTypes)) { + return self::$_audioMimeTypes[$extension]; + } else { + throw new Services_Soundcloud_Unsupported_Audio_Format_Exception(); + } + } + + /** + * Get development mode. + * + * @return boolean + */ + function getDevelopment() { + return $this->_development; + } + + /** + * Get HTTP response header. + * + * @param string $header Name of the header + * + * @return mixed + */ + function getHttpHeader($header) { + if (is_array($this->_lastHttpResponseHeaders) + && array_key_exists($header, $this->_lastHttpResponseHeaders) + ) { + return $this->_lastHttpResponseHeaders[$header]; + } else { + return false; + } + } + + /** + * Get redirect uri. + * + * @return mixed + */ + function getRedirectUri() { + return $this->_redirectUri; + } + + /** + * Get response format. + * + * @return string + */ + function getResponseFormat() { + return $this->_responseFormat; + } + + /** + * Set access token. + * + * @param string $accessToken + * + * @return object + */ + function setAccessToken($accessToken) { + $this->_accessToken = $accessToken; + + return $this; + } + + /** + * Set redirect uri. + * + * @param string $redirectUri + * + * @return object + */ + function setRedirectUri($redirectUri) { + $this->_redirectUri = $redirectUri; + + return $this; + } + + /** + * Set response format. + * + * @param string $format Could either be xml or json + * + * @throws Services_Soundcloud_Unsupported_Response_Format_Exception if the given response format isn't supported + * @return object + */ + function setResponseFormat($format) { + if (array_key_exists($format, self::$_responseFormats)) { + $this->_responseFormat = self::$_responseFormats[$format]; + } else { + throw new Services_Soundcloud_Unsupported_Response_Format_Exception(); + } + + return $this; + } + + /** + * Set development mode. + * + * @param boolean $development + * + * @return object + */ + function setDevelopment($development) { + $this->_development = $development; + + return $this; + } + + /** + * Send a GET HTTP request. + * + * @param string $path URI to request + * @param array $params Optional query string parameters + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_request() + */ + function get($path, $params = array(), $curlOptions = array()) { + $url = $this->_buildUrl($path, $params); + + return $this->_request($url, $curlOptions); + } + + /** + * Send a POST HTTP request. + * + * @param string $path URI to request + * @param array $postData Optional post data + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_request() + */ + function post($path, $postData = array(), $curlOptions = array()) { + $url = $this->_buildUrl($path); + $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData); + $options += $curlOptions; + + return $this->_request($url, $options); + } + + /** + * Send a PUT HTTP request. + * + * @param string $path URI to request + * @param array $postData Optional post data + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_request() + */ + function put($path, $postData, $curlOptions = array()) { + $url = $this->_buildUrl($path); + $options = array( + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => $postData + ); + $options += $curlOptions; + + return $this->_request($url, $options); + } + + /** + * Send a DELETE HTTP request. + * + * @param string $path URI to request + * @param array $params Optional query string parameters + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_request() + */ + function delete($path, $params = array(), $curlOptions = array()) { + $url = $this->_buildUrl($path, $params); + $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE'); + $options += $curlOptions; + + return $this->_request($url, $options); + } + + /** + * Download track. + * + * @param integer $trackId + * @param array Optional query string parameters + * @param array $curlOptions Optional cURL options + * + * @return mixed + * @see Soundcloud::_request() + */ + function download($trackId, $params = array(), $curlOptions = array()) { + $lastResponseFormat = array_pop( + preg_split('/\//', $this->getResponseFormat()) + ); + $defaultParams = array('oauth_token' => $this->getAccessToken()); + $defaultCurlOptions = array( + CURLOPT_FOLLOWLOCATION => true, + self::CURLOPT_OAUTH_TOKEN => false + ); + $url = $this->_buildUrl( + 'tracks/' . $trackId . '/download', + array_merge($defaultParams, $params) + ); + $options = $defaultCurlOptions + $curlOptions; + + $this->setResponseFormat('*'); + + $response = $this->_request($url, $options); + + // rollback to the previously defined response format. + $this->setResponseFormat($lastResponseFormat); + + return $response; + } + + /** + * Construct default HTTP headers including response format and authorization. + * + * @param boolean Include access token or not + * + * @return array $headers + */ + protected function _buildDefaultHeaders($includeAccessToken = true) { + $headers = array(); + + if ($this->_responseFormat) { + array_push($headers, 'Accept: ' . $this->_responseFormat); + } + + if ($includeAccessToken && $this->_accessToken) { + array_push($headers, 'Authorization: OAuth ' . $this->_accessToken); + } + + return $headers; + } + + /** + * Construct a URL. + * + * @param string $path Relative or absolute URI + * @param array $params Optional query string parameters + * @param boolean $includeVersion Include API version + * + * @return string $url + */ + protected function _buildUrl($path, $params = null, $includeVersion = true) { + if (preg_match('/^https?\:\/\//', $path)) { + $url = $path; + } else { + $url = 'https://'; + $url .= (!preg_match('/connect/', $path)) ? 'api.' : ''; + $url .= ($this->_development) + ? self::$_domains['development'] + : self::$_domains['production']; + $url .= '/'; + $url .= ($includeVersion) ? 'v' . self::$_apiVersion . '/' : ''; + $url .= $path; + } + + $url .= (count($params)) ? '?' . http_build_query($params) : ''; + + return $url; + } + + /** + * Retrieve access token. + * + * @param array $postData Post data + * @param array $curlOptions Optional cURL options + * + * @return mixed + */ + protected function _getAccessToken($postData, $curlOptions = array()) { + $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData); + $options += $curlOptions; + $response = json_decode( + $this->_request($this->getAccessTokenUrl(), $options), + true + ); + + if (array_key_exists('access_token', $response)) { + $this->_accessToken = $response['access_token']; + + return $response; + } else { + return false; + } + } + + /** + * Get HTTP user agent. + * + * @access protected + * + * @return string + */ + protected function _getUserAgent() { + return self::$_userAgent . '/' . $this->version; + } + + /** + * Parse HTTP response headers. + * + * @param string $headers + * + * @return array + */ + protected function _parseHttpHeaders($headers) { + $headers = preg_split('/\n/', trim($headers)); + $parsedHeaders = array(); + + foreach ($headers as $header) { + if (!preg_match('/\:\s/', $header)) { + continue; + } + + list($key, $val) = preg_split('/\:\s/', $header, 2); + $key = str_replace('-', '_', strtolower($key)); + $val = trim($val); + + $parsedHeaders[$key] = $val; + } + + return $parsedHeaders; + } + + /** + * Validates HTTP response code. + * + * @access protected + * + * @return boolean + */ + protected function _validResponseCode($code) { + return (bool)preg_match('/^20[0-9]{1}$/', $code); + } + + /** + * Performs the actual HTTP request using curl. Can be overwritten by extending classes. + * + * @access protected + * + * @param string $url + * @param array $curlOptions Optional cURL options + * + * @throws Services_Soundcloud_Invalid_Http_Response_Code_Exception if the response code isn't valid + * @return mixed + */ + protected function _request($url, $curlOptions = array()) { + $ch = curl_init(); + $options = array( + CURLOPT_URL => $url, + CURLOPT_HEADER => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => $this->_getUserAgent() + ); + $options += $curlOptions; + + if (array_key_exists(self::CURLOPT_OAUTH_TOKEN, $options)) { + $includeAccessToken = $options[self::CURLOPT_OAUTH_TOKEN]; + unset($options[self::CURLOPT_OAUTH_TOKEN]); + } else { + $includeAccessToken = true; + } + + if (array_key_exists(CURLOPT_HTTPHEADER, $options)) { + $options[CURLOPT_HTTPHEADER] = array_merge( + $this->_buildDefaultHeaders(), + $curlOptions[CURLOPT_HTTPHEADER] + ); + } else { + $options[CURLOPT_HTTPHEADER] = $this->_buildDefaultHeaders($includeAccessToken); + } + + curl_setopt_array($ch, $options); + + $data = curl_exec($ch); + $info = curl_getinfo($ch); + + curl_close($ch); + + $this->_lastHttpResponseHeaders = $this->_parseHttpHeaders( + substr($data, 0, $info['header_size']) + ); + $this->_lastHttpResponseBody = substr($data, $info['header_size']); + $this->_lastHttpResponseCode = $info['http_code']; + + if ($this->_validResponseCode($this->_lastHttpResponseCode)) { + return $this->_lastHttpResponseBody; + } else { + throw new Services_Soundcloud_Invalid_Http_Response_Code_Exception( + null, + 0, + $this->_lastHttpResponseBody, + $this->_lastHttpResponseCode + ); + } + } + +} diff --git a/library/soundcloud-api/Services/Soundcloud/Exception.php b/library/soundcloud-api/Services/Soundcloud/Exception.php new file mode 100644 index 000000000..76e3370ad --- /dev/null +++ b/library/soundcloud-api/Services/Soundcloud/Exception.php @@ -0,0 +1,146 @@ + + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Missing_Client_Id_Exception extends Exception { + + /** + * Default message. + * + * @access protected + * + * @var string + */ + protected $message = 'All requests must include a consumer key. Referred to as client_id in OAuth2.'; + +} + +/** + * Soundcloud invalid HTTP response code exception. + * + * @category Services + * @package Services_Soundcloud + * @author Anton Lindqvist + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Invalid_Http_Response_Code_Exception extends Exception { + + /** + * HTTP response body. + * + * @access protected + * + * @var string + */ + protected $httpBody; + + /** + * HTTP response code. + * + * @access protected + * + * @var integer + */ + protected $httpCode; + + /** + * Default message. + * + * @access protected + * + * @var string + */ + protected $message = 'The requested URL responded with HTTP code %d.'; + + /** + * Constructor. + * + * @param string $message + * @param string $code + * @param string $httpBody + * @param integer $httpCode + * + * @return void + */ + function __construct($message = null, $code = 0, $httpBody = null, $httpCode = 0) { + $this->httpBody = $httpBody; + $this->httpCode = $httpCode; + $message = sprintf($this->message, $httpCode); + + parent::__construct($message, $code); + } + + /** + * Get HTTP response body. + * + * @return mixed + */ + function getHttpBody() { + return $this->httpBody; + } + + /** + * Get HTTP response code. + * + * @return mixed + */ + function getHttpCode() { + return $this->httpCode; + } + +} + +/** + * Soundcloud unsupported response format exception. + * + * @category Services + * @package Services_Soundcloud + * @author Anton Lindqvist + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Unsupported_Response_Format_Exception extends Exception { + + /** + * Default message. + * + * @access protected + * + * @var string + */ + protected $message = 'The given response format is unsupported.'; + +} + +/** + * Soundcloud unsupported audio format exception. + * + * @category Services + * @package Services_Soundcloud + * @author Anton Lindqvist + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Unsupported_Audio_Format_Exception extends Exception { + + /** + * Default message. + * + * @access protected + * + * @var string + */ + protected $message = 'The given audio format is unsupported.'; + +} diff --git a/library/soundcloud-api/Services/Soundcloud/Version.php b/library/soundcloud-api/Services/Soundcloud/Version.php new file mode 100644 index 000000000..6ee964a23 --- /dev/null +++ b/library/soundcloud-api/Services/Soundcloud/Version.php @@ -0,0 +1,22 @@ + + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Version { + + const MAJOR = 2; + const MINOR = 1; + const PATCH = 1; + + public static function get() { + return implode('.', array(self::MAJOR, self::MINOR, self::PATCH)); + } + +} diff --git a/library/soundcloud-api/tests/Soundcloud_Test.php b/library/soundcloud-api/tests/Soundcloud_Test.php new file mode 100644 index 000000000..cfc3e9c4a --- /dev/null +++ b/library/soundcloud-api/tests/Soundcloud_Test.php @@ -0,0 +1,310 @@ +soundcloud = new Services_Soundcloud_Expose( + '1337', + '1337', + 'http://soundcloud.local/callback' + ); + } + + function tearDown() { + $this->soundcloud = null; + } + + function testVersionFormat() { + $this->assertRegExp( + '/^[0-9]+\.[0-9]+\.[0-9]+$/', + Services_Soundcloud_Version::get() + ); + } + + function testGetUserAgent() { + $this->assertRegExp( + '/^PHP\-SoundCloud\/[0-9]+\.[0-9]+\.[0-9]+$/', + $this->soundcloud->getUserAgent() + ); + } + + function testApiVersion() { + $this->assertEquals(1, $this->soundcloud->getApiVersion()); + } + + function testGetAudioMimeTypes() { + $supportedExtensions = array( + 'aac' => 'video/mp4', + 'aiff' => 'audio/x-aiff', + 'flac' => 'audio/flac', + 'mp3' => 'audio/mpeg', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/x-wav' + ); + $unsupportedExtensions = array('gif', 'html', 'jpg', 'mp4', 'xml', 'xspf'); + + foreach ($supportedExtensions as $extension => $mimeType) { + $this->assertEquals( + $mimeType, + $this->soundcloud->getAudioMimeType($extension) + ); + } + + foreach ($unsupportedExtensions as $extension => $mimeType) { + $this->setExpectedException('Services_Soundcloud_Unsupported_Audio_Format_Exception'); + + $this->soundcloud->getAudioMimeType($extension); + } + } + + function testGetAuthorizeUrl() { + $this->assertEquals( + 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code', + $this->soundcloud->getAuthorizeUrl() + ); + } + + function testGetAuthorizeUrlWithCustomQueryParameters() { + $this->assertEquals( + 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar', + $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar')) + ); + + $this->assertEquals( + 'https://soundcloud.com/connect?client_id=1337&redirect_uri=http%3A%2F%2Fsoundcloud.local%2Fcallback&response_type=code&foo=bar&bar=foo', + $this->soundcloud->getAuthorizeUrl(array('foo' => 'bar', 'bar' => 'foo')) + ); + } + + function testGetAccessTokenUrl() { + $this->assertEquals( + 'https://api.soundcloud.com/oauth2/token', + $this->soundcloud->getAccessTokenUrl() + ); + } + + function testSetAccessToken() { + $this->soundcloud->setAccessToken('1337'); + + $this->assertEquals('1337', $this->soundcloud->getAccessToken()); + } + + function testSetDevelopment() { + $this->soundcloud->setDevelopment(true); + + $this->assertTrue($this->soundcloud->getDevelopment()); + } + + function testSetRedirectUri() { + $this->soundcloud->setRedirectUri('http://soundcloud.local/callback'); + + $this->assertEquals( + 'http://soundcloud.local/callback', + $this->soundcloud->getRedirectUri() + ); + } + + function testDefaultResponseFormat() { + $this->assertEquals( + 'application/json', + $this->soundcloud->getResponseFormat() + ); + } + + function testSetResponseFormatHtml() { + $this->setExpectedException('Services_Soundcloud_Unsupported_Response_Format_Exception'); + + $this->soundcloud->setResponseFormat('html'); + } + + function testSetResponseFormatAll() { + $this->soundcloud->setResponseFormat('*'); + + $this->assertEquals( + '*/*', + $this->soundcloud->getResponseFormat() + ); + } + + function testSetResponseFormatJson() { + $this->soundcloud->setResponseFormat('json'); + + $this->assertEquals( + 'application/json', + $this->soundcloud->getResponseFormat() + ); + } + + function testSetResponseFormatXml() { + $this->soundcloud->setResponseFormat('xml'); + + $this->assertEquals( + 'application/xml', + $this->soundcloud->getResponseFormat() + ); + } + + function testResponseCodeSuccess() { + $this->assertTrue($this->soundcloud->validResponseCode(200)); + } + + function testResponseCodeRedirect() { + $this->assertFalse($this->soundcloud->validResponseCode(301)); + } + + function testResponseCodeClientError() { + $this->assertFalse($this->soundcloud->validResponseCode(400)); + } + + function testResponseCodeServerError() { + $this->assertFalse($this->soundcloud->validResponseCode(500)); + } + + function testBuildDefaultHeaders() { + $this->assertEquals( + array('Accept: application/json'), + $this->soundcloud->buildDefaultHeaders() + ); + } + + function testBuildDefaultHeadersWithAccessToken() { + $this->soundcloud->setAccessToken('1337'); + + $this->assertEquals( + array('Accept: application/json', 'Authorization: OAuth 1337'), + $this->soundcloud->buildDefaultHeaders() + ); + } + + function testBuildUrl() { + $this->assertEquals( + 'https://api.soundcloud.com/v1/me', + $this->soundcloud->buildUrl('me') + ); + } + + function testBuildUrlWithQueryParameters() { + $this->assertEquals( + 'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep', + $this->soundcloud->buildUrl( + 'tracks', + array('q' => 'rofl dubstep') + ) + ); + + $this->assertEquals( + 'https://api.soundcloud.com/v1/tracks?q=rofl+dubstep&filter=public', + $this->soundcloud->buildUrl( + 'tracks', + array('q' => 'rofl dubstep', 'filter' => 'public') + ) + ); + } + + function testBuildUrlWithDevelopmentDomain() { + $this->soundcloud->setDevelopment(true); + + $this->assertEquals( + 'https://api.sandbox-soundcloud.com/v1/me', + $this->soundcloud->buildUrl('me') + ); + } + + function testBuildUrlWithoutApiVersion() { + $this->assertEquals( + 'https://api.soundcloud.com/me', + $this->soundcloud->buildUrl('me', null, false) + ); + } + + function testBuildUrlWithAbsoluteUrl() { + $this->assertEquals( + 'https://api.soundcloud.com/me', + $this->soundcloud->buildUrl('https://api.soundcloud.com/me') + ); + } + + /** + * @dataProvider dataProviderHttpHeaders + */ + function testParseHttpHeaders($rawHeaders, $expectedHeaders) { + $parsedHeaders = $this->soundcloud->parseHttpHeaders($rawHeaders); + + foreach ($parsedHeaders as $key => $val) { + $this->assertEquals($val, $expectedHeaders[$key]); + } + } + + function testSoundcloudMissingConsumerKeyException() { + $this->setExpectedException('Services_Soundcloud_Missing_Client_Id_Exception'); + + $soundcloud = new Services_Soundcloud('', ''); + } + + function testSoundcloudInvalidHttpResponseCodeException() { + $this->setExpectedException('Services_Soundcloud_Invalid_Http_Response_Code_Exception'); + + $this->soundcloud->get('me'); + } + + /** + * @dataProvider dataProviderSoundcloudInvalidHttpResponseCode + */ + function testSoundcloudInvalidHttpResponseCode($expectedHeaders) { + try { + $this->soundcloud->get('me'); + } catch (Services_Soundcloud_Invalid_Http_Response_Code_Exception $e) { + $this->assertEquals( + '{"error":"401 - Unauthorized"}', + $e->getHttpBody() + ); + + $this->assertEquals(401, $e->getHttpCode()); + + foreach ($expectedHeaders as $key => $val) { + $this->assertEquals( + $val, + $this->soundcloud->getHttpHeader($key) + ); + } + } + } + + static function dataProviderHttpHeaders() { + $rawHeaders = << 'Wed, 17 Nov 2010 15:39:52 GMT', + 'cache_control' => 'public', + 'content_type' => 'text/html; charset=utf-8', + 'content_encoding' => 'gzip', + 'server' => 'foobar', + 'content_length' => '1337' + ); + + return array(array($rawHeaders, $expectedHeaders)); + } + + static function dataProviderSoundcloudInvalidHttpResponseCode() { + $expectedHeaders = array( + 'server' => 'nginx', + 'content_type' => 'application/json; charset=utf-8', + 'connection' => 'keep-alive', + 'cache_control' => 'no-cache', + 'content_length' => '30' + ); + + return array(array($expectedHeaders)); + } + +} diff --git a/library/soundcloud-api/tests/Soundcloud_Test_Helper.php b/library/soundcloud-api/tests/Soundcloud_Test_Helper.php new file mode 100644 index 000000000..2959d0813 --- /dev/null +++ b/library/soundcloud-api/tests/Soundcloud_Test_Helper.php @@ -0,0 +1,94 @@ + + * @copyright 2010 Anton Lindqvist + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link http://github.com/mptre/php-soundcloud + */ +class Services_Soundcloud_Expose extends Services_Soundcloud { + + /** + * Class constructor. See parent constructor for further reference. + * + * @param string $clientId Application client id + * @param string $clientSecret Application client secret + * @param string $redirectUri Application redirect uri + * @param boolean $development Sandbox mode + * + * @return void + * @see Soundcloud + */ + function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) { + parent::__construct($clientId, $clientSecret, $redirectUri, $development); + } + + /** + * Construct default http headers including response format and authorization. + * + * @return array + * @see Soundcloud::_buildDefaultHeaders() + */ + function buildDefaultHeaders() { + return $this->_buildDefaultHeaders(); + } + + /** + * Construct a url. + * + * @param string $path Relative or absolute uri + * @param array $params Optional query string parameters + * @param boolean $includeVersion Include the api version + * + * @return string + * @see Soundcloud::_buildUrl() + */ + function buildUrl($path, $params = null, $includeVersion = true) { + return $this->_buildUrl($path, $params, $includeVersion); + } + + /** + * Get http user agent. + * + * @return string + * @see Soundcloud::_getUserAgent() + */ + function getUserAgent() { + return $this->_getUserAgent(); + } + + /** + * Parse HTTP response headers. + * + * @param string $headers + * + * @return array + * @see Soundcloud::_parseHttpHeaders() + */ + function parseHttpHeaders($headers) { + return $this->_parseHttpHeaders($headers); + } + + /** + * Validates http response code. + * + * @return boolean + * @see Soundcloud::_validResponseCode() + */ + function validResponseCode($code) { + return $this->_validResponseCode($code); + } + +} diff --git a/python_apps/show-recorder/config.cfg b/python_apps/show-recorder/config.cfg new file mode 100644 index 000000000..86cf8d194 --- /dev/null +++ b/python_apps/show-recorder/config.cfg @@ -0,0 +1,8 @@ +# Hostname +base_url = 'http://campcaster.dev/' + +# URL to get the version number of the server API +show_schedule_url = 'Recorder/get-show-schedule/format/json' + +# base path to store recordered shows at +base_recorded_files = '/home/naomi/Music/' diff --git a/python_apps/show-recorder/testrecordscript.py b/python_apps/show-recorder/testrecordscript.py new file mode 100644 index 000000000..43bed928a --- /dev/null +++ b/python_apps/show-recorder/testrecordscript.py @@ -0,0 +1,118 @@ +#!/usr/local/bin/python +import urllib +import logging +import json +import time +import datetime + +from eci import * +from configobj import ConfigObj +import subprocess + +# loading config file +try: + config = ConfigObj('config.cfg') +except Exception, e: + print 'Error loading config file: ', e + sys.exit() + +shows_to_record = {} + + +def record_show(filelength, filename, filetype="mp3"): + + length = str(filelength)+".0" + filename = filename.replace(" ", "-") + filepath = "%s%s.%s" % (config["base_recorded_files"], filename, filetype) + + e = ECI() + + e("cs-add play_chainsetup") + e("c-add 1st_chain") + e("ai-add alsa") + e("ao-add "+filepath) + e("cs-set-length "+length) + e("cop-select 1") + e("cs-connect") + e("start") + + while 1: + time.sleep(1) + + if e("engine-status") != "running": + break + + e("stop") + e("cs-disconnect") + + return filepath + + +def getDateTimeObj(time): + + timeinfo = time.split(" ") + date = timeinfo[0].split("-") + time = timeinfo[1].split(":") + + return datetime.datetime(int(date[0]), int(date[1]), int(date[2]), int(time[0]), int(time[1]), int(time[2])) + +def process_shows(shows): + + for show in shows: + show_starts = getDateTimeObj(show[u'starts']) + show_end = getDateTimeObj(show[u'ends']) + time_delta = show_end - show_starts + + shows_to_record[show[u'starts']] = time_delta + + +def check_record(): + + tnow = datetime.datetime.now() + sorted_show_keys = sorted(shows_to_record.keys()) + start_time = sorted_show_keys[0] + next_show = getDateTimeObj(start_time) + + #print tnow, next_show + + #tnow = getDateTimeObj("2011-03-04 16:00:00") + #next_show = getDateTimeObj("2011-03-04 16:00:01") + + delta = next_show - tnow + + if delta <= datetime.timedelta(seconds=60): + time.sleep(delta.seconds) + + show_length = shows_to_record[start_time] + filepath = record_show(show_length.seconds, start_time) + #filepath = record_show(10, "2011-03-04 16:00:00") + + command = "%s -c %s" %("../../utils/airtime-import", filepath) + subprocess.call([command],shell=True) + + +def get_shows(): + + url = config["base_url"] + config["show_schedule_url"] + #url = url.replace("%%from%%", "2011-03-13 20:00:00") + #url = url.replace("%%to%%", "2011-04-17 21:00:00") + + response = urllib.urlopen(url) + data = response.read() + response_json = json.loads(data) + shows = response_json[u'shows'] + print shows + + if len(shows): + process_shows(shows) + check_record() + + +if __name__ == '__main__': + + while True: + get_shows() + time.sleep(30) + + + diff --git a/python_apps/show-recorder/testsoundcloud.py b/python_apps/show-recorder/testsoundcloud.py new file mode 100644 index 000000000..0ba6fd20d --- /dev/null +++ b/python_apps/show-recorder/testsoundcloud.py @@ -0,0 +1,59 @@ +import webbrowser +import scapi + +# the host to connect to. Normally, this +# would be api.soundcloud.com +API_HOST = "api.soundcloud.com" + +# This needs to be the consumer ID you got from +# http://soundcloud.com/settings/applications/new +CONSUMER = "2CLCxcSXYzx7QhhPVHN4A" +# This needs to be the consumer secret password you got from +# http://soundcloud.com/settings/applications/new +CONSUMER_SECRET = "pZ7beWmF06epXLHVUP1ufOg2oEnIt9XhE8l8xt0bBs" + +# first, we create an OAuthAuthenticator that only knows about consumer +# credentials. This is done so that we can get an request-token as +# first step. +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + None, + None) + +# The connector works with the authenticator to create and sign the requests. It +# has some helper-methods that allow us to do the OAuth-dance. +connector = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator) + +# First step is to get a request-token, and to let the user authorize that +# via the browser. +token, secret = connector.fetch_request_token() +authorization_url = connector.get_request_token_authorization_url(token) +webbrowser.open(authorization_url) +oauth_verifier = raw_input("please enter verifier code as seen in the browser:") + +# Now we create a new authenticator with the temporary token & secret we got from +# the request-token. This will give us the access-token +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + +# we need a new connector with the new authenticator! +connector = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) +token, secret = connector.fetch_access_token(oauth_verifier) + + +# now we are finally ready to go - with all four parameters OAuth requires, +# we can setup an authenticator that allows for actual API-calls. +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + +# we pass the connector to a Scope - a Scope is essentially a path in the REST-url-space. +# Without any path-component, it's the root from which we can then query into the +# resources. +root = scapi.Scope(scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)) + +# Hey, nice meeting you! Connected to SoundCloud using OAuth will allow you to access protected resources, like the current user's name. +print "Hello, %s" % root.me().username diff --git a/python_apps/soundcloud-api/AUTHORS b/python_apps/soundcloud-api/AUTHORS new file mode 100644 index 000000000..ae92c1d26 --- /dev/null +++ b/python_apps/soundcloud-api/AUTHORS @@ -0,0 +1,5 @@ +Authors +------- + +Diez B. Roggisch, deets@web.de + diff --git a/python_apps/soundcloud-api/ChangeLog b/python_apps/soundcloud-api/ChangeLog new file mode 100644 index 000000000..9b5bb4679 --- /dev/null +++ b/python_apps/soundcloud-api/ChangeLog @@ -0,0 +1,9 @@ +2009-09-10 Diez Roggisch + + * OAuth 1.0a working + * Query-Parameters for GET-requests to allow e.g. filtering + * Setting file-objects as attributes working. + * share to emails working. + * groups + * downloading/streaming private tracks + diff --git a/python_apps/soundcloud-api/LICENSE b/python_apps/soundcloud-api/LICENSE new file mode 100644 index 000000000..3b473dbfc --- /dev/null +++ b/python_apps/soundcloud-api/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/python_apps/soundcloud-api/README b/python_apps/soundcloud-api/README new file mode 100644 index 000000000..729a8faac --- /dev/null +++ b/python_apps/soundcloud-api/README @@ -0,0 +1,45 @@ +Running tests +============= + +The **SCAPI** comes with a small testsuite. It can be run automatically through either setuptools_ +or nose_. + +Configuring tests +----------------- + +Before you can run the tests, you need to configure them. You do this using the `test.ini` file in the +root of python **SCAPI** workingcopy. + +Running tests through setuptools +-------------------------------- + +You can run the whole testsuite through setuptools_ by doing :: + + host:~/SoundCloudAPI deets$ python setup.py test + +Running tests through nose +-------------------------- + +If you want a more fine-grained control over which tests to run, you can use the `nosetests`-commandline tool. + +Then to run individual tests, you can e.g. do:: + + host:~/SoundCloudAPI deets$ nosetests -s scapi.tests.scapi_tests:SCAPITests.test_setting_permissions + + +See the nose_-website for more options. + + + +.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools +.. _ConfigObj: http://www.voidspace.org.uk/python/configobj.html + + +Creating the API-docs +===================== + +Do:: + epydoc -v --name="SoundCloud API" --html -o docs/api scapi --exclude="os|mimetypes|urllib2|exceptions|mimetools" + + diff --git a/python_apps/soundcloud-api/bootstrap.py b/python_apps/soundcloud-api/bootstrap.py new file mode 100644 index 000000000..c04723c5b --- /dev/null +++ b/python_apps/soundcloud-api/bootstrap.py @@ -0,0 +1,58 @@ +import webbrowser +import scapi + +# the host to connect to. Normally, this +# would be api.soundcloud.com +API_HOST = "api.sandbox-soundcloud.com" + +# This needs to be the consumer ID you got from +# http://soundcloud.com/settings/applications/new +CONSUMER = "gLnhFeUBnBCZF8a6Ngqq7w" +# This needs to be the consumer secret password you got from +# http://soundcloud.com/settings/applications/new +CONSUMER_SECRET = "nbWRdG5X9xUb63l4nIeFYm3nmeVJ2v4s1ROpvRSBvU8" + +# first, we create an OAuthAuthenticator that only knows about consumer +# credentials. This is done so that we can get an request-token as +# first step. +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + None, + None) + +# The connector works with the authenticator to create and sign the requests. It +# has some helper-methods that allow us to do the OAuth-dance. +connector = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator) + +# First step is to get a request-token, and to let the user authorize that +# via the browser. +token, secret = connector.fetch_request_token() +authorization_url = connector.get_request_token_authorization_url(token) +webbrowser.open(authorization_url) +oauth_verifier = raw_input("please enter verifier code as seen in the browser:") + +# Now we create a new authenticator with the temporary token & secret we got from +# the request-token. This will give us the access-token +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + +# we need a new connector with the new authenticator! +connector = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) +token, secret = connector.fetch_access_token(oauth_verifier) + +# now we are finally ready to go - with all four parameters OAuth requires, +# we can setup an authenticator that allows for actual API-calls. +oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + +# we pass the connector to a Scope - a Scope is essentiall a path in the REST-url-space. +# Without any path-component, it's the root from which we can then query into the +# resources. +root = scapi.Scope(scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator)) + +# Hey, nice meeting you! +print "Hello, %s" % root.me().username diff --git a/python_apps/soundcloud-api/docs/api/api-objects.txt b/python_apps/soundcloud-api/docs/api/api-objects.txt new file mode 100644 index 000000000..8f3e21555 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/api-objects.txt @@ -0,0 +1,333 @@ +scapi scapi-module.html +scapi.escape scapi.util-module.html#escape +scapi.USE_PROXY scapi-module.html#USE_PROXY +scapi.REQUEST_TOKEN_URL scapi-module.html#REQUEST_TOKEN_URL +scapi.PROXY scapi-module.html#PROXY +scapi.ACCESS_TOKEN_URL scapi-module.html#ACCESS_TOKEN_URL +scapi.register_classes scapi-module.html#register_classes +scapi.logger scapi-module.html#logger +scapi.AUTHORIZATION_URL scapi-module.html#AUTHORIZATION_URL +scapi.authentication scapi.authentication-module.html +scapi.authentication.USE_DOUBLE_ESCAPE_HACK scapi.authentication-module.html#USE_DOUBLE_ESCAPE_HACK +scapi.authentication.escape scapi.util-module.html#escape +scapi.authentication.logger scapi.authentication-module.html#logger +scapi.config scapi.config-module.html +scapi.json scapi.json-module.html +scapi.json.read scapi.json-module.html#read +scapi.json.write scapi.json-module.html#write +scapi.multidict scapi.multidict-module.html +scapi.tests scapi.tests-module.html +scapi.tests.scapi_tests scapi.tests.scapi_tests-module.html +scapi.tests.scapi_tests.api_logger scapi.tests.scapi_tests-module.html#api_logger +scapi.tests.scapi_tests.logger scapi.tests.scapi_tests-module.html#logger +scapi.tests.test_connect scapi.tests.test_connect-module.html +scapi.tests.test_connect.test_me_having_stress scapi.tests.test_connect-module.html#test_me_having_stress +scapi.tests.test_connect.test_scoped_track_creation scapi.tests.test_connect-module.html#test_scoped_track_creation +scapi.tests.test_connect.test_permissions scapi.tests.test_connect-module.html#test_permissions +scapi.tests.test_connect.CONSUMER_SECRET scapi.tests.test_connect-module.html#CONSUMER_SECRET +scapi.tests.test_connect.CONSUMER scapi.tests.test_connect-module.html#CONSUMER +scapi.tests.test_connect.load_config scapi.tests.test_connect-module.html#load_config +scapi.tests.test_connect.test_upload scapi.tests.test_connect-module.html#test_upload +scapi.tests.test_connect.USER scapi.tests.test_connect-module.html#USER +scapi.tests.test_connect.test_load_config scapi.tests.test_connect-module.html#test_load_config +scapi.tests.test_connect.test_access_token_acquisition scapi.tests.test_connect-module.html#test_access_token_acquisition +scapi.tests.test_connect.test_track_creation scapi.tests.test_connect-module.html#test_track_creation +scapi.tests.test_connect.test_setting_comments scapi.tests.test_connect-module.html#test_setting_comments +scapi.tests.test_connect.test_track_update scapi.tests.test_connect-module.html#test_track_update +scapi.tests.test_connect.SECRET scapi.tests.test_connect-module.html#SECRET +scapi.tests.test_connect.test_contact_add_and_removal scapi.tests.test_connect-module.html#test_contact_add_and_removal +scapi.tests.test_connect.logger scapi.tests.test_connect-module.html#logger +scapi.tests.test_connect.test_connect scapi.tests.test_connect-module.html#test_connect +scapi.tests.test_connect.ROOT scapi.tests.test_connect-module.html#ROOT +scapi.tests.test_connect.test_contact_list scapi.tests.test_connect-module.html#test_contact_list +scapi.tests.test_connect.test_playlists scapi.tests.test_connect-module.html#test_playlists +scapi.tests.test_connect.API_HOST scapi.tests.test_connect-module.html#API_HOST +scapi.tests.test_connect._logger scapi.tests.test_connect-module.html#_logger +scapi.tests.test_connect.TOKEN scapi.tests.test_connect-module.html#TOKEN +scapi.tests.test_connect.test_events scapi.tests.test_connect-module.html#test_events +scapi.tests.test_connect.test_non_global_api scapi.tests.test_connect-module.html#test_non_global_api +scapi.tests.test_connect.test_setting_permissions scapi.tests.test_connect-module.html#test_setting_permissions +scapi.tests.test_connect.PASSWORD scapi.tests.test_connect-module.html#PASSWORD +scapi.tests.test_connect.test_setting_comments_the_way_shawn_says_its_correct scapi.tests.test_connect-module.html#test_setting_comments_the_way_shawn_says_its_correct +scapi.tests.test_connect.test_large_list scapi.tests.test_connect-module.html#test_large_list +scapi.tests.test_connect.CONNECTOR scapi.tests.test_connect-module.html#CONNECTOR +scapi.tests.test_connect.CONFIG_NAME scapi.tests.test_connect-module.html#CONFIG_NAME +scapi.tests.test_connect.RUN_INTERACTIVE_TESTS scapi.tests.test_connect-module.html#RUN_INTERACTIVE_TESTS +scapi.tests.test_connect.setup scapi.tests.test_connect-module.html#setup +scapi.tests.test_connect.test_favorites scapi.tests.test_connect-module.html#test_favorites +scapi.tests.test_connect.USE_OAUTH scapi.tests.test_connect-module.html#USE_OAUTH +scapi.tests.test_oauth scapi.tests.test_oauth-module.html +scapi.tests.test_oauth._logger scapi.tests.test_oauth-module.html#_logger +scapi.tests.test_oauth.test_oauth_connect scapi.tests.test_oauth-module.html#test_oauth_connect +scapi.tests.test_oauth.TOKEN scapi.tests.test_oauth-module.html#TOKEN +scapi.tests.test_oauth.test_base64_connect scapi.tests.test_oauth-module.html#test_base64_connect +scapi.tests.test_oauth.CONSUMER_SECRET scapi.tests.test_oauth-module.html#CONSUMER_SECRET +scapi.tests.test_oauth.SECRET scapi.tests.test_oauth-module.html#SECRET +scapi.tests.test_oauth.logger scapi.tests.test_oauth-module.html#logger +scapi.tests.test_oauth.CONSUMER scapi.tests.test_oauth-module.html#CONSUMER +scapi.util scapi.util-module.html +scapi.util.escape scapi.util-module.html#escape +exceptions.AssertionError exceptions.AssertionError-class.html +exceptions.AssertionError.__init__ exceptions.AssertionError-class.html#__init__ +exceptions.AssertionError.__new__ exceptions.AssertionError-class.html#__new__ +scapi.ApiConnector scapi.ApiConnector-class.html +scapi.ApiConnector.fetch_access_token scapi.ApiConnector-class.html#fetch_access_token +scapi.ApiConnector.LIST_LIMIT scapi.ApiConnector-class.html#LIST_LIMIT +scapi.ApiConnector.LIST_LIMIT_PARAMETER scapi.ApiConnector-class.html#LIST_LIMIT_PARAMETER +scapi.ApiConnector.fetch_request_token scapi.ApiConnector-class.html#fetch_request_token +scapi.ApiConnector.get_request_token_authorization_url scapi.ApiConnector-class.html#get_request_token_authorization_url +scapi.ApiConnector.normalize_method scapi.ApiConnector-class.html#normalize_method +scapi.ApiConnector.__init__ scapi.ApiConnector-class.html#__init__ +scapi.ApiConnector.LIST_OFFSET_PARAMETER scapi.ApiConnector-class.html#LIST_OFFSET_PARAMETER +scapi.Comment scapi.Comment-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.Comment.KIND scapi.Comment-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.Event scapi.Event-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.Event.KIND scapi.Event-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.Group scapi.Group-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.Group.KIND scapi.Group-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.InvalidMethodException scapi.InvalidMethodException-class.html +scapi.InvalidMethodException.__repr__ scapi.InvalidMethodException-class.html#__repr__ +scapi.InvalidMethodException.__init__ scapi.InvalidMethodException-class.html#__init__ +scapi.NoResultFromRequest scapi.NoResultFromRequest-class.html +scapi.Playlist scapi.Playlist-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.Playlist.KIND scapi.Playlist-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.RESTBase scapi.RESTBase-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.RESTBase.ALIASES scapi.RESTBase-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.KIND scapi.RESTBase-class.html#KIND +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.SCRedirectHandler scapi.SCRedirectHandler-class.html +scapi.SCRedirectHandler.alternate_method scapi.SCRedirectHandler-class.html#alternate_method +scapi.SCRedirectHandler.http_error_303 scapi.SCRedirectHandler-class.html#http_error_303 +scapi.SCRedirectHandler.http_error_201 scapi.SCRedirectHandler-class.html#http_error_201 +scapi.Scope scapi.Scope-class.html +scapi.Scope.oauth_sign_get_request scapi.Scope-class.html#oauth_sign_get_request +scapi.Scope._map scapi.Scope-class.html#_map +scapi.Scope.__str__ scapi.Scope-class.html#__str__ +scapi.Scope.__getattr__ scapi.Scope-class.html#__getattr__ +scapi.Scope._call scapi.Scope-class.html#_call +scapi.Scope._create_query_string scapi.Scope-class.html#_create_query_string +scapi.Scope._create_request scapi.Scope-class.html#_create_request +scapi.Scope._get_connector scapi.Scope-class.html#_get_connector +scapi.Scope.__init__ scapi.Scope-class.html#__init__ +scapi.Scope.__repr__ scapi.Scope-class.html#__repr__ +scapi.Track scapi.Track-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.Track.KIND scapi.Track-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.Track.ALIASES scapi.Track-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.UnknownContentType scapi.UnknownContentType-class.html +scapi.UnknownContentType.__str__ scapi.UnknownContentType-class.html#__str__ +scapi.UnknownContentType.__repr__ scapi.UnknownContentType-class.html#__repr__ +scapi.UnknownContentType.__init__ scapi.UnknownContentType-class.html#__init__ +scapi.User scapi.User-class.html +scapi.RESTBase._scope scapi.RESTBase-class.html#_scope +scapi.RESTBase.__init__ scapi.RESTBase-class.html#__init__ +scapi.User.KIND scapi.User-class.html#KIND +scapi.RESTBase.create scapi.RESTBase-class.html#create +scapi.RESTBase._as_arguments scapi.RESTBase-class.html#_as_arguments +scapi.RESTBase.__getattr__ scapi.RESTBase-class.html#__getattr__ +scapi.RESTBase.ALL_DOMAIN_CLASSES scapi.RESTBase-class.html#ALL_DOMAIN_CLASSES +scapi.RESTBase.new scapi.RESTBase-class.html#new +scapi.User.ALIASES scapi.User-class.html#ALIASES +scapi.RESTBase.__ne__ scapi.RESTBase-class.html#__ne__ +scapi.RESTBase.get scapi.RESTBase-class.html#get +scapi.RESTBase._convert_value scapi.RESTBase-class.html#_convert_value +scapi.RESTBase.__setattr__ scapi.RESTBase-class.html#__setattr__ +scapi.RESTBase._singleton scapi.RESTBase-class.html#_singleton +scapi.RESTBase.REGISTRY scapi.RESTBase-class.html#REGISTRY +scapi.RESTBase.__eq__ scapi.RESTBase-class.html#__eq__ +scapi.RESTBase.__repr__ scapi.RESTBase-class.html#__repr__ +scapi.RESTBase.__hash__ scapi.RESTBase-class.html#__hash__ +scapi.authentication.BasicAuthenticator scapi.authentication.BasicAuthenticator-class.html +scapi.authentication.BasicAuthenticator.augment_request scapi.authentication.BasicAuthenticator-class.html#augment_request +scapi.authentication.BasicAuthenticator.__init__ scapi.authentication.BasicAuthenticator-class.html#__init__ +scapi.authentication.OAuthAuthenticator scapi.authentication.OAuthAuthenticator-class.html +scapi.authentication.OAuthAuthenticator.augment_request scapi.authentication.OAuthAuthenticator-class.html#augment_request +scapi.authentication.OAuthAuthenticator.generate_nonce scapi.authentication.OAuthAuthenticator-class.html#generate_nonce +scapi.authentication.OAuthAuthenticator.OAUTH_API_VERSION scapi.authentication.OAuthAuthenticator-class.html#OAUTH_API_VERSION +scapi.authentication.OAuthAuthenticator.generate_timestamp scapi.authentication.OAuthAuthenticator-class.html#generate_timestamp +scapi.authentication.OAuthAuthenticator.AUTHORIZATION_HEADER scapi.authentication.OAuthAuthenticator-class.html#AUTHORIZATION_HEADER +scapi.authentication.OAuthAuthenticator.__init__ scapi.authentication.OAuthAuthenticator-class.html#__init__ +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1 scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.FORBIDDEN scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#FORBIDDEN +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_name scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_name +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_http_method scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_http_method +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_http_url scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_http_url +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.get_normalized_parameters scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#get_normalized_parameters +scapi.authentication.OAuthSignatureMethod_HMAC_SHA1.build_signature scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html#build_signature +scapi.json.JsonReader scapi.json.JsonReader-class.html +scapi.json.JsonReader.escapes scapi.json.JsonReader-class.html#escapes +scapi.json.JsonReader._readObject scapi.json.JsonReader-class.html#_readObject +scapi.json.JsonReader._assertNext scapi.json.JsonReader-class.html#_assertNext +scapi.json.JsonReader._readNumber scapi.json.JsonReader-class.html#_readNumber +scapi.json.JsonReader._peek scapi.json.JsonReader-class.html#_peek +scapi.json.JsonReader._readNull scapi.json.JsonReader-class.html#_readNull +scapi.json.JsonReader._hexDigitToInt scapi.json.JsonReader-class.html#_hexDigitToInt +scapi.json.JsonReader._readTrue scapi.json.JsonReader-class.html#_readTrue +scapi.json.JsonReader._readDoubleSolidusComment scapi.json.JsonReader-class.html#_readDoubleSolidusComment +scapi.json.JsonReader.read scapi.json.JsonReader-class.html#read +scapi.json.JsonReader._readFalse scapi.json.JsonReader-class.html#_readFalse +scapi.json.JsonReader.hex_digits scapi.json.JsonReader-class.html#hex_digits +scapi.json.JsonReader._readComment scapi.json.JsonReader-class.html#_readComment +scapi.json.JsonReader._readArray scapi.json.JsonReader-class.html#_readArray +scapi.json.JsonReader._eatWhitespace scapi.json.JsonReader-class.html#_eatWhitespace +scapi.json.JsonReader._read scapi.json.JsonReader-class.html#_read +scapi.json.JsonReader._readString scapi.json.JsonReader-class.html#_readString +scapi.json.JsonReader._readCStyleComment scapi.json.JsonReader-class.html#_readCStyleComment +scapi.json.JsonReader._next scapi.json.JsonReader-class.html#_next +scapi.json.JsonWriter scapi.json.JsonWriter-class.html +scapi.json.JsonWriter.write scapi.json.JsonWriter-class.html#write +scapi.json.JsonWriter._append scapi.json.JsonWriter-class.html#_append +scapi.json.JsonWriter._write scapi.json.JsonWriter-class.html#_write +scapi.json.ReadException scapi.json.ReadException-class.html +scapi.json.WriteException scapi.json.WriteException-class.html +scapi.json._StringGenerator scapi.json._StringGenerator-class.html +scapi.json._StringGenerator.peek scapi.json._StringGenerator-class.html#peek +scapi.json._StringGenerator.all scapi.json._StringGenerator-class.html#all +scapi.json._StringGenerator.next scapi.json._StringGenerator-class.html#next +scapi.json._StringGenerator.__init__ scapi.json._StringGenerator-class.html#__init__ +scapi.tests.scapi_tests.SCAPITests scapi.tests.scapi_tests.SCAPITests-class.html +scapi.tests.scapi_tests.SCAPITests._load_config scapi.tests.scapi_tests.SCAPITests-class.html#_load_config +scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_artwork scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_artwork +scapi.tests.scapi_tests.SCAPITests.test_upload scapi.tests.scapi_tests.SCAPITests-class.html#test_upload +scapi.tests.scapi_tests.SCAPITests.test_scoped_track_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_scoped_track_creation +scapi.tests.scapi_tests.SCAPITests.test_permissions scapi.tests.scapi_tests.SCAPITests-class.html#test_permissions +scapi.tests.scapi_tests.SCAPITests.CONSUMER_SECRET scapi.tests.scapi_tests.SCAPITests-class.html#CONSUMER_SECRET +scapi.tests.scapi_tests.SCAPITests.CONSUMER scapi.tests.scapi_tests.SCAPITests-class.html#CONSUMER +scapi.tests.scapi_tests.SCAPITests.test_me_having_stress scapi.tests.scapi_tests.SCAPITests-class.html#test_me_having_stress +scapi.tests.scapi_tests.SCAPITests.USER scapi.tests.scapi_tests.SCAPITests-class.html#USER +scapi.tests.scapi_tests.SCAPITests.CONFIGSPEC scapi.tests.scapi_tests.SCAPITests-class.html#CONFIGSPEC +scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_email_sharers scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_email_sharers +scapi.tests.scapi_tests.SCAPITests.test_setting_comments scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_comments +scapi.tests.scapi_tests.SCAPITests.test_access_token_acquisition scapi.tests.scapi_tests.SCAPITests-class.html#test_access_token_acquisition +scapi.tests.scapi_tests.SCAPITests.test_track_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation +scapi.tests.scapi_tests.SCAPITests.test_groups scapi.tests.scapi_tests.SCAPITests-class.html#test_groups +scapi.tests.scapi_tests.SCAPITests.test_track_update scapi.tests.scapi_tests.SCAPITests-class.html#test_track_update +scapi.tests.scapi_tests.SCAPITests.test_modifying_playlists scapi.tests.scapi_tests.SCAPITests-class.html#test_modifying_playlists +scapi.tests.scapi_tests.SCAPITests.SECRET scapi.tests.scapi_tests.SCAPITests-class.html#SECRET +scapi.tests.scapi_tests.SCAPITests.test_oauth_get_signing scapi.tests.scapi_tests.SCAPITests-class.html#test_oauth_get_signing +scapi.tests.scapi_tests.SCAPITests.test_contact_add_and_removal scapi.tests.scapi_tests.SCAPITests-class.html#test_contact_add_and_removal +scapi.tests.scapi_tests.SCAPITests.test_connect scapi.tests.scapi_tests.SCAPITests-class.html#test_connect +scapi.tests.scapi_tests.SCAPITests.test_large_list scapi.tests.scapi_tests.SCAPITests-class.html#test_large_list +unittest.TestCase.failureException exceptions.AssertionError-class.html +scapi.tests.scapi_tests.SCAPITests.test_contact_list scapi.tests.scapi_tests.SCAPITests-class.html#test_contact_list +scapi.tests.scapi_tests.SCAPITests.test_playlists scapi.tests.scapi_tests.SCAPITests-class.html#test_playlists +scapi.tests.scapi_tests.SCAPITests.API_HOST scapi.tests.scapi_tests.SCAPITests-class.html#API_HOST +scapi.tests.scapi_tests.SCAPITests.setUp scapi.tests.scapi_tests.SCAPITests-class.html#setUp +scapi.tests.scapi_tests.SCAPITests.test_downloadable scapi.tests.scapi_tests.SCAPITests-class.html#test_downloadable +scapi.tests.scapi_tests.SCAPITests.TOKEN scapi.tests.scapi_tests.SCAPITests-class.html#TOKEN +scapi.tests.scapi_tests.SCAPITests.test_events scapi.tests.scapi_tests.SCAPITests-class.html#test_events +scapi.tests.scapi_tests.SCAPITests.test_non_global_api scapi.tests.scapi_tests.SCAPITests-class.html#test_non_global_api +scapi.tests.scapi_tests.SCAPITests.PASSWORD scapi.tests.scapi_tests.SCAPITests-class.html#PASSWORD +scapi.tests.scapi_tests.SCAPITests.test_setting_comments_the_way_shawn_says_its_correct scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_comments_the_way_shawn_says_its_correct +scapi.tests.scapi_tests.SCAPITests.CONFIG_NAME scapi.tests.scapi_tests.SCAPITests-class.html#CONFIG_NAME +scapi.tests.scapi_tests.SCAPITests.test_setting_permissions scapi.tests.scapi_tests.SCAPITests-class.html#test_setting_permissions +scapi.tests.scapi_tests.SCAPITests.RUN_INTERACTIVE_TESTS scapi.tests.scapi_tests.SCAPITests-class.html#RUN_INTERACTIVE_TESTS +scapi.tests.scapi_tests.SCAPITests.test_favorites scapi.tests.scapi_tests.SCAPITests-class.html#test_favorites +scapi.tests.scapi_tests.SCAPITests.test_track_creation_with_updated_artwork scapi.tests.scapi_tests.SCAPITests-class.html#test_track_creation_with_updated_artwork +scapi.tests.scapi_tests.SCAPITests.AUTHENTICATOR scapi.tests.scapi_tests.SCAPITests-class.html#AUTHENTICATOR +scapi.tests.scapi_tests.SCAPITests.test_streaming scapi.tests.scapi_tests.SCAPITests-class.html#test_streaming +scapi.tests.scapi_tests.SCAPITests.test_playlist_creation scapi.tests.scapi_tests.SCAPITests-class.html#test_playlist_creation +scapi.tests.scapi_tests.SCAPITests.test_track_deletion scapi.tests.scapi_tests.SCAPITests-class.html#test_track_deletion +scapi.tests.scapi_tests.SCAPITests.test_filtered_list scapi.tests.scapi_tests.SCAPITests-class.html#test_filtered_list +scapi.tests.scapi_tests.SCAPITests.root scapi.tests.scapi_tests.SCAPITests-class.html#root +scapi.util.MultiDict scapi.util.MultiDict-class.html +scapi.util.MultiDict.add scapi.util.MultiDict-class.html#add +scapi.util.MultiDict.iteritemslist scapi.util.MultiDict-class.html#iteritemslist diff --git a/python_apps/soundcloud-api/docs/api/class-tree.html b/python_apps/soundcloud-api/docs/api/class-tree.html new file mode 100644 index 000000000..b2473382f --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/class-tree.html @@ -0,0 +1,216 @@ + + + + + Class Hierarchy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  + + + + +
[hide private]
[frames] | no frames]
+
+
+ [ Module Hierarchy + | Class Hierarchy ] +

+

Class Hierarchy

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/crarr.png b/python_apps/soundcloud-api/docs/api/crarr.png new file mode 100644 index 0000000000000000000000000000000000000000..26b43c52433b71e72a9a478c52d446278335f0e4 GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^f?NMQuI$%1#8??M1uoZK z0}62#ctjR6FvuMOVaB`*rFK9;mUKs7M+SzC{oH>NS%G}l0G|-o|NsA=J-p%i`2!7U zCdJ_j4{u-SDsoA1U`TRixpVcz%O`iHHAYk?=&YaLkmD!Pp6~GW^M_S4D^grJKD>P~ zuPf!ku`N^TLavn`Edv_JSQ6wH%;50sMjDXg>*?YcQgJIe!GUqln>_|<+Os&OOUQS1 zY~Wzutud*iVS#|PHMc&?2WHoZpEo8l+6!Oc$x~=%U)469Gl^f?nq7UBw#1AXkrEde cmFKWBXcRFE*(?@T0vgQV>FVdQ&MBb@0LpZ4r2qf` literal 0 HcmV?d00001 diff --git a/python_apps/soundcloud-api/docs/api/epydoc.css b/python_apps/soundcloud-api/docs/api/epydoc.css new file mode 100644 index 000000000..86d417068 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/epydoc.css @@ -0,0 +1,322 @@ + + +/* Epydoc CSS Stylesheet + * + * This stylesheet can be used to customize the appearance of epydoc's + * HTML output. + * + */ + +/* Default Colors & Styles + * - Set the default foreground & background color with 'body'; and + * link colors with 'a:link' and 'a:visited'. + * - Use bold for decision list terms. + * - The heading styles defined here are used for headings *within* + * docstring descriptions. All headings used by epydoc itself use + * either class='epydoc' or class='toc' (CSS styles for both + * defined below). + */ +body { background: #ffffff; color: #000000; } +p { margin-top: 0.5em; margin-bottom: 0.5em; } +a:link { color: #0000ff; } +a:visited { color: #204080; } +dt { font-weight: bold; } +h1 { font-size: +140%; font-style: italic; + font-weight: bold; } +h2 { font-size: +125%; font-style: italic; + font-weight: bold; } +h3 { font-size: +110%; font-style: italic; + font-weight: normal; } +code { font-size: 100%; } +/* N.B.: class, not pseudoclass */ +a.link { font-family: monospace; } + +/* Page Header & Footer + * - The standard page header consists of a navigation bar (with + * pointers to standard pages such as 'home' and 'trees'); a + * breadcrumbs list, which can be used to navigate to containing + * classes or modules; options links, to show/hide private + * variables and to show/hide frames; and a page title (using + *

). The page title may be followed by a link to the + * corresponding source code (using 'span.codelink'). + * - The footer consists of a navigation bar, a timestamp, and a + * pointer to epydoc's homepage. + */ +h1.epydoc { margin: 0; font-size: +140%; font-weight: bold; } +h2.epydoc { font-size: +130%; font-weight: bold; } +h3.epydoc { font-size: +115%; font-weight: bold; + margin-top: 0.2em; } +td h3.epydoc { font-size: +115%; font-weight: bold; + margin-bottom: 0; } +table.navbar { background: #a0c0ff; color: #000000; + border: 2px groove #c0d0d0; } +table.navbar table { color: #000000; } +th.navbar-select { background: #70b0ff; + color: #000000; } +table.navbar a { text-decoration: none; } +table.navbar a:link { color: #0000ff; } +table.navbar a:visited { color: #204080; } +span.breadcrumbs { font-size: 85%; font-weight: bold; } +span.options { font-size: 70%; } +span.codelink { font-size: 85%; } +td.footer { font-size: 85%; } + +/* Table Headers + * - Each summary table and details section begins with a 'header' + * row. This row contains a section title (marked by + * 'span.table-header') as well as a show/hide private link + * (marked by 'span.options', defined above). + * - Summary tables that contain user-defined groups mark those + * groups using 'group header' rows. + */ +td.table-header { background: #70b0ff; color: #000000; + border: 1px solid #608090; } +td.table-header table { color: #000000; } +td.table-header table a:link { color: #0000ff; } +td.table-header table a:visited { color: #204080; } +span.table-header { font-size: 120%; font-weight: bold; } +th.group-header { background: #c0e0f8; color: #000000; + text-align: left; font-style: italic; + font-size: 115%; + border: 1px solid #608090; } + +/* Summary Tables (functions, variables, etc) + * - Each object is described by a single row of the table with + * two cells. The left cell gives the object's type, and is + * marked with 'code.summary-type'. The right cell gives the + * object's name and a summary description. + * - CSS styles for the table's header and group headers are + * defined above, under 'Table Headers' + */ +table.summary { border-collapse: collapse; + background: #e8f0f8; color: #000000; + border: 1px solid #608090; + margin-bottom: 0.5em; } +td.summary { border: 1px solid #608090; } +code.summary-type { font-size: 85%; } +table.summary a:link { color: #0000ff; } +table.summary a:visited { color: #204080; } + + +/* Details Tables (functions, variables, etc) + * - Each object is described in its own div. + * - A single-row summary table w/ table-header is used as + * a header for each details section (CSS style for table-header + * is defined above, under 'Table Headers'). + */ +table.details { border-collapse: collapse; + background: #e8f0f8; color: #000000; + border: 1px solid #608090; + margin: .2em 0 0 0; } +table.details table { color: #000000; } +table.details a:link { color: #0000ff; } +table.details a:visited { color: #204080; } + +/* Fields */ +dl.fields { margin-left: 2em; margin-top: 1em; + margin-bottom: 1em; } +dl.fields dd ul { margin-left: 0em; padding-left: 0em; } +dl.fields dd ul li ul { margin-left: 2em; padding-left: 0em; } +div.fields { margin-left: 2em; } +div.fields p { margin-bottom: 0.5em; } + +/* Index tables (identifier index, term index, etc) + * - link-index is used for indices containing lists of links + * (namely, the identifier index & term index). + * - index-where is used in link indices for the text indicating + * the container/source for each link. + * - metadata-index is used for indices containing metadata + * extracted from fields (namely, the bug index & todo index). + */ +table.link-index { border-collapse: collapse; + background: #e8f0f8; color: #000000; + border: 1px solid #608090; } +td.link-index { border-width: 0px; } +table.link-index a:link { color: #0000ff; } +table.link-index a:visited { color: #204080; } +span.index-where { font-size: 70%; } +table.metadata-index { border-collapse: collapse; + background: #e8f0f8; color: #000000; + border: 1px solid #608090; + margin: .2em 0 0 0; } +td.metadata-index { border-width: 1px; border-style: solid; } +table.metadata-index a:link { color: #0000ff; } +table.metadata-index a:visited { color: #204080; } + +/* Function signatures + * - sig* is used for the signature in the details section. + * - .summary-sig* is used for the signature in the summary + * table, and when listing property accessor functions. + * */ +.sig-name { color: #006080; } +.sig-arg { color: #008060; } +.sig-default { color: #602000; } +.summary-sig { font-family: monospace; } +.summary-sig-name { color: #006080; font-weight: bold; } +table.summary a.summary-sig-name:link + { color: #006080; font-weight: bold; } +table.summary a.summary-sig-name:visited + { color: #006080; font-weight: bold; } +.summary-sig-arg { color: #006040; } +.summary-sig-default { color: #501800; } + +/* Subclass list + */ +ul.subclass-list { display: inline; } +ul.subclass-list li { display: inline; } + +/* To render variables, classes etc. like functions */ +table.summary .summary-name { color: #006080; font-weight: bold; + font-family: monospace; } +table.summary + a.summary-name:link { color: #006080; font-weight: bold; + font-family: monospace; } +table.summary + a.summary-name:visited { color: #006080; font-weight: bold; + font-family: monospace; } + +/* Variable values + * - In the 'variable details' sections, each varaible's value is + * listed in a 'pre.variable' box. The width of this box is + * restricted to 80 chars; if the value's repr is longer than + * this it will be wrapped, using a backslash marked with + * class 'variable-linewrap'. If the value's repr is longer + * than 3 lines, the rest will be ellided; and an ellipsis + * marker ('...' marked with 'variable-ellipsis') will be used. + * - If the value is a string, its quote marks will be marked + * with 'variable-quote'. + * - If the variable is a regexp, it is syntax-highlighted using + * the re* CSS classes. + */ +pre.variable { padding: .5em; margin: 0; + background: #dce4ec; color: #000000; + border: 1px solid #708890; } +.variable-linewrap { color: #604000; font-weight: bold; } +.variable-ellipsis { color: #604000; font-weight: bold; } +.variable-quote { color: #604000; font-weight: bold; } +.variable-group { color: #008000; font-weight: bold; } +.variable-op { color: #604000; font-weight: bold; } +.variable-string { color: #006030; } +.variable-unknown { color: #a00000; font-weight: bold; } +.re { color: #000000; } +.re-char { color: #006030; } +.re-op { color: #600000; } +.re-group { color: #003060; } +.re-ref { color: #404040; } + +/* Base tree + * - Used by class pages to display the base class hierarchy. + */ +pre.base-tree { font-size: 80%; margin: 0; } + +/* Frames-based table of contents headers + * - Consists of two frames: one for selecting modules; and + * the other listing the contents of the selected module. + * - h1.toc is used for each frame's heading + * - h2.toc is used for subheadings within each frame. + */ +h1.toc { text-align: center; font-size: 105%; + margin: 0; font-weight: bold; + padding: 0; } +h2.toc { font-size: 100%; font-weight: bold; + margin: 0.5em 0 0 -0.3em; } + +/* Syntax Highlighting for Source Code + * - doctest examples are displayed in a 'pre.py-doctest' block. + * If the example is in a details table entry, then it will use + * the colors specified by the 'table pre.py-doctest' line. + * - Source code listings are displayed in a 'pre.py-src' block. + * Each line is marked with 'span.py-line' (used to draw a line + * down the left margin, separating the code from the line + * numbers). Line numbers are displayed with 'span.py-lineno'. + * The expand/collapse block toggle button is displayed with + * 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not + * modify the font size of the text.) + * - If a source code page is opened with an anchor, then the + * corresponding code block will be highlighted. The code + * block's header is highlighted with 'py-highlight-hdr'; and + * the code block's body is highlighted with 'py-highlight'. + * - The remaining py-* classes are used to perform syntax + * highlighting (py-string for string literals, py-name for names, + * etc.) + */ +pre.py-doctest { padding: .5em; margin: 1em; + background: #e8f0f8; color: #000000; + border: 1px solid #708890; } +table pre.py-doctest { background: #dce4ec; + color: #000000; } +pre.py-src { border: 2px solid #000000; + background: #f0f0f0; color: #000000; } +.py-line { border-left: 2px solid #000000; + margin-left: .2em; padding-left: .4em; } +.py-lineno { font-style: italic; font-size: 90%; + padding-left: .5em; } +a.py-toggle { text-decoration: none; } +div.py-highlight-hdr { border-top: 2px solid #000000; + border-bottom: 2px solid #000000; + background: #d8e8e8; } +div.py-highlight { border-bottom: 2px solid #000000; + background: #d0e0e0; } +.py-prompt { color: #005050; font-weight: bold;} +.py-more { color: #005050; font-weight: bold;} +.py-string { color: #006030; } +.py-comment { color: #003060; } +.py-keyword { color: #600000; } +.py-output { color: #404040; } +.py-name { color: #000050; } +.py-name:link { color: #000050 !important; } +.py-name:visited { color: #000050 !important; } +.py-number { color: #005000; } +.py-defname { color: #000060; font-weight: bold; } +.py-def-name { color: #000060; font-weight: bold; } +.py-base-class { color: #000060; } +.py-param { color: #000060; } +.py-docstring { color: #006030; } +.py-decorator { color: #804020; } +/* Use this if you don't want links to names underlined: */ +/*a.py-name { text-decoration: none; }*/ + +/* Graphs & Diagrams + * - These CSS styles are used for graphs & diagrams generated using + * Graphviz dot. 'img.graph-without-title' is used for bare + * diagrams (to remove the border created by making the image + * clickable). + */ +img.graph-without-title { border: none; } +img.graph-with-title { border: 1px solid #000000; } +span.graph-title { font-weight: bold; } +span.graph-caption { } + +/* General-purpose classes + * - 'p.indent-wrapped-lines' defines a paragraph whose first line + * is not indented, but whose subsequent lines are. + * - The 'nomargin-top' class is used to remove the top margin (e.g. + * from lists). The 'nomargin' class is used to remove both the + * top and bottom margin (but not the left or right margin -- + * for lists, that would cause the bullets to disappear.) + */ +p.indent-wrapped-lines { padding: 0 0 0 7em; text-indent: -7em; + margin: 0; } +.nomargin-top { margin-top: 0; } +.nomargin { margin-top: 0; margin-bottom: 0; } + +/* HTML Log */ +div.log-block { padding: 0; margin: .5em 0 .5em 0; + background: #e8f0f8; color: #000000; + border: 1px solid #000000; } +div.log-error { padding: .1em .3em .1em .3em; margin: 4px; + background: #ffb0b0; color: #000000; + border: 1px solid #000000; } +div.log-warning { padding: .1em .3em .1em .3em; margin: 4px; + background: #ffffb0; color: #000000; + border: 1px solid #000000; } +div.log-info { padding: .1em .3em .1em .3em; margin: 4px; + background: #b0ffb0; color: #000000; + border: 1px solid #000000; } +h2.log-hdr { background: #70b0ff; color: #000000; + margin: 0; padding: 0em 0.5em 0em 0.5em; + border-bottom: 1px solid #000000; font-size: 110%; } +p.log { font-weight: bold; margin: .5em 0 .5em 0; } +tr.opt-changed { color: #000000; font-weight: bold; } +tr.opt-default { color: #606060; } +pre.log { margin: 0; padding: 0; padding-left: 1em; } diff --git a/python_apps/soundcloud-api/docs/api/epydoc.js b/python_apps/soundcloud-api/docs/api/epydoc.js new file mode 100644 index 000000000..e787dbcf4 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/epydoc.js @@ -0,0 +1,293 @@ +function toggle_private() { + // Search for any private/public links on this page. Store + // their old text in "cmd," so we will know what action to + // take; and change their text to the opposite action. + var cmd = "?"; + var elts = document.getElementsByTagName("a"); + for(var i=0; i...
"; + elt.innerHTML = s; + } +} + +function toggle(id) { + elt = document.getElementById(id+"-toggle"); + if (elt.innerHTML == "-") + collapse(id); + else + expand(id); + return false; +} + +function highlight(id) { + var elt = document.getElementById(id+"-def"); + if (elt) elt.className = "py-highlight-hdr"; + var elt = document.getElementById(id+"-expanded"); + if (elt) elt.className = "py-highlight"; + var elt = document.getElementById(id+"-collapsed"); + if (elt) elt.className = "py-highlight"; +} + +function num_lines(s) { + var n = 1; + var pos = s.indexOf("\n"); + while ( pos > 0) { + n += 1; + pos = s.indexOf("\n", pos+1); + } + return n; +} + +// Collapse all blocks that mave more than `min_lines` lines. +function collapse_all(min_lines) { + var elts = document.getElementsByTagName("div"); + for (var i=0; i 0) + if (elt.id.substring(split, elt.id.length) == "-expanded") + if (num_lines(elt.innerHTML) > min_lines) + collapse(elt.id.substring(0, split)); + } +} + +function expandto(href) { + var start = href.indexOf("#")+1; + if (start != 0 && start != href.length) { + if (href.substring(start, href.length) != "-") { + collapse_all(4); + pos = href.indexOf(".", start); + while (pos != -1) { + var id = href.substring(start, pos); + expand(id); + pos = href.indexOf(".", pos+1); + } + var id = href.substring(start, href.length); + expand(id); + highlight(id); + } + } +} + +function kill_doclink(id) { + var parent = document.getElementById(id); + parent.removeChild(parent.childNodes.item(0)); +} +function auto_kill_doclink(ev) { + if (!ev) var ev = window.event; + if (!this.contains(ev.toElement)) { + var parent = document.getElementById(this.parentID); + parent.removeChild(parent.childNodes.item(0)); + } +} + +function doclink(id, name, targets_id) { + var elt = document.getElementById(id); + + // If we already opened the box, then destroy it. + // (This case should never occur, but leave it in just in case.) + if (elt.childNodes.length > 1) { + elt.removeChild(elt.childNodes.item(0)); + } + else { + // The outer box: relative + inline positioning. + var box1 = document.createElement("div"); + box1.style.position = "relative"; + box1.style.display = "inline"; + box1.style.top = 0; + box1.style.left = 0; + + // A shadow for fun + var shadow = document.createElement("div"); + shadow.style.position = "absolute"; + shadow.style.left = "-1.3em"; + shadow.style.top = "-1.3em"; + shadow.style.background = "#404040"; + + // The inner box: absolute positioning. + var box2 = document.createElement("div"); + box2.style.position = "relative"; + box2.style.border = "1px solid #a0a0a0"; + box2.style.left = "-.2em"; + box2.style.top = "-.2em"; + box2.style.background = "white"; + box2.style.padding = ".3em .4em .3em .4em"; + box2.style.fontStyle = "normal"; + box2.onmouseout=auto_kill_doclink; + box2.parentID = id; + + // Get the targets + var targets_elt = document.getElementById(targets_id); + var targets = targets_elt.getAttribute("targets"); + var links = ""; + target_list = targets.split(","); + for (var i=0; i" + + target[0] + ""; + } + + // Put it all together. + elt.insertBefore(box1, elt.childNodes.item(0)); + //box1.appendChild(box2); + box1.appendChild(shadow); + shadow.appendChild(box2); + box2.innerHTML = + "Which "+name+" do you want to see documentation for?" + + ""; + } + return false; +} + +function get_anchor() { + var href = location.href; + var start = href.indexOf("#")+1; + if ((start != 0) && (start != href.length)) + return href.substring(start, href.length); + } +function redirect_url(dottedName) { + // Scan through each element of the "pages" list, and check + // if "name" matches with any of them. + for (var i=0; i-m" or "-c"; + // extract the portion & compare it to dottedName. + var pagename = pages[i].substring(0, pages[i].length-2); + if (pagename == dottedName.substring(0,pagename.length)) { + + // We've found a page that matches `dottedName`; + // construct its URL, using leftover `dottedName` + // content to form an anchor. + var pagetype = pages[i].charAt(pages[i].length-1); + var url = pagename + ((pagetype=="m")?"-module.html": + "-class.html"); + if (dottedName.length > pagename.length) + url += "#" + dottedName.substring(pagename.length+1, + dottedName.length); + return url; + } + } + } diff --git a/python_apps/soundcloud-api/docs/api/exceptions.AssertionError-class.html b/python_apps/soundcloud-api/docs/api/exceptions.AssertionError-class.html new file mode 100644 index 000000000..2dbca8db5 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/exceptions.AssertionError-class.html @@ -0,0 +1,299 @@ + + + + + exceptions.AssertionError + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + exceptions :: + AssertionError :: + Class AssertionError + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class AssertionError

+
+   object --+            
+            |            
+BaseException --+        
+                |        
+        Exception --+    
+                    |    
+        StandardError --+
+                        |
+                       AssertionError
+
+ +
+

Assertion failed.

+ + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(...)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ + +
+ +
+ a new object with type S, a subtype of T + + + + + + +
__new__(T, + S, + ...) + + +
+ +
+

Inherited from BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __repr__, + __setattr__, + __setstate__, + __str__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(...) +
(Constructor) +

+
  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
+
+
+
+ +
+ +
+ + +
+

__new__(T, + S, + ...) +

+
  +
+ + +
+
Returns: a new object with type S, a subtype of T
+
Overrides: + object.__new__ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/frames.html b/python_apps/soundcloud-api/docs/api/frames.html new file mode 100644 index 000000000..6d0191e9d --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/frames.html @@ -0,0 +1,17 @@ + + + + + SoundCloud API + + + + + + + + + diff --git a/python_apps/soundcloud-api/docs/api/help.html b/python_apps/soundcloud-api/docs/api/help.html new file mode 100644 index 000000000..ff7d397f2 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/help.html @@ -0,0 +1,278 @@ + + + + + Help + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  + + + + +
[hide private]
[frames] | no frames]
+
+ +

API Documentation

+ +

This document contains the API (Application Programming Interface) +documentation for SoundCloud API. Documentation for the Python +objects defined by the project is divided into separate pages for each +package, module, and class. The API documentation also includes two +pages containing information about the project as a whole: a trees +page, and an index page.

+ +

Object Documentation

+ +

Each Package Documentation page contains:

+
    +
  • A description of the package.
  • +
  • A list of the modules and sub-packages contained by the + package.
  • +
  • A summary of the classes defined by the package.
  • +
  • A summary of the functions defined by the package.
  • +
  • A summary of the variables defined by the package.
  • +
  • A detailed description of each function defined by the + package.
  • +
  • A detailed description of each variable defined by the + package.
  • +
+ +

Each Module Documentation page contains:

+
    +
  • A description of the module.
  • +
  • A summary of the classes defined by the module.
  • +
  • A summary of the functions defined by the module.
  • +
  • A summary of the variables defined by the module.
  • +
  • A detailed description of each function defined by the + module.
  • +
  • A detailed description of each variable defined by the + module.
  • +
+ +

Each Class Documentation page contains:

+
    +
  • A class inheritance diagram.
  • +
  • A list of known subclasses.
  • +
  • A description of the class.
  • +
  • A summary of the methods defined by the class.
  • +
  • A summary of the instance variables defined by the class.
  • +
  • A summary of the class (static) variables defined by the + class.
  • +
  • A detailed description of each method defined by the + class.
  • +
  • A detailed description of each instance variable defined by the + class.
  • +
  • A detailed description of each class (static) variable defined + by the class.
  • +
+ +

Project Documentation

+ +

The Trees page contains the module and class hierarchies:

+
    +
  • The module hierarchy lists every package and module, with + modules grouped into packages. At the top level, and within each + package, modules and sub-packages are listed alphabetically.
  • +
  • The class hierarchy lists every class, grouped by base + class. If a class has more than one base class, then it will be + listed under each base class. At the top level, and under each base + class, classes are listed alphabetically.
  • +
+ +

The Index page contains indices of terms and + identifiers:

+
    +
  • The term index lists every term indexed by any object's + documentation. For each term, the index provides links to each + place where the term is indexed.
  • +
  • The identifier index lists the (short) name of every package, + module, class, method, function, variable, and parameter. For each + identifier, the index provides a short description, and a link to + its documentation.
  • +
+ +

The Table of Contents

+ +

The table of contents occupies the two frames on the left side of +the window. The upper-left frame displays the project +contents, and the lower-left frame displays the module +contents:

+ + + + + + + + + +
+ Project
Contents
...
+ API
Documentation
Frame


+
+ Module
Contents
 
...
  +

+ +

The project contents frame contains a list of all packages +and modules that are defined by the project. Clicking on an entry +will display its contents in the module contents frame. Clicking on a +special entry, labeled "Everything," will display the contents of +the entire project.

+ +

The module contents frame contains a list of every +submodule, class, type, exception, function, and variable defined by a +module or package. Clicking on an entry will display its +documentation in the API documentation frame. Clicking on the name of +the module, at the top of the frame, will display the documentation +for the module itself.

+ +

The "frames" and "no frames" buttons below the top +navigation bar can be used to control whether the table of contents is +displayed or not.

+ +

The Navigation Bar

+ +

A navigation bar is located at the top and bottom of every page. +It indicates what type of page you are currently viewing, and allows +you to go to related pages. The following table describes the labels +on the navigation bar. Note that not some labels (such as +[Parent]) are not displayed on all pages.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LabelHighlighted when...Links to...
[Parent](never highlighted) the parent of the current package
[Package]viewing a packagethe package containing the current object +
[Module]viewing a modulethe module containing the current object +
[Class]viewing a class the class containing the current object
[Trees]viewing the trees page the trees page
[Index]viewing the index page the index page
[Help]viewing the help page the help page
+ +

The "show private" and "hide private" buttons below +the top navigation bar can be used to control whether documentation +for private objects is displayed. Private objects are usually defined +as objects whose (short) names begin with a single underscore, but do +not end with an underscore. For example, "_x", +"__pprint", and "epydoc.epytext._tokenize" +are private objects; but "re.sub", +"__init__", and "type_" are not. However, +if a module defines the "__all__" variable, then its +contents are used to decide which objects are private.

+ +

A timestamp below the bottom navigation bar indicates when each +page was last updated.

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/identifier-index.html b/python_apps/soundcloud-api/docs/api/identifier-index.html new file mode 100644 index 000000000..94e006634 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/identifier-index.html @@ -0,0 +1,892 @@ + + + + + Identifier Index + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  + + + + +
[hide private]
[frames] | no frames]
+
+ +
+

Identifier Index

+
+[ + A + B + C + D + E + F + G + H + I + J + K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z + _ +] +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

A

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

B

+ + + + + + + + +

C

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

E

+ + + + + + + + +

F

+ + + + + + + + +

G

+ + + + + + + + + + + + + + + + + +

H

+ + + + + + + + +

I

+ + + + + + + + +

J

+ + + + + + + + +

K

+ + + + + + + + + + + + + + + + + +

L

+ + + + + + + + + + + + + + + + + +

M

+ + + + + + + + +

N

+ + + + + + + + + + + + +

O

+ + + + + + + + + + + + +

P

+ + + + + + + + + + + + +

R

+ + + + + + + + + + + + + + + + + + + + + + +

S

+ + + + + + + + + + + + + + + + + + + + + + +

T

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

U

+ + + + + + + + + + + + + + + + + +

W

+ + + + + + + + +

_

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/index.html b/python_apps/soundcloud-api/docs/api/index.html new file mode 100644 index 000000000..6d0191e9d --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/index.html @@ -0,0 +1,17 @@ + + + + + SoundCloud API + + + + + + + + + diff --git a/python_apps/soundcloud-api/docs/api/module-tree.html b/python_apps/soundcloud-api/docs/api/module-tree.html new file mode 100644 index 000000000..bf467d991 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/module-tree.html @@ -0,0 +1,130 @@ + + + + + Module Hierarchy + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  + + + + +
[hide private]
[frames] | no frames]
+
+
+ [ Module Hierarchy + | Class Hierarchy ] +

+

Module Hierarchy

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/redirect.html b/python_apps/soundcloud-api/docs/api/redirect.html new file mode 100644 index 000000000..8ac436448 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/redirect.html @@ -0,0 +1,38 @@ +Epydoc Redirect Page + + + + + + + + +

Epydoc Auto-redirect page

+ +

When javascript is enabled, this page will redirect URLs of +the form redirect.html#dotted.name to the +documentation for the object with the given fully-qualified +dotted name.

+

 

+ + + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi-module.html b/python_apps/soundcloud-api/docs/api/scapi-module.html new file mode 100644 index 000000000..e63c5dac9 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi-module.html @@ -0,0 +1,444 @@ + + + + + scapi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Package scapi

source code

+ + + + + + + +
+ + + + + +
Submodules[hide private]
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Classes[hide private]
+
+   + + ApiConnector
+ The ApiConnector holds all the data necessary to authenticate + against the soundcloud-api. +
+   + + Comment
+ A comment domain object/resource. +
+   + + Event
+ A event domain object/resource. +
+   + + Group
+ A group domain object/resource +
+   + + InvalidMethodException +
+   + + NoResultFromRequest +
+   + + Playlist
+ A playlist/set domain object/resource +
+   + + RESTBase
+ The baseclass for all our domain-objects/resources. +
+   + + SCRedirectHandler
+ A urllib2-Handler to deal with the redirects the RESTful API of SC + uses. +
+   + + Scope
+ The basic means to query and create resources. +
+   + + Track
+ A track domain object/resource. +
+   + + UnknownContentType +
+   + + User
+ A user domain object/resource. +
+ + + + + + + + + +
+ + + + + +
Functions[hide private]
+
+   + + + + + + +
register_classes() + source code + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Variables[hide private]
+
+   + + ACCESS_TOKEN_URL = 'http://api.soundcloud.com/oauth/ac...
+ The url Soundcould offers to make users authorize a concrete request + token. +
+   + + AUTHORIZATION_URL = 'http://api.soundcloud.com/oauth/a... +
+   + + PROXY = ''
+ The url Soundcould offers to obtain request-tokens +
+   + + REQUEST_TOKEN_URL = 'http://api.soundcloud.com/oauth/r...
+ The url Soundcould offers to exchange access-tokens for + request-tokens. +
+   + + USE_PROXY = False
+ Something like http://127.0.0.1:10000/ +
+   + + logger = logging.getLogger("scapi") +
+ + + + + + +
+ + + + + +
Variables Details[hide private]
+
+ +
+ +
+

ACCESS_TOKEN_URL

+

The url Soundcould offers to make users authorize a concrete request + token.

+
+
+
+
Value:
+
+'http://api.soundcloud.com/oauth/access_token'
+
+
+
+
+
+ +
+ +
+

AUTHORIZATION_URL

+ +
+
+
+
Value:
+
+'http://api.soundcloud.com/oauth/authorize'
+
+
+
+
+
+ +
+ +
+

REQUEST_TOKEN_URL

+

The url Soundcould offers to exchange access-tokens for + request-tokens.

+
+
+
+
Value:
+
+'http://api.soundcloud.com/oauth/request_token'
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi-pysrc.html new file mode 100644 index 000000000..73d83533b --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi-pysrc.html @@ -0,0 +1,1263 @@ + + + + + scapi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + 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 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.ApiConnector-class.html b/python_apps/soundcloud-api/docs/api/scapi.ApiConnector-class.html new file mode 100644 index 000000000..7e4a213ac --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.ApiConnector-class.html @@ -0,0 +1,544 @@ + + + + + scapi.ApiConnector + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class ApiConnector + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class ApiConnector

source code

+
+object --+
+         |
+        ApiConnector
+
+ +
+

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.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + host, + user=None, + password=None, + authenticator=None, + base='', + collapse_scope=True)
+ Constructor for the API-Singleton.
+ source code + +
+ +
+   + + + + + + +
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.
+ source code + +
+ +
+   + + + + + + +
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.
+ source code + +
+ +
+   + + + + + + +
fetch_access_token(self, + oauth_verifier)
+ Helper-function for a registered consumer to exchange an access token + for a request token.
+ source code + +
+ +
+   + + + + + + +
get_request_token_authorization_url(self, + token)
+ Simple helper function to generate the url needed to ask a user for + request token authorization.
+ source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + 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. +
+   + + LIST_LIMIT_PARAMETER = 'limit' +
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + host, + user=None, + password=None, + authenticator=None, + base='', + collapse_scope=True) +
(Constructor) +

+
source code  +
+ +

Constructor for the API-Singleton. Use it once with parameters, and + then the subsequent calls internal to the API will work.

+
+
Parameters:
+
    +
  • host (str) - the host to connect to, e.g. "api.soundcloud.com". If a + port is needed, use "api.soundcloud.com:1234"
  • +
  • user (str) - if given, the username for basic HTTP authentication
  • +
  • password (str) - if the user is given, you have to give a password as well
  • +
  • authenticator (OAuthAuthenticator | BasicAuthenticator) - the authenticator to use, see scapi.authentication
  • +
+
Overrides: + object.__init__ +
+
+
+
+ +
+ +
+ + +
+

normalize_method(self, + method) +

+
source code  +
+ +

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.

+
+
+
+
+ +
+ +
+ + +
+

fetch_request_token(self, + url=None, + oauth_callback='oob', + oauth_verifier=None) +

+
source code  +
+ +

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.

+
+
+
+
+ +
+ +
+ + +
+

fetch_access_token(self, + oauth_verifier) +

+
source code  +
+ +

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.

+
+
+
+
+ +
+ +
+ + +
+

get_request_token_authorization_url(self, + token) +

+
source code  +
+ +

Simple helper function to generate the url needed to ask a user for + request token authorization.

+

See also fetch_request_token.

+

Possible usage:

+
+>>> import webbrowser
+>>> sca = scapi.ApiConnector()
+>>> authorization_url = sca.get_request_token_authorization_url(token)
+>>> webbrowser.open(authorization_url)
+
+
+
+
+
+ + + + + + +
+ + + + + +
Class Variable Details[hide private]
+
+ +
+ +
+

LIST_OFFSET_PARAMETER

+

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.

+
+
+
+
Value:
+
+'offset'
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Asset-class.html b/python_apps/soundcloud-api/docs/api/scapi.Asset-class.html new file mode 100644 index 000000000..f64bc0d2b --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Asset-class.html @@ -0,0 +1,258 @@ + + + + + scapi.Asset + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Asset + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Asset

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Asset
+
+ +
+An asset domain object/resource.

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'assets' +
+

Inherited from RESTBase: + ALIASES, + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Comment-class.html b/python_apps/soundcloud-api/docs/api/scapi.Comment-class.html new file mode 100644 index 000000000..4f2c4c950 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Comment-class.html @@ -0,0 +1,258 @@ + + + + + scapi.Comment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Comment + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Comment

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Comment
+
+ +
+

A comment domain object/resource.

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'comments' +
+

Inherited from RESTBase: + ALIASES, + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Event-class.html b/python_apps/soundcloud-api/docs/api/scapi.Event-class.html new file mode 100644 index 000000000..40095b067 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Event-class.html @@ -0,0 +1,258 @@ + + + + + scapi.Event + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Event + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Event

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Event
+
+ +
+

A event domain object/resource.

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'events' +
+

Inherited from RESTBase: + ALIASES, + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Group-class.html b/python_apps/soundcloud-api/docs/api/scapi.Group-class.html new file mode 100644 index 000000000..eb30ab842 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Group-class.html @@ -0,0 +1,258 @@ + + + + + scapi.Group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Group + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Group

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Group
+
+ +
+

A group domain object/resource

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'groups' +
+

Inherited from RESTBase: + ALIASES, + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.InvalidMethodException-class.html b/python_apps/soundcloud-api/docs/api/scapi.InvalidMethodException-class.html new file mode 100644 index 000000000..476502352 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.InvalidMethodException-class.html @@ -0,0 +1,297 @@ + + + + + scapi.InvalidMethodException + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class InvalidMethodException + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class InvalidMethodException

source code

+
+              object --+        
+                       |        
+exceptions.BaseException --+    
+                           |    
+        exceptions.Exception --+
+                               |
+                              InvalidMethodException
+
+ +
+ + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + message)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
__repr__(self)
+ repr(x)
+ source code + +
+ +
+

Inherited from exceptions.Exception: + __new__ +

+

Inherited from exceptions.BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __setattr__, + __setstate__, + __str__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from exceptions.BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + message) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__repr__(self) +
(Representation operator) +

+
source code  +
+ +

repr(x)

+
+
Overrides: + object.__repr__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.NoResultFromRequest-class.html b/python_apps/soundcloud-api/docs/api/scapi.NoResultFromRequest-class.html new file mode 100644 index 000000000..5bcb71794 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.NoResultFromRequest-class.html @@ -0,0 +1,195 @@ + + + + + scapi.NoResultFromRequest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class NoResultFromRequest + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class NoResultFromRequest

source code

+
+              object --+        
+                       |        
+exceptions.BaseException --+    
+                           |    
+        exceptions.Exception --+
+                               |
+                              NoResultFromRequest
+
+ +
+ + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from exceptions.Exception: + __init__, + __new__ +

+

Inherited from exceptions.BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __repr__, + __setattr__, + __setstate__, + __str__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from exceptions.BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Playlist-class.html b/python_apps/soundcloud-api/docs/api/scapi.Playlist-class.html new file mode 100644 index 000000000..557f05831 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Playlist-class.html @@ -0,0 +1,258 @@ + + + + + scapi.Playlist + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Playlist + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Playlist

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Playlist
+
+ +
+

A playlist/set domain object/resource

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'playlists' +
+

Inherited from RESTBase: + ALIASES, + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.RESTBase-class.html b/python_apps/soundcloud-api/docs/api/scapi.RESTBase-class.html new file mode 100644 index 000000000..855d8fd1e --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.RESTBase-class.html @@ -0,0 +1,895 @@ + + + + + scapi.RESTBase + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class RESTBase + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class RESTBase

source code

+
+object --+
+         |
+        RESTBase
+
+ +
Known Subclasses:
+
+ +
+ +
+

The baseclass for all our domain-objects/resources.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + data, + scope, + path_stack=None)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
__getattr__(self, + name) + source code + +
+ +
+   + + + + + + +
__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.
+ source code + +
+ +
+   + + + + + + +
_as_arguments(self)
+ Converts a resource to a argument-string the way Rails expects it.
+ source code + +
+ +
+   + + + + + + +
_convert_value(self, + value) + source code + +
+ +
+   + + + + + + +
_scope(self)
+ Return the scope this resource lives in, which is the KIND and id
+ source code + +
+ +
+   + + + + + + +
__repr__(self)
+ repr(x)
+ source code + +
+ +
+   + + + + + + +
__hash__(self)
+ hash(x)
+ source code + +
+ +
+   + + + + + + +
__eq__(self, + other)
+ Test for equality.
+ source code + +
+ +
+   + + + + + + +
__ne__(self, + other) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+   + + + + + + +
create(cls, + scope, + **data)
+ This is a convenience-method for creating an object that will be + passed as parameter - e.g.
+ source code + +
+ +
+   + + + + + + +
new(cls, + scope, + **data)
+ Create a new resource inside a given Scope.
+ source code + +
+ +
+   + + + + + + +
get(cls, + scope, + id)
+ Fetch a resource by id.
+ source code + +
+ +
+   + + + + + + +
_singleton(cls)
+ This method will take a resource name like "users" and + return the single-case, in the example "user".
+ source code + +
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + REGISTRY = {'comments': <class 'scapi.Comment'>, 'contacts': <... +
+   + + ALL_DOMAIN_CLASSES = {'Comment': <class 'scapi.Comment'>, 'Eve... +
+   + + ALIASES = [] +
+   + + KIND = None +
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + data, + scope, + path_stack=None) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__setattr__(self, + name, + value) +

+
source code  +
+ +

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"
+
+
Parameters:
+
    +
  • name (str) - the property name
  • +
  • value (RESTBase | list<RESTBase> | basestring | long | int | float) - the property, resource or resources to set
  • +
+
Returns:
+
None
+
Overrides: + object.__setattr__ +
+
+
+
+ +
+ +
+ + +
+

create(cls, + scope, + **data) +
Class Method +

+
source code  +
+ +

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
+
+
+
+
+ +
+ +
+ + +
+

new(cls, + scope, + **data) +
Class Method +

+
source code  +
+ +

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>
    +
  • +
+
+
Parameters:
+
    +
  • scope (tuple<Scope>[1]) - if not empty, a one-element tuple containing the Scope
  • +
  • data (dict) - the data
  • +
+
Returns:
+
new instance of the resource
+
+
+
+ +
+ +
+ + +
+

get(cls, + scope, + id) +
Class Method +

+
source code  +
+ +

Fetch a resource by id.

+

Simply pass a known id as argument. For example

+
+>>> sca = scapi.Scope(connector)
+>>> track = sca.Track.get(id)
+
+
+
+
+ +
+ +
+ + +
+

_scope(self) +

+
source code  +
+ +

Return the scope this resource lives in, which is the KIND and id

+
+
Returns:
+
"<KIND>/<id>"
+
+
+
+ +
+ +
+ + +
+

_singleton(cls) +
Class Method +

+
source code  +
+ +

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.

+
+
+
+
+ +
+ +
+ + +
+

__repr__(self) +
(Representation operator) +

+
source code  +
+ +

repr(x)

+
+
Overrides: + object.__repr__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__hash__(self) +
(Hashing function) +

+
source code  +
+ +

hash(x)

+
+
Overrides: + object.__hash__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__eq__(self, + other) +
(Equality operator) +

+
source code  +
+ +

Test for equality.

+

Resources are considered equal if the have the same kind and id.

+
+
+
+
+
+ + + + + + +
+ + + + + +
Class Variable Details[hide private]
+
+ +
+ +
+

REGISTRY

+ +
+
+
+
Value:
+
+{'comments': <class 'scapi.Comment'>,
+ 'contacts': <class 'scapi.User'>,
+ 'events': <class 'scapi.Event'>,
+ 'favorites': <class 'scapi.Track'>,
+ 'groups': <class 'scapi.Group'>,
+ 'me': <class 'scapi.User'>,
+ 'permissions': <class 'scapi.User'>,
+ 'playlists': <class 'scapi.Playlist'>,
+...
+
+
+
+
+
+ +
+ +
+

ALL_DOMAIN_CLASSES

+ +
+
+
+
Value:
+
+{'Comment': <class 'scapi.Comment'>,
+ 'Event': <class 'scapi.Event'>,
+ 'Group': <class 'scapi.Group'>,
+ 'Playlist': <class 'scapi.Playlist'>,
+ 'Track': <class 'scapi.Track'>,
+ 'User': <class 'scapi.User'>}
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.SCRedirectHandler-class.html b/python_apps/soundcloud-api/docs/api/scapi.SCRedirectHandler-class.html new file mode 100644 index 000000000..7fa8dedee --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.SCRedirectHandler-class.html @@ -0,0 +1,319 @@ + + + + + scapi.SCRedirectHandler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class SCRedirectHandler + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class SCRedirectHandler

source code

+
+    urllib2.BaseHandler --+    
+                          |    
+urllib2.HTTPRedirectHandler --+
+                              |
+                             SCRedirectHandler
+
+ +
+

A urllib2-Handler to deal with the redirects the RESTful API of SC + uses.

+ + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
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.
+ source code + +
+ +
+   + + + + + + +
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.
+ source code + +
+ +
+

Inherited from urllib2.HTTPRedirectHandler: + http_error_301, + http_error_302, + http_error_307, + redirect_request +

+

Inherited from urllib2.BaseHandler: + __lt__, + add_parent, + close +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + alternate_method = None +
+

Inherited from urllib2.HTTPRedirectHandler: + inf_msg, + max_redirections, + max_repeats +

+

Inherited from urllib2.BaseHandler: + handler_order +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

http_error_303(self, + req, + fp, + code, + msg, + hdrs) +

+
source code  +
+ +

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.

+
+
Overrides: + urllib2.HTTPRedirectHandler.http_error_302 +
+
+
+
+ +
+ +
+ + +
+

http_error_201(self, + req, + fp, + code, + msg, + hdrs) +

+
source code  +
+ +

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.

+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Scope-class.html b/python_apps/soundcloud-api/docs/api/scapi.Scope-class.html new file mode 100644 index 000000000..0b008e17f --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Scope-class.html @@ -0,0 +1,682 @@ + + + + + scapi.Scope + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Scope + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Scope

source code

+
+object --+
+         |
+        Scope
+
+ +
+

The basic means to query and create resources. The Scope uses the 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 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 + 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>, ...]
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + connector, + scope=None, + parent=None)
+ Create the Scope.
+ source code + +
+ +
+   + + + + + + +
_get_connector(self) + source code + +
+ +
+   + + + + + + +
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.
+ source code + +
+ +
+ urllib2.Request + + + + + + +
_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.
+ source code + +
+ +
+ str + + + + + + +
_create_query_string(self, + queryparams)
+ Small helpermethod to create the querystring from a dict.
+ source code + +
+ +
+   + + + + + + +
_call(self, + method, + *args, + **kwargs)
+ The workhorse.
+ source code + +
+ +
+   + + + + + + +
_map(self, + res, + method, + continue_list_fetching)
+ This method will take the JSON-result of a HTTP-call and return our + domain-objects.
+ source code + +
+ +
+   + + + + + + +
__getattr__(self, + _name)
+ Retrieve an API-method or a scoped domain-class.
+ source code + +
+ +
+   + + + + + + +
__repr__(self)
+ repr(x)
+ source code + +
+ +
+   + + + + + + +
__str__(self)
+ str(x)
+ source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __setattr__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + connector, + scope=None, + parent=None) +
(Constructor) +

+
source code  +
+ +

Create the Scope. It can have a resource as scope, and possibly a + parent-scope.

+
+
Parameters:
+
    +
  • connector (ApiConnector) - The connector to use.
  • +
  • scope (scapi.RESTBase) - the resource to make this scope belong to
  • +
  • parent (scapi.Scope) - the parent scope of this scope
  • +
+
Overrides: + object.__init__ +
+
+
+
+ +
+ +
+ + +
+

oauth_sign_get_request(self, + url) +

+
source code  +
+ +

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()
+
+
+
+
+ +
+ +
+ + +
+

_create_request(self, + url, + connector, + parameters, + queryparams, + alternate_http_method=None, + use_multipart=False) +

+
source code  +
+ +

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.

+
+
Parameters:
+
    +
  • url - the destination url
  • +
  • connector - our connector-instance
  • +
  • parameters (None|dict<str, basestring|list<basestring>>) - the POST-parameters to use.
  • +
  • queryparams (None|dict<str, basestring|list<basestring>>) - the queryparams to use
  • +
  • alternate_http_method (str) - an alternate HTTP-method to use
  • +
+
Returns: urllib2.Request
+
the fully equipped request
+
+
+
+ +
+ +
+ + +
+

_create_query_string(self, + queryparams) +

+
source code  +
+ +

Small helpermethod to create the querystring from a dict.

+
+
Parameters:
+
    +
  • queryparams (None|dict<str, basestring|list<basestring>>) - the queryparameters.
  • +
+
Returns: str
+
either the empty string, or a "?" followed by the + parameters joined by "&"
+
+
+
+ +
+ +
+ + +
+

_call(self, + method, + *args, + **kwargs) +

+
source code  +
+ +

The workhorse. It's complicated, convoluted and beyond understanding + of a mortal being.

+

You have been warned.

+
+
+
+
+ +
+ +
+ + +
+

_map(self, + res, + method, + continue_list_fetching) +

+
source code  +
+ +

This method will take the JSON-result of a HTTP-call and return our + domain-objects.

+

It's also deep magic, don't look.

+
+
+
+
+ +
+ +
+ + +
+

__getattr__(self, + _name) +
(Qualification operator) +

+
source code  +
+ +

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 params. This must be a dict or 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

    +
  • +
+
+
+
+
+ +
+ +
+ + +
+

__repr__(self) +
(Representation operator) +

+
source code  +
+ +

repr(x)

+
+
Overrides: + object.__repr__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__str__(self) +
(Informal representation operator) +

+
source code  +
+ +

str(x)

+
+
Overrides: + object.__str__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.Track-class.html b/python_apps/soundcloud-api/docs/api/scapi.Track-class.html new file mode 100644 index 000000000..e8257566a --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.Track-class.html @@ -0,0 +1,264 @@ + + + + + scapi.Track + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class Track + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class Track

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            Track
+
+ +
+

A track domain object/resource.

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'tracks' +
+   + + ALIASES = ['favorites'] +
+

Inherited from RESTBase: + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.UnknownContentType-class.html b/python_apps/soundcloud-api/docs/api/scapi.UnknownContentType-class.html new file mode 100644 index 000000000..94f9ee872 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.UnknownContentType-class.html @@ -0,0 +1,337 @@ + + + + + scapi.UnknownContentType + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class UnknownContentType + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class UnknownContentType

source code

+
+              object --+        
+                       |        
+exceptions.BaseException --+    
+                           |    
+        exceptions.Exception --+
+                               |
+                              UnknownContentType
+
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + msg)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
__repr__(self)
+ repr(x)
+ source code + +
+ +
+   + + + + + + +
__str__(self)
+ str(x)
+ source code + +
+ +
+

Inherited from exceptions.Exception: + __new__ +

+

Inherited from exceptions.BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __setattr__, + __setstate__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from exceptions.BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + msg) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__repr__(self) +
(Representation operator) +

+
source code  +
+ +

repr(x)

+
+
Overrides: + object.__repr__ +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

__str__(self) +
(Informal representation operator) +

+
source code  +
+ +

str(x)

+
+
Overrides: + object.__str__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.User-class.html b/python_apps/soundcloud-api/docs/api/scapi.User-class.html new file mode 100644 index 000000000..19b1053a9 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.User-class.html @@ -0,0 +1,264 @@ + + + + + scapi.User + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Class User + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class User

source code

+
+object --+    
+         |    
+  RESTBase --+
+             |
+            User
+
+ +
+

A user domain object/resource.

+ + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from RESTBase: + __eq__, + __getattr__, + __hash__, + __init__, + __ne__, + __repr__, + __setattr__ +

+

Inherited from RESTBase (private): + _as_arguments, + _convert_value, + _scope +

+

Inherited from object: + __delattr__, + __getattribute__, + __new__, + __reduce__, + __reduce_ex__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Methods[hide private]
+
+

Inherited from RESTBase: + create, + get, + new +

+

Inherited from RESTBase (private): + _singleton +

+
+ + + + + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + KIND = 'users' +
+   + + ALIASES = ['me', 'permissions', 'contacts', 'user'] +
+

Inherited from RESTBase: + ALL_DOMAIN_CLASSES, + REGISTRY +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.authentication-module.html b/python_apps/soundcloud-api/docs/api/scapi.authentication-module.html new file mode 100644 index 000000000..2f6f5fb8e --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.authentication-module.html @@ -0,0 +1,228 @@ + + + + + scapi.authentication + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module authentication + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module authentication

source code

+ + + + + + + + + + + + + + + +
+ + + + + +
Classes[hide private]
+
+   + + OAuthSignatureMethod_HMAC_SHA1 +
+   + + OAuthAuthenticator +
+   + + BasicAuthenticator +
+ + + + + + + + + + + + +
+ + + + + +
Variables[hide private]
+
+   + + USE_DOUBLE_ESCAPE_HACK = True
+ There seems to be an uncertainty on the way parameters are to be + escaped. +
+   + + logger = logging.getLogger(__name__) +
+ + + + + + +
+ + + + + +
Variables Details[hide private]
+
+ +
+ +
+

USE_DOUBLE_ESCAPE_HACK

+

There seems to be an uncertainty on the way parameters are to be + escaped. For now, this variable switches between two escaping + mechanisms.

+

If True, the passed parameters - GET or POST - are escaped + *twice*.

+
+
+
+
Value:
+
+True
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.authentication-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.authentication-pysrc.html new file mode 100644 index 000000000..65492b3c9 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.authentication-pysrc.html @@ -0,0 +1,348 @@ + + + + + scapi.authentication + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module authentication + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.authentication

+
+  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 base64 
+ 22  import time, random 
+ 23  import urlparse 
+ 24  import hmac 
+ 25  import hashlib 
+ 26  from scapi.util import escape 
+ 27  import logging 
+ 28   
+ 29   
+ 30  USE_DOUBLE_ESCAPE_HACK = True 
+ 31  """ 
+ 32  There seems to be an uncertainty on the way 
+ 33  parameters are to be escaped. For now, this 
+ 34  variable switches between two escaping mechanisms. 
+ 35   
+ 36  If True, the passed parameters - GET or POST - are 
+ 37  escaped *twice*. 
+ 38  """ 
+ 39   
+ 40  logger = logging.getLogger(__name__) 
+ 41   
+
42 -class OAuthSignatureMethod_HMAC_SHA1(object): +
43 + 44 FORBIDDEN = ['realm', 'oauth_signature'] + 45 +
46 - def get_name(self): +
47 return 'HMAC-SHA1' +
48 +
49 - def build_signature(self, request, parameters, consumer_secret, token_secret, oauth_parameters): +
50 if logger.level == logging.DEBUG: + 51 logger.debug("request: %r", request) + 52 logger.debug("parameters: %r", parameters) + 53 logger.debug("consumer_secret: %r", consumer_secret) + 54 logger.debug("token_secret: %r", token_secret) + 55 logger.debug("oauth_parameters: %r", oauth_parameters) + 56 + 57 + 58 temp = {} + 59 temp.update(oauth_parameters) + 60 for p in self.FORBIDDEN: + 61 if p in temp: + 62 del temp[p] + 63 if parameters is not None: + 64 temp.update(parameters) + 65 sig = ( + 66 escape(self.get_normalized_http_method(request)), + 67 escape(self.get_normalized_http_url(request)), + 68 self.get_normalized_parameters(temp), # these are escaped in the method already + 69 ) + 70 + 71 key = '%s&' % consumer_secret + 72 if token_secret is not None: + 73 key += token_secret + 74 raw = '&'.join(sig) + 75 logger.debug("raw basestring: %s", raw) + 76 logger.debug("key: %s", key) + 77 # hmac object + 78 hashed = hmac.new(key, raw, hashlib.sha1) + 79 # calculate the digest base 64 + 80 signature = escape(base64.b64encode(hashed.digest())) + 81 return signature +
82 + 83 +
84 - def get_normalized_http_method(self, request): +
85 return request.get_method().upper() +
86 + 87 + 88 # parses the url and rebuilds it to be scheme://host/path +
89 - def get_normalized_http_url(self, request): +
90 url = request.get_full_url() + 91 parts = urlparse.urlparse(url) + 92 url_string = '%s://%s%s' % (parts.scheme, parts.netloc, parts.path) + 93 return url_string +
94 + 95 +
96 - def get_normalized_parameters(self, params): +
97 if params is None: + 98 params = {} + 99 try: +100 # exclude the signature if it exists +101 del params['oauth_signature'] +102 except: +103 pass +104 key_values = [] +105 +106 for key, values in params.iteritems(): +107 if isinstance(values, file): +108 continue +109 if isinstance(values, (int, long, float)): +110 values = str(values) +111 if isinstance(values, (list, tuple)): +112 values = [str(v) for v in values] +113 if isinstance(values, basestring): +114 values = [values] +115 if USE_DOUBLE_ESCAPE_HACK and not key.startswith("ouath"): +116 key = escape(key) +117 for v in values: +118 v = v.encode("utf-8") +119 key = key.encode("utf-8") +120 if USE_DOUBLE_ESCAPE_HACK and not key.startswith("oauth"): +121 # this is a dirty hack to make the +122 # thing work with the current server-side +123 # implementation. Or is it by spec? +124 v = escape(v) +125 key_values.append(escape("%s=%s" % (key, v))) +126 # sort lexicographically, first after key, then after value +127 key_values.sort() +128 # combine key value pairs in string +129 return escape('&').join(key_values) +
130 +131 +
132 -class OAuthAuthenticator(object): +
133 OAUTH_API_VERSION = '1.0' +134 AUTHORIZATION_HEADER = "Authorization" +135 +
136 - def __init__(self, consumer, consumer_secret, token, secret, signature_method=OAuthSignatureMethod_HMAC_SHA1()): +
137 self._consumer, self._token, self._secret = consumer, token, secret +138 self._consumer_secret = consumer_secret +139 self._signature_method = signature_method +140 random.seed() +
141 +142 +
143 - def augment_request(self, req, parameters, use_multipart=False, oauth_callback=None, oauth_verifier=None): +
144 oauth_parameters = { +145 'oauth_consumer_key': self._consumer, +146 'oauth_timestamp': self.generate_timestamp(), +147 'oauth_nonce': self.generate_nonce(), +148 'oauth_version': self.OAUTH_API_VERSION, +149 'oauth_signature_method' : self._signature_method.get_name(), +150 #'realm' : "http://soundcloud.com", +151 } +152 if self._token is not None: +153 oauth_parameters['oauth_token'] = self._token +154 +155 if oauth_callback is not None: +156 oauth_parameters['oauth_callback'] = oauth_callback +157 +158 if oauth_verifier is not None: +159 oauth_parameters['oauth_verifier'] = oauth_verifier +160 +161 # in case we upload large files, we don't +162 # sign the request over the parameters +163 if use_multipart: +164 parameters = None +165 +166 oauth_parameters['oauth_signature'] = self._signature_method.build_signature(req, +167 parameters, +168 self._consumer_secret, +169 self._secret, +170 oauth_parameters) +171 def to_header(d): +172 return ",".join('%s="%s"' % (key, value) for key, value in sorted(oauth_parameters.items())) +
173 +174 req.add_header(self.AUTHORIZATION_HEADER, "OAuth %s" % to_header(oauth_parameters)) +
175 +
176 - def generate_timestamp(self): +
177 return int(time.time())# * 1000.0) +
178 +
179 - def generate_nonce(self, length=8): +
180 return ''.join(str(random.randint(0, 9)) for i in range(length)) +
181 +182 +
183 -class BasicAuthenticator(object): +
184 +
185 - def __init__(self, user, password, consumer, consumer_secret): +
186 self._base64string = base64.encodestring("%s:%s" % (user, password))[:-1] +187 self._x_auth_header = 'OAuth oauth_consumer_key="%s" oauth_consumer_secret="%s"' % (consumer, consumer_secret) +
188 +
189 - def augment_request(self, req, parameters): +
190 req.add_header("Authorization", "Basic %s" % self._base64string) +191 req.add_header("X-Authorization", self._x_auth_header) +
192 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.authentication.BasicAuthenticator-class.html b/python_apps/soundcloud-api/docs/api/scapi.authentication.BasicAuthenticator-class.html new file mode 100644 index 000000000..437ef8444 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.authentication.BasicAuthenticator-class.html @@ -0,0 +1,267 @@ + + + + + scapi.authentication.BasicAuthenticator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module authentication :: + Class BasicAuthenticator + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class BasicAuthenticator

source code

+
+object --+
+         |
+        BasicAuthenticator
+
+ +
+ + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + user, + password, + consumer, + consumer_secret)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
augment_request(self, + req, + parameters) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + user, + password, + consumer, + consumer_secret) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthAuthenticator-class.html b/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthAuthenticator-class.html new file mode 100644 index 000000000..de4541aa2 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthAuthenticator-class.html @@ -0,0 +1,337 @@ + + + + + scapi.authentication.OAuthAuthenticator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module authentication :: + Class OAuthAuthenticator + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class OAuthAuthenticator

source code

+
+object --+
+         |
+        OAuthAuthenticator
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + consumer, + consumer_secret, + token, + secret, + signature_method=OAuthSignatureMethod_HMAC_SHA1())
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
augment_request(self, + req, + parameters, + use_multipart=False, + oauth_callback=None, + oauth_verifier=None) + source code + +
+ +
+   + + + + + + +
generate_timestamp(self) + source code + +
+ +
+   + + + + + + +
generate_nonce(self, + length=8) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + OAUTH_API_VERSION = '1.0' +
+   + + AUTHORIZATION_HEADER = 'Authorization' +
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + consumer, + consumer_secret, + token, + secret, + signature_method=OAuthSignatureMethod_HMAC_SHA1()) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html b/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html new file mode 100644 index 000000000..82e3a5ee0 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.authentication.OAuthSignatureMethod_HMAC_SHA1-class.html @@ -0,0 +1,294 @@ + + + + + scapi.authentication.OAuthSignatureMethod_HMAC_SHA1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module authentication :: + Class OAuthSignatureMethod_HMAC_SHA1 + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class OAuthSignatureMethod_HMAC_SHA1

source code

+
+object --+
+         |
+        OAuthSignatureMethod_HMAC_SHA1
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
get_name(self) + source code + +
+ +
+   + + + + + + +
build_signature(self, + request, + parameters, + consumer_secret, + token_secret, + oauth_parameters) + source code + +
+ +
+   + + + + + + +
get_normalized_http_method(self, + request) + source code + +
+ +
+   + + + + + + +
get_normalized_http_url(self, + request) + source code + +
+ +
+   + + + + + + +
get_normalized_parameters(self, + params) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __init__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + FORBIDDEN = ['realm', 'oauth_signature'] +
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.config-module.html b/python_apps/soundcloud-api/docs/api/scapi.config-module.html new file mode 100644 index 000000000..41aa49291 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.config-module.html @@ -0,0 +1,114 @@ + + + + + scapi.config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module config + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module config

source code

+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.config-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.config-pysrc.html new file mode 100644 index 000000000..27eedf60c --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.config-pysrc.html @@ -0,0 +1,122 @@ + + + + + scapi.config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module config + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.config

+
+1   
+2   
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json-module.html b/python_apps/soundcloud-api/docs/api/scapi.json-module.html new file mode 100644 index 000000000..9ac8f0d48 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json-module.html @@ -0,0 +1,218 @@ + + + + + scapi.json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module json

source code

+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Classes[hide private]
+
+   + + _StringGenerator +
+   + + WriteException +
+   + + ReadException +
+   + + JsonReader +
+   + + JsonWriter +
+ + + + + + + + + + + + +
+ + + + + +
Functions[hide private]
+
+   + + + + + + +
write(obj, + escaped_forward_slash=False) + source code + +
+ +
+   + + + + + + +
read(s) + source code + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.json-pysrc.html new file mode 100644 index 000000000..cde009caf --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json-pysrc.html @@ -0,0 +1,433 @@ + + + + + scapi.json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.json

+
+  1  import string 
+  2  import types 
+  3   
+  4  ##    json.py implements a JSON (http://json.org) reader and writer. 
+  5  ##    Copyright (C) 2005  Patrick D. Logan 
+  6  ##    Contact mailto:patrickdlogan@stardecisions.com 
+  7  ## 
+  8  ##    This library is free software; you can redistribute it and/or 
+  9  ##    modify it under the terms of the GNU Lesser General Public 
+ 10  ##    License as published by the Free Software Foundation; either 
+ 11  ##    version 2.1 of the License, or (at your option) any later version. 
+ 12  ## 
+ 13  ##    This library is distributed in the hope that it will be useful, 
+ 14  ##    but WITHOUT ANY WARRANTY; without even the implied warranty of 
+ 15  ##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
+ 16  ##    Lesser General Public License for more details. 
+ 17  ## 
+ 18  ##    You should have received a copy of the GNU Lesser General Public 
+ 19  ##    License along with this library; if not, write to the Free Software 
+ 20  ##    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
+ 21   
+ 22   
+
23 -class _StringGenerator(object): +
24 - def __init__(self, string): +
25 self.string = string + 26 self.index = -1 +
27 - def peek(self): +
28 i = self.index + 1 + 29 if i < len(self.string): + 30 return self.string[i] + 31 else: + 32 return None +
33 - def next(self): +
34 self.index += 1 + 35 if self.index < len(self.string): + 36 return self.string[self.index] + 37 else: + 38 raise StopIteration +
39 - def all(self): +
40 return self.string +
41 +
42 -class WriteException(Exception): +
43 pass +
44 +
45 -class ReadException(Exception): +
46 pass +
47 +
48 -class JsonReader(object): +
49 hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15} + 50 escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'} + 51 +
52 - def read(self, s): +
53 self._generator = _StringGenerator(s) + 54 result = self._read() + 55 return result +
56 +
57 - def _read(self): +
58 self._eatWhitespace() + 59 peek = self._peek() + 60 if peek is None: + 61 raise ReadException, "Nothing to read: '%s'" % self._generator.all() + 62 if peek == '{': + 63 return self._readObject() + 64 elif peek == '[': + 65 return self._readArray() + 66 elif peek == '"': + 67 return self._readString() + 68 elif peek == '-' or peek.isdigit(): + 69 return self._readNumber() + 70 elif peek == 't': + 71 return self._readTrue() + 72 elif peek == 'f': + 73 return self._readFalse() + 74 elif peek == 'n': + 75 return self._readNull() + 76 elif peek == '/': + 77 self._readComment() + 78 return self._read() + 79 else: + 80 raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all() +
81 +
82 - def _readTrue(self): +
83 self._assertNext('t', "true") + 84 self._assertNext('r', "true") + 85 self._assertNext('u', "true") + 86 self._assertNext('e', "true") + 87 return True +
88 +
89 - def _readFalse(self): +
90 self._assertNext('f', "false") + 91 self._assertNext('a', "false") + 92 self._assertNext('l', "false") + 93 self._assertNext('s', "false") + 94 self._assertNext('e', "false") + 95 return False +
96 +
97 - def _readNull(self): +
98 self._assertNext('n', "null") + 99 self._assertNext('u', "null") +100 self._assertNext('l', "null") +101 self._assertNext('l', "null") +102 return None +
103 +
104 - def _assertNext(self, ch, target): +
105 if self._next() != ch: +106 raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all()) +
107 +
108 - def _readNumber(self): +
109 isfloat = False +110 result = self._next() +111 peek = self._peek() +112 while peek is not None and (peek.isdigit() or peek == "."): +113 isfloat = isfloat or peek == "." +114 result = result + self._next() +115 peek = self._peek() +116 try: +117 if isfloat: +118 return float(result) +119 else: +120 return int(result) +121 except ValueError: +122 raise ReadException, "Not a valid JSON number: '%s'" % result +
123 +
124 - def _readString(self): +
125 result = "" +126 assert self._next() == '"' +127 try: +128 while self._peek() != '"': +129 ch = self._next() +130 if ch == "\\": +131 ch = self._next() +132 if ch in 'brnft': +133 ch = self.escapes[ch] +134 elif ch == "u": +135 ch4096 = self._next() +136 ch256 = self._next() +137 ch16 = self._next() +138 ch1 = self._next() +139 n = 4096 * self._hexDigitToInt(ch4096) +140 n += 256 * self._hexDigitToInt(ch256) +141 n += 16 * self._hexDigitToInt(ch16) +142 n += self._hexDigitToInt(ch1) +143 ch = unichr(n) +144 elif ch not in '"/\\': +145 raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all()) +146 result = result + ch +147 except StopIteration: +148 raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all() +149 assert self._next() == '"' +150 return result +
151 +
152 - def _hexDigitToInt(self, ch): +
153 try: +154 result = self.hex_digits[ch.upper()] +155 except KeyError: +156 try: +157 result = int(ch) +158 except ValueError: +159 raise ReadException, "The character %s is not a hex digit." % ch +160 return result +
161 +
162 - def _readComment(self): +
163 assert self._next() == "/" +164 second = self._next() +165 if second == "/": +166 self._readDoubleSolidusComment() +167 elif second == '*': +168 self._readCStyleComment() +169 else: +170 raise ReadException, "Not a valid JSON comment: %s" % self._generator.all() +
171 +
172 - def _readCStyleComment(self): +
173 try: +174 done = False +175 while not done: +176 ch = self._next() +177 done = (ch == "*" and self._peek() == "/") +178 if not done and ch == "/" and self._peek() == "*": +179 raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all() +180 self._next() +181 except StopIteration: +182 raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all() +
183 +
184 - def _readDoubleSolidusComment(self): +
185 try: +186 ch = self._next() +187 while ch != "\r" and ch != "\n": +188 ch = self._next() +189 except StopIteration: +190 pass +
191 +
192 - def _readArray(self): +
193 result = [] +194 assert self._next() == '[' +195 done = self._peek() == ']' +196 while not done: +197 item = self._read() +198 result.append(item) +199 self._eatWhitespace() +200 done = self._peek() == ']' +201 if not done: +202 ch = self._next() +203 if ch != ",": +204 raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) +205 assert ']' == self._next() +206 return result +
207 +
208 - def _readObject(self): +
209 result = {} +210 assert self._next() == '{' +211 done = self._peek() == '}' +212 while not done: +213 key = self._read() +214 if type(key) is not types.StringType: +215 raise ReadException, "Not a valid JSON object key (should be a string): %s" % key +216 self._eatWhitespace() +217 ch = self._next() +218 if ch != ":": +219 raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch) +220 self._eatWhitespace() +221 val = self._read() +222 result[key] = val +223 self._eatWhitespace() +224 done = self._peek() == '}' +225 if not done: +226 ch = self._next() +227 if ch != ",": +228 raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) +229 assert self._next() == "}" +230 return result +
231 +
232 - def _eatWhitespace(self): +
233 p = self._peek() +234 while p is not None and p in string.whitespace or p == '/': +235 if p == '/': +236 self._readComment() +237 else: +238 self._next() +239 p = self._peek() +
240 +
241 - def _peek(self): +
242 return self._generator.peek() +
243 +
244 - def _next(self): +
245 return self._generator.next() +
246 +
247 -class JsonWriter(object): +
248 +
249 - def _append(self, s): +
250 self._results.append(s) +
251 +
252 - def write(self, obj, escaped_forward_slash=False): +
253 self._escaped_forward_slash = escaped_forward_slash +254 self._results = [] +255 self._write(obj) +256 return "".join(self._results) +
257 +
258 - def _write(self, obj): +
259 ty = type(obj) +260 if ty is types.DictType: +261 n = len(obj) +262 self._append("{") +263 for k, v in obj.items(): +264 self._write(k) +265 self._append(":") +266 self._write(v) +267 n = n - 1 +268 if n > 0: +269 self._append(",") +270 self._append("}") +271 elif ty is types.ListType or ty is types.TupleType: +272 n = len(obj) +273 self._append("[") +274 for item in obj: +275 self._write(item) +276 n = n - 1 +277 if n > 0: +278 self._append(",") +279 self._append("]") +280 elif ty is types.StringType or ty is types.UnicodeType: +281 self._append('"') +282 obj = obj.replace('\\', r'\\') +283 if self._escaped_forward_slash: +284 obj = obj.replace('/', r'\/') +285 obj = obj.replace('"', r'\"') +286 obj = obj.replace('\b', r'\b') +287 obj = obj.replace('\f', r'\f') +288 obj = obj.replace('\n', r'\n') +289 obj = obj.replace('\r', r'\r') +290 obj = obj.replace('\t', r'\t') +291 self._append(obj) +292 self._append('"') +293 elif ty is types.IntType or ty is types.LongType: +294 self._append(str(obj)) +295 elif ty is types.FloatType: +296 self._append("%f" % obj) +297 elif obj is True: +298 self._append("true") +299 elif obj is False: +300 self._append("false") +301 elif obj is None: +302 self._append("null") +303 else: +304 raise WriteException, "Cannot write in JSON: %s" % repr(obj) +
305 +
306 -def write(obj, escaped_forward_slash=False): +
307 return JsonWriter().write(obj, escaped_forward_slash) +
308 +
309 -def read(s): +
310 return JsonReader().read(s) +
311 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json.JsonReader-class.html b/python_apps/soundcloud-api/docs/api/scapi.json.JsonReader-class.html new file mode 100644 index 000000000..bad1801ae --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json.JsonReader-class.html @@ -0,0 +1,544 @@ + + + + + scapi.json.JsonReader + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json :: + Class JsonReader + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class JsonReader

source code

+
+object --+
+         |
+        JsonReader
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
read(self, + s) + source code + +
+ +
+   + + + + + + +
_read(self) + source code + +
+ +
+   + + + + + + +
_readTrue(self) + source code + +
+ +
+   + + + + + + +
_readFalse(self) + source code + +
+ +
+   + + + + + + +
_readNull(self) + source code + +
+ +
+   + + + + + + +
_assertNext(self, + ch, + target) + source code + +
+ +
+   + + + + + + +
_readNumber(self) + source code + +
+ +
+   + + + + + + +
_readString(self) + source code + +
+ +
+   + + + + + + +
_hexDigitToInt(self, + ch) + source code + +
+ +
+   + + + + + + +
_readComment(self) + source code + +
+ +
+   + + + + + + +
_readCStyleComment(self) + source code + +
+ +
+   + + + + + + +
_readDoubleSolidusComment(self) + source code + +
+ +
+   + + + + + + +
_readArray(self) + source code + +
+ +
+   + + + + + + +
_readObject(self) + source code + +
+ +
+   + + + + + + +
_eatWhitespace(self) + source code + +
+ +
+   + + + + + + +
_peek(self) + source code + +
+ +
+   + + + + + + +
_next(self) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __init__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + hex_digits = {'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F'... +
+   + + escapes = {'b': '\x08', 'f': '\x0c', 'n': '\n', 'r': '\r', 't'... +
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Class Variable Details[hide private]
+
+ +
+ +
+

hex_digits

+ +
+
+
+
Value:
+
+{'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15}
+
+
+
+
+
+ +
+ +
+

escapes

+ +
+
+
+
Value:
+
+{'b': '\x08', 'f': '\x0c', 'n': '\n', 'r': '\r', 't': '\t'}
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json.JsonWriter-class.html b/python_apps/soundcloud-api/docs/api/scapi.json.JsonWriter-class.html new file mode 100644 index 000000000..c376a942f --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json.JsonWriter-class.html @@ -0,0 +1,233 @@ + + + + + scapi.json.JsonWriter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json :: + Class JsonWriter + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class JsonWriter

source code

+
+object --+
+         |
+        JsonWriter
+
+ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
_append(self, + s) + source code + +
+ +
+   + + + + + + +
write(self, + obj, + escaped_forward_slash=False) + source code + +
+ +
+   + + + + + + +
_write(self, + obj) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __init__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json.ReadException-class.html b/python_apps/soundcloud-api/docs/api/scapi.json.ReadException-class.html new file mode 100644 index 000000000..acbf8e4c3 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json.ReadException-class.html @@ -0,0 +1,196 @@ + + + + + scapi.json.ReadException + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json :: + Class ReadException + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class ReadException

source code

+
+              object --+        
+                       |        
+exceptions.BaseException --+    
+                           |    
+        exceptions.Exception --+
+                               |
+                              ReadException
+
+ +
+ + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from exceptions.Exception: + __init__, + __new__ +

+

Inherited from exceptions.BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __repr__, + __setattr__, + __setstate__, + __str__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from exceptions.BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json.WriteException-class.html b/python_apps/soundcloud-api/docs/api/scapi.json.WriteException-class.html new file mode 100644 index 000000000..b97e08ff6 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json.WriteException-class.html @@ -0,0 +1,196 @@ + + + + + scapi.json.WriteException + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json :: + Class WriteException + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class WriteException

source code

+
+              object --+        
+                       |        
+exceptions.BaseException --+    
+                           |    
+        exceptions.Exception --+
+                               |
+                              WriteException
+
+ +
+ + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+

Inherited from exceptions.Exception: + __init__, + __new__ +

+

Inherited from exceptions.BaseException: + __delattr__, + __getattribute__, + __getitem__, + __getslice__, + __reduce__, + __repr__, + __setattr__, + __setstate__, + __str__ +

+

Inherited from object: + __hash__, + __reduce_ex__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from exceptions.BaseException: + args, + message +

+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.json._StringGenerator-class.html b/python_apps/soundcloud-api/docs/api/scapi.json._StringGenerator-class.html new file mode 100644 index 000000000..ead736d6f --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.json._StringGenerator-class.html @@ -0,0 +1,291 @@ + + + + + scapi.json._StringGenerator + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module json :: + Class _StringGenerator + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class _StringGenerator

source code

+
+object --+
+         |
+        _StringGenerator
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
__init__(self, + string)
+ x.__init__(...) initializes x; see x.__class__.__doc__ for signature
+ source code + +
+ +
+   + + + + + + +
peek(self) + source code + +
+ +
+   + + + + + + +
next(self) + source code + +
+ +
+   + + + + + + +
all(self) + source code + +
+ +
+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __repr__, + __setattr__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

__init__(self, + string) +
(Constructor) +

+
source code  +
+ +

x.__init__(...) initializes x; see x.__class__.__doc__ for + signature

+
+
Overrides: + object.__init__ +
(inherited documentation)
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests-module.html b/python_apps/soundcloud-api/docs/api/scapi.tests-module.html new file mode 100644 index 000000000..41a0acbfc --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests-module.html @@ -0,0 +1,140 @@ + + + + + scapi.tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Package tests

source code

+ + + + + + + +
+ + + + + +
Submodules[hide private]
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.tests-pysrc.html new file mode 100644 index 000000000..a967c5152 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests-pysrc.html @@ -0,0 +1,122 @@ + + + + + scapi.tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Package scapi.tests

+
+1   
+2   
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-module.html b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-module.html new file mode 100644 index 000000000..cff686d84 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-module.html @@ -0,0 +1,172 @@ + + + + + scapi.tests.scapi_tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module scapi_tests + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module scapi_tests

source code

+ + + + + + + + + +
+ + + + + +
Classes[hide private]
+
+   + + SCAPITests +
+ + + + + + + + + + + + +
+ + + + + +
Variables[hide private]
+
+   + + logger = logging.getLogger("scapi.tests") +
+   + + api_logger = logging.getLogger("scapi") +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-pysrc.html new file mode 100644 index 000000000..bca835c59 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests-pysrc.html @@ -0,0 +1,760 @@ + + + + + scapi.tests.scapi_tests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module scapi_tests + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.tests.scapi_tests

+
+  1  from __future__ import with_statement 
+  2   
+  3  import os 
+  4  import urllib2 
+  5  import itertools 
+  6  from textwrap import dedent 
+  7  import pkg_resources 
+  8  import logging 
+  9  import webbrowser 
+ 10  from unittest import TestCase 
+ 11   
+ 12  from configobj import ConfigObj 
+ 13  from validate import Validator 
+ 14   
+ 15   
+ 16  import scapi 
+ 17  import scapi.authentication 
+ 18   
+ 19  logger = logging.getLogger("scapi.tests") 
+ 20   
+ 21  api_logger = logging.getLogger("scapi") 
+
22 + 23 + 24 -class SCAPITests(TestCase): +
25 + 26 CONFIG_NAME = "test.ini" + 27 TOKEN = None + 28 SECRET = None + 29 CONSUMER = None + 30 CONSUMER_SECRET = None + 31 API_HOST = None + 32 USER = None + 33 PASSWORD = None + 34 AUTHENTICATOR = None + 35 RUN_INTERACTIVE_TESTS = False + 36 + 37 +
38 - def setUp(self): +
39 self._load_config() + 40 assert pkg_resources.resource_exists("scapi.tests.test_connect", "knaster.mp3") + 41 self.data = pkg_resources.resource_stream("scapi.tests.test_connect", "knaster.mp3") + 42 self.artwork_data = pkg_resources.resource_stream("scapi.tests.test_connect", "spam.jpg") +
43 + 44 CONFIGSPEC=dedent(""" + 45 [api] + 46 token=string + 47 secret=string + 48 consumer=string + 49 consumer_secret=string + 50 api_host=string + 51 user=string + 52 password=string + 53 authenticator=option('oauth', 'base', default='oauth') + 54 + 55 [proxy] + 56 use_proxy=boolean(default=false) + 57 proxy=string(default=http://127.0.0.1:10000/) + 58 + 59 [logging] + 60 test_logger=string(default=ERROR) + 61 api_logger=string(default=ERROR) + 62 + 63 [test] + 64 run_interactive_tests=boolean(default=false) + 65 """) + 66 + 67 +
68 - def _load_config(self): +
69 """ + 70 Loads the configuration by looking from + 71 + 72 - the environment variable SCAPI_CONFIG + 73 - the installation location upwards until it finds test.ini + 74 - the current working directory upwards until it finds test.ini + 75 + 76 Raises an error if there is no config found + 77 """ + 78 config_name = self.CONFIG_NAME + 79 + 80 name = None + 81 + 82 if "SCAPI_CONFIG" in os.environ: + 83 if os.path.exists(os.environ["SCAPI_CONFIG"]): + 84 name = os.environ["SCAPI_CONFIG"] + 85 + 86 def search_for_config(current): + 87 while current: + 88 name = os.path.join(current, config_name) + 89 if os.path.exists(name): + 90 return name + 91 new_current = os.path.dirname(current) + 92 if new_current == current: + 93 return + 94 current = new_current +
95 + 96 if name is None: + 97 name = search_for_config(os.path.dirname(__file__)) + 98 if name is None: + 99 name = search_for_config(os.getcwd()) +100 +101 if not name: +102 raise Exception("No test configuration file found!") +103 +104 parser = ConfigObj(name, configspec=self.CONFIGSPEC.split("\n")) +105 val = Validator() +106 if not parser.validate(val): +107 raise Exception("Config file validation error") +108 +109 api = parser['api'] +110 self.TOKEN = api.get('token') +111 self.SECRET = api.get('secret') +112 self.CONSUMER = api.get('consumer') +113 self.CONSUMER_SECRET = api.get('consumer_secret') +114 self.API_HOST = api.get('api_host') +115 self.USER = api.get('user', None) +116 self.PASSWORD = api.get('password', None) +117 self.AUTHENTICATOR = api.get("authenticator") +118 +119 # reset the hard-coded values in the api +120 if self.API_HOST: +121 scapi.AUTHORIZATION_URL = "http://%s/oauth/authorize" % self.API_HOST +122 scapi.REQUEST_TOKEN_URL = 'http://%s/oauth/request_token' % self.API_HOST +123 scapi.ACCESS_TOKEN_URL = 'http://%s/oauth/access_token' % self.API_HOST +124 +125 if "proxy" in parser and parser["proxy"]["use_proxy"]: +126 scapi.USE_PROXY = True +127 scapi.PROXY = parser["proxy"]["proxy"] +128 +129 if "logging" in parser: +130 logger.setLevel(getattr(logging, parser["logging"]["test_logger"])) +131 api_logger.setLevel(getattr(logging, parser["logging"]["api_logger"])) +132 +133 self.RUN_INTERACTIVE_TESTS = parser["test"]["run_interactive_tests"] +
134 +135 +136 @property +
137 - def root(self): +
138 """ +139 Return the properly configured root-scope. +140 """ +141 if self.AUTHENTICATOR == "oauth": +142 authenticator = scapi.authentication.OAuthAuthenticator(self.CONSUMER, +143 self.CONSUMER_SECRET, +144 self.TOKEN, +145 self.SECRET) +146 elif self.AUTHENTICATOR == "base": +147 authenticator = scapi.authentication.BasicAuthenticator(self.USER, self.PASSWORD, self.CONSUMER, self.CONSUMER_SECRET) +148 else: +149 raise Exception("Unknown authenticator setting: %s", self.AUTHENTICATOR) +150 +151 connector = scapi.ApiConnector(host=self.API_HOST, +152 authenticator=authenticator) +153 +154 logger.debug("RootScope: %s authenticator: %s", self.API_HOST, self.AUTHENTICATOR) +155 return scapi.Scope(connector) +
156 +157 +
158 - def test_connect(self): +
159 """ +160 test_connect +161 +162 Tries to connect & performs some read-only operations. +163 """ +164 sca = self.root +165 # quite_a_few_users = list(itertools.islice(sca.users(), 0, 127)) +166 +167 # logger.debug(quite_a_few_users) +168 # assert isinstance(quite_a_few_users, list) and isinstance(quite_a_few_users[0], scapi.User) +169 user = sca.me() +170 logger.debug(user) +171 assert isinstance(user, scapi.User) +172 contacts = list(user.contacts()) +173 assert isinstance(contacts, list) +174 if contacts: +175 assert isinstance(contacts[0], scapi.User) +176 logger.debug(contacts) +177 tracks = list(user.tracks()) +178 assert isinstance(tracks, list) +179 if tracks: +180 assert isinstance(tracks[0], scapi.Track) +181 logger.debug(tracks) +
182 +183 +
185 """ +186 This test is commented out because it needs user-interaction. +187 """ +188 if not self.RUN_INTERACTIVE_TESTS: +189 return +190 oauth_authenticator = scapi.authentication.OAuthAuthenticator(self.CONSUMER, +191 self.CONSUMER_SECRET, +192 None, +193 None) +194 +195 sca = scapi.ApiConnector(host=self.API_HOST, authenticator=oauth_authenticator) +196 token, secret = sca.fetch_request_token() +197 authorization_url = sca.get_request_token_authorization_url(token) +198 webbrowser.open(authorization_url) +199 oauth_verifier = raw_input("please enter verifier code as seen in the browser:") +200 +201 oauth_authenticator = scapi.authentication.OAuthAuthenticator(self.CONSUMER, +202 self.CONSUMER_SECRET, +203 token, +204 secret) +205 +206 sca = scapi.ApiConnector(self.API_HOST, authenticator=oauth_authenticator) +207 token, secret = sca.fetch_access_token(oauth_verifier) +208 logger.info("Access token: '%s'", token) +209 logger.info("Access token secret: '%s'", secret) +210 # force oauth-authentication with the new parameters, and +211 # then invoke some simple test +212 self.AUTHENTICATOR = "oauth" +213 self.TOKEN = token +214 self.SECRET = secret +215 self.test_connect() +
216 +217 +
218 - def test_track_creation(self): +
219 sca = self.root +220 track = sca.Track.new(title='bar', asset_data=self.data) +221 assert isinstance(track, scapi.Track) +
222 +223 +
224 - def test_track_update(self): +
225 sca = self.root +226 track = sca.Track.new(title='bar', asset_data=self.data) +227 assert isinstance(track, scapi.Track) +228 track.title='baz' +229 track = sca.Track.get(track.id) +230 assert track.title == "baz" +
231 +232 +
233 - def test_scoped_track_creation(self): +
234 sca = self.root +235 user = sca.me() +236 track = user.tracks.new(title="bar", asset_data=self.data) +237 assert isinstance(track, scapi.Track) +
238 +239 +
240 - def test_upload(self): +
241 sca = self.root +242 sca = self.root +243 track = sca.Track.new(title='bar', asset_data=self.data) +244 assert isinstance(track, scapi.Track) +
245 +246 +
247 - def test_contact_list(self): +
248 sca = self.root +249 user = sca.me() +250 contacts = list(user.contacts()) +251 assert isinstance(contacts, list) +252 if contacts: +253 assert isinstance(contacts[0], scapi.User) +
254 +255 +
256 - def test_permissions(self): +
257 sca = self.root +258 user = sca.me() +259 tracks = itertools.islice(user.tracks(), 1) +260 for track in tracks: +261 permissions = list(track.permissions()) +262 logger.debug(permissions) +263 assert isinstance(permissions, list) +264 if permissions: +265 assert isinstance(permissions[0], scapi.User) +
266 +267 +
268 - def test_setting_permissions(self): +
269 sca = self.root +270 me = sca.me() +271 track = sca.Track.new(title='bar', sharing="private", asset_data=self.data) +272 assert track.sharing == "private" +273 users = itertools.islice(sca.users(), 10) +274 users_to_set = [user for user in users if user != me] +275 assert users_to_set, "Didn't find any suitable users" +276 track.permissions = users_to_set +277 assert set(track.permissions()) == set(users_to_set) +
278 +279 +
280 - def test_setting_comments(self): +
281 sca = self.root +282 user = sca.me() +283 track = sca.Track.new(title='bar', sharing="private", asset_data=self.data) +284 comment = sca.Comment.create(body="This is the body of my comment", timestamp=10) +285 track.comments = comment +286 assert track.comments().next().body == comment.body +
287 +288 +
290 sca = self.root +291 track = sca.Track.new(title='bar', sharing="private", asset_data=self.data) +292 cbody = "This is the body of my comment" +293 track.comments.new(body=cbody, timestamp=10) +294 assert list(track.comments())[0].body == cbody +
295 +296 +
298 sca = self.root +299 me = sca.me() +300 for user in sca.users(): +301 if user != me: +302 user_to_set = user +303 break +304 +305 contacts = list(me.contacts()) +306 if user_to_set in contacts: +307 me.contacts.remove(user_to_set) +308 +309 me.contacts.append(user_to_set) +310 +311 contacts = list(me.contacts() ) +312 assert user_to_set.id in [c.id for c in contacts] +313 +314 me.contacts.remove(user_to_set) +315 +316 contacts = list(me.contacts() ) +317 assert user_to_set not in contacts +
318 +319 +
320 - def test_favorites(self): +
321 sca = self.root +322 me = sca.me() +323 +324 favorites = list(me.favorites()) +325 assert favorites == [] or isinstance(favorites[0], scapi.Track) +326 +327 track = None +328 for user in sca.users(): +329 if user == me: +330 continue +331 for track in user.tracks(): +332 break +333 if track is not None: +334 break +335 +336 me.favorites.append(track) +337 +338 favorites = list(me.favorites()) +339 assert track in favorites +340 +341 me.favorites.remove(track) +342 +343 favorites = list(me.favorites()) +344 assert track not in favorites +
345 +346 +
347 - def test_large_list(self): +
348 sca = self.root +349 +350 tracks = list(sca.tracks()) +351 if len(tracks) < scapi.ApiConnector.LIST_LIMIT: +352 for i in xrange(scapi.ApiConnector.LIST_LIMIT): +353 sca.Track.new(title='test_track_%i' % i, asset_data=self.data) +354 all_tracks = sca.tracks() +355 assert not isinstance(all_tracks, list) +356 all_tracks = list(all_tracks) +357 assert len(all_tracks) > scapi.ApiConnector.LIST_LIMIT +
358 +359 +360 +
361 - def test_filtered_list(self): +
362 sca = self.root +363 +364 tracks = list(sca.tracks(params={ +365 "bpm[from]" : "180", +366 })) +367 if len(tracks) < scapi.ApiConnector.LIST_LIMIT: +368 for i in xrange(scapi.ApiConnector.LIST_LIMIT): +369 sca.Track.new(title='test_track_%i' % i, asset_data=self.data) +370 all_tracks = sca.tracks() +371 assert not isinstance(all_tracks, list) +372 all_tracks = list(all_tracks) +373 assert len(all_tracks) > scapi.ApiConnector.LIST_LIMIT +
374 +375 +
376 - def test_events(self): +
377 events = list(self.root.events()) +378 assert isinstance(events, list) +379 assert isinstance(events[0], scapi.Event) +
380 +381 +
382 - def test_me_having_stress(self): +
383 sca = self.root +384 for _ in xrange(20): +385 self.setUp() +386 sca.me() +
387 +388 +
389 - def test_non_global_api(self): +
390 root = self.root +391 me = root.me() +392 assert isinstance(me, scapi.User) +393 +394 # now get something *from* that user +395 list(me.favorites()) +
396 +397 +
398 - def test_playlists(self): +
399 sca = self.root +400 playlists = list(itertools.islice(sca.playlists(), 0, 127)) +401 for playlist in playlists: +402 tracks = playlist.tracks +403 if not isinstance(tracks, list): +404 tracks = [tracks] +405 for trackdata in tracks: +406 print trackdata +407 #user = trackdata.user +408 #print user +409 #print user.tracks() +410 print playlist.user +411 break +
412 +413 +414 +415 +
416 - def test_playlist_creation(self): +
417 sca = self.root +418 sca.Playlist.new(title="I'm so happy, happy, happy, happy!") +
419 +420 +421 +
422 - def test_groups(self): +
423 sca = self.root +424 groups = list(itertools.islice(sca.groups(), 0, 127)) +425 for group in groups: +426 users = group.users() +427 for user in users: +428 pass +
429 +430 +
432 sca = self.root +433 emails = [dict(address="deets@web.de"), dict(address="hannes@soundcloud.com")] +434 track = sca.Track.new(title='bar', asset_data=self.data, +435 shared_to=dict(emails=emails) +436 ) +437 assert isinstance(track, scapi.Track) +
438 +439 +440 +
442 sca = self.root +443 track = sca.Track.new(title='bar', +444 asset_data=self.data, +445 artwork_data=self.artwork_data, +446 ) +447 assert isinstance(track, scapi.Track) +448 +449 track.title = "foobarbaz" +
450 +451 +452 +
453 - def test_oauth_get_signing(self): +
454 sca = self.root +455 +456 url = "http://api.soundcloud.dev/oauth/test_request" +457 params = dict(foo="bar", +458 baz="padamm", +459 ) +460 url += sca._create_query_string(params) +461 signed_url = sca.oauth_sign_get_request(url) +462 +463 +464 res = urllib2.urlopen(signed_url).read() +465 assert "oauth_nonce" in res +
466 +467 +
468 - def test_streaming(self): +
469 sca = self.root +470 +471 track = sca.tracks(params={ +472 "filter" : "streamable", +473 }).next() +474 +475 +476 assert isinstance(track, scapi.Track) +477 +478 stream_url = track.stream_url +479 +480 signed_url = track.oauth_sign_get_request(stream_url) +
481 +482 +
483 - def test_downloadable(self): +
484 sca = self.root +485 +486 track = sca.tracks(params={ +487 "filter" : "downloadable", +488 }).next() +489 +490 +491 assert isinstance(track, scapi.Track) +492 +493 download_url = track.download_url +494 +495 signed_url = track.oauth_sign_get_request(download_url) +496 +497 data = urllib2.urlopen(signed_url).read() +498 assert data +
499 +500 +501 +
502 - def test_modifying_playlists(self): +
503 sca = self.root +504 +505 me = sca.me() +506 my_tracks = list(me.tracks()) +507 +508 assert my_tracks +509 +510 playlist = me.playlists().next() +511 playlist = sca.Playlist.get(playlist.id) +512 +513 assert isinstance(playlist, scapi.Playlist) +514 +515 pl_tracks = playlist.tracks +516 +517 playlist.title = "foobarbaz" +
518 +519 +520 +
521 - def test_track_deletion(self): +
522 sca = self.root +523 track = sca.Track.new(title='bar', asset_data=self.data, +524 ) +525 +526 sca.tracks.remove(track) +
527 +528 +529 +
531 sca = self.root +532 track = sca.Track.new(title='bar', +533 asset_data=self.data, +534 ) +535 assert isinstance(track, scapi.Track) +536 +537 track.artwork_data = self.artwork_data +
538 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests.SCAPITests-class.html b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests.SCAPITests-class.html new file mode 100644 index 000000000..0c5a5a449 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.scapi_tests.SCAPITests-class.html @@ -0,0 +1,1025 @@ + + + + + scapi.tests.scapi_tests.SCAPITests + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module scapi_tests :: + Class SCAPITests + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class SCAPITests

source code

+
+       object --+    
+                |    
+unittest.TestCase --+
+                    |
+                   SCAPITests
+
+ +
+ + + + + + + + + +
+ + + + + +
Nested Classes[hide private]
+
+

Inherited from unittest.TestCase: + failureException +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
setUp(self)
+ Hook method for setting up the test fixture before exercising it.
+ source code + +
+ +
+   + + + + + + +
_load_config(self)
+ Loads the configuration by looking from
+ source code + +
+ +
+   + + + + + + +
test_connect(self)
+ test_connect
+ source code + +
+ +
+   + + + + + + +
test_access_token_acquisition(self)
+ This test is commented out because it needs user-interaction.
+ source code + +
+ +
+   + + + + + + +
test_track_creation(self) + source code + +
+ +
+   + + + + + + +
test_track_update(self) + source code + +
+ +
+   + + + + + + +
test_scoped_track_creation(self) + source code + +
+ +
+   + + + + + + +
test_upload(self) + source code + +
+ +
+   + + + + + + +
test_contact_list(self) + source code + +
+ +
+   + + + + + + +
test_permissions(self) + source code + +
+ +
+   + + + + + + +
test_setting_permissions(self) + source code + +
+ +
+   + + + + + + +
test_setting_comments(self) + source code + +
+ +
+   + + + + + + +
test_setting_comments_the_way_shawn_says_its_correct(self) + source code + +
+ +
+   + + + + + + +
test_contact_add_and_removal(self) + source code + +
+ +
+   + + + + + + +
test_favorites(self) + source code + +
+ +
+   + + + + + + +
test_large_list(self) + source code + +
+ +
+   + + + + + + +
test_filtered_list(self) + source code + +
+ +
+   + + + + + + +
test_events(self) + source code + +
+ +
+   + + + + + + +
test_me_having_stress(self) + source code + +
+ +
+   + + + + + + +
test_non_global_api(self) + source code + +
+ +
+   + + + + + + +
test_playlists(self) + source code + +
+ +
+   + + + + + + +
test_playlist_creation(self) + source code + +
+ +
+   + + + + + + +
test_groups(self) + source code + +
+ +
+   + + + + + + +
test_track_creation_with_email_sharers(self) + source code + +
+ +
+   + + + + + + +
test_track_creation_with_artwork(self) + source code + +
+ +
+   + + + + + + +
test_oauth_get_signing(self) + source code + +
+ +
+   + + + + + + +
test_streaming(self) + source code + +
+ +
+   + + + + + + +
test_downloadable(self) + source code + +
+ +
+   + + + + + + +
test_modifying_playlists(self) + source code + +
+ +
+   + + + + + + +
test_track_deletion(self) + source code + +
+ +
+   + + + + + + +
test_track_creation_with_updated_artwork(self) + source code + +
+ +
+

Inherited from unittest.TestCase: + __call__, + __init__, + __repr__, + __str__, + assertAlmostEqual, + assertAlmostEquals, + assertEqual, + assertEquals, + assertFalse, + assertNotAlmostEqual, + assertNotAlmostEquals, + assertNotEqual, + assertNotEquals, + assertRaises, + assertTrue, + assert_, + countTestCases, + debug, + defaultTestResult, + fail, + failIf, + failIfAlmostEqual, + failIfEqual, + failUnless, + failUnlessAlmostEqual, + failUnlessEqual, + failUnlessRaises, + id, + run, + shortDescription, + tearDown +

+

Inherited from unittest.TestCase (private): + _exc_info +

+

Inherited from object: + __delattr__, + __getattribute__, + __hash__, + __new__, + __reduce__, + __reduce_ex__, + __setattr__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Class Variables[hide private]
+
+   + + CONFIG_NAME = 'test.ini' +
+   + + TOKEN = None +
+   + + SECRET = None +
+   + + CONSUMER = None +
+   + + CONSUMER_SECRET = None +
+   + + API_HOST = None +
+   + + USER = None +
+   + + PASSWORD = None +
+   + + AUTHENTICATOR = None +
+   + + RUN_INTERACTIVE_TESTS = False +
+   + + CONFIGSPEC = '\n[api]\ntoken=string\nsecret=string\nconsumer=s... +
+ + + + + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+   + + root
+ Return the properly configured root-scope. +
+

Inherited from object: + __class__ +

+
+ + + + + + +
+ + + + + +
Method Details[hide private]
+
+ +
+ +
+ + +
+

setUp(self) +

+
source code  +
+ +

Hook method for setting up the test fixture before exercising it.

+
+
Overrides: + unittest.TestCase.setUp +
(inherited documentation)
+ +
+
+
+ +
+ +
+ + +
+

_load_config(self) +

+
source code  +
+ +

Loads the configuration by looking from

+
    +
  • + the environment variable SCAPI_CONFIG +
  • +
  • + the installation location upwards until it finds test.ini +
  • +
  • + the current working directory upwards until it finds test.ini +
  • +
+

Raises an error if there is no config found

+
+
+
+
+ +
+ +
+ + +
+

test_connect(self) +

+
source code  +
+ +

test_connect

+

Tries to connect & performs some read-only operations.

+
+
+
+
+
+ + + + + + +
+ + + + + +
Class Variable Details[hide private]
+
+ +
+ +
+

CONFIGSPEC

+ +
+
+
+
Value:
+
+'''
+[api]
+token=string
+secret=string
+consumer=string
+consumer_secret=string
+api_host=string
+user=string
+...
+
+
+
+
+
+
+ + + + + + +
+ + + + + +
Property Details[hide private]
+
+ +
+ +
+

root

+

Return the properly configured root-scope.

+
+
Get Method:
+
unreachable.root(self) + - Return the properly configured root-scope. +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-module.html b/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-module.html new file mode 100644 index 000000000..e4780ff79 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-module.html @@ -0,0 +1,586 @@ + + + + + scapi.tests.test_connect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module test_connect + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module test_connect

source code

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Functions[hide private]
+
+   + + + + + + +
setup() + source code + +
+ +
+   + + + + + + +
load_config(config_name=None) + source code + +
+ +
+   + + + + + + +
test_load_config() + source code + +
+ +
+   + + + + + + +
test_connect() + source code + +
+ +
+   + + + + + + +
test_access_token_acquisition()
+ This test is commented out because it needs user-interaction.
+ source code + +
+ +
+   + + + + + + +
test_track_creation() + source code + +
+ +
+   + + + + + + +
test_track_update() + source code + +
+ +
+   + + + + + + +
test_scoped_track_creation() + source code + +
+ +
+   + + + + + + +
test_upload() + source code + +
+ +
+   + + + + + + +
test_contact_list() + source code + +
+ +
+   + + + + + + +
test_permissions() + source code + +
+ +
+   + + + + + + +
test_setting_permissions() + source code + +
+ +
+   + + + + + + +
test_setting_comments() + source code + +
+ +
+   + + + + + + +
test_setting_comments_the_way_shawn_says_its_correct() + source code + +
+ +
+   + + + + + + +
test_contact_add_and_removal() + source code + +
+ +
+   + + + + + + +
test_favorites() + source code + +
+ +
+   + + + + + + +
test_large_list() + source code + +
+ +
+   + + + + + + +
test_events() + source code + +
+ +
+   + + + + + + +
test_me_having_stress() + source code + +
+ +
+   + + + + + + +
test_non_global_api() + source code + +
+ +
+   + + + + + + +
test_playlists() + source code + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Variables[hide private]
+
+   + + logger = logging.getLogger(__name__) +
+   + + _logger = logging.getLogger("scapi") +
+   + + RUN_INTERACTIVE_TESTS = False +
+   + + USE_OAUTH = True +
+   + + TOKEN = 'FjNE9aRTg8kpxuOjzwsX8Q' +
+   + + SECRET = 'NP5PGoyKcQv64E0aZgV4CRNzHfPwR4QghrWoqEgEE' +
+   + + CONSUMER = 'EEi2URUfM97pAAxHTogDpQ' +
+   + + CONSUMER_SECRET = 'NFYd8T3i4jVKGZ9TMy9LHaBQB3Sh8V5sxBiMeMZBow' +
+   + + API_HOST = 'api.soundcloud.dev:3000' +
+   + + USER = '' +
+   + + PASSWORD = '' +
+   + + CONFIG_NAME = 'soundcloud.cfg' +
+   + + CONNECTOR = None +
+   + + ROOT = None +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-pysrc.html new file mode 100644 index 000000000..3c0a41c84 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.test_connect-pysrc.html @@ -0,0 +1,627 @@ + + + + + scapi.tests.test_connect + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module test_connect + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.tests.test_connect

+
+  1  from __future__ import with_statement 
+  2  import os 
+  3  import tempfile 
+  4  import itertools 
+  5  from ConfigParser import SafeConfigParser 
+  6  import pkg_resources 
+  7  import scapi 
+  8  import scapi.authentication 
+  9  import logging 
+ 10  import webbrowser 
+ 11   
+ 12  logger = logging.getLogger(__name__) 
+ 13  logger.setLevel(logging.DEBUG) 
+ 14  _logger = logging.getLogger("scapi") 
+ 15  #_logger.setLevel(logging.DEBUG) 
+ 16   
+ 17  RUN_INTERACTIVE_TESTS = False 
+ 18  USE_OAUTH = True 
+ 19   
+ 20  TOKEN  = "FjNE9aRTg8kpxuOjzwsX8Q" 
+ 21  SECRET = "NP5PGoyKcQv64E0aZgV4CRNzHfPwR4QghrWoqEgEE" 
+ 22  CONSUMER = "EEi2URUfM97pAAxHTogDpQ" 
+ 23  CONSUMER_SECRET = "NFYd8T3i4jVKGZ9TMy9LHaBQB3Sh8V5sxBiMeMZBow" 
+ 24  API_HOST = "api.soundcloud.dev:3000" 
+ 25  USER = "" 
+ 26  PASSWORD = "" 
+ 27   
+ 28  CONFIG_NAME = "soundcloud.cfg" 
+ 29   
+ 30  CONNECTOR = None 
+ 31  ROOT = None 
+
32 -def setup(): +
33 global CONNECTOR, ROOT + 34 # load_config() + 35 #scapi.ApiConnector(host='192.168.2.101:3000', user='tiga', password='test') + 36 #scapi.ApiConnector(host='sandbox-api.soundcloud.com:3030', user='tiga', password='test') + 37 scapi.USE_PROXY = False + 38 scapi.PROXY = 'http://127.0.0.1:10000/' + 39 + 40 if USE_OAUTH: + 41 authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + 42 CONSUMER_SECRET, + 43 TOKEN, + 44 SECRET) + 45 else: + 46 authenticator = scapi.authentication.BasicAuthenticator(USER, PASSWORD, CONSUMER, CONSUMER_SECRET) + 47 + 48 logger.debug("API_HOST: %s", API_HOST) + 49 CONNECTOR = scapi.ApiConnector(host=API_HOST, + 50 authenticator=authenticator) + 51 ROOT = scapi.Scope(CONNECTOR) +
52 +
53 -def load_config(config_name=None): +
54 global TOKEN, SECRET, CONSUMER_SECRET, CONSUMER, API_HOST, USER, PASSWORD + 55 if config_name is None: + 56 config_name = CONFIG_NAME + 57 parser = SafeConfigParser() + 58 current = os.getcwd() + 59 while current: + 60 name = os.path.join(current, config_name) + 61 if os.path.exists(name): + 62 parser.read([name]) + 63 TOKEN = parser.get('global', 'accesstoken') + 64 SECRET = parser.get('global', 'accesstoken_secret') + 65 CONSUMER = parser.get('global', 'consumer') + 66 CONSUMER_SECRET = parser.get('global', 'consumer_secret') + 67 API_HOST = parser.get('global', 'host') + 68 USER = parser.get('global', 'user') + 69 PASSWORD = parser.get('global', 'password') + 70 logger.debug("token: %s", TOKEN) + 71 logger.debug("secret: %s", SECRET) + 72 logger.debug("consumer: %s", CONSUMER) + 73 logger.debug("consumer_secret: %s", CONSUMER_SECRET) + 74 logger.debug("user: %s", USER) + 75 logger.debug("password: %s", PASSWORD) + 76 logger.debug("host: %s", API_HOST) + 77 break + 78 new_current = os.path.dirname(current) + 79 if new_current == current: + 80 break + 81 current = new_current +
82 + 83 +
84 -def test_load_config(): +
85 base = tempfile.mkdtemp() + 86 oldcwd = os.getcwd() + 87 cdir = os.path.join(base, "foo") + 88 os.mkdir(cdir) + 89 os.chdir(cdir) + 90 test_config = """ + 91 [global] + 92 host=host + 93 consumer=consumer + 94 consumer_secret=consumer_secret + 95 accesstoken=accesstoken + 96 accesstoken_secret=accesstoken_secret + 97 user=user + 98 password=password + 99 """ +100 with open(os.path.join(base, CONFIG_NAME), "w") as cf: +101 cf.write(test_config) +102 load_config() +103 assert TOKEN == "accesstoken" and SECRET == "accesstoken_secret" and API_HOST == 'host' +104 assert CONSUMER == "consumer" and CONSUMER_SECRET == "consumer_secret" +105 assert USER == "user" and PASSWORD == "password" +106 os.chdir(oldcwd) +107 load_config() +
108 +109 +
110 -def test_connect(): +
111 sca = ROOT +112 quite_a_few_users = list(itertools.islice(sca.users(), 0, 127)) +113 +114 logger.debug(quite_a_few_users) +115 assert isinstance(quite_a_few_users, list) and isinstance(quite_a_few_users[0], scapi.User) +116 user = sca.me() +117 logger.debug(user) +118 assert isinstance(user, scapi.User) +119 contacts = list(user.contacts()) +120 assert isinstance(contacts, list) +121 assert isinstance(contacts[0], scapi.User) +122 logger.debug(contacts) +123 tracks = list(user.tracks()) +124 assert isinstance(tracks, list) +125 assert isinstance(tracks[0], scapi.Track) +126 logger.debug(tracks) +
127 +128 +
130 """ +131 This test is commented out because it needs user-interaction. +132 """ +133 if not RUN_INTERACTIVE_TESTS: +134 return +135 oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, +136 CONSUMER_SECRET, +137 None, +138 None) +139 +140 sca = scapi.ApiConnector(host=API_HOST, authenticator=oauth_authenticator) +141 token, secret = sca.fetch_request_token() +142 authorization_url = sca.get_request_token_authorization_url(token) +143 webbrowser.open(authorization_url) +144 raw_input("please press return") +145 oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, +146 CONSUMER_SECRET, +147 token, +148 secret) +149 +150 sca = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) +151 token, secret = sca.fetch_access_token() +152 logger.info("Access token: '%s'", token) +153 logger.info("Access token secret: '%s'", secret) +154 oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, +155 CONSUMER_SECRET, +156 token, +157 secret) +158 +159 sca = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) +160 test_track_creation() +
161 +
163 sca = ROOT +164 track = sca.Track.new(title='bar') +165 assert isinstance(track, scapi.Track) +
166 +
167 -def test_track_update(): +
168 sca = ROOT +169 track = sca.Track.new(title='bar') +170 assert isinstance(track, scapi.Track) +171 track.title='baz' +172 track = sca.Track.get(track.id) +173 assert track.title == "baz" +
174 +
176 sca = ROOT +177 user = sca.me() +178 track = user.tracks.new(title="bar") +179 assert isinstance(track, scapi.Track) +
180 +
181 -def test_upload(): +
182 assert pkg_resources.resource_exists("scapi.tests.test_connect", "knaster.mp3") +183 data = pkg_resources.resource_stream("scapi.tests.test_connect", "knaster.mp3") +184 sca = ROOT +185 user = sca.me() +186 logger.debug(user) +187 asset = sca.assets.new(filedata=data) +188 assert isinstance(asset, scapi.Asset) +189 logger.debug(asset) +190 tracks = list(user.tracks()) +191 track = tracks[0] +192 track.assets.append(asset) +
193 +
194 -def test_contact_list(): +
195 sca = ROOT +196 user = sca.me() +197 contacts = list(user.contacts()) +198 assert isinstance(contacts, list) +199 assert isinstance(contacts[0], scapi.User) +
200 +
201 -def test_permissions(): +
202 sca = ROOT +203 user = sca.me() +204 tracks = itertools.islice(user.tracks(), 1) +205 for track in tracks: +206 permissions = list(track.permissions()) +207 logger.debug(permissions) +208 assert isinstance(permissions, list) +209 if permissions: +210 assert isinstance(permissions[0], scapi.User) +
211 +
213 sca = ROOT +214 me = sca.me() +215 track = sca.Track.new(title='bar', sharing="private") +216 assert track.sharing == "private" +217 users = itertools.islice(sca.users(), 10) +218 users_to_set = [user for user in users if user != me] +219 assert users_to_set, "Didn't find any suitable users" +220 track.permissions = users_to_set +221 assert set(track.permissions()) == set(users_to_set) +
222 +
224 sca = ROOT +225 user = sca.me() +226 track = sca.Track.new(title='bar', sharing="private") +227 comment = sca.Comment.create(body="This is the body of my comment", timestamp=10) +228 track.comments = comment +229 assert track.comments().next().body == comment.body +
230 +231 +
233 sca = ROOT +234 track = sca.Track.new(title='bar', sharing="private") +235 cbody = "This is the body of my comment" +236 track.comments.new(body=cbody, timestamp=10) +237 assert list(track.comments())[0].body == cbody +
238 +
240 sca = ROOT +241 me = sca.me() +242 for user in sca.users(): +243 if user != me: +244 user_to_set = user +245 break +246 +247 contacts = list(me.contacts()) +248 if user_to_set in contacts: +249 me.contacts.remove(user_to_set) +250 +251 me.contacts.append(user_to_set) +252 +253 contacts = list(me.contacts() ) +254 assert user_to_set.id in [c.id for c in contacts] +255 +256 me.contacts.remove(user_to_set) +257 +258 contacts = list(me.contacts() ) +259 assert user_to_set not in contacts +
260 +261 +
262 -def test_favorites(): +
263 sca = ROOT +264 me = sca.me() +265 +266 favorites = list(me.favorites()) +267 assert favorites == [] or isinstance(favorites[0], scapi.Track) +268 +269 track = None +270 for user in sca.users(): +271 if user == me: +272 continue +273 for track in user.tracks(): +274 break +275 if track is not None: +276 break +277 +278 me.favorites.append(track) +279 +280 favorites = list(me.favorites()) +281 assert track in favorites +282 +283 me.favorites.remove(track) +284 +285 favorites = list(me.favorites()) +286 assert track not in favorites +
287 +
288 -def test_large_list(): +
289 sca = ROOT +290 tracks = list(sca.tracks()) +291 if len(tracks) < scapi.ApiConnector.LIST_LIMIT: +292 for i in xrange(scapi.ApiConnector.LIST_LIMIT): +293 scapi.Track.new(title='test_track_%i' % i) +294 all_tracks = sca.tracks() +295 assert not isinstance(all_tracks, list) +296 all_tracks = list(all_tracks) +297 assert len(all_tracks) > scapi.ApiConnector.LIST_LIMIT +
298 +299 +
300 -def test_events(): +
301 events = list(ROOT.events()) +302 assert isinstance(events, list) +303 assert isinstance(events[0], scapi.Event) +
304 +
306 sca = ROOT +307 for _ in xrange(20): +308 setup() +309 sca.me() +
310 +
312 root = scapi.Scope(CONNECTOR) +313 me = root.me() +314 assert isinstance(me, scapi.User) +315 +316 # now get something *from* that user +317 favorites = list(me.favorites()) +318 assert favorites +
319 +
320 -def test_playlists(): +
321 sca = ROOT +322 playlists = list(itertools.islice(sca.playlists(), 0, 127)) +323 found = False +324 for playlist in playlists: +325 tracks = playlist.tracks +326 if not isinstance(tracks, list): +327 tracks = [tracks] +328 for trackdata in tracks: +329 print trackdata +330 user = trackdata.user +331 print user +332 print user.tracks() +333 print playlist.user +334 break +
335 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-module.html b/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-module.html new file mode 100644 index 000000000..6bf493f9d --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-module.html @@ -0,0 +1,225 @@ + + + + + scapi.tests.test_oauth + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module test_oauth + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module test_oauth

source code

+ + + + + + + + + + + + +
+ + + + + +
Functions[hide private]
+
+   + + + + + + +
test_base64_connect() + source code + +
+ +
+   + + + + + + +
test_oauth_connect() + source code + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
Variables[hide private]
+
+   + + logger = logging.getLogger(__name__) +
+   + + _logger = logging.getLogger("scapi") +
+   + + TOKEN = 'QcciYu1FSwDSGKAG2mNw' +
+   + + SECRET = 'gJ2ok6ULUsYQB3rsBmpHCRHoFCAPOgK8ZjoIyxzris' +
+   + + CONSUMER = 'Cy2eLPrIMp4vOxjz9icdQ' +
+   + + CONSUMER_SECRET = 'KsBa272x6M2to00Vo5FdvZXt9kakcX7CDIPJoGwTro' +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-pysrc.html new file mode 100644 index 000000000..6ecf3c628 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.tests.test_oauth-pysrc.html @@ -0,0 +1,182 @@ + + + + + scapi.tests.test_oauth + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Package tests :: + Module test_oauth + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.tests.test_oauth

+
+ 1  import pkg_resources 
+ 2  import scapi 
+ 3  import scapi.authentication 
+ 4  import urllib 
+ 5  import logging 
+ 6   
+ 7  logger = logging.getLogger(__name__) 
+ 8  logger.setLevel(logging.DEBUG) 
+ 9  _logger = logging.getLogger("scapi") 
+10  _logger.setLevel(logging.DEBUG) 
+11   
+12  TOKEN  = "QcciYu1FSwDSGKAG2mNw" 
+13  SECRET = "gJ2ok6ULUsYQB3rsBmpHCRHoFCAPOgK8ZjoIyxzris" 
+14  CONSUMER = "Cy2eLPrIMp4vOxjz9icdQ" 
+15  CONSUMER_SECRET = "KsBa272x6M2to00Vo5FdvZXt9kakcX7CDIPJoGwTro" 
+16   
+
18 scapi.USE_PROXY = True +19 scapi.PROXY = 'http://127.0.0.1:10000/' +20 scapi.SoundCloudAPI(host='192.168.2.31:3000', authenticator=scapi.authentication.BasicAuthenticator('tiga', 'test')) +21 sca = scapi.Scope() +22 assert isinstance(sca.me(), scapi.User) +
23 +24 +
26 scapi.USE_PROXY = True +27 scapi.PROXY = 'http://127.0.0.1:10000/' +28 scapi.SoundCloudAPI(host='192.168.2.31:3000', +29 authenticator=scapi.authentication.OAuthAuthenticator(CONSUMER, +30 CONSUMER_SECRET, +31 TOKEN, SECRET)) +32 +33 sca = scapi.Scope() +34 assert isinstance(sca.me(), scapi.User) +
35 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.util-module.html b/python_apps/soundcloud-api/docs/api/scapi.util-module.html new file mode 100644 index 000000000..2603fb0b1 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.util-module.html @@ -0,0 +1,173 @@ + + + + + scapi.util + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module util + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Module util

source code

+ + + + + + + + + +
+ + + + + +
Classes[hide private]
+
+   + + MultiDict +
+ + + + + + + + + +
+ + + + + +
Functions[hide private]
+
+   + + + + + + +
escape(s) + source code + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.util-pysrc.html b/python_apps/soundcloud-api/docs/api/scapi.util-pysrc.html new file mode 100644 index 000000000..1a018ed72 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.util-pysrc.html @@ -0,0 +1,171 @@ + + + + + scapi.util + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module util + + + + + + +
[hide private]
[frames] | no frames]
+
+

Source Code for Module scapi.util

+
+ 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   
+
23 -def escape(s): +
24 # escape '/' too +25 return urllib.quote(s, safe='') +
26 +27 +28 +29 +30 +31 +
32 -class MultiDict(dict): +
33 +34 +
35 - def add(self, key, new_value): +
36 if key in self: +37 value = self[key] +38 if not isinstance(value, list): +39 value = [value] +40 self[key] = value +41 value.append(new_value) +42 else: +43 self[key] = new_value +
44 +45 +
46 - def iteritemslist(self): +
47 for key, value in self.iteritems(): +48 if not isinstance(value, list): +49 value = [value] +50 yield key, value +
51 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/scapi.util.MultiDict-class.html b/python_apps/soundcloud-api/docs/api/scapi.util.MultiDict-class.html new file mode 100644 index 000000000..fe7b460e9 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/scapi.util.MultiDict-class.html @@ -0,0 +1,247 @@ + + + + + scapi.util.MultiDict + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Package scapi :: + Module util :: + Class MultiDict + + + + + + +
[hide private]
[frames] | no frames]
+
+ +

Class MultiDict

source code

+
+object --+    
+         |    
+      dict --+
+             |
+            MultiDict
+
+ +
+ + + + + + + + + + + + + + + +
+ + + + + +
Instance Methods[hide private]
+
+   + + + + + + +
add(self, + key, + new_value) + source code + +
+ +
+   + + + + + + +
iteritemslist(self) + source code + +
+ +
+

Inherited from dict: + __cmp__, + __contains__, + __delitem__, + __eq__, + __ge__, + __getattribute__, + __getitem__, + __gt__, + __hash__, + __init__, + __iter__, + __le__, + __len__, + __lt__, + __ne__, + __new__, + __repr__, + __setitem__, + clear, + copy, + fromkeys, + get, + has_key, + items, + iteritems, + iterkeys, + itervalues, + keys, + pop, + popitem, + setdefault, + update, + values +

+

Inherited from object: + __delattr__, + __reduce__, + __reduce_ex__, + __setattr__, + __str__ +

+
+ + + + + + + + + +
+ + + + + +
Properties[hide private]
+
+

Inherited from object: + __class__ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-everything.html b/python_apps/soundcloud-api/docs/api/toc-everything.html new file mode 100644 index 000000000..3ced23f23 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-everything.html @@ -0,0 +1,151 @@ + + + + + Everything + + + + + +

Everything

+
+

All Classes

+ exceptions.AssertionError
+ scapi.Comment
scapi.Event
scapi.Group
+ + scapi.Playlist
+ + + scapi.Track
+ scapi.User
+ + + scapi.json.JsonReader
scapi.json.JsonWriter
scapi.json.ReadException
scapi.json.WriteException
+ scapi.tests.scapi_tests.SCAPITests
+

All Functions

+ scapi.json.read
scapi.json.write
+ scapi.tests.test_connect.load_config
scapi.tests.test_connect.setup
scapi.tests.test_connect.test_access_token_acquisition
scapi.tests.test_connect.test_connect
scapi.tests.test_connect.test_contact_add_and_removal
scapi.tests.test_connect.test_contact_list
scapi.tests.test_connect.test_events
scapi.tests.test_connect.test_favorites
scapi.tests.test_connect.test_large_list
scapi.tests.test_connect.test_load_config
scapi.tests.test_connect.test_me_having_stress
scapi.tests.test_connect.test_non_global_api
scapi.tests.test_connect.test_permissions
scapi.tests.test_connect.test_playlists
scapi.tests.test_connect.test_scoped_track_creation
scapi.tests.test_connect.test_setting_comments
scapi.tests.test_connect.test_setting_comments_the_way_shawn_says_its_correct
scapi.tests.test_connect.test_setting_permissions
scapi.tests.test_connect.test_track_creation
scapi.tests.test_connect.test_track_update
scapi.tests.test_connect.test_upload
scapi.tests.test_oauth.test_base64_connect
scapi.tests.test_oauth.test_oauth_connect
+

All Variables

+ scapi.ACCESS_TOKEN_URL
scapi.AUTHORIZATION_URL
scapi.PROXY
scapi.REQUEST_TOKEN_URL
scapi.USE_PROXY
+ + + scapi.tests.scapi_tests.api_logger
scapi.tests.scapi_tests.logger
scapi.tests.test_connect.API_HOST
scapi.tests.test_connect.CONFIG_NAME
scapi.tests.test_connect.CONNECTOR
scapi.tests.test_connect.CONSUMER
scapi.tests.test_connect.CONSUMER_SECRET
scapi.tests.test_connect.PASSWORD
scapi.tests.test_connect.ROOT
scapi.tests.test_connect.RUN_INTERACTIVE_TESTS
scapi.tests.test_connect.SECRET
scapi.tests.test_connect.TOKEN
scapi.tests.test_connect.USER
scapi.tests.test_connect.USE_OAUTH
+ scapi.tests.test_connect.logger
scapi.tests.test_oauth.CONSUMER
scapi.tests.test_oauth.CONSUMER_SECRET
scapi.tests.test_oauth.SECRET
scapi.tests.test_oauth.TOKEN
+ scapi.tests.test_oauth.logger

+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi-module.html new file mode 100644 index 000000000..b82804b3f --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi-module.html @@ -0,0 +1,70 @@ + + + + + scapi + + + + + +

Module scapi

+
+

Classes

+ + Comment
Event
Group
+ + Playlist
+ +
+ Scope
+ Track
+ User

Functions

+ +

Variables

+ ACCESS_TOKEN_URL
AUTHORIZATION_URL
PROXY
REQUEST_TOKEN_URL
USE_PROXY
+ logger
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.authentication-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.authentication-module.html new file mode 100644 index 000000000..e86f597b8 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.authentication-module.html @@ -0,0 +1,46 @@ + + + + + authentication + + + + + +

Module authentication

+
+

Classes

+ + + +

Variables

+ +
+ logger
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.config-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.config-module.html new file mode 100644 index 000000000..bc4635ec9 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.config-module.html @@ -0,0 +1,29 @@ + + + + + config + + + + + +

Module config

+
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.json-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.json-module.html new file mode 100644 index 000000000..49c7fa3f6 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.json-module.html @@ -0,0 +1,40 @@ + + + + + json + + + + + +

Module json

+
+

Classes

+ JsonReader
JsonWriter
ReadException
WriteException
+

Functions

+ read
write

+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.multidict-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.multidict-module.html new file mode 100644 index 000000000..a4494cd7b --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.multidict-module.html @@ -0,0 +1,29 @@ + + + + + multidict + + + + + +

Module multidict

+
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.tests-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.tests-module.html new file mode 100644 index 000000000..4c66376d1 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.tests-module.html @@ -0,0 +1,29 @@ + + + + + tests + + + + + +

Module tests

+
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.tests.scapi_tests-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.scapi_tests-module.html new file mode 100644 index 000000000..ecc870d8c --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.scapi_tests-module.html @@ -0,0 +1,34 @@ + + + + + scapi_tests + + + + + +

Module scapi_tests

+
+

Classes

+ SCAPITests

Variables

+ api_logger
logger

+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_connect-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_connect-module.html new file mode 100644 index 000000000..791b5ba85 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_connect-module.html @@ -0,0 +1,68 @@ + + + + + test_connect + + + + + +

Module test_connect

+
+

Functions

+ load_config
setup
test_access_token_acquisition
test_connect
test_contact_add_and_removal
test_contact_list
test_events
test_favorites
test_large_list
test_load_config
test_me_having_stress
test_non_global_api
test_permissions
test_playlists
test_scoped_track_creation
test_setting_comments
test_setting_comments_the_way_shawn_says_its_correct
test_setting_permissions
test_track_creation
test_track_update
test_upload

Variables

+ API_HOST
CONFIG_NAME
CONNECTOR
CONSUMER
CONSUMER_SECRET
PASSWORD
ROOT
RUN_INTERACTIVE_TESTS
SECRET
TOKEN
USER
USE_OAUTH
+ logger

+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_oauth-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_oauth-module.html new file mode 100644 index 000000000..0d504cee4 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.tests.test_oauth-module.html @@ -0,0 +1,41 @@ + + + + + test_oauth + + + + + +

Module test_oauth

+
+

Functions

+ test_base64_connect
test_oauth_connect

Variables

+ CONSUMER
CONSUMER_SECRET
SECRET
TOKEN
+ logger

+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc-scapi.util-module.html b/python_apps/soundcloud-api/docs/api/toc-scapi.util-module.html new file mode 100644 index 000000000..c94b46b34 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc-scapi.util-module.html @@ -0,0 +1,37 @@ + + + + + util + + + + + +

Module util

+
+

Classes

+ +

Functions

+
+ escape
+
+[hide private] + + + + diff --git a/python_apps/soundcloud-api/docs/api/toc.html b/python_apps/soundcloud-api/docs/api/toc.html new file mode 100644 index 000000000..2e38a4044 --- /dev/null +++ b/python_apps/soundcloud-api/docs/api/toc.html @@ -0,0 +1,46 @@ + + + + + Table of Contents + + + + + +

Table of Contents

+
+ Everything +
+

Modules

+ scapi
+ scapi.config
scapi.json
scapi.multidict
scapi.tests
scapi.tests.scapi_tests
scapi.tests.test_connect
scapi.tests.test_oauth
+
+ [hide private] + + + + diff --git a/python_apps/soundcloud-api/oauth/__init__.py b/python_apps/soundcloud-api/oauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_apps/soundcloud-api/oauth/example/client.py b/python_apps/soundcloud-api/oauth/example/client.py new file mode 100644 index 000000000..3f995c1a5 --- /dev/null +++ b/python_apps/soundcloud-api/oauth/example/client.py @@ -0,0 +1,157 @@ +''' +Example consumer. +''' +import httplib +import time +import oauth.oauth as oauth +import webbrowser +from scapi import util + +SERVER = 'sandbox-soundcloud.com' # Change to soundcloud.com to reach the live site +PORT = 80 + +REQUEST_TOKEN_URL = 'http://api.' + SERVER + '/oauth/request_token' +ACCESS_TOKEN_URL = 'http://api.' + SERVER + '/oauth/access_token' +AUTHORIZATION_URL = 'http://' + SERVER + '/oauth/authorize' + +CALLBACK_URL = '' +RESOURCE_URL = "http://api." + SERVER + "/me" + +# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore +CONSUMER_KEY = 'JysXkO8ErA4EluFnF5nWg' +CONSUMER_SECRET = 'fauVjm61niGckeufkmMvgUo77oWzRHdMmeylJblHk' + +# example client using httplib with headers +class SimpleOAuthClient(oauth.OAuthClient): + + def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''): + self.server = server + self.port = port + self.request_token_url = request_token_url + self.access_token_url = access_token_url + self.authorization_url = authorization_url + self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port)) + + def fetch_request_token(self, oauth_request): + # via headers + # -> OAuthToken + print oauth_request.to_url() + #self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header()) + self.connection.request(oauth_request.http_method, oauth_request.to_url()) + response = self.connection.getresponse() + print "response status", response.status + return oauth.OAuthToken.from_string(response.read()) + + def fetch_access_token(self, oauth_request): + # via headers + # -> OAuthToken + + # This should proably be elsewhere but stays here for now + oauth_request.set_parameter("oauth_signature", util.escape(oauth_request.get_parameter("oauth_signature"))) + self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header()) + response = self.connection.getresponse() + resp = response.read() + print "*" * 90 + print "response:", resp + print "*" * 90 + + return oauth.OAuthToken.from_string(resp) + + def authorize_token(self, oauth_request): + webbrowser.open(oauth_request.to_url()) + raw_input("press return when authorizing is finished") + + return + + # via url + # -> typically just some okay response + self.connection.request(oauth_request.http_method, oauth_request.to_url()) + response = self.connection.getresponse() + return response.read() + + def access_resource(self, oauth_request): + print "resource url:", oauth_request.to_url() + webbrowser.open(oauth_request.to_url()) + + return + + # via post body + # -> some protected resources + self.connection.request('GET', oauth_request.to_url()) + response = self.connection.getresponse() + return response.read() + +def run_example(): + + # setup + print '** OAuth Python Library Example **' + client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL) + consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET) + signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT() + signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1() + pause() + # get request token + print '* Obtain a request token ...' + pause() + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, http_url=client.request_token_url) + #oauth_request.sign_request(signature_method_plaintext, consumer, None) + oauth_request.sign_request(signature_method_hmac_sha1, consumer, None) + + print 'REQUEST (via headers)' + print 'parameters: %s' % str(oauth_request.parameters) + pause() + #import pdb; pdb.set_trace() + + token = client.fetch_request_token(oauth_request) + print 'GOT' + print 'key: %s' % str(token.key) + print 'secret: %s' % str(token.secret) + pause() + + print '* Authorize the request token ...' + pause() + oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, callback=CALLBACK_URL, http_url=client.authorization_url) + print 'REQUEST (via url query string)' + print 'parameters: %s' % str(oauth_request.parameters) + pause() + # this will actually occur only on some callback + response = client.authorize_token(oauth_request) + print 'GOT' + print response + pause() + + # get access token + print '* Obtain an access token ...' + pause() + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=client.access_token_url) + oauth_request.sign_request(signature_method_hmac_sha1, consumer, token) + print 'REQUEST (via headers)' + print 'parameters: %s' % str(oauth_request.parameters) + pause() + token = client.fetch_access_token(oauth_request) + print 'GOT' + print 'key: %s' % str(token.key) + print 'secret: %s' % str(token.secret) + pause() + + # access some protected resources + print '* Access protected resources ...' + pause() + parameters = {} + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='GET', http_url=RESOURCE_URL, parameters=parameters) + oauth_request.sign_request(signature_method_hmac_sha1, consumer, token) + print 'REQUEST (via get body)' + print 'parameters: %s' % str(oauth_request.parameters) + pause() + params = client.access_resource(oauth_request) + print 'GOT' + print 'non-oauth parameters: %s' % params + pause() + +def pause(): + print '' + time.sleep(1) + +if __name__ == '__main__': + run_example() + print 'Done.' diff --git a/python_apps/soundcloud-api/oauth/example/server.py b/python_apps/soundcloud-api/oauth/example/server.py new file mode 100644 index 000000000..91fb71538 --- /dev/null +++ b/python_apps/soundcloud-api/oauth/example/server.py @@ -0,0 +1,167 @@ +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import urllib + +import oauth.oauth as oauth + +REQUEST_TOKEN_URL = 'https://photos.example.net/request_token' +ACCESS_TOKEN_URL = 'https://photos.example.net/access_token' +AUTHORIZATION_URL = 'https://photos.example.net/authorize' +RESOURCE_URL = 'http://photos.example.net/photos' +REALM = 'http://photos.example.net/' + +# example store for one of each thing +class MockOAuthDataStore(oauth.OAuthDataStore): + + def __init__(self): + self.consumer = oauth.OAuthConsumer('key', 'secret') + self.request_token = oauth.OAuthToken('requestkey', 'requestsecret') + self.access_token = oauth.OAuthToken('accesskey', 'accesssecret') + self.nonce = 'nonce' + + def lookup_consumer(self, key): + if key == self.consumer.key: + return self.consumer + return None + + def lookup_token(self, token_type, token): + token_attrib = getattr(self, '%s_token' % token_type) + if token == token_attrib.key: + return token_attrib + return None + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce): + if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or token.key == self.access_token.key) and nonce == self.nonce: + return self.nonce + else: + raise oauth.OAuthError('Nonce not found: %s' % str(nonce)) + return None + + def fetch_request_token(self, oauth_consumer): + if oauth_consumer.key == self.consumer.key: + return self.request_token + return None + + def fetch_access_token(self, oauth_consumer, oauth_token): + if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key: + # want to check here if token is authorized + # for mock store, we assume it is + return self.access_token + return None + + def authorize_request_token(self, oauth_token): + if oauth_token.key == self.request_token.key: + # authorize the request token in the store + # for mock store, do nothing + return self.request_token + return None + +class RequestHandler(BaseHTTPRequestHandler): + + def __init__(self, *args, **kwargs): + self.oauth_server = oauth.OAuthServer(MockOAuthDataStore()) + self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT()) + self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1()) + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + # example way to send an oauth error + def send_oauth_error(self, err=None): + # send a 401 error + self.send_error(401, str(err.message)) + # return the authenticate header + header = oauth.build_authenticate_header(realm=REALM) + for k, v in header.iteritems(): + self.send_header(k, v) + + def do_GET(self): + + # debug info + #print self.command, self.path, self.headers + + # get the post data (if any) + postdata = None + if self.command == 'POST': + try: + length = int(self.headers.getheader('content-length')) + postdata = self.rfile.read(length) + except: + pass + + # construct the oauth request from the request parameters + oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, postdata=postdata) + + # request token + if self.path.startswith(REQUEST_TOKEN_URL): + try: + # create a request token + token = self.oauth_server.fetch_request_token(oauth_request) + # send okay response + self.send_response(200, 'OK') + self.end_headers() + # return the token + self.wfile.write(token.to_string()) + except oauth.OAuthError, err: + self.send_oauth_error(err) + return + + # user authorization + if self.path.startswith(AUTHORIZATION_URL): + try: + # get the request token + token = self.oauth_server.fetch_request_token(oauth_request) + callback = self.oauth_server.get_callback(oauth_request) + # send okay response + self.send_response(200, 'OK') + self.end_headers() + # return the callback url (to show server has it) + self.wfile.write('callback: %s' %callback) + # authorize the token (kind of does nothing for now) + token = self.oauth_server.authorize_token(token) + self.wfile.write('\n') + # return the token key + token_key = urllib.urlencode({'oauth_token': token.key}) + self.wfile.write('token key: %s' % token_key) + except oauth.OAuthError, err: + self.send_oauth_error(err) + return + + # access token + if self.path.startswith(ACCESS_TOKEN_URL): + try: + # create an access token + token = self.oauth_server.fetch_access_token(oauth_request) + # send okay response + self.send_response(200, 'OK') + self.end_headers() + # return the token + self.wfile.write(token.to_string()) + except oauth.OAuthError, err: + self.send_oauth_error(err) + return + + # protected resources + if self.path.startswith(RESOURCE_URL): + try: + # verify the request has been oauth authorized + consumer, token, params = self.oauth_server.verify_request(oauth_request) + # send okay response + self.send_response(200, 'OK') + self.end_headers() + # return the extra parameters - just for something to return + self.wfile.write(str(params)) + except oauth.OAuthError, err: + self.send_oauth_error(err) + return + + def do_POST(self): + return self.do_GET() + +def main(): + try: + server = HTTPServer(('', 8080), RequestHandler) + print 'Test server running...' + server.serve_forever() + except KeyboardInterrupt: + server.socket.close() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/python_apps/soundcloud-api/oauth/oauth.py b/python_apps/soundcloud-api/oauth/oauth.py new file mode 100644 index 000000000..274bbd25b --- /dev/null +++ b/python_apps/soundcloud-api/oauth/oauth.py @@ -0,0 +1,505 @@ +import cgi +import urllib +import time +import random +import urlparse +import hmac +import hashlib +import base64 + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join(str(random.randint(0, 9)) for i in range(length)) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + @staticmethod + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + auth_header += ',\n\t %s="%s"' % (k, v) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join('%s=%s' % (str(k), str(p)) for k, p in key_values) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + url_string = '%s://%s%s' % (parts.scheme, parts.netloc, parts.path) + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + @staticmethod + def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): + + # let the library user override things however they'd like, if they know + # which parameters to use then go for it, for example XMLRPC might want to + # do this + if parameters is not None: + return OAuthRequest(http_method, http_url, parameters) + + # from the headers + if headers is not None: + try: + auth_header = headers['Authorization'] + # check that the authorization header is OAuth + auth_header.index('OAuth') + # get the parameters from the header + parameters = OAuthRequest._split_header(auth_header) + return OAuthRequest(http_method, http_url, parameters) + except: + pass + + # from the parameter string (post body) + if http_method == 'POST' and postdata is not None: + parameters = OAuthRequest._split_url_string(postdata) + + # from the url string + elif http_method == 'GET': + param_str = urlparse.urlparse(http_url).query + parameters = OAuthRequest._split_url_string(param_str) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') + + @staticmethod + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + + @staticmethod + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = escape(callback) + + return OAuthRequest(http_method, http_url, parameters) + + # util function: turn Authorization: header into parameters, has to do some unescaping + @staticmethod + def _split_header(header): + params = {} + parts = header.split(',') + for param in parts: + # ignore realm parameter + if param.find('OAuth realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + + # util function: turn url string into parameters, has to do some unescaping + @staticmethod + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token): + return self.data_store.authorize_request_token(token) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + token = self.data_store.lookup_token(token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature') + # attempt to construct the same signature + built = signature_method.build_signature(oauth_request, consumer, token) + if signature != built: + raise OAuthError('Invalid signature') + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + try: + self.data_store.lookup_nonce(consumer, token, nonce) + raise OAuthError('Nonce already used: %s' % str(nonce)) + except: + pass + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(): + # -> str + raise NotImplementedError + + def build_signature(oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % consumer.secret + if token: + key += token.secret + raw = '&'.join(sig) + + # hmac object + hashed = hmac.new(key, raw, hashlib.sha1) + + # calculate the digest base 64 + return base64.b64encode(hashed.digest()) + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + if token: + sig = '&'.join((sig, escape(token.secret))) + return sig diff --git a/python_apps/soundcloud-api/scapi/MultipartPostHandler.py b/python_apps/soundcloud-api/scapi/MultipartPostHandler.py new file mode 100644 index 000000000..34b12943f --- /dev/null +++ b/python_apps/soundcloud-api/scapi/MultipartPostHandler.py @@ -0,0 +1,135 @@ +#!/usr/bin/python + +#### +# 02/2006 Will Holcomb +# +# 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. +# +""" +Usage: + Enables the use of multipart/form-data for posting forms + +Inspirations: + Upload files in python: + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 + urllib2_file: + Fabien Seisen: + +Example: + import MultipartPostHandler, urllib2, cookielib + + cookies = cookielib.CookieJar() + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), + MultipartPostHandler.MultipartPostHandler) + params = { "username" : "bob", "password" : "riviera", + "file" : open("filename", "rb") } + opener.open("http://wwww.bobsite.com/upload/", params) + +Further Example: + The main function of this file is a sample which downloads a page and + then uploads it to the W3C validator. +""" + +import urllib +import urllib2 +import mimetools, mimetypes +import os, stat + +class Callable: + def __init__(self, anycallable): + self.__call__ = anycallable + +# Controls how sequences are uncoded. If true, elements may be given multiple values by +# assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) == file: + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = self.multipart_encode(v_vars, v_files) + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + return request + + def multipart_encode(vars, files, boundary = None, buffer = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buffer is None: + buffer = '' + for(key, value) in vars: + if isinstance(value, basestring): + value = [value] + for sub_value in value: + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"' % key + buffer += '\r\n\r\n' + sub_value + '\r\n' + for(key, fd) in files: + file_size = os.fstat(fd.fileno())[stat.ST_SIZE] + filename = fd.name.split('/')[-1] + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buffer += '--%s\r\n' % boundary + buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) + buffer += 'Content-Type: %s\r\n' % contenttype + # buffer += 'Content-Length: %s\r\n' % file_size + fd.seek(0) + buffer += '\r\n' + fd.read() + '\r\n' + buffer += '--%s--\r\n\r\n' % boundary + return boundary, buffer + multipart_encode = Callable(multipart_encode) + + https_request = http_request + +def main(): + import tempfile, sys + + validatorURL = "http://validator.w3.org/check" + opener = urllib2.build_opener(MultipartPostHandler) + + def validateFile(url): + temp = tempfile.mkstemp(suffix=".html") + os.write(temp[0], opener.open(url).read()) + params = { "ss" : "0", # show source + "doctype" : "Inline", + "uploaded_file" : open(temp[1], "rb") } + print opener.open(validatorURL, params).read() + os.remove(temp[1]) + + if len(sys.argv[1:]) > 0: + for arg in sys.argv[1:]: + validateFile(arg) + else: + validateFile("http://www.google.com") + +if __name__=="__main__": + main() diff --git a/python_apps/soundcloud-api/scapi/__init__.py b/python_apps/soundcloud-api/scapi/__init__.py new file mode 100644 index 000000000..ae737cb81 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/__init__.py @@ -0,0 +1,1012 @@ +## 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()) + [, ...] + + 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()) + [, ...] + + """ + 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> + @param queryparams: the queryparams to use + @type queryparams: None|dict> + @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> + @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 | 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(...) + + + - create a instance in a certain scope: + + >>> scope = scapi.Scope(connector) + >>> user = scapi.User("1") + >>> track = user.tracks.new() + + + @param scope: if not empty, a one-element tuple containing the Scope + @type scope: tuple[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: "/" + """ + 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() diff --git a/python_apps/soundcloud-api/scapi/authentication.py b/python_apps/soundcloud-api/scapi/authentication.py new file mode 100644 index 000000000..52c527704 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/authentication.py @@ -0,0 +1,195 @@ +## 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 base64 +import time, random +import urlparse +import hmac +import hashlib +from scapi.util import escape +import logging + + +USE_DOUBLE_ESCAPE_HACK = True +""" +There seems to be an uncertainty on the way +parameters are to be escaped. For now, this +variable switches between two escaping mechanisms. + +If True, the passed parameters - GET or POST - are +escaped *twice*. +""" + +logger = logging.getLogger(__name__) + +class OAuthSignatureMethod_HMAC_SHA1(object): + + FORBIDDEN = ['realm', 'oauth_signature'] + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature(self, request, parameters, consumer_secret, token_secret, oauth_parameters): + if logger.level == logging.DEBUG: + logger.debug("request: %r", request) + logger.debug("parameters: %r", parameters) + logger.debug("consumer_secret: %r", consumer_secret) + logger.debug("token_secret: %r", token_secret) + logger.debug("oauth_parameters: %r", oauth_parameters) + + + temp = {} + temp.update(oauth_parameters) + for p in self.FORBIDDEN: + if p in temp: + del temp[p] + if parameters is not None: + temp.update(parameters) + sig = ( + escape(self.get_normalized_http_method(request)), + escape(self.get_normalized_http_url(request)), + self.get_normalized_parameters(temp), # these are escaped in the method already + ) + + key = '%s&' % consumer_secret + if token_secret is not None: + key += token_secret + raw = '&'.join(sig) + logger.debug("raw basestring: %s", raw) + logger.debug("key: %s", key) + # hmac object + hashed = hmac.new(key, raw, hashlib.sha1) + # calculate the digest base 64 + signature = escape(base64.b64encode(hashed.digest())) + return signature + + + def get_normalized_http_method(self, request): + return request.get_method().upper() + + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self, request): + url = request.get_full_url() + parts = urlparse.urlparse(url) + url_string = '%s://%s%s' % (parts.scheme, parts.netloc, parts.path) + return url_string + + + def get_normalized_parameters(self, params): + if params is None: + params = {} + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = [] + + for key, values in params.iteritems(): + if isinstance(values, file): + continue + if isinstance(values, (int, long, float)): + values = str(values) + if isinstance(values, (list, tuple)): + values = [str(v) for v in values] + if isinstance(values, basestring): + values = [values] + if USE_DOUBLE_ESCAPE_HACK and not key.startswith("ouath"): + key = escape(key) + for v in values: + v = v.encode("utf-8") + key = key.encode("utf-8") + if USE_DOUBLE_ESCAPE_HACK and not key.startswith("oauth"): + # this is a dirty hack to make the + # thing work with the current server-side + # implementation. Or is it by spec? + v = escape(v) + key_values.append(escape("%s=%s" % (key, v))) + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string + return escape('&').join(key_values) + + +class OAuthAuthenticator(object): + OAUTH_API_VERSION = '1.0' + AUTHORIZATION_HEADER = "Authorization" + + def __init__(self, consumer=None, consumer_secret=None, token=None, secret=None, signature_method=OAuthSignatureMethod_HMAC_SHA1()): + if consumer == None: + raise ValueError("The consumer key must be passed for all public requests; it may not be None") + self._consumer, self._token, self._secret = consumer, token, secret + self._consumer_secret = consumer_secret + self._signature_method = signature_method + random.seed() + + + def augment_request(self, req, parameters, use_multipart=False, oauth_callback=None, oauth_verifier=None): + oauth_parameters = { + 'oauth_consumer_key': self._consumer, + 'oauth_timestamp': self.generate_timestamp(), + 'oauth_nonce': self.generate_nonce(), + 'oauth_version': self.OAUTH_API_VERSION, + 'oauth_signature_method': self._signature_method.get_name(), + #'realm' : "http://soundcloud.com", + } + if self._token is not None: + oauth_parameters['oauth_token'] = self._token + + if oauth_callback is not None: + oauth_parameters['oauth_callback'] = oauth_callback + + if oauth_verifier is not None: + oauth_parameters['oauth_verifier'] = oauth_verifier + + # in case we upload large files, we don't + # sign the request over the parameters + # There's a bug in the OAuth 1.0 (and a) specs that says that PUT request should omit parameters from the base string. + # This is fixed in the IETF draft, don't know when this will be released though. - HT + if use_multipart or req.get_method() == 'PUT': + parameters = None + + oauth_parameters['oauth_signature'] = self._signature_method.build_signature(req, + parameters, + self._consumer_secret, + self._secret, + oauth_parameters) + def to_header(d): + return ",".join('%s="%s"' % (key, value) for key, value in sorted(oauth_parameters.items())) + + req.add_header(self.AUTHORIZATION_HEADER, "OAuth %s" % to_header(oauth_parameters)) + + def generate_timestamp(self): + return int(time.time())# * 1000.0) + + def generate_nonce(self, length=8): + return ''.join(str(random.randint(0, 9)) for i in range(length)) + + +class BasicAuthenticator(object): + + def __init__(self, user, password, consumer, consumer_secret): + self._base64string = base64.encodestring("%s:%s" % (user, password))[:-1] + self._x_auth_header = 'OAuth oauth_consumer_key="%s" oauth_consumer_secret="%s"' % (consumer, consumer_secret) + + def augment_request(self, req, parameters): + req.add_header("Authorization", "Basic %s" % self._base64string) + req.add_header("X-Authorization", self._x_auth_header) diff --git a/python_apps/soundcloud-api/scapi/config.py b/python_apps/soundcloud-api/scapi/config.py new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/python_apps/soundcloud-api/scapi/config.py @@ -0,0 +1,2 @@ + + diff --git a/python_apps/soundcloud-api/scapi/json.py b/python_apps/soundcloud-api/scapi/json.py new file mode 100644 index 000000000..a28a13e39 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/json.py @@ -0,0 +1,310 @@ +import string +import types + +## json.py implements a JSON (http://json.org) reader and writer. +## Copyright (C) 2005 Patrick D. Logan +## Contact mailto:patrickdlogan@stardecisions.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 + + +class _StringGenerator(object): + def __init__(self, string): + self.string = string + self.index = -1 + def peek(self): + i = self.index + 1 + if i < len(self.string): + return self.string[i] + else: + return None + def next(self): + self.index += 1 + if self.index < len(self.string): + return self.string[self.index] + else: + raise StopIteration + def all(self): + return self.string + +class WriteException(Exception): + pass + +class ReadException(Exception): + pass + +class JsonReader(object): + hex_digits = {'A': 10,'B': 11,'C': 12,'D': 13,'E': 14,'F':15} + escapes = {'t':'\t','n':'\n','f':'\f','r':'\r','b':'\b'} + + def read(self, s): + self._generator = _StringGenerator(s) + result = self._read() + return result + + def _read(self): + self._eatWhitespace() + peek = self._peek() + if peek is None: + raise ReadException, "Nothing to read: '%s'" % self._generator.all() + if peek == '{': + return self._readObject() + elif peek == '[': + return self._readArray() + elif peek == '"': + return self._readString() + elif peek == '-' or peek.isdigit(): + return self._readNumber() + elif peek == 't': + return self._readTrue() + elif peek == 'f': + return self._readFalse() + elif peek == 'n': + return self._readNull() + elif peek == '/': + self._readComment() + return self._read() + else: + raise ReadException, "Input is not valid JSON: '%s'" % self._generator.all() + + def _readTrue(self): + self._assertNext('t', "true") + self._assertNext('r', "true") + self._assertNext('u', "true") + self._assertNext('e', "true") + return True + + def _readFalse(self): + self._assertNext('f', "false") + self._assertNext('a', "false") + self._assertNext('l', "false") + self._assertNext('s', "false") + self._assertNext('e', "false") + return False + + def _readNull(self): + self._assertNext('n', "null") + self._assertNext('u', "null") + self._assertNext('l', "null") + self._assertNext('l', "null") + return None + + def _assertNext(self, ch, target): + if self._next() != ch: + raise ReadException, "Trying to read %s: '%s'" % (target, self._generator.all()) + + def _readNumber(self): + isfloat = False + result = self._next() + peek = self._peek() + while peek is not None and (peek.isdigit() or peek == "."): + isfloat = isfloat or peek == "." + result = result + self._next() + peek = self._peek() + try: + if isfloat: + return float(result) + else: + return int(result) + except ValueError: + raise ReadException, "Not a valid JSON number: '%s'" % result + + def _readString(self): + result = "" + assert self._next() == '"' + try: + while self._peek() != '"': + ch = self._next() + if ch == "\\": + ch = self._next() + if ch in 'brnft': + ch = self.escapes[ch] + elif ch == "u": + ch4096 = self._next() + ch256 = self._next() + ch16 = self._next() + ch1 = self._next() + n = 4096 * self._hexDigitToInt(ch4096) + n += 256 * self._hexDigitToInt(ch256) + n += 16 * self._hexDigitToInt(ch16) + n += self._hexDigitToInt(ch1) + ch = unichr(n) + elif ch not in '"/\\': + raise ReadException, "Not a valid escaped JSON character: '%s' in %s" % (ch, self._generator.all()) + result = result + ch + except StopIteration: + raise ReadException, "Not a valid JSON string: '%s'" % self._generator.all() + assert self._next() == '"' + return result + + def _hexDigitToInt(self, ch): + try: + result = self.hex_digits[ch.upper()] + except KeyError: + try: + result = int(ch) + except ValueError: + raise ReadException, "The character %s is not a hex digit." % ch + return result + + def _readComment(self): + assert self._next() == "/" + second = self._next() + if second == "/": + self._readDoubleSolidusComment() + elif second == '*': + self._readCStyleComment() + else: + raise ReadException, "Not a valid JSON comment: %s" % self._generator.all() + + def _readCStyleComment(self): + try: + done = False + while not done: + ch = self._next() + done = (ch == "*" and self._peek() == "/") + if not done and ch == "/" and self._peek() == "*": + raise ReadException, "Not a valid JSON comment: %s, '/*' cannot be embedded in the comment." % self._generator.all() + self._next() + except StopIteration: + raise ReadException, "Not a valid JSON comment: %s, expected */" % self._generator.all() + + def _readDoubleSolidusComment(self): + try: + ch = self._next() + while ch != "\r" and ch != "\n": + ch = self._next() + except StopIteration: + pass + + def _readArray(self): + result = [] + assert self._next() == '[' + done = self._peek() == ']' + while not done: + item = self._read() + result.append(item) + self._eatWhitespace() + done = self._peek() == ']' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert ']' == self._next() + return result + + def _readObject(self): + result = {} + assert self._next() == '{' + done = self._peek() == '}' + while not done: + key = self._read() + if type(key) is not types.StringType: + raise ReadException, "Not a valid JSON object key (should be a string): %s" % key + self._eatWhitespace() + ch = self._next() + if ch != ":": + raise ReadException, "Not a valid JSON object: '%s' due to: '%s'" % (self._generator.all(), ch) + self._eatWhitespace() + val = self._read() + result[key] = val + self._eatWhitespace() + done = self._peek() == '}' + if not done: + ch = self._next() + if ch != ",": + raise ReadException, "Not a valid JSON array: '%s' due to: '%s'" % (self._generator.all(), ch) + assert self._next() == "}" + return result + + def _eatWhitespace(self): + p = self._peek() + while p is not None and p in string.whitespace or p == '/': + if p == '/': + self._readComment() + else: + self._next() + p = self._peek() + + def _peek(self): + return self._generator.peek() + + def _next(self): + return self._generator.next() + +class JsonWriter(object): + + def _append(self, s): + self._results.append(s) + + def write(self, obj, escaped_forward_slash=False): + self._escaped_forward_slash = escaped_forward_slash + self._results = [] + self._write(obj) + return "".join(self._results) + + def _write(self, obj): + ty = type(obj) + if ty is types.DictType: + n = len(obj) + self._append("{") + for k, v in obj.items(): + self._write(k) + self._append(":") + self._write(v) + n = n - 1 + if n > 0: + self._append(",") + self._append("}") + elif ty is types.ListType or ty is types.TupleType: + n = len(obj) + self._append("[") + for item in obj: + self._write(item) + n = n - 1 + if n > 0: + self._append(",") + self._append("]") + elif ty is types.StringType or ty is types.UnicodeType: + self._append('"') + obj = obj.replace('\\', r'\\') + if self._escaped_forward_slash: + obj = obj.replace('/', r'\/') + obj = obj.replace('"', r'\"') + obj = obj.replace('\b', r'\b') + obj = obj.replace('\f', r'\f') + obj = obj.replace('\n', r'\n') + obj = obj.replace('\r', r'\r') + obj = obj.replace('\t', r'\t') + self._append(obj) + self._append('"') + elif ty is types.IntType or ty is types.LongType: + self._append(str(obj)) + elif ty is types.FloatType: + self._append("%f" % obj) + elif obj is True: + self._append("true") + elif obj is False: + self._append("false") + elif obj is None: + self._append("null") + else: + raise WriteException, "Cannot write in JSON: %s" % repr(obj) + +def write(obj, escaped_forward_slash=False): + return JsonWriter().write(obj, escaped_forward_slash) + +def read(s): + return JsonReader().read(s) diff --git a/python_apps/soundcloud-api/scapi/tests/__init__.py b/python_apps/soundcloud-api/scapi/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python_apps/soundcloud-api/scapi/tests/knaster.mp3 b/python_apps/soundcloud-api/scapi/tests/knaster.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..138749d65d71cdb8cdb76e9d7bc1edff173e96a1 GIT binary patch literal 80493 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

?ce9^_{qLDvurO(SktEdBp_~@@pj-2k0bGnG6GY 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 Tp scapi.ApiConnector.LIST_LIMIT + + + + def test_filtered_list(self): + if not self.RUN_LONG_TESTS: + return + + sca = self.root + + tracks = list(sca.tracks(params={ + "bpm[from]" : "180", + })) + if len(tracks) < scapi.ApiConnector.LIST_LIMIT: + for i in xrange(scapi.ApiConnector.LIST_LIMIT): + sca.Track.new(title='test_track_%i' % i, asset_data=self.data) + all_tracks = sca.tracks() + assert not isinstance(all_tracks, list) + all_tracks = list(all_tracks) + assert len(all_tracks) > scapi.ApiConnector.LIST_LIMIT + + + def test_events(self): + events = list(self.root.events()) + assert isinstance(events, list) + assert isinstance(events[0], scapi.Event) + + + def test_me_having_stress(self): + sca = self.root + for _ in xrange(20): + self.setUp() + sca.me() + + + def test_non_global_api(self): + root = self.root + me = root.me() + assert isinstance(me, scapi.User) + + # now get something *from* that user + list(me.favorites()) + + + def test_playlists(self): + sca = self.root + playlists = list(itertools.islice(sca.playlists(), 0, 127)) + for playlist in playlists: + tracks = playlist.tracks + if not isinstance(tracks, list): + tracks = [tracks] + for trackdata in tracks: + print trackdata + #user = trackdata.user + #print user + #print user.tracks() + print playlist.user + break + + + + + def test_playlist_creation(self): + sca = self.root + sca.Playlist.new(title="I'm so happy, happy, happy, happy!") + + + + def test_groups(self): + if not self.RUN_LONG_TESTS: + return + + sca = self.root + groups = list(itertools.islice(sca.groups(), 0, 127)) + for group in groups: + users = group.users() + for user in users: + pass + + + def test_track_creation_with_email_sharers(self): + sca = self.root + emails = [dict(address="deets@web.de"), dict(address="hannes@soundcloud.com")] + track = sca.Track.new(title='bar', asset_data=self.data, + shared_to=dict(emails=emails) + ) + assert isinstance(track, scapi.Track) + + + + def test_track_creation_with_artwork(self): + sca = self.root + track = sca.Track.new(title='bar', + asset_data=self.data, + artwork_data=self.artwork_data, + ) + assert isinstance(track, scapi.Track) + + track.title = "foobarbaz" + + + + def test_oauth_get_signing(self): + sca = self.root + + url = "http://api.soundcloud.dev/oauth/test_request" + params = dict(foo="bar", + baz="padamm", + ) + url += sca._create_query_string(params) + signed_url = sca.oauth_sign_get_request(url) + + + res = urllib2.urlopen(signed_url).read() + assert "oauth_nonce" in res + + + def test_streaming(self): + sca = self.root + + track = sca.tracks(params={ + "filter" : "streamable", + }).next() + + + assert isinstance(track, scapi.Track) + + stream_url = track.stream_url + + signed_url = track.oauth_sign_get_request(stream_url) + + + def test_downloadable(self): + sca = self.root + + track = sca.tracks(params={ + "filter" : "downloadable", + }).next() + + + assert isinstance(track, scapi.Track) + + download_url = track.download_url + + signed_url = track.oauth_sign_get_request(download_url) + + data = urllib2.urlopen(signed_url).read() + assert data + + + + def test_modifying_playlists(self): + sca = self.root + + me = sca.me() + my_tracks = list(me.tracks()) + + assert my_tracks + + playlist = me.playlists().next() + # playlist = sca.Playlist.get(playlist.id) + + assert isinstance(playlist, scapi.Playlist) + + pl_tracks = playlist.tracks + + playlist.title = "foobarbaz" + + + + def test_track_deletion(self): + sca = self.root + track = sca.Track.new(title='bar', asset_data=self.data, + ) + + sca.tracks.remove(track) + + + + def test_track_creation_with_updated_artwork(self): + sca = self.root + track = sca.Track.new(title='bar', + asset_data=self.data, + ) + assert isinstance(track, scapi.Track) + + track.artwork_data = self.artwork_data + + def test_update_own_description(self): + sca = self.root + me = sca.me() + + new_description = "This is my new description" + old_description = "This is my old description" + + if me.description == new_description: + change_to_description = old_description + else: + change_to_description = new_description + + me.description = change_to_description + + user = sca.User.get(me.id) + assert user.description == change_to_description diff --git a/python_apps/soundcloud-api/scapi/tests/spam.jpg b/python_apps/soundcloud-api/scapi/tests/spam.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a5ca9387927d8cf721f79b6386a8b674bf216618 GIT binary patch literal 85062 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 b9~uk>B`)Z{ki_mp6}y6(mX`kj=0E@0vR)f1 literal 0 HcmV?d00001 diff --git a/python_apps/soundcloud-api/scapi/tests/test_connect.py b/python_apps/soundcloud-api/scapi/tests/test_connect.py new file mode 100644 index 000000000..3e045e6c1 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/tests/test_connect.py @@ -0,0 +1,334 @@ +from __future__ import with_statement +import os +import tempfile +import itertools +from ConfigParser import SafeConfigParser +import pkg_resources +import scapi +import scapi.authentication +import logging +import webbrowser + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +_logger = logging.getLogger("scapi") +#_logger.setLevel(logging.DEBUG) + +RUN_INTERACTIVE_TESTS = False +USE_OAUTH = True + +TOKEN = "FjNE9aRTg8kpxuOjzwsX8Q" +SECRET = "NP5PGoyKcQv64E0aZgV4CRNzHfPwR4QghrWoqEgEE" +CONSUMER = "EEi2URUfM97pAAxHTogDpQ" +CONSUMER_SECRET = "NFYd8T3i4jVKGZ9TMy9LHaBQB3Sh8V5sxBiMeMZBow" +API_HOST = "api.soundcloud.dev:3000" +USER = "" +PASSWORD = "" + +CONFIG_NAME = "soundcloud.cfg" + +CONNECTOR = None +ROOT = None +def setup(): + global CONNECTOR, ROOT + # load_config() + #scapi.ApiConnector(host='192.168.2.101:3000', user='tiga', password='test') + #scapi.ApiConnector(host='sandbox-api.soundcloud.com:3030', user='tiga', password='test') + scapi.USE_PROXY = False + scapi.PROXY = 'http://127.0.0.1:10000/' + + if USE_OAUTH: + authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + TOKEN, + SECRET) + else: + authenticator = scapi.authentication.BasicAuthenticator(USER, PASSWORD, CONSUMER, CONSUMER_SECRET) + + logger.debug("API_HOST: %s", API_HOST) + CONNECTOR = scapi.ApiConnector(host=API_HOST, + authenticator=authenticator) + ROOT = scapi.Scope(CONNECTOR) + +def load_config(config_name=None): + global TOKEN, SECRET, CONSUMER_SECRET, CONSUMER, API_HOST, USER, PASSWORD + if config_name is None: + config_name = CONFIG_NAME + parser = SafeConfigParser() + current = os.getcwd() + while current: + name = os.path.join(current, config_name) + if os.path.exists(name): + parser.read([name]) + TOKEN = parser.get('global', 'accesstoken') + SECRET = parser.get('global', 'accesstoken_secret') + CONSUMER = parser.get('global', 'consumer') + CONSUMER_SECRET = parser.get('global', 'consumer_secret') + API_HOST = parser.get('global', 'host') + USER = parser.get('global', 'user') + PASSWORD = parser.get('global', 'password') + logger.debug("token: %s", TOKEN) + logger.debug("secret: %s", SECRET) + logger.debug("consumer: %s", CONSUMER) + logger.debug("consumer_secret: %s", CONSUMER_SECRET) + logger.debug("user: %s", USER) + logger.debug("password: %s", PASSWORD) + logger.debug("host: %s", API_HOST) + break + new_current = os.path.dirname(current) + if new_current == current: + break + current = new_current + + +def test_load_config(): + base = tempfile.mkdtemp() + oldcwd = os.getcwd() + cdir = os.path.join(base, "foo") + os.mkdir(cdir) + os.chdir(cdir) + test_config = """ +[global] +host=host +consumer=consumer +consumer_secret=consumer_secret +accesstoken=accesstoken +accesstoken_secret=accesstoken_secret +user=user +password=password +""" + with open(os.path.join(base, CONFIG_NAME), "w") as cf: + cf.write(test_config) + load_config() + assert TOKEN == "accesstoken" and SECRET == "accesstoken_secret" and API_HOST == 'host' + assert CONSUMER == "consumer" and CONSUMER_SECRET == "consumer_secret" + assert USER == "user" and PASSWORD == "password" + os.chdir(oldcwd) + load_config() + + +def test_connect(): + sca = ROOT + quite_a_few_users = list(itertools.islice(sca.users(), 0, 127)) + + logger.debug(quite_a_few_users) + assert isinstance(quite_a_few_users, list) and isinstance(quite_a_few_users[0], scapi.User) + user = sca.me() + logger.debug(user) + assert isinstance(user, scapi.User) + contacts = list(user.contacts()) + assert isinstance(contacts, list) + assert isinstance(contacts[0], scapi.User) + logger.debug(contacts) + tracks = list(user.tracks()) + assert isinstance(tracks, list) + assert isinstance(tracks[0], scapi.Track) + logger.debug(tracks) + + +def test_access_token_acquisition(): + """ + This test is commented out because it needs user-interaction. + """ + if not RUN_INTERACTIVE_TESTS: + return + 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) + webbrowser.open(authorization_url) + raw_input("please press return") + oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + + sca = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) + token, secret = sca.fetch_access_token() + logger.info("Access token: '%s'", token) + logger.info("Access token secret: '%s'", secret) + oauth_authenticator = scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + token, + secret) + + sca = scapi.ApiConnector(API_HOST, authenticator=oauth_authenticator) + test_track_creation() + +def test_track_creation(): + sca = ROOT + track = sca.Track.new(title='bar') + assert isinstance(track, scapi.Track) + +def test_track_update(): + sca = ROOT + track = sca.Track.new(title='bar') + assert isinstance(track, scapi.Track) + track.title='baz' + track = sca.Track.get(track.id) + assert track.title == "baz" + +def test_scoped_track_creation(): + sca = ROOT + user = sca.me() + track = user.tracks.new(title="bar") + assert isinstance(track, scapi.Track) + +def test_upload(): + assert pkg_resources.resource_exists("scapi.tests.test_connect", "knaster.mp3") + data = pkg_resources.resource_stream("scapi.tests.test_connect", "knaster.mp3") + sca = ROOT + user = sca.me() + logger.debug(user) + asset = sca.assets.new(filedata=data) + assert isinstance(asset, scapi.Asset) + logger.debug(asset) + tracks = list(user.tracks()) + track = tracks[0] + track.assets.append(asset) + +def test_contact_list(): + sca = ROOT + user = sca.me() + contacts = list(user.contacts()) + assert isinstance(contacts, list) + assert isinstance(contacts[0], scapi.User) + +def test_permissions(): + sca = ROOT + user = sca.me() + tracks = itertools.islice(user.tracks(), 1) + for track in tracks: + permissions = list(track.permissions()) + logger.debug(permissions) + assert isinstance(permissions, list) + if permissions: + assert isinstance(permissions[0], scapi.User) + +def test_setting_permissions(): + sca = ROOT + me = sca.me() + track = sca.Track.new(title='bar', sharing="private") + assert track.sharing == "private" + users = itertools.islice(sca.users(), 10) + users_to_set = [user for user in users if user != me] + assert users_to_set, "Didn't find any suitable users" + track.permissions = users_to_set + assert set(track.permissions()) == set(users_to_set) + +def test_setting_comments(): + sca = ROOT + user = sca.me() + 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 + assert track.comments().next().body == comment.body + + +def test_setting_comments_the_way_shawn_says_its_correct(): + sca = ROOT + track = sca.Track.new(title='bar', sharing="private") + cbody = "This is the body of my comment" + track.comments.new(body=cbody, timestamp=10) + assert list(track.comments())[0].body == cbody + +def test_contact_add_and_removal(): + sca = ROOT + me = sca.me() + for user in sca.users(): + if user != me: + user_to_set = user + break + + contacts = list(me.contacts()) + if user_to_set in contacts: + me.contacts.remove(user_to_set) + + me.contacts.append(user_to_set) + + contacts = list(me.contacts() ) + assert user_to_set.id in [c.id for c in contacts] + + me.contacts.remove(user_to_set) + + contacts = list(me.contacts() ) + assert user_to_set not in contacts + + +def test_favorites(): + sca = ROOT + me = sca.me() + + favorites = list(me.favorites()) + assert favorites == [] or isinstance(favorites[0], scapi.Track) + + track = None + for user in sca.users(): + if user == me: + continue + for track in user.tracks(): + break + if track is not None: + break + + me.favorites.append(track) + + favorites = list(me.favorites()) + assert track in favorites + + me.favorites.remove(track) + + favorites = list(me.favorites()) + assert track not in favorites + +def test_large_list(): + sca = ROOT + tracks = list(sca.tracks()) + if len(tracks) < scapi.ApiConnector.LIST_LIMIT: + for i in xrange(scapi.ApiConnector.LIST_LIMIT): + scapi.Track.new(title='test_track_%i' % i) + all_tracks = sca.tracks() + assert not isinstance(all_tracks, list) + all_tracks = list(all_tracks) + assert len(all_tracks) > scapi.ApiConnector.LIST_LIMIT + + +def test_events(): + events = list(ROOT.events()) + assert isinstance(events, list) + assert isinstance(events[0], scapi.Event) + +def test_me_having_stress(): + sca = ROOT + for _ in xrange(20): + setup() + sca.me() + +def test_non_global_api(): + root = scapi.Scope(CONNECTOR) + me = root.me() + assert isinstance(me, scapi.User) + + # now get something *from* that user + favorites = list(me.favorites()) + assert favorites + +def test_playlists(): + sca = ROOT + playlists = list(itertools.islice(sca.playlists(), 0, 127)) + found = False + for playlist in playlists: + tracks = playlist.tracks + if not isinstance(tracks, list): + tracks = [tracks] + for trackdata in tracks: + print trackdata + user = trackdata.user + print user + print user.tracks() + print playlist.user + break diff --git a/python_apps/soundcloud-api/scapi/tests/test_oauth.py b/python_apps/soundcloud-api/scapi/tests/test_oauth.py new file mode 100644 index 000000000..d283c0048 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/tests/test_oauth.py @@ -0,0 +1,36 @@ +import pkg_resources +import scapi +import scapi.authentication +import urllib +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +_logger = logging.getLogger("scapi") +_logger.setLevel(logging.DEBUG) + +TOKEN = "QcciYu1FSwDSGKAG2mNw" +SECRET = "gJ2ok6ULUsYQB3rsBmpHCRHoFCAPOgK8ZjoIyxzris" +CONSUMER = "Cy2eLPrIMp4vOxjz9icdQ" +CONSUMER_SECRET = "KsBa272x6M2to00Vo5FdvZXt9kakcX7CDIPJoGwTro" + +def test_base64_connect(): + scapi.USE_PROXY = True + scapi.PROXY = 'http://127.0.0.1:10000/' + scapi.SoundCloudAPI(host='192.168.2.31:3000', authenticator=scapi.authentication.BasicAuthenticator('tiga', 'test')) + sca = scapi.Scope() + assert isinstance(sca.me(), scapi.User) + + +def test_oauth_connect(): + scapi.USE_PROXY = True + scapi.PROXY = 'http://127.0.0.1:10000/' + scapi.SoundCloudAPI(host='192.168.2.31:3000', + authenticator=scapi.authentication.OAuthAuthenticator(CONSUMER, + CONSUMER_SECRET, + TOKEN, SECRET)) + + sca = scapi.Scope() + assert isinstance(sca.me(), scapi.User) + + diff --git a/python_apps/soundcloud-api/scapi/util.py b/python_apps/soundcloud-api/scapi/util.py new file mode 100644 index 000000000..00fa66897 --- /dev/null +++ b/python_apps/soundcloud-api/scapi/util.py @@ -0,0 +1,53 @@ +## 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 + +def escape(s): + # escape '/' too + return urllib.quote(s, safe='') + + + + + + +class MultiDict(dict): + + + def add(self, key, new_value): + if key in self: + value = self[key] + if not isinstance(value, list): + value = [value] + self[key] = value + value.append(new_value) + else: + self[key] = new_value + + + def iteritemslist(self): + for key, value in self.iteritems(): + if not isinstance(value, list): + value = [value] + yield key, value + + + diff --git a/python_apps/soundcloud-api/setup.py b/python_apps/soundcloud-api/setup.py new file mode 100644 index 000000000..dc057a603 --- /dev/null +++ b/python_apps/soundcloud-api/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages + +TEST_REQUIRES = ["ConfigObj>=4.5.3", "nose>=0.10"] + + +setup( + name = "SoundCloudAPI", + version = "0.1", + packages = find_packages(), + author = "Diez B. Roggisch", + author_email = "deets@web.de", + description = "This is an implementation of the SoundCloud RESTful API", + license = "MIT", + keywords = "Soundcloud client API REST", + url = "http://wiki.github.com/soundcloud/api/python-api-wrapper/", + install_requires = ['simplejson'] + TEST_REQUIRES, +# tests_require = TEST_REQUIRES, +# extras_require = dict( +# test = TEST_REQUIRES +# ), + test_suite = 'nose.collector' +) diff --git a/python_apps/soundcloud-api/test.ini b/python_apps/soundcloud-api/test.ini new file mode 100644 index 000000000..c9d1422b3 --- /dev/null +++ b/python_apps/soundcloud-api/test.ini @@ -0,0 +1,33 @@ +[api] + +# api.soundcloud.dev credentials +api_host=api.soundcloud.dev +consumer=gLnhFeUBnBCZF8a6Ngqq7w +consumer_secret=nbWRdG5X9xUb63l4nIeFYm3nmeVJ2v4s1ROpvRSBvU8 +token=xt5f76c7qPVCBNX3Vrw6A +secret=Ow2WHDKN4YRxrVfcPOt9pHf52Pzv70fp8lG0catQ5w + +# api.sandbox-soundcloud.com credentials +#api_host=api.sandbox-soundcloud.com +#consumer=gLnhFeUBnBCZF8a6Ngqq7w +#consumer_secret=nbWRdG5X9xUb63l4nIeFYm3nmeVJ2v4s1ROpvRSBvU8 +#token=8aX4ApxRweZJsZYe1PTFxQ +#secret=ydZOeG5zQXe8AiExvnzQVfqgySCrbFp0TLSz7BsnBqo + +user=deets +password=kgl27f + +#request_token_url=api.sandbox-soundcloud.com/oauth/request_token +#authorize_token_url=sandbox-soundcloud.com/oauth/authorize_token +#access_token_url=api.sandbox-soundcloud.com/oauth/request_token + +[proxy] +use_proxy=false + +[logging] +test_logger=DEBUG +api_logger=DEBUG + +[test] +run_interactive_tests=false +run_long_tests=false diff --git a/tests/application/controllers/RecorderControllerTest.php b/tests/application/controllers/RecorderControllerTest.php new file mode 100644 index 000000000..5e7700b8c --- /dev/null +++ b/tests/application/controllers/RecorderControllerTest.php @@ -0,0 +1,20 @@ +