├── .gitignore ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── README.md ├── clockwork ├── __init__.py ├── clockwork.py ├── clockwork_exceptions.py └── clockwork_http.py ├── setup.py └── tests └── clockwork_tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mediaburst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | LICENSE.txt 3 | README.md 4 | setup.py 5 | clockwork\__init__.py 6 | clockwork\clockwork.py 7 | clockwork\clockwork_exceptions.py 8 | clockwork\clockwork_http.py 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clockwork SMS API for Python 2 | 3 | ## Install 4 | 5 | The easiest way to install is through "pip": 6 | 7 | pip install clockwork 8 | 9 | ## Requirements 10 | 11 | * Python 2.6+ 12 | 13 | ## Usage 14 | 15 | ### Send a single SMS message 16 | 17 | ```python 18 | from clockwork import clockwork 19 | api = clockwork.API('API_KEY_GOES_HERE') #Be careful not to post your API Keys to public repositories. 20 | message = clockwork.SMS(to = '441234123456', message = 'This is a test message.') 21 | response = api.send(message) 22 | 23 | if response.success: 24 | print (response.id) 25 | else: 26 | print (response.error_code) 27 | print (response.error_message) 28 | ``` 29 | 30 | ### Send multiple SMS messages 31 | 32 | Simply pass an array of sms objects to the send method. Instead of sending back a single sms response, an array of sms responses will be returned: 33 | 34 | ```python 35 | from clockwork import clockwork 36 | api = clockwork.API('API_KEY_GOES_HERE') #Be careful not to post your API Keys to public repositories. 37 | message1 = clockwork.SMS(to = '441234123456', message = 'This is a test message 1.') 38 | message2 = clockwork.SMS(to = '441234123457', message = 'This is a test message 2.') 39 | message3 = clockwork.SMS(to = '441234123458', message = 'This is a test message 3.') 40 | response = api.send([message1,message2,message3]) 41 | 42 | for sms_response in response: 43 | if sms_response.success: 44 | print (sms_response.id) 45 | else: 46 | print (sms_response.error_code) 47 | print (sms_response.error_message) 48 | ``` 49 | 50 | Passing an array of messages to the send method is much more efficient than making multiple calls to the `send` method; as well making less round-trips to the server the messages are "batched" in clockwork, which is significantly better for performance. 51 | 52 | ### Send messages - available parameters 53 | 54 | This wrapper supports a subset of the available clockwork API parameters for sending (for the full set see [here][2]). 55 | 56 | ##### Setting parameters for all messages 57 | 58 | You create an `api` object with `api = clockwork.API(api_key,[optional_setting = value,..]` 59 | The `optional_setting` parameters allows you to set the following, which will be used for all messages sent through the `api` object: 60 | 61 | Parameter | Description 62 | --------- | ----------- 63 | from_name | Sets the [from name](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-from "from address") 64 | concat | Sets the [concat](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-concat) setting 65 | invalid_char_option | Sets the [InvalidCharOption](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-invalidcharaction) setting 66 | truncate | Sets the [truncate](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-truncate) setting 67 | 68 | So for example if I want all messages to use the from address 'bobby', I would do: 69 | 70 | ```python 71 | api = clockwork.API('MY_API_KEY', from_name = 'Bobby') 72 | ``` 73 | 74 | ##### Setting parameters for each message. 75 | 76 | You create an `sms` object with `sms = clockwork.SMS(to = 'xxx', message = 'xxx', [optional_setting = value,..]` 77 | 78 | In a similar pattern to the API parameters, the `optional_setting` parameters allows you to set the following additional parameters for an individual message: 79 | 80 | Parameter | Description 81 | --------- | ----------- 82 | client_id | Sets the [ClientId](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-clientid) setting 83 | from_name | Sets the [from name](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-from "from address") 84 | invalid_char_option | Sets the [InvalidCharOption](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-invalidcharaction) setting 85 | truncate | Sets the [truncate](http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/#param-truncate) setting 86 | 87 | Any parameters defined here will take precedence over the same one defined on the `api` object: 88 | 89 | ```python 90 | api = clockwork.API('MY_API_KEY',from_name = 'Bobby') 91 | sms = clockwork.SMS(to = '441234123456', message = 'This is a test message 1.', from_name = 'Sammy') 92 | response = api.send(sms) # WILL SEND WITH FROM ADDRESS 'Sammy' 93 | ``` 94 | 95 | ### Check balance 96 | 97 | ```python 98 | from clockwork import clockwork 99 | api = clockwork.API('API_KEY_GOES_HERE') #Be careful not to post your API Keys to public repositories. 100 | balance = api.get_balance() 101 | print (balance) # => {'currency': None, 'balance': '231.03', 'account_type': 'PAYG'} 102 | ``` 103 | 104 | ## License 105 | 106 | This project is licensed under the MIT open-source license. 107 | 108 | A copy of this license can be found in LICENSE.txt 109 | 110 | ## Contributing 111 | 112 | If you have any feedback on this wrapper drop us an email to [hello@clockworksms.com][3]. 113 | 114 | The project is hosted on GitHub at [http://www.github.com/mediaburst/clockwork-python][4]. 115 | 116 | If you would like to contribute a bug fix or improvement please fork the project 117 | and submit a pull request. Please add tests for your use case. 118 | 119 | [2]: http://www.clockworksms.com/doc/clever-stuff/xml-interface/send-sms/ 120 | [3]: mailto:hello@clockworksms.com 121 | [4]: http://www.github.com/mediaburst/clockwork-python 122 | 123 | ## Changelog 124 | 125 | ### 1.2.0 (24th February 2016) 126 | 127 | * Removed lxml dependency 128 | 129 | ### 1.1.0 (5th January 2015) 130 | 131 | * Python3 Support 132 | 133 | ### 1.03 (23rd December 2014) 134 | 135 | * Replacing Distribute with Setuptools 136 | 137 | ### 1.0.2 (18th May 2014) 138 | 139 | * Unicode support added [MR] 140 | 141 | ### 1.0.1 (01st September, 2013) 142 | 143 | * Minor changes 144 | 145 | ### 1.0.0 (01st August, 2013) 146 | 147 | * Initial release of wrapper [MR] 148 | 149 | 150 | ## Credits and Acknowledgements 151 | 152 | Thanks to [zeroSteiner](https://github.com/zeroSteiner) for removing the lxml dependency and bringing ElementTree into the wrapper. 153 | 154 | Thanks to [bjornpost](https://github.com/bjornpost) for his work on Python 3 support and replacing Distribute with Setuptools 155 | 156 | Many thanks to [cHemingway](https://github.com/cHemingway) for adding Unicode support. 157 | -------------------------------------------------------------------------------- /clockwork/__init__.py: -------------------------------------------------------------------------------- 1 | from . clockwork import API, SMS 2 | from . clockwork_exceptions import ApiException, AuthException, GenericException, HttpException 3 | -------------------------------------------------------------------------------- /clockwork/clockwork.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree as etree 2 | from . import clockwork_http 3 | from . import clockwork_exceptions 4 | 5 | SMS_URL = 'https://api.clockworksms.com/xml/send.aspx' 6 | CREDIT_URL = 'https://api.clockworksms.com/xml/credit.aspx' 7 | BALANCE_URL = 'https://api.clockworksms.com/xml/balance.aspx' 8 | 9 | 10 | class SMS(object): 11 | """An SMS object""" 12 | 13 | def __init__(self, to, message, client_id=None, from_name=None, long=None, truncate=None, invalid_char_option=None): 14 | self.client_id = client_id 15 | self.from_name = from_name 16 | self.long = long 17 | self.truncate = truncate 18 | self.invalid_char_option = invalid_char_option 19 | self.to = to 20 | self.message = message 21 | 22 | class SMSResponse(object): 23 | """An wrapper around an SMS reponse""" 24 | 25 | def __init__(self, sms, id, error_code, error_message, success): 26 | self.sms = sms 27 | self.id = id 28 | self.error_code = error_code 29 | self.error_message = error_message 30 | self.success = success 31 | 32 | 33 | class API(object): 34 | """Wraps the clockwork API""" 35 | 36 | def __init__(self, apikey, from_name='Clockwork', concat=3, 37 | invalid_char_option='error', long=False, truncate=True, 38 | use_ssl=True): 39 | self.apikey = apikey 40 | self.from_name = from_name 41 | self.concat = concat 42 | self.invalid_char_option = invalid_char_option 43 | self.long = long 44 | self.truncate = truncate 45 | self.use_ssl = use_ssl 46 | 47 | def get_balance(self): 48 | """Check the balance fot this account. 49 | Returns a dictionary containing: 50 | account_type: The account type 51 | balance: The balance remaining on the account 52 | currency: The currency used for the account balance. Assume GBP in not set""" 53 | 54 | xml_root = self.__init_xml('Balance') 55 | 56 | response = clockwork_http.request(BALANCE_URL, etree.tostring(xml_root, encoding='utf-8')) 57 | data_etree = etree.fromstring(response['data']) 58 | 59 | err_desc = data_etree.find('ErrDesc') 60 | if err_desc is not None: 61 | raise clockwork_exceptions.ApiException(err_desc.text, data_etree.find('ErrNo').text) 62 | 63 | result = {} 64 | result['account_type'] = data_etree.find('AccountType').text 65 | result['balance'] = data_etree.find('Balance').text 66 | result['currency'] = data_etree.find('Currency').text 67 | return result 68 | 69 | def send(self, messages): 70 | """Send a SMS message, or an array of SMS messages""" 71 | 72 | tmpSms = SMS(to='', message='') 73 | if str(type(messages)) == str(type(tmpSms)): 74 | messages = [messages] 75 | 76 | xml_root = self.__init_xml('Message') 77 | wrapper_id = 0 78 | 79 | for m in messages: 80 | m.wrapper_id = wrapper_id 81 | msg = self.__build_sms_data(m) 82 | sms = etree.SubElement(xml_root, 'SMS') 83 | for sms_element in msg: 84 | element = etree.SubElement(sms, sms_element) 85 | element.text = msg[sms_element] 86 | 87 | # print etree.tostring(xml_root) 88 | response = clockwork_http.request(SMS_URL, etree.tostring(xml_root, encoding='utf-8')) 89 | response_data = response['data'] 90 | 91 | # print response_data 92 | data_etree = etree.fromstring(response_data) 93 | 94 | # Check for general error 95 | err_desc = data_etree.find('ErrDesc') 96 | if err_desc is not None: 97 | raise clockwork_exceptions.ApiException(err_desc.text, data_etree.find('ErrNo').text) 98 | 99 | # Return a consistent object 100 | results = [] 101 | for sms in data_etree: 102 | matching_sms = next((s for s in messages if str(s.wrapper_id) == sms.find('WrapperID').text), None) 103 | new_result = SMSResponse( 104 | sms = matching_sms, 105 | id = '' if sms.find('MessageID') is None else sms.find('MessageID').text, 106 | error_code = 0 if sms.find('ErrNo') is None else sms.find('ErrNo').text, 107 | error_message = '' if sms.find('ErrDesc') is None else sms.find('ErrDesc').text, 108 | success = True if sms.find('ErrNo') is None else (sms.find('ErrNo').text == 0) 109 | ) 110 | results.append(new_result) 111 | 112 | if len(results) > 1: 113 | return results 114 | 115 | return results[0] 116 | 117 | def __init_xml(self, rootElementTag): 118 | """Init a etree element and pop a key in there""" 119 | xml_root = etree.Element(rootElementTag) 120 | key = etree.SubElement(xml_root, "Key") 121 | key.text = self.apikey 122 | return xml_root 123 | 124 | 125 | def __build_sms_data(self, message): 126 | """Build a dictionary of SMS message elements""" 127 | 128 | attributes = {} 129 | 130 | attributes_to_translate = { 131 | 'to' : 'To', 132 | 'message' : 'Content', 133 | 'client_id' : 'ClientID', 134 | 'concat' : 'Concat', 135 | 'from_name': 'From', 136 | 'invalid_char_option' : 'InvalidCharOption', 137 | 'truncate' : 'Truncate', 138 | 'wrapper_id' : 'WrapperId' 139 | } 140 | 141 | for attr in attributes_to_translate: 142 | val_to_use = None 143 | if hasattr(message, attr): 144 | val_to_use = getattr(message, attr) 145 | if val_to_use is None and hasattr(self, attr): 146 | val_to_use = getattr(self, attr) 147 | if val_to_use is not None: 148 | attributes[attributes_to_translate[attr]] = str(val_to_use) 149 | 150 | return attributes 151 | -------------------------------------------------------------------------------- /clockwork/clockwork_exceptions.py: -------------------------------------------------------------------------------- 1 | # Exception classes 2 | 3 | class HttpException(Exception): 4 | def __init__(self, value): 5 | self.value = value 6 | 7 | def __str__(self): 8 | return repr(self.value) 9 | 10 | class AuthException(Exception): 11 | def __init__(self, value): 12 | self.value = value 13 | 14 | def __str__(self): 15 | return repr(self.value) 16 | 17 | class GenericException(Exception): 18 | def __init__(self, value): 19 | self.value = value 20 | 21 | def __str__(self): 22 | return repr(self.value) 23 | 24 | class ApiException(Exception): 25 | def __init__(self, value, errNum): 26 | self.value = value 27 | self.errNum = errNum 28 | 29 | def __str__(self): 30 | return repr(self.value) 31 | -------------------------------------------------------------------------------- /clockwork/clockwork_http.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | try: 3 | import urllib.request as _urllib # py3 4 | from urllib.error import URLError 5 | except ImportError: 6 | import urllib2 as _urllib # py2 7 | from urllib2 import URLError 8 | 9 | from . import clockwork_exceptions 10 | 11 | def request(url, xml): 12 | """Make a http request to clockwork, using the XML provided 13 | Sets sensible headers for the request. 14 | 15 | If there is a problem with the http connection a clockwork_exceptions.HttpException is raised 16 | """ 17 | 18 | r = _urllib.Request(url, xml) 19 | r.add_header('Content-Type', 'application/xml') 20 | r.add_header('User-Agent', 'Clockwork Python wrapper/1.0') 21 | 22 | result = {} 23 | try: 24 | f = _urllib.urlopen(r) 25 | except URLError as error: 26 | raise clockwork_exceptions.HttpException("Error connecting to clockwork server: %s" % error) 27 | 28 | result['data'] = f.read() 29 | result['status'] = f.getcode() 30 | 31 | if hasattr(f, 'headers'): 32 | result['etag'] = f.headers.get('ETag') 33 | result['lastmodified'] = f.headers.get('Last-Modified') 34 | if f.headers.get('content−encoding', '') == 'gzip': 35 | result['data'] = gzip.GzipFile(fileobj=StringIO(result['data'])).read() 36 | if hasattr(f, 'url'): 37 | result['url'] = f.url 38 | result['status'] = 200 39 | f.close() 40 | 41 | if result['status'] != 200: 42 | raise clockwork_exceptions.HttpException("Error connecting to clockwork server - status code %s" % result['status']) 43 | 44 | return result 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='Clockwork', 5 | version='1.2.0', 6 | packages=['clockwork'], 7 | license='MIT', 8 | author='Mediaburst', 9 | author_email='hello@clockworksms.com', 10 | long_description=open('README.md').read(), 11 | description='Python wrapper for the clockwork SMS Api', 12 | url='https://github.com/mediaburst/clockwork-python' 13 | ) 14 | -------------------------------------------------------------------------------- /tests/clockwork_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from clockwork import clockwork 5 | from clockwork import clockwork_exceptions 6 | 7 | class ApiTests(unittest.TestCase): 8 | 9 | api_key = "YOUR_API_KEY_HERE" 10 | 11 | def test_should_send_single_message(self): 12 | """Sending a single SMS with the minimum detail and no errors should work""" 13 | api = clockwork.API(self.api_key) 14 | sms = clockwork.SMS(to="441234567890", message="This is a test message") 15 | response = api.send(sms) 16 | self.assertTrue(response.success) 17 | 18 | def test_should_send_single_unicode_message(self): 19 | """Sending a single SMS with the full GSM character set (apart from ESC and form feed) should work""" 20 | api = clockwork.API(self.api_key) 21 | sms = clockwork.SMS( 22 | to="441234567890", 23 | #Message table copied from http://www.clockworksms.com/doc/reference/faqs/gsm-character-set/ 24 | message=u'''@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ''' 25 | u''' !"#¤%&'()*+,-./''' 26 | u'''0123456789:;<=>?''' 27 | u'''¡ABCDEFGHIJKLMNO''' 28 | u'''PQRSTUVWXYZÄÖÑܧ''' 29 | u'''¿abcdefghijklmno''' 30 | u'''pqrstuvwxyzäöñüà''' 31 | u'''€[\]^{|}~''' 32 | ,long=True) 33 | response = api.send(sms) 34 | self.assertTrue(response.success) 35 | 36 | def test_should_fail_with_no_message(self): 37 | """Sending a single SMS with no message should fail""" 38 | api = clockwork.API(self.api_key) 39 | sms = clockwork.SMS(to="441234567890", message="") 40 | response = api.send(sms) 41 | self.assertFalse(response.success) 42 | 43 | def test_should_fail_with_no_to(self): 44 | """Sending a single SMS with no message should fail""" 45 | api = clockwork.API(self.api_key) 46 | sms = clockwork.SMS(to="", message="This is a test message") 47 | response = api.send(sms) 48 | self.assertFalse(response.success) 49 | 50 | def test_should_send_multiple_messages(self): 51 | """Sending multiple sms messages should work""" 52 | api = clockwork.API(self.api_key) 53 | sms1 = clockwork.SMS(to="441234567890", message="This is a test message 1") 54 | sms2 = clockwork.SMS(to="441234567890", message="This is a test message 2") 55 | response = api.send([sms1,sms2]) 56 | 57 | for r in response: 58 | self.assertTrue(r.success) 59 | 60 | def test_should_send_multiple_messages_with_erros(self): 61 | """Sending multiple sms messages, one of which has an invalid message should work""" 62 | api = clockwork.API(self.api_key) 63 | sms1 = clockwork.SMS(to="441234567890", message="This is a test message 1") 64 | sms2 = clockwork.SMS(to="441234567890", message="") 65 | response = api.send([sms1, sms2]) 66 | 67 | self.assertTrue(response[0].success) 68 | self.assertFalse(response[1].success) 69 | 70 | def test_should_fail_with_invalid_key(self): 71 | api = clockwork.API("this_key_is_wrong") 72 | sms = clockwork.SMS(to="441234567890", message="This is a test message 1") 73 | self.assertRaises(clockwork_exceptions.ApiException, api.send, sms) 74 | 75 | def test_should_be_able_to_get_balance(self): 76 | api = clockwork.API(self.api_key) 77 | balance = api.get_balance() 78 | self.assertEqual('PAYG', balance['account_type']) 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | --------------------------------------------------------------------------------