├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py ├── test_threetaps.py └── threetaps ├── __init__.py └── client.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 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # vim 39 | *.sw[op] 40 | 41 | # pyenv 42 | .python-version 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 mkolodny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE test_threetaps.py requirements.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 3taps 2 | ===== 3 | 4 | A Python interface for the 3taps API. 5 | 6 | The goal of this library is to map 3taps' endpoints one-to-one. Calls to 3taps are made with clean, Pythonic methods. It only handles raw data, allowing you to define your own models. 7 | 8 | Dependencies: 9 | 10 | - requests 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | Install via pip: 17 | 18 | .. code-block:: bash 19 | 20 | $ pip install threetaps 21 | 22 | Install from source: 23 | 24 | .. code-block:: bash 25 | 26 | $ git clone https://github.com/mkolodny/3taps.git 27 | $ cd 3taps 28 | $ python setup.py install 29 | 30 | 31 | Usage 32 | ----- 33 | 34 | Instantiating a client: 35 | 36 | .. code-block:: bash 37 | 38 | >>> client = threetaps.Threetaps('YOUR_API_KEY') 39 | 40 | 41 | Examples 42 | -------- 43 | 44 | |Reference|_ 45 | 46 | Sources: 47 | 48 | .. code-block:: pycon 49 | 50 | >>> client.reference.sources() 51 | 52 | Category Groups: 53 | 54 | .. code-block:: pycon 55 | 56 | >>> client.reference.category_groups() 57 | 58 | Categories: 59 | 60 | .. code-block:: pycon 61 | 62 | >>> client.reference.categories() 63 | 64 | Locations: 65 | 66 | .. code-block:: pycon 67 | 68 | >>> client.reference.locations('locality', params={'city': 'USA-NYM-NEY'}) 69 | 70 | Locations Lookup: 71 | 72 | .. code-block:: pycon 73 | 74 | >>> client.reference.location_lookup('CAN-YUL') 75 | 76 | .. |Reference| replace:: **Reference** 77 | .. _Reference: http://docs.3taps.com/reference_api.html 78 | 79 | ---- 80 | 81 | |Search|_ 82 | 83 | Search: 84 | 85 | .. code-block:: pycon 86 | 87 | >>> client.search.search(params={'location.city': 'USA-NYM-NEY'}) 88 | 89 | Count: 90 | 91 | .. code-block:: pycon 92 | 93 | >>> client.search.count('category', params={'status': 'for_sale'}) 94 | 95 | .. |Search| replace:: **Search** 96 | .. _Search: http://docs.3taps.com/search_api.html 97 | 98 | ---- 99 | 100 | |Polling|_ 101 | 102 | Anchor: 103 | 104 | .. code-block:: pycon 105 | 106 | >>> utc_dt = datetime.today() 107 | >>> client.polling.anchor(utc_dt) 108 | 109 | Poll: 110 | 111 | .. code-block:: pycon 112 | 113 | >>> client.polling.poll(params={'anchor': '306785687'}) 114 | 115 | .. |Polling| replace:: **Polling** 116 | .. _Polling: http://docs.3taps.com/polling_api.html 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpretty==0.7.0 2 | requests==2.0.1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | if sys.argv[-1] == 'publish': 12 | os.system('python setup.py sdist bdist_wininst upload -r pypi') 13 | sys.exit() 14 | 15 | with open('README.rst') as f: 16 | readme = f.read() 17 | with open('LICENSE') as f: 18 | license = f.read() 19 | 20 | setup( 21 | name='threetaps', 22 | version='0.2.2', 23 | description='3taps API Client.', 24 | long_description=readme, 25 | author='Michael Kolodny', 26 | author_email='michaelckolodny@gmail.com', 27 | url='https://github.com/mkolodny/3taps', 28 | packages=['threetaps'], 29 | package_data={'': ['LICENSE']}, 30 | package_dir={'threetaps': 'threetaps'}, 31 | install_requires=['requests'], 32 | license=license, 33 | ) 34 | -------------------------------------------------------------------------------- /test_threetaps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # (c) 2014 Michael Kolodny 4 | 5 | """Tests for threetaps.""" 6 | 7 | from __future__ import unicode_literals 8 | import unittest 9 | import os 10 | import json 11 | from datetime import datetime 12 | try: 13 | from urlparse import urljoin 14 | except ImportError: 15 | from urllib.parse import urljoin 16 | import httpretty 17 | import threetaps 18 | 19 | AUTH_TOKEN = 'AUTH_TOKEN' 20 | 21 | 22 | class BaseTestCase(unittest.TestCase): 23 | 24 | def setUp(self): 25 | self.api = threetaps.Threetaps(AUTH_TOKEN) 26 | 27 | # default params 28 | self.params = {'auth_token': AUTH_TOKEN} 29 | 30 | # default http response body 31 | self.body = [] 32 | self.jbody = json.dumps(self.body) 33 | 34 | # mock http requests 35 | httpretty.enable() 36 | httpretty.register_uri(httpretty.GET, self.uri, 37 | body=self.jbody) 38 | 39 | def tearDown(self): 40 | httpretty.disable() 41 | httpretty.reset() 42 | 43 | def get_last_query(self): 44 | return {key: val[0] for key, val in 45 | httpretty.last_request().querystring.items()} 46 | 47 | def register_uri(self, uri): 48 | uri = urljoin(self.uri, uri) 49 | httpretty.reset() 50 | httpretty.register_uri(httpretty.GET, uri, 51 | body=self.jbody) 52 | 53 | 54 | class RequesterTestCase(BaseTestCase): 55 | 56 | def setUp(self): 57 | self.uri = 'http://3taps.com/' 58 | 59 | super(RequesterTestCase, self).setUp() 60 | 61 | def test_auth_token(self): 62 | self.assertEqual(self.api.requester.auth_token, AUTH_TOKEN) 63 | 64 | def test_get_request(self): 65 | response = self.api.requester.GET(self.uri, self.params) 66 | 67 | self.assertEqual(response, self.body) 68 | self.assertEqual(self.get_last_query(), self.params) 69 | 70 | def test_bad_get_request(self): 71 | # mock bad status code 72 | status = 302 73 | httpretty.reset() 74 | httpretty.register_uri(httpretty.GET, self.uri, 75 | body=self.jbody, 76 | status=status) 77 | 78 | response = self.api.requester.GET(self.uri, self.params) 79 | self.assertFalse(response['success']) 80 | self.assertIn('error', response) 81 | 82 | def test_response_not_json(self): 83 | # mock html response 84 | httpretty.reset() 85 | httpretty.register_uri(httpretty.GET, self.uri, 86 | body='') 87 | 88 | response = self.api.requester.GET(self.uri, self.params) 89 | self.assertFalse(response['success']) 90 | self.assertIn('error', response) 91 | 92 | class SearchTestCase(BaseTestCase): 93 | 94 | def setUp(self): 95 | self.uri = 'http://search.3taps.com' 96 | 97 | super(SearchTestCase, self).setUp() 98 | 99 | def test_entry_points(self): 100 | 101 | self.api.search.search 102 | self.api.search.count 103 | 104 | def test_search_defaults(self): 105 | response = self.api.search.search() 106 | 107 | self.assertEqual(response, self.body) 108 | self.assertEqual(self.get_last_query(), self.params) 109 | 110 | def test_search_query(self): 111 | self.params['id'] = '234567' 112 | 113 | response = self.api.search.search(self.params) 114 | 115 | self.assertEqual(response, self.body) 116 | self.assertEqual(self.get_last_query(), self.params) 117 | 118 | def test_count_defaults(self): 119 | count_field = 'source' 120 | 121 | response = self.api.search.count(count_field) 122 | self.assertEqual(response, self.body) 123 | 124 | # field should be included in the params 125 | self.params['count'] = count_field 126 | self.assertEqual(self.get_last_query(), self.params) 127 | 128 | def test_count_query(self): 129 | self.params['id'] = 1234567 130 | count_field = 'source' 131 | 132 | response = self.api.search.count(count_field, 133 | params=self.params) 134 | self.assertEqual(response, self.body) 135 | 136 | # TODO: get next page 137 | 138 | 139 | class PollingTestCase(BaseTestCase): 140 | 141 | def setUp(self): 142 | self.uri = 'http://polling.3taps.com' 143 | 144 | super(PollingTestCase, self).setUp() 145 | 146 | def test_entry_points(self): 147 | 148 | self.api.polling.anchor 149 | self.api.polling.poll 150 | 151 | def test_anchor(self): 152 | # mock the request 153 | self.register_uri('anchor') 154 | 155 | timestamp = 1384365735 156 | utc_date = datetime.utcfromtimestamp(timestamp) 157 | response = self.api.polling.anchor(utc_date) 158 | 159 | # response 160 | self.assertEqual(response, self.body) 161 | 162 | # timestamp should be included in the params 163 | self.params['timestamp'] = str(timestamp) 164 | self.assertEqual(self.get_last_query(), self.params) 165 | 166 | def test_poll_defaults(self): 167 | # mock the request 168 | self.register_uri('poll') 169 | 170 | response = self.api.polling.poll() 171 | 172 | # response 173 | self.assertEqual(response, self.body) 174 | 175 | # request 176 | self.assertEqual(self.get_last_query(), self.params) 177 | 178 | def test_poll_query(self): 179 | # mock the request 180 | self.register_uri('poll') 181 | 182 | # query 183 | params = {'anchor': 'awoiefj'} 184 | 185 | response = self.api.polling.poll(params) 186 | 187 | # response 188 | self.assertEqual(response, self.body) 189 | 190 | # the query should be added to the request 191 | self.params['anchor'] = params['anchor'] 192 | self.assertEqual(self.get_last_query(), self.params) 193 | 194 | 195 | class ReferenceTestCase(BaseTestCase): 196 | 197 | def setUp(self): 198 | self.uri = 'http://reference.3taps.com' 199 | 200 | super(ReferenceTestCase, self).setUp() 201 | 202 | def test_entry_points(self): 203 | 204 | self.api.reference.sources 205 | self.api.reference.category_groups 206 | self.api.reference.categories 207 | self.api.reference.locations 208 | self.api.reference.location_lookup 209 | 210 | def test_reference_sources(self): 211 | # mock the request 212 | self.register_uri('sources') 213 | 214 | response = self.api.reference.sources() 215 | 216 | # response 217 | self.assertEqual(response, self.body) 218 | 219 | def test_reference_category_groups(self): 220 | # mock the request 221 | self.register_uri('category_groups') 222 | 223 | response = self.api.reference.category_groups() 224 | 225 | # response 226 | self.assertEqual(response, self.body) 227 | 228 | def test_reference_categories(self): 229 | # mock the request 230 | self.register_uri('categories') 231 | 232 | response = self.api.reference.categories() 233 | 234 | # response 235 | self.assertEqual(response, self.body) 236 | 237 | def test_reference_locations_defaults(self): 238 | # mock the request 239 | self.register_uri('locations') 240 | 241 | level = 'country' 242 | response = self.api.reference.locations(level) 243 | 244 | # response 245 | self.assertEqual(response, self.body) 246 | 247 | # request 248 | self.params['level'] = level 249 | self.assertEqual(self.get_last_query(), self.params) 250 | 251 | def test_reference_locations_query(self): 252 | # mock the request 253 | self.register_uri('locations') 254 | 255 | # query 256 | params = {'country': 'awfeaw'} 257 | 258 | level = 'country' 259 | response = self.api.reference.locations(level, params) 260 | 261 | # response 262 | self.assertEqual(response, self.body) 263 | 264 | # request 265 | self.params['level'] = level 266 | self.params['country'] = params['country'] 267 | self.assertEqual(self.get_last_query(), self.params) 268 | 269 | def test_reference_location_lookup_defaults(self): 270 | # mock the request 271 | self.register_uri('locations/lookup') 272 | 273 | code = 'oaiwjef' 274 | response = self.api.reference.location_lookup(code) 275 | 276 | # response 277 | self.assertEqual(response, self.body) 278 | 279 | # request 280 | self.params['code'] = code 281 | self.assertEqual(self.get_last_query(), self.params) 282 | 283 | 284 | if __name__ == '__main__': 285 | unittest.main() 286 | -------------------------------------------------------------------------------- /threetaps/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | 3taps API client 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | 3taps is a library, written in Python, which makes interacting 8 | with the 3taps API simple. 9 | 10 | Basic usage: 11 | 12 | :copyright: (c) 2014 by Michael Kolodny. 13 | :license: MIT, see LICENSE for more details. 14 | 15 | """ 16 | 17 | __title__ = 'threetaps' 18 | __version__ = '0.2.2' 19 | __author__ = 'Michael Kolodny' 20 | __license__ = 'MIT' 21 | __copyright__ = 'Copyright 2014 Michael Kolodny' 22 | 23 | from .client import Threetaps 24 | -------------------------------------------------------------------------------- /threetaps/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | threetaps.client 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | This module provides a Threetaps object to make requests to the 3taps 8 | api. 9 | 10 | """ 11 | 12 | from __future__ import unicode_literals 13 | import inspect 14 | import json 15 | import calendar 16 | try: 17 | from urlparse import urljoin 18 | except ImportError: 19 | from urllib.parse import urljoin 20 | import requests 21 | 22 | 23 | class Threetaps(object): 24 | """A 3taps API wrapper.""" 25 | 26 | __attrs__ = ['auth_token'] 27 | 28 | def __init__(self, auth_token): 29 | 30 | # prepare endpoints 31 | self.requester = self.Requester(auth_token) 32 | 33 | # dynamically attach endpoints 34 | self._attach_endpoints() 35 | 36 | def _attach_endpoints(self): 37 | """Dynamically attach endpoints to this client.""" 38 | 39 | for name, endpoint in inspect.getmembers(self): 40 | if (inspect.isclass(endpoint) and 41 | issubclass(endpoint, self._Endpoint) and 42 | endpoint is not self._Endpoint): 43 | endpoint_instance = endpoint(self.requester) 44 | setattr(self, endpoint.name, endpoint_instance) 45 | 46 | class Requester(object): 47 | """An API requesting object.""" 48 | 49 | def __init__(self, auth_token): 50 | 51 | self.auth_token = auth_token 52 | 53 | def GET(self, url, params={}): 54 | """Make a GET request to 3taps. 55 | 56 | :param url: String. 3taps endpoint. 57 | :param params: Dictionary. Params to send to 3taps. 58 | """ 59 | params = dict({'auth_token': self.auth_token}, **params) 60 | response = requests.get(url, params=params) 61 | if response.status_code != 200: 62 | error = 'HTTPError: {}'.format(response.status_code) 63 | return {'success': False, 'error': error} 64 | try: 65 | return response.json() 66 | except ValueError as err: 67 | return {'success': False, 'error': err} 68 | 69 | 70 | class _Endpoint(object): 71 | """Base class for endpoints.""" 72 | 73 | def __init__(self, requester): 74 | 75 | self.requester = requester 76 | 77 | def _GET(self, path='', params={}): 78 | """Make a GET request to the current endpoint. 79 | 80 | :param path: String. Path to append to the endpoint url. 81 | :param params: Dictionary. Params to send to 3taps. 82 | """ 83 | url = urljoin(self.url, path) 84 | return self.requester.GET(url, params) 85 | 86 | 87 | class Search(_Endpoint): 88 | """3taps Search API endpoints.""" 89 | 90 | name = 'search' 91 | url = 'http://search.3taps.com' 92 | 93 | def search(self, params={}): 94 | """Search the 3taps database of postings.""" 95 | 96 | return self._GET(self.url, params) 97 | 98 | def count(self, field, params={}): 99 | """Count the number of postings matching a search. 100 | 101 | :param field: String. Field to use for calculating the count. 102 | """ 103 | params = dict({'count': field}, **params) 104 | return self._GET(self.url, params) 105 | 106 | 107 | class Polling(_Endpoint): 108 | """3taps Polling API endpoints.""" 109 | 110 | name = 'polling' 111 | url = 'http://polling.3taps.com' 112 | 113 | def anchor(self, utc_dt): 114 | """Get the value to use to find postings since `utc_dt`. 115 | 116 | :param utc_dt: Datetime object. Start-time for finding 117 | postings. 118 | """ 119 | url = urljoin(self.url, 'anchor') 120 | 121 | # set anchor timestamp 122 | params = {'timestamp': self._timestamp_from_utc(utc_dt)} 123 | 124 | return self._GET(url, params) 125 | 126 | def poll(self, params={}): 127 | """Retrieve postings that have changed since the last poll.""" 128 | 129 | url = urljoin(self.url, 'poll') 130 | return self._GET(url, params) 131 | 132 | def _timestamp_from_utc(self, utc_dt): 133 | """Convert a UTC datetime object to a UTC timestamp. 134 | Returns an Integer representing the number of seconds since the 135 | epoch. 136 | """ 137 | return calendar.timegm(utc_dt.timetuple()) 138 | 139 | 140 | class Reference(_Endpoint): 141 | """3taps Reference API endpoints.""" 142 | 143 | name = 'reference' 144 | url = 'http://reference.3taps.com' 145 | 146 | def sources(self): 147 | """Get a list of 3taps data sources.""" 148 | 149 | return self._GET('sources') 150 | 151 | def category_groups(self): 152 | """Get a list of 3taps category groups.""" 153 | 154 | return self._GET('category_groups') 155 | 156 | def categories(self): 157 | """Get a list of 3taps categories.""" 158 | 159 | return self._GET('categories') 160 | 161 | def locations(self, level, params={}): 162 | """Get a list of 3taps locations. 163 | 164 | :param level: String. The level of the desired locations. 165 | Options: 'country', 'state', 'metro', 'region', 'county', 166 | 'city', 'locality', 'zipcode'. 167 | """ 168 | params = dict({'level': level}, **params) 169 | return self._GET('locations', params) 170 | 171 | def location_lookup(self, code): 172 | """Get the details for a location based on the 3taps location 173 | code. 174 | 175 | :param code: String. 3taps location code. 176 | """ 177 | return self._GET('locations/lookup', {'code': code}) 178 | --------------------------------------------------------------------------------