2222THE SOFTWARE.
2323"""
2424
25- import os
26- import json
2725import hmac
2826import hashlib
2927import base64
3028import random
3129import sys
32-
33- APPENGINE = 'APPENGINE_RUNTIME' in os .environ
34- SSL_CERTIFICATE_DOMAIN = 'algolia.net'
3530
3631try :
3732 from urllib import urlencode
3833except 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-
5336from .version import VERSION
5437from .index import Index
5538
56- from .helpers import AlgoliaException
57- from .helpers import CustomJSONEncoder
39+ from .transport import Transport
5840from .helpers import deprecated
5941from .helpers import safe
6042from .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