Skip to content

Commit c3b30cc

Browse files
Merge pull request #59 from algolia/refactor-async
Refactor Transport out of Client
2 parents 8b1dd19 + 44e097a commit c3b30cc

File tree

5 files changed

+215
-194
lines changed

5 files changed

+215
-194
lines changed

algoliasearch/client.py

Lines changed: 54 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,21 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
import os
26-
import json
2725
import hmac
2826
import hashlib
2927
import base64
3028
import random
3129
import sys
32-
33-
APPENGINE = 'APPENGINE_RUNTIME' in os.environ
34-
SSL_CERTIFICATE_DOMAIN = 'algolia.net'
3530

3631
try:
3732
from urllib import urlencode
3833
except ImportError:
3934
from urllib.parse import urlencode
4035

41-
if APPENGINE:
42-
from google.appengine.api import urlfetch
43-
APPENGINE_METHODS = {
44-
'GET' : urlfetch.GET,
45-
'POST' : urlfetch.POST,
46-
'PUT' : urlfetch.PUT,
47-
'DELETE' : urlfetch.DELETE
48-
}
49-
50-
from requests import Session
51-
from requests import exceptions
52-
5336
from .version import VERSION
5437
from .index import Index
5538

56-
from .helpers import AlgoliaException
57-
from .helpers import CustomJSONEncoder
39+
from .transport import Transport
5840
from .helpers import deprecated
5941
from .helpers import safe
6042
from .helpers import urlify
@@ -67,49 +49,62 @@ class Client(object):
6749
start using Algolia Search API.
6850
"""
6951

70-
def __init__(self, app_id, api_key, hosts_array=None):
52+
def __init__(self, app_id, api_key, hosts=None, _transport=None):
7153
"""
7254
Algolia Search Client initialization
7355
7456
@param app_id the application ID you have in your admin interface
7557
@param api_key a valid API key for the service
7658
@param hosts_array the list of hosts that you have received for the service
7759
"""
78-
if not hosts_array:
60+
self._transport = Transport() if _transport is None else _transport
61+
62+
if not hosts:
7963
fallbacks = [
8064
'%s-1.algolianet.com' % app_id,
8165
'%s-2.algolianet.com' % app_id,
8266
'%s-3.algolianet.com' % app_id,
8367
]
8468
random.shuffle(fallbacks)
8569

86-
self.read_hosts = ['%s-dsn.algolia.net' % app_id]
87-
self.read_hosts.extend(fallbacks)
88-
self.write_hosts = ['%s.algolia.net' % app_id]
89-
self.write_hosts.extend(fallbacks)
90-
70+
self._transport.read_hosts = ['%s-dsn.algolia.net' % app_id]
71+
self._transport.read_hosts.extend(fallbacks)
72+
self._transport.write_hosts = ['%s.algolia.net' % app_id]
73+
self._transport.write_hosts.extend(fallbacks)
9174
else:
92-
self.read_hosts = hosts_array
93-
self.write_hosts = hosts_array
75+
self._transport.read_hosts = hosts
76+
self._transport.write_hosts = hosts
9477

95-
self._app_id = app_id
96-
self._api_key = api_key
97-
self.timeout = (1, 30)
98-
self.search_timeout = (1, 5)
99-
100-
self._session = Session()
101-
self._session.verify = os.path.join(os.path.dirname(__file__),
102-
'resources/ca-bundle.crt')
103-
self._session.headers = {
104-
'X-Algolia-API-Key': self.api_key,
105-
'X-Algolia-Application-Id': self.app_id,
78+
self._transport.headers = {
79+
'X-Algolia-API-Key': api_key,
80+
'X-Algolia-Application-Id': app_id,
10681
'Content-Type': 'gzip',
10782
'Accept-Encoding': 'gzip',
10883
'User-Agent': 'Algolia Search for Python %s' % VERSION
10984
}
85+
86+
self._app_id = app_id
87+
self._api_key = api_key
88+
11089
# Fix for AppEngine bug when using urlfetch_stub
11190
if 'google.appengine.api.apiproxy_stub_map' in sys.modules.keys():
112-
self._session.headers.pop('Accept-Encoding', None)
91+
self.headers.pop('Accept-Encoding', None)
92+
93+
@property
94+
def timeout(self):
95+
return self._transport.timeout
96+
97+
@timeout.setter
98+
def timeout(self, t):
99+
self._transport.timeout = t
100+
101+
@property
102+
def search_timeout(self):
103+
return self._transport.search_timeout
104+
105+
@search_timeout.setter
106+
def search_timeout(self, t):
107+
self._transport.search_timeout = t
113108

114109
@property
115110
def app_id(self):
@@ -185,7 +180,7 @@ def set_extra_headers(self, **kwargs):
185180

186181
@property
187182
def headers(self):
188-
return self._session.headers
183+
return self._transport.headers
189184

190185
@deprecated
191186
def set_timeout(self, connect_timeout, read_timeout, search_timeout=5):
@@ -227,17 +222,16 @@ def multiple_queries(self, queries,
227222
'params': urlencode(urlify(query))
228223
})
229224

230-
return self._perform_request(
231-
self.read_hosts, path, 'POST', params=params,
232-
body={'requests': requests}, is_search=True)
225+
data = {'requests': requests}
226+
return self._req(True, path, 'POST', params, data)
233227

234228
def batch(self, requests):
235-
"""Send a batch request targetting multiple indices."""
229+
"""Send a batch request targeting multiple indices."""
236230
if isinstance(requests, (list, tuple)):
237231
requests = {'requests': requests}
238232

239-
return self._perform_request(self.write_hosts, '/1/indexes/*/batch',
240-
'POST', body=requests)
233+
path = '/1/indexes/*/batch'
234+
return self._req(False, path, 'POST', data=requests)
241235

242236
@deprecated
243237
def listIndexes(self):
@@ -250,7 +244,7 @@ def list_indexes(self):
250244
{'items': [{ 'name': 'contacts', 'created_at': '2013-01-18T15:33:13.556Z'},
251245
{'name': 'notes', 'created_at': '2013-01-18T15:33:13.556Z'}]}
252246
"""
253-
return self._perform_request(self.read_hosts, '/1/indexes', 'GET')
247+
return self._req(True, '/1/indexes', 'GET')
254248

