Skip to content

Commit c20a6e0

Browse files
authored
Header handling (#36)
* Merge headers in AbstractAPI * Merge headers in AbstractAPI instead of overwriting them completely. * Remove unnecessary copy() * Avoid getattr * Do not use getattr to obtain values from objects in copy-constructor-parts because this is not the same as accessing the values directly. We might want to give a reference to a central abstract_api._session across many TokenApiHandlers without making an extra copy of it - which will happen when using getattr(). * Code documentation
1 parent 21de77f commit c20a6e0

File tree

3 files changed

+101
-40
lines changed

3 files changed

+101
-40
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v5.2.4
2+
3+
* Merge headers in AbstractAPI instead of replacing them.
4+
15
# v5.2.3
26

37
* Remove accept_all_certs for good. Directly use the flag in SSLConfig.

src/hiro_graph_client/VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.2.3
1+
5.2.4

src/hiro_graph_client/clientlib.py

+96-39
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)