From da828ae56d5f8e3cf4b97446731259fcec2bd16e Mon Sep 17 00:00:00 2001 From: Ant Date: Tue, 20 Feb 2024 15:05:56 -0800 Subject: [PATCH 1/6] Start isolating error handling in functions --- spylib/admin_api.py | 124 +++++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/spylib/admin_api.py b/spylib/admin_api.py index bc5996d..d795e39 100644 --- a/spylib/admin_api.py +++ b/spylib/admin_api.py @@ -5,7 +5,7 @@ from json.decoder import JSONDecodeError from math import ceil, floor from time import monotonic -from typing import Annotated, Any, ClassVar, Dict, List, Optional +from typing import Annotated, Any, ClassVar, Dict, List, NoReturn, Optional from httpx import AsyncClient, Response from pydantic import BaseModel, BeforeValidator, ConfigDict @@ -191,19 +191,8 @@ async def execute_gql( # Handle any response that is not 200, which will return with error message # https://shopify.dev/api/admin-graphql#status_and_error_codes - if resp.status_code in [500, 503]: - raise ShopifyIntermittentError( - f'The Shopify API returned an intermittent error: {resp.status_code}.' - ) - if resp.status_code != 200: - try: - jsondata = resp.json() - error_msg = f'{resp.status_code}. {jsondata["errors"]}' - except JSONDecodeError: - error_msg = f'{resp.status_code}.' - - raise ShopifyGQLError(f'GQL query failed, status code: {error_msg}') + self._handle_non_200_status_codes(response=resp) try: jsondata = resp.json() @@ -220,50 +209,89 @@ async def execute_gql( ) raise ConnectionRefusedError - if 'data' not in jsondata and 'errors' in jsondata: + if 'data' not in jsondata and 'errors' in jsondata and jsondata['errors']: + # Only report on the first error just to simplify + err = jsondata['errors'][0] + + if 'extensions' in err and 'code' in err['extensions']: + error_code = err['extensions']['code'] + self._handle_max_cost_exceeded_error_code(error_code=error_code) + await self._handle_throttled_error_code(error_code=error_code, jsondata=jsondata) + + if 'message' in err: + self._handle_operation_name_required_error(error_message=err['message']) + self._handle_wrong_operation_name_error( + error_message=err['message'], operation_name=operation_name + ) + errorlist = '\n'.join( [err['message'] for err in jsondata['errors'] if 'message' in err] ) - error_code_list = '\n'.join( - [ - err['extensions']['code'] - for err in jsondata['errors'] - if 'extensions' in err and 'code' in err['extensions'] - ] - ) - if MAX_COST_EXCEEDED_ERROR_CODE in error_code_list: - raise ShopifyExceedingMaxCostError( - f'Store {self.store_name}: This query was rejected by the Shopify' - f' API, and will never run as written, as the query cost' - f' is larger than the max possible query size (>{self.graphql_bucket_max})' - ' for Shopify.' - ) - elif THROTTLED_ERROR_CODE in error_code_list: # This should be the last condition - query_cost = jsondata['extensions']['cost']['requestedQueryCost'] - available = jsondata['extensions']['cost']['throttleStatus']['currentlyAvailable'] - rate = jsondata['extensions']['cost']['throttleStatus']['restoreRate'] - sleep_time = ceil((query_cost - available) / rate) - await sleep(sleep_time) - raise ShopifyThrottledError - elif OPERATION_NAME_REQUIRED_ERROR_MESSAGE in errorlist: - raise ShopifyCallInvalidError( - f'Store {self.store_name}: Operation name was required for this query.' - 'This likely means you have multiple queries within one call ' - 'and you must specify which to run.' - ) - elif WRONG_OPERATION_NAME_ERROR_MESSAGE.format(operation_name) in errorlist: - raise ShopifyCallInvalidError( - f'Store {self.store_name}: Operation name {operation_name}' - 'does not exist in the query.' - ) - else: - raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') + raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') if not suppress_errors and len(jsondata.get('errors', [])) >= 1: raise ShopifyGQLError(jsondata) return jsondata['data'] + def _handle_non_200_status_codes(self, response) -> NoReturn: + if response.status_code in [500, 503]: + raise ShopifyIntermittentError( + f'The Shopify API returned an intermittent error: {response.status_code}.' + ) + + try: + jsondata = response.json() + error_msg = f'{response.status_code}. {jsondata["errors"]}' + except JSONDecodeError: + error_msg = f'{response.status_code}.' + + raise ShopifyGQLError(f'GQL query failed, status code: {error_msg}') + + def _handle_max_cost_exceeded_error_code(self, error_code: str) -> None: + if error_code != MAX_COST_EXCEEDED_ERROR_CODE: + return + + raise ShopifyExceedingMaxCostError( + f'Store {self.store_name}: This query was rejected by the Shopify' + f' API, and will never run as written, as the query cost' + f' is larger than the max possible query size (>{self.graphql_bucket_max})' + ' for Shopify.' + ) + + async def _handle_throttled_error_code(self, error_code: str, jsondata: dict) -> None: + if error_code != THROTTLED_ERROR_CODE: + return + + cost = jsondata['extensions']['cost'] + query_cost = cost['requestedQueryCost'] + available = cost['throttleStatus']['currentlyAvailable'] + rate = cost['throttleStatus']['restoreRate'] + sleep_time = ceil((query_cost - available) / rate) + await sleep(sleep_time) + raise ShopifyThrottledError + + def _handle_operation_name_required_error(self, error_message: str) -> None: + if error_message != OPERATION_NAME_REQUIRED_ERROR_MESSAGE: + return + + raise ShopifyCallInvalidError( + f'Store {self.store_name}: Operation name was required for this query.' + 'This likely means you have multiple queries within one call ' + 'and you must specify which to run.' + ) + + def _handle_wrong_operation_name_error( + self, error_message: str, operation_name: str | None + ) -> None: + if error_message != WRONG_OPERATION_NAME_ERROR_MESSAGE.format(operation_name): + return + + raise ShopifyCallInvalidError( + f'Store {self.store_name}: Operation name {operation_name}' + 'does not exist in the query.' + ) + class OfflineTokenABC(Token, ABC): """Offline tokens are used for long term access, and do not have a set expiry.""" From ed8f10c0d3dbac70845155e70a755ef2f5b2b23e Mon Sep 17 00:00:00 2001 From: Ant Date: Tue, 20 Feb 2024 15:45:57 -0800 Subject: [PATCH 2/6] Isolate error checking into one method --- spylib/admin_api.py | 94 ++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/spylib/admin_api.py b/spylib/admin_api.py index d795e39..5dd6db1 100644 --- a/spylib/admin_api.py +++ b/spylib/admin_api.py @@ -189,50 +189,42 @@ async def execute_gql( headers=headers, ) + jsondata = await self._check_for_errors( + response=resp, suppress_errors=suppress_errors, operation_name=operation_name + ) + + return jsondata['data'] + + async def _check_for_errors( + self, response, suppress_errors: bool, operation_name: str | None + ) -> dict: # Handle any response that is not 200, which will return with error message # https://shopify.dev/api/admin-graphql#status_and_error_codes - if resp.status_code != 200: - self._handle_non_200_status_codes(response=resp) + if response.status_code != 200: + self._handle_non_200_status_codes(response=response) - try: - jsondata = resp.json() - except JSONDecodeError as exc: - raise ShopifyInvalidResponseBody from exc - - if type(jsondata) is not dict: - raise ValueError('JSON data is not a dictionary') - if 'Invalid API key or access token' in jsondata.get('errors', ''): - self.access_token_invalid = True - logging.warning( - f'Store {self.store_name}: The Shopify API token is invalid. ' - 'Flag the access token as invalid.' - ) - raise ConnectionRefusedError + jsondata = self._extract_jsondata_from(response=response) - if 'data' not in jsondata and 'errors' in jsondata and jsondata['errors']: - # Only report on the first error just to simplify - err = jsondata['errors'][0] + errors: list | str = jsondata.get('errors', []) + if errors: + has_data_field = 'data' in jsondata + if has_data_field and not suppress_errors: + raise ShopifyGQLError(jsondata) - if 'extensions' in err and 'code' in err['extensions']: - error_code = err['extensions']['code'] - self._handle_max_cost_exceeded_error_code(error_code=error_code) - await self._handle_throttled_error_code(error_code=error_code, jsondata=jsondata) + if isinstance(errors, str): + self._handle_invalid_access_token(errors) + raise ShopifyGQLError(f'Unknown errors string: {jsondata}') - if 'message' in err: - self._handle_operation_name_required_error(error_message=err['message']) - self._handle_wrong_operation_name_error( - error_message=err['message'], operation_name=operation_name - ) + await self._handle_errors_list( + jsondata=jsondata, errors=errors, operation_name=operation_name + ) errorlist = '\n'.join( [err['message'] for err in jsondata['errors'] if 'message' in err] ) raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') - if not suppress_errors and len(jsondata.get('errors', [])) >= 1: - raise ShopifyGQLError(jsondata) - - return jsondata['data'] + return jsondata def _handle_non_200_status_codes(self, response) -> NoReturn: if response.status_code in [500, 503]: @@ -248,6 +240,44 @@ def _handle_non_200_status_codes(self, response) -> NoReturn: raise ShopifyGQLError(f'GQL query failed, status code: {error_msg}') + @staticmethod + def _extract_jsondata_from(response) -> dict: + try: + jsondata = response.json() + except JSONDecodeError as exc: + raise ShopifyInvalidResponseBody from exc + + if not isinstance(jsondata, dict): + raise ValueError('JSON data is not a dictionary') + + return jsondata + + def _handle_invalid_access_token(self, errors: str) -> None: + if 'Invalid API key or access token' in errors: + self.access_token_invalid = True + logging.warning( + f'Store {self.store_name}: The Shopify API token is invalid. ' + 'Flag the access token as invalid.' + ) + raise ConnectionRefusedError + + async def _handle_errors_list( + self, jsondata: dict, errors: list, operation_name: str | None + ) -> None: + # Only report on the first error just to simplify: We will raise an exception anyway. + err = errors[0] + + if 'extensions' in err and 'code' in err['extensions']: + error_code = err['extensions']['code'] + self._handle_max_cost_exceeded_error_code(error_code=error_code) + await self._handle_throttled_error_code(error_code=error_code, jsondata=jsondata) + + if 'message' in err: + self._handle_operation_name_required_error(error_message=err['message']) + self._handle_wrong_operation_name_error( + error_message=err['message'], operation_name=operation_name + ) + def _handle_max_cost_exceeded_error_code(self, error_code: str) -> None: if error_code != MAX_COST_EXCEEDED_ERROR_CODE: return From 94b2e7985e9b0f00035c8a7c592abc554f0a72be Mon Sep 17 00:00:00 2001 From: Ant Date: Tue, 20 Feb 2024 15:51:00 -0800 Subject: [PATCH 3/6] Convert admin_api module to folder --- spylib/admin_api/__init__.py | 3 +++ spylib/{admin_api.py => admin_api/tokens.py} | 0 2 files changed, 3 insertions(+) create mode 100644 spylib/admin_api/__init__.py rename spylib/{admin_api.py => admin_api/tokens.py} (100%) diff --git a/spylib/admin_api/__init__.py b/spylib/admin_api/__init__.py new file mode 100644 index 0000000..8d0ab47 --- /dev/null +++ b/spylib/admin_api/__init__.py @@ -0,0 +1,3 @@ +from .tokens import OfflineTokenABC, OnlineTokenABC, PrivateTokenABC, Token + +__all__ = ['Token', 'OfflineTokenABC', 'OnlineTokenABC', 'PrivateTokenABC'] diff --git a/spylib/admin_api.py b/spylib/admin_api/tokens.py similarity index 100% rename from spylib/admin_api.py rename to spylib/admin_api/tokens.py From 8b602ec5592bc74473cde6a7894686a36081aac3 Mon Sep 17 00:00:00 2001 From: Ant Date: Tue, 20 Feb 2024 16:09:34 -0800 Subject: [PATCH 4/6] Move error handling to its own class --- spylib/admin_api/gql_error_handler.py | 154 ++++++++++++++++++++++++++ spylib/admin_api/tokens.py | 153 ++----------------------- 2 files changed, 165 insertions(+), 142 deletions(-) create mode 100644 spylib/admin_api/gql_error_handler.py diff --git a/spylib/admin_api/gql_error_handler.py b/spylib/admin_api/gql_error_handler.py new file mode 100644 index 0000000..8bdbccd --- /dev/null +++ b/spylib/admin_api/gql_error_handler.py @@ -0,0 +1,154 @@ +import logging +from asyncio import sleep +from json.decoder import JSONDecodeError +from math import ceil +from typing import NoReturn + +from spylib.constants import ( + MAX_COST_EXCEEDED_ERROR_CODE, + OPERATION_NAME_REQUIRED_ERROR_MESSAGE, + THROTTLED_ERROR_CODE, + WRONG_OPERATION_NAME_ERROR_MESSAGE, +) +from spylib.exceptions import ( + ShopifyCallInvalidError, + ShopifyExceedingMaxCostError, + ShopifyGQLError, + ShopifyIntermittentError, + ShopifyInvalidResponseBody, + ShopifyThrottledError, +) + + +class GQLErrorHandler: + """Handle the bad status codes and errors codes + https://shopify.dev/api/admin-graphql#status_and_error_codes + """ + + def __init__( + self, + store_name: str, + graphql_bucket_max: int, + suppress_errors: bool, + operation_name: str | None, + ): + self.store_name = store_name + self.graphql_bucket_max = graphql_bucket_max + self.suppress_errors = suppress_errors + self.operation_name = operation_name + + async def check(self, response) -> dict: + if response.status_code != 200: + self._handle_non_200_status_codes(response=response) + + jsondata = self._extract_jsondata_from(response=response) + + errors: list | str = jsondata.get('errors', []) + if errors: + await self._check_errors_field(errors=errors, jsondata=jsondata) + + return jsondata + + def _handle_non_200_status_codes(self, response) -> NoReturn: + if response.status_code in [500, 503]: + raise ShopifyIntermittentError( + f'The Shopify API returned an intermittent error: {response.status_code}.' + ) + + try: + jsondata = response.json() + error_msg = f'{response.status_code}. {jsondata["errors"]}' + except JSONDecodeError: + error_msg = f'{response.status_code}.' + + raise ShopifyGQLError(f'GQL query failed, status code: {error_msg}') + + @staticmethod + def _extract_jsondata_from(response) -> dict: + try: + jsondata = response.json() + except JSONDecodeError as exc: + raise ShopifyInvalidResponseBody from exc + + if not isinstance(jsondata, dict): + raise ValueError('JSON data is not a dictionary') + + return jsondata + + async def _check_errors_field(self, errors: list | str, jsondata: dict): + has_data_field = 'data' in jsondata + if has_data_field and not self.suppress_errors: + raise ShopifyGQLError(jsondata) + + if isinstance(errors, str): + self._handle_invalid_access_token(errors) + raise ShopifyGQLError(f'Unknown errors string: {jsondata}') + + await self._handle_errors_list(jsondata=jsondata, errors=errors) + + errorlist = '\n'.join([err['message'] for err in jsondata['errors'] if 'message' in err]) + raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') + + def _handle_invalid_access_token(self, errors: str) -> None: + if 'Invalid API key or access token' in errors: + self.access_token_invalid = True + logging.warning( + f'Store {self.store_name}: The Shopify API token is invalid. ' + 'Flag the access token as invalid.' + ) + raise ConnectionRefusedError + + async def _handle_errors_list(self, jsondata: dict, errors: list) -> None: + # Only report on the first error just to simplify: We will raise an exception anyway. + err = errors[0] + + if 'extensions' in err and 'code' in err['extensions']: + error_code = err['extensions']['code'] + self._handle_max_cost_exceeded_error_code(error_code=error_code) + await self._handle_throttled_error_code(error_code=error_code, jsondata=jsondata) + + if 'message' in err: + self._handle_operation_name_required_error(error_message=err['message']) + self._handle_wrong_operation_name_error(error_message=err['message']) + + def _handle_max_cost_exceeded_error_code(self, error_code: str) -> None: + if error_code != MAX_COST_EXCEEDED_ERROR_CODE: + return + + raise ShopifyExceedingMaxCostError( + f'Store {self.store_name}: This query was rejected by the Shopify' + f' API, and will never run as written, as the query cost' + f' is larger than the max possible query size (>{self.graphql_bucket_max})' + ' for Shopify.' + ) + + async def _handle_throttled_error_code(self, error_code: str, jsondata: dict) -> None: + if error_code != THROTTLED_ERROR_CODE: + return + + cost = jsondata['extensions']['cost'] + query_cost = cost['requestedQueryCost'] + available = cost['throttleStatus']['currentlyAvailable'] + rate = cost['throttleStatus']['restoreRate'] + sleep_time = ceil((query_cost - available) / rate) + await sleep(sleep_time) + raise ShopifyThrottledError + + def _handle_operation_name_required_error(self, error_message: str) -> None: + if error_message != OPERATION_NAME_REQUIRED_ERROR_MESSAGE: + return + + raise ShopifyCallInvalidError( + f'Store {self.store_name}: Operation name was required for this query.' + 'This likely means you have multiple queries within one call ' + 'and you must specify which to run.' + ) + + def _handle_wrong_operation_name_error(self, error_message: str) -> None: + if error_message != WRONG_OPERATION_NAME_ERROR_MESSAGE.format(self.operation_name): + return + + raise ShopifyCallInvalidError( + f'Store {self.store_name}: Operation name {self.operation_name}' + 'does not exist in the query.' + ) diff --git a/spylib/admin_api/tokens.py b/spylib/admin_api/tokens.py index 5dd6db1..8270cb1 100644 --- a/spylib/admin_api/tokens.py +++ b/spylib/admin_api/tokens.py @@ -1,11 +1,9 @@ -import logging from abc import ABC, abstractmethod from asyncio import sleep from datetime import datetime, timedelta -from json.decoder import JSONDecodeError -from math import ceil, floor +from math import floor from time import monotonic -from typing import Annotated, Any, ClassVar, Dict, List, NoReturn, Optional +from typing import Annotated, Any, ClassVar, Dict, List, Optional from httpx import AsyncClient, Response from pydantic import BaseModel, BeforeValidator, ConfigDict @@ -15,18 +13,10 @@ from tenacity.stop import stop_after_attempt from tenacity.wait import wait_random -from spylib.constants import ( - API_CALL_NUMBER_RETRY_ATTEMPTS, - MAX_COST_EXCEEDED_ERROR_CODE, - OPERATION_NAME_REQUIRED_ERROR_MESSAGE, - THROTTLED_ERROR_CODE, - WRONG_OPERATION_NAME_ERROR_MESSAGE, -) +from spylib.constants import API_CALL_NUMBER_RETRY_ATTEMPTS from spylib.exceptions import ( ShopifyCallInvalidError, ShopifyError, - ShopifyExceedingMaxCostError, - ShopifyGQLError, ShopifyIntermittentError, ShopifyInvalidResponseBody, ShopifyThrottledError, @@ -35,6 +25,8 @@ from spylib.utils.misc import parse_scope from spylib.utils.rest import Request +from .gql_error_handler import GQLErrorHandler + class Token(ABC, BaseModel): """Abstract class for token objects. @@ -189,139 +181,16 @@ async def execute_gql( headers=headers, ) - jsondata = await self._check_for_errors( - response=resp, suppress_errors=suppress_errors, operation_name=operation_name + error_handler = GQLErrorHandler( + store_name=self.store_name, + graphql_bucket_max=self.graphql_bucket_max, + suppress_errors=suppress_errors, + operation_name=operation_name, ) + jsondata = await error_handler.check(response=resp) return jsondata['data'] - async def _check_for_errors( - self, response, suppress_errors: bool, operation_name: str | None - ) -> dict: - # Handle any response that is not 200, which will return with error message - # https://shopify.dev/api/admin-graphql#status_and_error_codes - if response.status_code != 200: - self._handle_non_200_status_codes(response=response) - - jsondata = self._extract_jsondata_from(response=response) - - errors: list | str = jsondata.get('errors', []) - if errors: - has_data_field = 'data' in jsondata - if has_data_field and not suppress_errors: - raise ShopifyGQLError(jsondata) - - if isinstance(errors, str): - self._handle_invalid_access_token(errors) - raise ShopifyGQLError(f'Unknown errors string: {jsondata}') - - await self._handle_errors_list( - jsondata=jsondata, errors=errors, operation_name=operation_name - ) - - errorlist = '\n'.join( - [err['message'] for err in jsondata['errors'] if 'message' in err] - ) - raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') - - return jsondata - - def _handle_non_200_status_codes(self, response) -> NoReturn: - if response.status_code in [500, 503]: - raise ShopifyIntermittentError( - f'The Shopify API returned an intermittent error: {response.status_code}.' - ) - - try: - jsondata = response.json() - error_msg = f'{response.status_code}. {jsondata["errors"]}' - except JSONDecodeError: - error_msg = f'{response.status_code}.' - - raise ShopifyGQLError(f'GQL query failed, status code: {error_msg}') - - @staticmethod - def _extract_jsondata_from(response) -> dict: - try: - jsondata = response.json() - except JSONDecodeError as exc: - raise ShopifyInvalidResponseBody from exc - - if not isinstance(jsondata, dict): - raise ValueError('JSON data is not a dictionary') - - return jsondata - - def _handle_invalid_access_token(self, errors: str) -> None: - if 'Invalid API key or access token' in errors: - self.access_token_invalid = True - logging.warning( - f'Store {self.store_name}: The Shopify API token is invalid. ' - 'Flag the access token as invalid.' - ) - raise ConnectionRefusedError - - async def _handle_errors_list( - self, jsondata: dict, errors: list, operation_name: str | None - ) -> None: - # Only report on the first error just to simplify: We will raise an exception anyway. - err = errors[0] - - if 'extensions' in err and 'code' in err['extensions']: - error_code = err['extensions']['code'] - self._handle_max_cost_exceeded_error_code(error_code=error_code) - await self._handle_throttled_error_code(error_code=error_code, jsondata=jsondata) - - if 'message' in err: - self._handle_operation_name_required_error(error_message=err['message']) - self._handle_wrong_operation_name_error( - error_message=err['message'], operation_name=operation_name - ) - - def _handle_max_cost_exceeded_error_code(self, error_code: str) -> None: - if error_code != MAX_COST_EXCEEDED_ERROR_CODE: - return - - raise ShopifyExceedingMaxCostError( - f'Store {self.store_name}: This query was rejected by the Shopify' - f' API, and will never run as written, as the query cost' - f' is larger than the max possible query size (>{self.graphql_bucket_max})' - ' for Shopify.' - ) - - async def _handle_throttled_error_code(self, error_code: str, jsondata: dict) -> None: - if error_code != THROTTLED_ERROR_CODE: - return - - cost = jsondata['extensions']['cost'] - query_cost = cost['requestedQueryCost'] - available = cost['throttleStatus']['currentlyAvailable'] - rate = cost['throttleStatus']['restoreRate'] - sleep_time = ceil((query_cost - available) / rate) - await sleep(sleep_time) - raise ShopifyThrottledError - - def _handle_operation_name_required_error(self, error_message: str) -> None: - if error_message != OPERATION_NAME_REQUIRED_ERROR_MESSAGE: - return - - raise ShopifyCallInvalidError( - f'Store {self.store_name}: Operation name was required for this query.' - 'This likely means you have multiple queries within one call ' - 'and you must specify which to run.' - ) - - def _handle_wrong_operation_name_error( - self, error_message: str, operation_name: str | None - ) -> None: - if error_message != WRONG_OPERATION_NAME_ERROR_MESSAGE.format(operation_name): - return - - raise ShopifyCallInvalidError( - f'Store {self.store_name}: Operation name {operation_name}' - 'does not exist in the query.' - ) - class OfflineTokenABC(Token, ABC): """Offline tokens are used for long term access, and do not have a set expiry.""" From 57e1a60fa831f0691fcad6ac1bab5f775a1ac790 Mon Sep 17 00:00:00 2001 From: Ant Date: Tue, 20 Feb 2024 16:17:04 -0800 Subject: [PATCH 5/6] Fix methods sorting to match call sorting --- spylib/admin_api/gql_error_handler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spylib/admin_api/gql_error_handler.py b/spylib/admin_api/gql_error_handler.py index 8bdbccd..67d2ea0 100644 --- a/spylib/admin_api/gql_error_handler.py +++ b/spylib/admin_api/gql_error_handler.py @@ -89,15 +89,6 @@ async def _check_errors_field(self, errors: list | str, jsondata: dict): errorlist = '\n'.join([err['message'] for err in jsondata['errors'] if 'message' in err]) raise ValueError(f'GraphQL query is incorrect:\n{errorlist}') - def _handle_invalid_access_token(self, errors: str) -> None: - if 'Invalid API key or access token' in errors: - self.access_token_invalid = True - logging.warning( - f'Store {self.store_name}: The Shopify API token is invalid. ' - 'Flag the access token as invalid.' - ) - raise ConnectionRefusedError - async def _handle_errors_list(self, jsondata: dict, errors: list) -> None: # Only report on the first error just to simplify: We will raise an exception anyway. err = errors[0] @@ -111,6 +102,15 @@ async def _handle_errors_list(self, jsondata: dict, errors: list) -> None: self._handle_operation_name_required_error(error_message=err['message']) self._handle_wrong_operation_name_error(error_message=err['message']) + def _handle_invalid_access_token(self, errors: str) -> None: + if 'Invalid API key or access token' in errors: + self.access_token_invalid = True + logging.warning( + f'Store {self.store_name}: The Shopify API token is invalid. ' + 'Flag the access token as invalid.' + ) + raise ConnectionRefusedError + def _handle_max_cost_exceeded_error_code(self, error_code: str) -> None: if error_code != MAX_COST_EXCEEDED_ERROR_CODE: return From b1232f689412bfc6ff68079d8a25c711eb500bf5 Mon Sep 17 00:00:00 2001 From: Ant Date: Wed, 21 Feb 2024 09:42:16 -0800 Subject: [PATCH 6/6] Use Optional and Union instead of | for typing --- spylib/admin_api/gql_error_handler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spylib/admin_api/gql_error_handler.py b/spylib/admin_api/gql_error_handler.py index 0220497..83e5814 100644 --- a/spylib/admin_api/gql_error_handler.py +++ b/spylib/admin_api/gql_error_handler.py @@ -2,7 +2,7 @@ from asyncio import sleep from json.decoder import JSONDecodeError from math import ceil -from typing import NoReturn +from typing import NoReturn, Optional, Union from spylib.constants import ( MAX_COST_EXCEEDED_ERROR_CODE, @@ -30,7 +30,7 @@ def __init__( store_name: str, graphql_bucket_max: int, suppress_errors: bool, - operation_name: str | None, + operation_name: Optional[str], ): self.store_name = store_name self.graphql_bucket_max = graphql_bucket_max @@ -43,7 +43,7 @@ async def check(self, response) -> dict: jsondata = self._extract_jsondata_from(response=response) - errors: list | str = jsondata.get('errors', []) + errors: Union[list, str] = jsondata.get('errors', []) if errors: await self._check_errors_field(errors=errors, jsondata=jsondata) @@ -75,7 +75,7 @@ def _extract_jsondata_from(response) -> dict: return jsondata - async def _check_errors_field(self, errors: list | str, jsondata: dict): + async def _check_errors_field(self, errors: Union[list, str], jsondata: dict): has_data_field = 'data' in jsondata if has_data_field and not self.suppress_errors: raise ShopifyGQLError(jsondata)