255249
@deprecated
256250
def deleteIndex(self, index_name):
@@ -264,7 +258,7 @@ def delete_index(self, index_name):
264258
@param index_name the name of index to delete
265259
"""
266260
path = '/1/indexes/%s' % safe(index_name)
267-
return self._perform_request(self.write_hosts, path, 'DELETE')
261+
return self._req(False, path, 'DELETE')
268262

269263
@deprecated
270264
def moveIndex(self, src_index_name, dst_index_name):
@@ -280,8 +274,7 @@ def move_index(self, src_index_name, dst_index_name):
280274
"""
281275
path = '/1/indexes/%s/operation' % safe(src_index_name)
282276
request = {'operation': 'move', 'destination': dst_index_name}
283-
return self._perform_request(self.write_hosts, path, 'POST',
284-
body=request)
277+
return self._req(False, path, 'POST', data=request)
285278

286279
@deprecated
287280
def copyIndex(self, src_index_name, dst_index_name):
@@ -297,8 +290,7 @@ def copy_index(self, src_index_name, dst_index_name):
297290
"""
298291
path = '/1/indexes/%s/operation' % safe(src_index_name)
299292
request = {'operation': 'copy', 'destination': dst_index_name}
300-
return self._perform_request(self.write_hosts, path, 'POST',
301-
body=request)
293+
return self._req(False, path, 'POST', data=request)
302294

303295
@deprecated
304296
def getLogs(self, offset=0, length=10, type='all'):
@@ -313,13 +305,8 @@ def get_logs(self, offset=0, length=10, type='all'):
313305
@param length Specify the maximum number of entries to retrieve
314306
starting at offset. Maximum allowed value: 1000.
315307
"""
316-
params = {
317-
'offset': offset,
318-
'length': length,
319-
'type': type
320-
}
321-
return self._perform_request(self.write_hosts, '/1/logs', 'GET',
322-
params=params)
308+
params = {'offset': offset, 'length': length, 'type': type}
309+
return self._req(False, '/1/logs', 'GET', params)
323310

324311
@deprecated
325312
def initIndex(self, index_name):
@@ -340,7 +327,7 @@ def listUserKeys(self):
340327

341328
def list_user_keys(self):
342329
"""List all existing user keys with their associated ACLs."""
343-
return self._perform_request(self.read_hosts, '/1/keys', 'GET')
330+
return self._req(True, '/1/keys', 'GET')
344331

345332
@deprecated
346333
def getUserKeyACL(self, key):
@@ -349,7 +336,7 @@ def getUserKeyACL(self, key):
349336
def get_user_key_acl(self, key):
350337
"""'Get ACL of a user key."""
351338
path = '/1/keys/%s' % key
352-
return self._perform_request(self.read_hosts, path, 'GET')
339+
return self._req(True, path, 'GET')
353340

354341
@deprecated
355342
def deleteUserKey(self, key):
@@ -358,7 +345,7 @@ def deleteUserKey(self, key):
358345
def delete_user_key(self, key):
359346
"""Delete an existing user key."""
360347
path = '/1/keys/%s' % key
361-
return self._perform_request(self.write_hosts, path, 'DELETE')
348+
return self._req(False, path, 'DELETE')
362349

