@@ -94,16 +94,34 @@ class AbstractAPI:
9494 """
9595
9696 _root_url : str = None
97+ """Servername and context path of the root of the API"""
9798
9899 _session : requests .Session = None
100+ """Reference to the session information"""
99101
100102 ssl_config : SSLConfig
103+ """Security configuration and location of certificate files"""
101104
102105 _client_name : str = "hiro-graph-client"
106+ """Used in header 'User-Agent'"""
103107
104108 _max_tries : int = 2
109+ """Retries for backoff"""
105110
106111 _timeout : int = 600
112+ """Timeout for requests-methods as needed by package 'requests'."""
113+
114+ _raise_exceptions : bool = True
115+ """Raise an exception when the status-code of results indicates an error"""
116+
117+ _proxies : dict = None
118+ """Proxy configuration as needed by package 'requests'."""
119+
120+ _headers : dict = {}
121+ """Common headers for HTTP requests."""
122+
123+ _log_communication_on_error : bool = False
124+ """Dump request and response into logging on errors"""
107125
108126 def __init__ (self ,
109127 root_url : str = None ,
@@ -121,11 +139,15 @@ def __init__(self,
121139 """
122140 Constructor
123141
142+ A note regarding headers: If you set a value in the dict to *None*, it will not show up in the HTTP-request
143+ headers. Use this to erase entries from existing default headers or headers copied from *apstract_api* (when
144+ given).
145+
124146 :param root_url: Root uri of the HIRO API, like *https://core.arago.co*.
125147 :param session: The requests.Session object for the connection pool. Required.
126148 :param raise_exceptions: Raise exceptions on HTTP status codes that denote an error. Default is True.
127149 :param proxies: Proxy configuration for *requests*. Default is None.
128- :param headers: Optional custom HTTP headers. Will override the internal headers. Default is None.
150+ :param headers: Optional custom HTTP headers. Will be merged with the internal default headers. Default is None.
129151 :param timeout: Optional timeout for requests. Default is 600 (10 min).
130152 :param client_name: Optional name for the client. Will also be part of the "User-Agent" header unless *headers*
131153 is given with another value for "User-Agent". Default is "hiro-graph-client".
@@ -135,43 +157,49 @@ def __init__(self,
135157 detected. Default is not to do this.
136158 :param max_tries: Max tries for BACKOFF. Default is 2.
137159 :param abstract_api: Set all parameters by copying them from the instance given by this parameter. Overrides
138- all other parameters.
139- """
140- self ._root_url = getattr (abstract_api , '_root_url' , root_url )
141- self ._session = getattr (abstract_api , '_session' , session )
160+ all other parameters except headers, which will be merged with existing ones.
161+ """
162+
163+ if isinstance (abstract_api , AbstractAPI ):
164+ root_url = abstract_api ._root_url
165+ session = abstract_api ._session
166+ raise_exceptions = abstract_api ._raise_exceptions
167+ proxies = abstract_api ._proxies
168+ initial_headers = abstract_api ._headers .copy ()
169+ timeout = abstract_api ._timeout
170+ client_name = abstract_api ._client_name
171+ ssl_config = abstract_api .ssl_config
172+ log_communication_on_error = abstract_api ._log_communication_on_error
173+ max_tries = abstract_api ._max_tries
174+ else :
175+ initial_headers = {
176+ 'Content-Type' : 'application/json' ,
177+ 'Accept' : 'text/plain, application/json' ,
178+ 'User-Agent' : f"{ client_name or self ._client_name } { __version__ } "
179+ }
180+
181+ self ._root_url = root_url
182+ self ._session = session
142183
143184 if not self ._root_url :
144185 raise ValueError ("'root_url' must not be empty." )
145186
146187 if not self ._session :
147188 raise ValueError ("'session' must not be empty." )
148189
149- self ._proxies = getattr (abstract_api , '_proxies' , proxies )
150- self ._raise_exceptions = getattr (abstract_api , '_raise_exceptions' , raise_exceptions )
151- self ._timeout = getattr (abstract_api , '_timeout' , timeout or self ._timeout )
152- self ._log_communication_on_error = getattr (abstract_api , '_log_communication_on_error' ,
153- log_communication_on_error or False )
190+ self ._client_name = client_name or self ._client_name
191+ self ._headers = AbstractAPI ._merge_headers (initial_headers , headers )
154192
155- self .ssl_config = getattr ( abstract_api , ' ssl_config' , ssl_config or SSLConfig () )
193+ self .ssl_config = ssl_config or SSLConfig ()
156194
157195 if not self .ssl_config .verify :
158196 requests .packages .urllib3 .disable_warnings (requests .packages .urllib3 .exceptions .InsecureRequestWarning )
159197
160- self ._client_name = getattr (abstract_api , '_client_name' , client_name or self ._client_name )
161-
162- if abstract_api :
163- self ._headers = getattr (abstract_api , '_headers' , None )
164- else :
165- self ._headers = {
166- 'Content-Type' : 'application/json' ,
167- 'Accept' : 'text/plain, application/json' ,
168- 'User-Agent' : f"{ self ._client_name } { __version__ } "
169- }
170-
171- if headers :
172- self ._headers .update ({self ._capitalize_header (k ): v for k , v in headers .items ()})
173-
174- self ._max_tries = getattr (abstract_api , '_max_tries' , max_tries )
198+ self ._proxies = proxies
199+ self ._raise_exceptions = raise_exceptions
200+ self ._timeout = timeout or self ._timeout
201+ self ._log_communication_on_error = log_communication_on_error or False
202+ self ._max_tries = max_tries
175203
176204 def _get_max_tries (self ):
177205 return self ._max_tries
@@ -429,6 +457,22 @@ def _get_proxies(self) -> dict:
429457 """
430458 return self ._proxies .copy () if self ._proxies else None
431459
460+ @staticmethod
461+ def _merge_headers (headers : dict , override : dict ) -> dict :
462+ """
463+ Merge headers with override.
464+
465+ :param headers: Headers to merge into.
466+ :param override: Dict of headers that override *headers*. If a header key is set to value None,
467+ it will be removed from *headers*.
468+ :return: The merged headers.
469+ """
470+ if isinstance (headers , dict ) and isinstance (override , dict ):
471+ headers .update ({AbstractAPI ._capitalize_header (k ): v for k , v in override .items ()})
472+ headers = {k : v for k , v in headers .items () if v is not None }
473+
474+ return headers
475+
432476 def _get_headers (self , override : dict = None ) -> dict :
433477 """
434478 Create a header dict for requests. Uses abstract method *self._handle_token()*.
@@ -437,11 +481,8 @@ def _get_headers(self, override: dict = None) -> dict:
437481 it will be removed from the headers.
438482 :return: A dict containing header values for requests.
439483 """
440- headers = self ._headers .copy ()
441484
442- if isinstance (override , dict ):
443- headers .update ({self ._capitalize_header (k ): v for k , v in override .items ()})
444- headers = {k : v for k , v in headers .items () if v is not None }
485+ headers = AbstractAPI ._merge_headers (self ._headers .copy (), override )
445486
446487 token = self ._handle_token ()
447488 if token :
@@ -672,15 +713,21 @@ class GraphConnectionHandler(AbstractAPI):
672713 """Default pool_maxsize for requests.adapters.HTTPAdapter."""
673714
674715 _pool_block = False
716+ """As used by requests.adapters.HTTPAdapter."""
675717
676718 _version_info : dict = None
719+ """Stores the result of /api/version"""
720+
721+ custom_endpoints : dict = None
722+ """Override API endpoints."""
677723
678724 _lock : threading .RLock
679725 """Reentrant mutex for thread safety"""
680726
681727 def __init__ (self ,
682728 root_url : str = None ,
683729 custom_endpoints : dict = None ,
730+ version_info : dict = None ,
684731 pool_maxsize : int = None ,
685732 pool_block : bool = None ,
686733 connection_handler = None ,
@@ -709,6 +756,8 @@ def __init__(self,
709756 :param root_url: Root url for HIRO, like https://core.arago.co.
710757 :param custom_endpoints: Optional map of {name:endpoint_path, ...} that overrides or adds to the endpoints taken
711758 from /api/version. Example see above.
759+ :param version_info: Optional full dict of the JSON result received via /api/version. Setting this will use it
760+ as the valid API version information and avoids the internal API-call altogether.
712761 :param pool_maxsize: Size of a connection pool for a single connection. See requests.adapters.HTTPAdapter.
713762 Default is 10. *pool_maxsize* is ignored when *session* is set.
714763 :param pool_block: Block any connections that exceed the pool_maxsize. Default is False: Allow more connections,
@@ -720,20 +769,22 @@ def __init__(self,
720769 """
721770 self ._lock = threading .RLock ()
722771
723- root_url = getattr (connection_handler , '_root_url' , root_url )
724- session = getattr (connection_handler , '_session' , None )
725-
726- if not root_url :
727- raise ValueError ("'root_url' must not be empty." )
772+ if isinstance (connection_handler , GraphConnectionHandler ):
773+ root_url = connection_handler ._root_url
774+ session = connection_handler ._session
775+ custom_endpoints = connection_handler .custom_endpoints
776+ version_info = connection_handler ._version_info
777+ else :
778+ if not root_url :
779+ raise ValueError ("'root_url' must not be empty." )
728780
729- if not session :
730781 adapter = requests .adapters .HTTPAdapter (
731782 pool_maxsize = pool_maxsize or self ._pool_maxsize ,
732783 pool_connections = 1 ,
733784 pool_block = pool_block or self ._pool_block
734785 )
735786 session = requests .Session ()
736- session .mount (root_url , adapter )
787+ session .mount (prefix = root_url , adapter = adapter )
737788
738789 super ().__init__ (
739790 root_url = root_url ,
@@ -743,8 +794,8 @@ def __init__(self,
743794 ** kwargs
744795 )
745796
746- self .custom_endpoints = getattr ( connection_handler , '_custom_endpoints' , custom_endpoints )
747- self ._version_info = getattr ( connection_handler , '_version_info' , None )
797+ self .custom_endpoints = custom_endpoints
798+ self ._version_info = version_info
748799
749800 self .get_version ()
750801
@@ -929,6 +980,7 @@ class FixedTokenApiHandler(AbstractTokenApiHandler):
929980 """
930981
931982 _token : str
983+ """Stores the fixed token."""
932984
933985 def __init__ (self , token : str = None , * args , ** kwargs ):
934986 """
@@ -966,6 +1018,7 @@ class EnvironmentTokenApiHandler(AbstractTokenApiHandler):
9661018 """
9671019
9681020 _env_var : str
1021+ """Stores the name of the environment variable."""
9691022
9701023 def __init__ (self , env_var : str = 'HIRO_TOKEN' , * args , ** kwargs ):
9711024 """
@@ -1123,6 +1176,7 @@ class PasswordAuthTokenApiHandler(AbstractTokenApiHandler):
11231176 _client_secret : str
11241177
11251178 _secure_logging : bool = True
1179+ """Avoid logging of sensitive data."""
11261180
11271181 def __init__ (self ,
11281182 username : str = None ,
@@ -1299,7 +1353,10 @@ class AuthenticatedAPIHandler(AbstractAPI):
12991353 """
13001354
13011355 _api_handler : AbstractTokenApiHandler
1356+ """Stores the TokenApiHandler used for this API."""
1357+
13021358 _api_name : str
1359+ """Name of the API."""
13031360
13041361 def __init__ (self ,
13051362 api_handler : AbstractTokenApiHandler ,
0 commit comments