Commits

Valentin Gorbunov committed 912eb65

Initial import

Comments (0)

Files changed (13)

+dist
+MANIFEST
+*.pyc
+smartresponder.egg-info
+syntax: glob
+
+#IDE files
+.settings/*
+.project
+.pydevproject
+.cache/*
+
+#temp files
+*.pyc
+*.pyo
+*.orig
+*~
+
+#misc files
+pip-log.txt
+.tox
+
+#os files
+.DS_Store
+Thumbs.db
+
+#setup files
+build/
+dist/
+.build/
+MANIFEST
+Authors
+=======
+
+* Valentin Gorbunov <valuerr@gmail.com>
+
+Contributors from vkontakte_api http://bitbucket.org/kmike/vkontakte/overview :
+
+* Mikhail Korobov <kmike84@gmail.com>
+* Morarenko Kirill <m0riarty@ya.ru>
+* ramusus <ramusus@gmail.com>
+* Alex Kamedov (http://kamedov.ru)
+* Pyotr Yermishkin <quasiyoke@gmail.com>
+
+Changes
+=======
+
+1.0.0 (2012-08-04)
+------------------
+Initial release.
+Copyright (c) 2010-2012 Valentin Gorbunov and contributors
+
+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.
+include *.txt
+include *.rst
+recursive-include docs *.txt
+=========
+smartresponder
+=========
+
+This is a smartresponder (aka smartresponder.ru)
+python API wrapper. The goal is to support all API methods (current and future)
+that can be accessed from server.
+
+Installation
+============
+
+::
+
+    $ pip install smartresponder
+
+Usage
+=====
+
+::
+
+    >>> import smartresponder
+    >>> api = smartresponder.API('my_api_id', 'my_api_secret')
+    >>> subscriber = api.subscribers.list(id='39715947')['list']['elements'][0]
+    >>> print subscriber['email']
+    test@test.ru
+
+All API methods that can be called from server should be supported.
+
+See http://goo.gl/DrmQU for detailed API help.
+#!/usr/bin/env python
+from distutils.core import setup
+
+version='1.0.0'
+
+setup(
+    name='smartresponder',
+    version=version,
+    author='Valentin Gorbunov',
+    author_email='valuerr@gmail.com',
+
+    packages=['smartresponder'],
+
+    url='http://bitbucket.org/valuerr/smartresponder/',
+    license = 'MIT license',
+    description = "smartresponder.ru API wrapper",
+
+    long_description = open('README.rst').read() + open('CHANGES.rst').read(),
+
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.5',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)

smartresponder/__init__.py

+from smartresponder.api import API, SMRError, signature

smartresponder/api.py

+# coding: utf-8
+import urllib
+from hashlib import md5
+from functools import partial
+from smartresponder import http
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+DEFAULT_TIMEOUT = 1
+API_URL = 'http://api.smartresponder.ru/'
+REQUEST_ENCODING = 'utf8'
+
+# See full list of VK API methods here: http://api.smartresponder.ru/doc/
+COMPLEX_METHODS = ['main', 'deliveries', 'files', 'templates', 'subscribers',
+                   'subscribe', 'groups', 'tracks', 'catalog']
+
+class SMRError(Exception):
+    __slots__ = ["error"]
+
+    def __init__(self, error_data):
+        self.error = error_data
+        Exception.__init__(self, str(self))
+
+    @property
+    def code(self):
+        return self.error['error_code']
+
+    @property
+    def description(self):
+        return self.error['error_msg']
+
+    @property
+    def params(self):
+        return self.error['request_params']
+
+    def __unicode__(self):
+        return "Error(code = '%s', description = '%s', params = '%s')" % (self.code, self.description, self.params)
+
+    def __str__(self):
+        return "Error(code = '%s', params = '%s')" % (self.code, self.params)
+
+
+def safe_json_loads(data):
+    return json.loads(data[data.find('{'):])
+
+
+def _encode(s):
+    if isinstance(s, (dict, list, tuple)):
+        s = json.dumps(s, ensure_ascii=False, encoding=REQUEST_ENCODING)
+
+    if isinstance(s, unicode):
+        s = s.encode(REQUEST_ENCODING)
+
+    return s # this can be number, etc.
+
+
+def signature(api_secret, params):
+    if isinstance(params, dict):
+        params = params.items()
+    param_str = ":".join(
+        ["%s=%s" % (str(key), _encode(value)) for key, value in params] +
+        ["%s=%s" % ('password', api_secret)])
+    return md5(param_str).hexdigest()
+
+
+class _API(object):
+    def __init__(self, api_id=None, api_secret=None, token=None, **defaults):
+        if not (api_id and api_secret or token):
+            raise ValueError("Arguments api_id and api_secret or token are required")
+
+        self.api_id = api_id
+        self.api_secret = api_secret
+        self.token = token
+        self.defaults = defaults
+
+    def __call__(self, **kwargs):
+        scope = kwargs.pop('scope', None) or getattr(self, 'scope')
+        params = self.defaults.copy()
+        params.update(kwargs)
+        return self._get(scope, **params)
+
+    def _signature(self, params):
+        return signature(self.api_secret, params)
+
+    def _get(self, method, timeout=DEFAULT_TIMEOUT, **kwargs):
+        status, response = self._request(method, timeout=timeout, **kwargs)
+        if not (200 <= status <= 299):
+            raise SMRError({
+                'error_code': status,
+                'error_msg': "HTTP error",
+                'request_params': kwargs,
+                })
+
+        data = json.loads(response, strict=False)
+        if "error" in data:
+            raise SMRError({
+                'error_code': data['error']['code'],
+                'error_msg': data['error']['message'],
+                'request_params': kwargs,
+                })
+        return data
+
+    def __getattr__(self, name):
+        '''
+        Support for api.<method>.<methodName> syntax
+        '''
+        if name in COMPLEX_METHODS:
+            api = _API(api_id=self.api_id, api_secret=self.api_secret, token=self.token, **self.defaults)
+            api.scope = name
+            return api
+
+        # the magic to convert instance attributes into method names
+        return partial(self, action=name)
+
+    def _request(self, scope, timeout=DEFAULT_TIMEOUT, **kwargs):
+        for key, value in kwargs.iteritems():
+            kwargs[key] = _encode(value)
+
+        params = {
+            'format': 'json',
+            }
+        params.update(kwargs)
+        params_list = params.items()
+
+        if self.token:
+            params_list += [('api_key', self.token)]
+        else:
+            params_list += [('api_id', self.api_id)]
+            sig = self._signature(params_list)
+            params_list += [('hash', sig)]
+
+        data = urllib.urlencode(params_list)
+        url = API_URL + scope + '.html'
+        secure = False
+
+        headers = {"Accept": "application/json",
+                   "Content-Type": "application/x-www-form-urlencoded"}
+        return http.post(url, data, headers, timeout, secure=secure)
+
+
+class API(_API):
+    def get(self, method, timeout=DEFAULT_TIMEOUT, **kwargs):
+        return self._get(method, timeout, **kwargs)

smartresponder/http.py

+#coding: utf-8
+from __future__ import with_statement
+from contextlib import closing
+import httplib
+
+# urllib2 doesn't support timeouts for python 2.5 so
+# custom function is used for making http requests
+
+def post(url, data, headers, timeout, secure=False):
+    host_port = url.split('/')[2]
+    timeout_set = False
+    connection = httplib.HTTPSConnection if secure else httplib.HTTPConnection
+    try:
+        connection = connection(host_port, timeout = timeout)
+        timeout_set = True
+    except TypeError:
+        connection = connection(host_port)
+
+    with closing(connection):
+        if not timeout_set:
+            connection.connect()
+            connection.sock.settimeout(timeout)
+            timeout_set = True
+
+        connection.request("POST", url, data, headers)
+        response = connection.getresponse()
+        return (response.status, response.read())

smartresponder/tests.py

+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Tests for smartresponder package.
+Requires mock >= 0.7.2.
+"""
+
+import os
+import sys
+import urllib
+sys.path.insert(0, os.path.abspath('..'))
+
+import unittest
+import mock
+import smartresponder
+import smartresponder.api
+
+API_ID = 'api_id'
+API_SECRET = 'api_secret'
+
+class SmartresponderTest(unittest.TestCase):
+    def test_api_creation_error(self):
+        self.assertRaises(ValueError, lambda: smartresponder.API())
+
+class SignatureTest(unittest.TestCase):
+    def test_signature_supports_unicode(self):
+        params = {'foo': u'клен'}
+        self.assertEqual(
+            smartresponder.signature(API_SECRET, params),
+            '46abe10921c93d6c45f839cf09c7d19b'
+        )
+
+if __name__ == '__main__':
+    unittest.main()
+[tox]
+envlist = py25,py26,py27,pypy
+
+[testenv]
+deps=
+    mock == 0.7.2
+
+commands=
+    python smartresponder/tests.py
+
+[testenv:py25]
+deps=
+    mock == 0.7.2
+    simplejson