363350
@deprecated
364351
def addUserKey(self, obj,
@@ -416,8 +403,7 @@ def add_user_key(self, obj,
416403
if indexes:
417404
obj['indexes'] = indexes
418405

419-
return self._perform_request(self.write_hosts, '/1/keys', 'POST',
420-
body=obj)
406+
return self._req(False, '/1/keys', 'POST', data=obj)
421407

422408
def update_user_key(self, key, obj,
423409
validity=None,
@@ -469,8 +455,7 @@ def update_user_key(self, key, obj,
469455
obj['indexes'] = indexes
470456

471457
path = '/1/keys/%s' % key
472-
return self._perform_request(self.write_hosts, path, 'PUT',
473-
body=obj)
458+
return self._req(False, path, 'PUT', data=obj)
474459

475460
@deprecated
476461
def generateSecuredApiKey(self, private_api_key, tag_filters,
@@ -504,70 +489,5 @@ def generate_secured_api_key(self, private_api_key, queryParameters,
504489
securedKey = hmac.new(private_api_key.encode('utf-8'), queryParameters.encode('utf-8'), hashlib.sha256).hexdigest()
505490
return str(base64.b64encode(("%s%s" % (securedKey, queryParameters)).encode('utf-8')).decode('utf-8'))
506491

507-
def _perform_appengine_request(self, host, path, method, timeout, params=None, data=None):
508-
"""
509-
Perform an HTTPS request with AppEngine's urlfetch. SSL certificate will not validate when
510-
the request is on a domain which is not a aloglia.net subdomain, a SNI is not available by
511-
default on GAE. Hence, we do set validate_certificate to False when calling those domains.
512-
"""
513-
method = APPENGINE_METHODS.get(method)
514-
if isinstance(timeout, tuple):
515-
timeout = timeout[1]
516-
url = 'https://%s%s' % (host, path)
517-
url = params and '%s?%s' %(url, urlencode(urlify(params))) or url
518-
res = urlfetch.fetch(url=url, method=method, payload=data,
519-
headers=self.headers, deadline=timeout,
520-
validate_certificate=host.endswith(SSL_CERTIFICATE_DOMAIN))
521-
content = res.content != None and json.loads(res.content) or None
522-
if (int(res.status_code / 100) == 2 and content):
523-
return content
524-
elif (int(res.status_code / 100) == 4):
525-
message = "HttpCode: %d" % res.status_code
526-
if content and content.get('message'):
527-
message = content['message']
528-
raise AlgoliaException(message)
529-
else:
530-
mesage = '%s Server Error: %s' % (res.status_code, res.content)
531-
raise Exception(http_error_msg, response=res)
532-
533-
def _perform_session_request(self, host, path, method, timeout, params=None, data=None):
534-
"""Perform an HTTPS request with request's Session."""
535-
res = self._session.request(
536-
method, 'https://%s%s' % (host, path),
537-
params=params, data=data, timeout=timeout)
538-
if (int(res.status_code / 100) == 2 and res.json != None):
539-
return res.json()
540-
elif (int(res.status_code / 100) == 4):
541-
message = "HttpCode: %d" % res.status_code
542-
if res.json != None and 'message' in res.json():
543-
message = res.json()['message']
544-
raise AlgoliaException(message)
545-
res.raise_for_status()
546-
547-
def _perform_request(self, hosts, path, method, params=None, body=None,
548-
is_search=False):
549-
"""Perform an HTTPS request with retry logic."""
550-
if params:
551-
params = urlify(params)
552-
if body:
553-
body = json.dumps(body, cls=CustomJSONEncoder)
554-
timeout = self.search_timeout if is_search else self.timeout
555-
exceptions_hosts = {}
556-
for i, host in enumerate(hosts):
557-
if i > 1:
558-
if isinstance(timeout, tuple):
559-
timeout = (timeout[0] + 2, timeout[1] + 10)
560-
else:
561-
timeout += 10
562-
try:
563-
_request = APPENGINE and self._perform_appengine_request or self._perform_session_request
564-
return _request(host, path, method, timeout, params=params, data=body)
565-
except AlgoliaException as e:
566-
raise e
567-
except Exception as e:
568-
exceptions_hosts[host] = "%s: %s" % (e.__class__.__name__, str(e))
569-
pass
570-
571-
# Impossible to connect
572-
raise AlgoliaException('%s %s' % ('Unreachable hosts:',
573-
exceptions_hosts))
492+
def _req(self, is_search, path, meth, params=None, data=None):
493+
return self._transport.req(is_search, path, meth, params, data)

0 commit comments

Comments
 (0)