From e90a9b0d321bd05774cf92ceac614b5a052f083d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Oct 2023 23:21:46 +1100 Subject: [PATCH 1/8] Improve introspection into connection failures --- inventree/api.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index f6656cc..3aff521 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -167,13 +167,13 @@ def testAuth(self): try: response = self.get('/user/me/') - except requests.exceptions.HTTPError as e: - logger.fatal(f"Athentication error: {str(type(e))}") + response.raise_for_status() + except Exception as err: + return False + + if 'username' not in response: + logger.fatal("Username not returned by server") return False - except Exception as e: - logger.fatal(f"Unhandled server error: {str(type(e))}") - # Re-throw the exception - raise e # set user_name if not initially set if not self.username: @@ -330,14 +330,20 @@ def request(self, api_url, **kwargs): # Send request to server! try: response = methods[method](api_url, **payload) + response.raise_for_status() except Timeout as e: # Re-throw Timeout, and add a message to the log - logger.critical(f"Server timed out during api.request - {method} @ {api_url}. Timeout {payload['timeout']} s.") - raise e - except Exception as e: + logger.exception(f"Server timed out during api.request - {method} @ {api_url}. Timeout {payload['timeout']} s.") + return None + except Exception as err: # Re-thrown any caught errors, and add a message to the log - logger.critical(f"Error at api.request - {method} @ {api_url}") - raise e + logger.exception(f"{str(err.__class__.__name__)} error - {method} @ {api_url}") + logger.error("Status Code: %s", err.response.status_code) + + for k, v in err.response.json().items(): + logger.error(" - %s: %s", k, v) + + return None if response is None: logger.error(f"Null response - {method} '{api_url}'") From 506f235947982e0046c5c70e2adf3130273ba2d8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Oct 2023 23:22:07 +1100 Subject: [PATCH 2/8] Bump version --- inventree/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/base.py b/inventree/base.py index 4f0beef..d918f77 100644 --- a/inventree/base.py +++ b/inventree/base.py @@ -7,7 +7,7 @@ from . import api as inventree_api -INVENTREE_PYTHON_VERSION = "0.12.2" +INVENTREE_PYTHON_VERSION = "0.13.0" logger = logging.getLogger('inventree') From ddf478f9b4857654bf19b8b57ba47f79ac43253b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Oct 2023 23:46:11 +1100 Subject: [PATCH 3/8] - Request custom token name - Set log verbosity level --- inventree/api.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 3aff521..6e72d04 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -63,6 +63,7 @@ def __init__(self, host=None, **kwargs): self.username = kwargs.get('username', os.environ.get('INVENTREE_API_USERNAME', None)) self.password = kwargs.get('password', os.environ.get('INVENTREE_API_PASSWORD', None)) self.token = kwargs.get('token', os.environ.get('INVENTREE_API_TOKEN', None)) + self.token_name = kwargs.get('token_name', 'inventree-python') self.timeout = kwargs.get('timeout', os.environ.get('INVENTREE_API_TIMEOUT', 10)) self.proxies = kwargs.get('proxies', dict()) @@ -72,6 +73,10 @@ def __init__(self, host=None, **kwargs): self.auth = None self.connected = False + self.verbose = kwargs.get('verbose', False) + + logger.setLevel(logging.DEBUG if self.verbose else logging.INFO) + if kwargs.get('connect', True): self.connect() @@ -167,7 +172,6 @@ def testAuth(self): try: response = self.get('/user/me/') - response.raise_for_status() except Exception as err: return False @@ -236,7 +240,7 @@ def requestToken(self): if not self.username or not self.password: raise AttributeError('Supply username and password to request token') - + logger.info("Requesting auth token from server...") if not self.connected: @@ -245,11 +249,14 @@ def requestToken(self): # Request an auth token from the server try: - response = self.get('/user/token/') + url = '/user/token/' + if self.token_name: + url += f'?name={self.token_name}' + response = self.get(url) except Exception as e: logger.error(f"Error requesting token: {str(type(e))}") return None - + if 'token' not in response: logger.error(f"Token not returned by server: {response}") return None From 0b9904f48fd7e020f1da2ba8b9b5732c5409635d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Oct 2023 23:47:13 +1100 Subject: [PATCH 4/8] Fix URL construction --- inventree/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventree/api.py b/inventree/api.py index 6e72d04..90a1e28 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -154,7 +154,7 @@ def constructApiUrl(self, endpoint_url): url = urljoin(self.api_url, endpoint_url) # Ensure the API URL ends with a trailing slash - if not url.endswith('/'): + if not url.endswith('/') and '?' not in url: url += '/' return url From 02b7b4d496b55a8311e10bf108cd223126471797 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 5 Oct 2023 23:48:49 +1100 Subject: [PATCH 5/8] Cleanup --- inventree/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 90a1e28..964ba29 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -44,6 +44,7 @@ def __init__(self, host=None, **kwargs): username - Login username password - Login password token - Authentication token (if provided, username/password are ignored) + token_name - Name of the token to request (default = 'inventree-python') use_token_auth - Use token authentication? (default = True) verbose - Print extra debug messages (default = False) timeout - Set timeout to use (in seconds). Default: 10 @@ -73,8 +74,6 @@ def __init__(self, host=None, **kwargs): self.auth = None self.connected = False - self.verbose = kwargs.get('verbose', False) - logger.setLevel(logging.DEBUG if self.verbose else logging.INFO) if kwargs.get('connect', True): From f06250aee13f2b8e4a8259841f332ff68f03a3e2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 6 Oct 2023 09:18:59 +1100 Subject: [PATCH 6/8] API tweaks --- inventree/api.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index 964ba29..c40abe3 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -171,7 +171,7 @@ def testAuth(self): try: response = self.get('/user/me/') - except Exception as err: + except Exception: return False if 'username' not in response: @@ -337,23 +337,20 @@ def request(self, api_url, **kwargs): try: response = methods[method](api_url, **payload) response.raise_for_status() - except Timeout as e: - # Re-throw Timeout, and add a message to the log - logger.exception(f"Server timed out during api.request - {method} @ {api_url}. Timeout {payload['timeout']} s.") - return None + except Timeout: + raise requests.exceptions.Timeout(f"Server timed out during api.request - {method} @ {api_url}. Timeout {payload['timeout']} s.") except Exception as err: # Re-thrown any caught errors, and add a message to the log - logger.exception(f"{str(err.__class__.__name__)} error - {method} @ {api_url}") + logger.exception(f"{str(err.__class__.__name__)} error - {method} @ {api_url} (status {err.response.status_code})") logger.error("Status Code: %s", err.response.status_code) for k, v in err.response.json().items(): logger.error(" - %s: %s", k, v) - return None + raise requests.exceptions.HTTPError(err) if response is None: - logger.error(f"Null response - {method} '{api_url}'") - return None + raise requests.exceptions.HTTPError(f"Null response - {method} '{api_url}'") logger.info(f"Request: {method} {api_url} - {response.status_code}") From a2f0507b705a8a0522d1e9b02fc350c8a27ee951 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 6 Oct 2023 12:03:25 +1100 Subject: [PATCH 7/8] Update unit tests --- inventree/api.py | 20 ++++++++++++++------ tasks.py | 4 ++++ test/test_api.py | 2 ++ test/test_part.py | 8 +++++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/inventree/api.py b/inventree/api.py index c40abe3..4a6acb2 100644 --- a/inventree/api.py +++ b/inventree/api.py @@ -336,18 +336,26 @@ def request(self, api_url, **kwargs): # Send request to server! try: response = methods[method](api_url, **payload) - response.raise_for_status() + # response.raise_for_status() except Timeout: raise requests.exceptions.Timeout(f"Server timed out during api.request - {method} @ {api_url}. Timeout {payload['timeout']} s.") except Exception as err: # Re-thrown any caught errors, and add a message to the log - logger.exception(f"{str(err.__class__.__name__)} error - {method} @ {api_url} (status {err.response.status_code})") - logger.error("Status Code: %s", err.response.status_code) + logger.exception(f"{str(err.__class__.__name__)} error - {method} @ {api_url} (status {err.response.status_code if err.response else 'None'})") - for k, v in err.response.json().items(): - logger.error(" - %s: %s", k, v) + try: + data = response.json() - raise requests.exceptions.HTTPError(err) + if type(data) is dict: + for k, v in err.response.json().items(): + logger.error(" - %s: %s", k, v) + else: + logger.error(" - %s", data) + except (UnboundLocalError, AttributeError): + # No response object available + pass + + raise err if response is None: raise requests.exceptions.HTTPError(f"Null response - {method} '{api_url}'") diff --git a/tasks.py b/tasks.py index 458bee4..22884a5 100644 --- a/tasks.py +++ b/tasks.py @@ -154,6 +154,10 @@ def test(c, source=None, update=False, reset=False, debug=False): # If a source file is provided, check that it actually exists if source: + + if not source.endswith('.py'): + source += '.py' + if not os.path.exists(source): source = os.path.join('test', source) diff --git a/test/test_api.py b/test/test_api.py index a1b6468..30f6cc1 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -137,6 +137,8 @@ def setUp(self): SERVER, username=USERNAME, password=PASSWORD, timeout=30, + token_name='python-test', + use_token_auth=True ) diff --git a/test/test_part.py b/test/test_part.py index 5ea8fbe..0555974 100644 --- a/test/test_part.py +++ b/test/test_part.py @@ -3,6 +3,7 @@ import os import requests import sys +import time from requests.exceptions import HTTPError @@ -376,7 +377,7 @@ def test_part_delete(self): p = Part.create( self.api, { - 'name': 'Delete Me', + 'name': f'Delete Me {n}', 'description': 'Not long for this world!', 'category': 1, } @@ -645,6 +646,11 @@ def test_part_related(self): parts = Part.list(self.api) + # First, delete *all* related parts + for rp in PartRelated.list(self.api): + rp.delete() + time.sleep(0.05) + # Take two parts, make them related # Try with pk values ret = PartRelated.add_related(self.api, parts[0].pk, parts[1].pk) From 58c4712c45fea39c4d05682a87cc0593628dc026 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 6 Oct 2023 12:15:14 +1100 Subject: [PATCH 8/8] Ignore C901 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 1f0b192..d59997b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [flake8] ignore = + C901, # - W293 - blank lines contain whitespace W293, # - E501 - line too long (82 characters)