import requests import requests.exceptions import json import time import re path_to_pk = re.compile( r".*/([0-9]+)/(\?.*)?(#.*)?" ) from . import urlqueryparse class objdict( dict ): @classmethod def response_to_objdicts( cls, i ): if isinstance( i, dict ): return cls( i ) elif isinstance( i, ( list, tuple ) ): return [ cls.response_to_objdicts( v ) for v in i ] return i def __getattr__( self, name ): if name in self: return self.__class__.response_to_objdicts( self[ name ] ) else: raise AttributeError( "No such attribute: " + name ) def __setattr__( self, name, value ): self[ name ] = value def __delattr__( self, name ): if name in self: del self[ name ] else: raise AttributeError( "No such attribute: " + name ) response_to_objdicts = objdict.response_to_objdicts class ApiKeyAuth( requests.auth.AuthBase ): def __init__( self, api_username, api_key ): self.api_username = api_username self.api_key = api_key def __call__( self, r ): r.headers[ 'Authorization' ] = "ApiKey %s:%s" % ( self.api_username, self.api_key ) r.headers[ 'Content-Type' ] = "application/json" r.headers[ 'Accept-Encoding' ] = "application/json" return r class APIv2( object ): def __init__( self, api_username, api_key, base = "https://fulcrm.org", api_path = "/api/v2", cache = None, cache_timeout = None ): self.api_username = api_username self.api_key = api_key self.api_path = api_path self.base = base self.cache = cache self.cache_timeout = cache_timeout self.auth = ApiKeyAuth( self.api_username, self.api_key ) def normalize_path( self, path ): if not path.startswith( self.api_path ): if not path.startswith( "/" ): path = "/" + path path = self.api_path + path if not path.endswith( "/" ): path = path + "/" return path def path_to_pk( self, path ): if not isinstance( path, str ): return path if path.startswith( self.base ) or path.startswith( self.api_path ): try: match = path_to_pk.split( path ) return match[ 1 ] except: return path return path def normalize_query( self, query ): for ( k, v ) in query.items(): if isinstance( v, tuple ): query[ k ] = list( v ) elif isinstance( v, list ): pass else: query[ k ] = [ v ] query[ k ] = [ self.path_to_pk( x ) for x in v ] if 'expand' in query: if query[ 'expand' ]: query[ 'expand' ] = [ ",".join( query[ 'expand' ] ) ] else: del query[ 'expand' ] return query def normalize_url( self, url, no_d = False, query = None ): url = urlqueryparse.set_url_components( url, scheme = 'https', netloc = 'fulcrm.org', path = self.normalize_path ) new_query = urlqueryparse.get_query_fields( url ) if no_d: if 'expand' in new_query: new_query[ 'expand' ] = [ x for x in new_query[ 'expand' ] if not ( ( x == 'd' ) or ( x.endswith( '.d' ) ) ) ] else: if 'expand' in new_query: new_query[ 'expand' ].append( 'd' ) else: new_query[ 'expand' ] = [ 'd' ] if query: new_query.update( query ) new_query = self.normalize_query( new_query ) new_url = urlqueryparse.set_query_fields( url, new_query ) return new_url def get( self, url, query = None, no_d = False, _countdown = 5 ): url = self.normalize_url( url, query = query, no_d = no_d ) return self.make_request( url, data = None, method = 'GET', _countdown = _countdown ) def post( self, url, data, query = None, no_d = False, _countdown = 5 ): url = self.normalize_url( url, query = query, no_d = no_d ) return self.make_request( url, data = data, method = 'POST', _countdown = _countdown ) def patch( self, url, data, query = None, no_d = False, _countdown = 5 ): url = self.normalize_url( url, query = query, no_d = no_d ) return self.make_request( url, data = data, method = 'PATCH', _countdown = _countdown ) def put( self, url, data, query = None, no_d = False, _countdown = 5 ): url = self.normalize_url( url, query = query, no_d = no_d ) return self.make_request( url, data = data, method = 'PUT', _countdown = _countdown ) def delete( self, url, query = None, no_d = False, _countdown = 5 ): url = self.normalize_url( url, query = query, no_d = no_d ) return self.make_request( url, data = None, method = 'DELETE', _countdown = _countdown ) def make_request( self, url, data, method, _countdown = 5 ): method = { 'POST': requests.post, 'PATCH': requests.patch, 'GET': requests.get, 'DELETE': requests.delete }.get( method, requests.get ) if self.cache and method is requests.get and not data: rval = self.cache.get( url ) if rval is not None: print( "CACHED", url ) return response_to_objdicts( rval ) else: print( "NOCACHE", url ) try: if data: request = method( url, data = json.dumps( data ), auth = self.auth, timeout = 30.0 ) else: request = method( url, auth = self.auth, timeout = 30.0 ) except requests.exceptions.ConnectionError: if _countdown > 0: time.sleep( 2 ** ( 5 - _countdown ) ) return self.make_request( url, data, method, _countdown - 1 ) else: raise if request.ok: try: if self.cache and self.cache_timeout and method is requests.get and not data: rval = request.json() self.cache.set( url, rval, timeout = self.cache_timeout ) return response_to_objdicts( rval ) else: return response_to_objdicts( request.json() ) except: return None else: request.raise_for_status() def get_one( self, url, no_d = False, query = None ): url = self.normalize_url( url, no_d = no_d, query = query ) return self.make_request( url, None, 'GET' ) def get_many( self, url, count = 10, no_d = False, query = None ): if query is None: query = {} query[ 'page_size' ] = [ count ] url = self.normalize_url( url, no_d = no_d, query = query ) next = None while True: result = self.make_request( url, None, 'GET' ) if 'results' in result: for object in result[ 'results' ]: yield response_to_objdicts( object ) if 'next' in result: next = result.get( 'next', None ) if next: url = next else: return