diff --git a/impectPy/helpers.py b/impectPy/helpers.py index cf56d39..1ff350f 100644 --- a/impectPy/helpers.py +++ b/impectPy/helpers.py @@ -10,9 +10,43 @@ # create logger for this module logger = logging.getLogger("impectPy") -# logger = logging.LoggerAdapter(logger, {"id": "-", "url": "-"}) logger.addHandler(logging.NullHandler()) + +###### +# +# This class inherits from Response and adds a function converts the response from an API call to a pandas dataframe, flattens it and fixes the column names +# +###### + +class ImpectResponse(requests.Response): + def process_response(self, endpoint: str, raise_exception: bool = True) -> pd.DataFrame: + # validate and get data from response + result = validate_response(response=self, endpoint=endpoint, raise_exception=raise_exception) + + # convert to df + result = pd.json_normalize(result) + + # fix column names using regex + result = result.rename(columns=lambda x: re.sub(r"\.(.)", lambda y: y.group(1).upper(), x)) + + # return result + return result + + +###### +# +# This class inherits from Session and ensure the response is of type ImpectResponse +# +###### + +class ImpectSession(requests.Session): + def request(self, *args, **kwargs) -> ImpectResponse: + response = super().request(*args, **kwargs) + response.__class__ = ImpectResponse + return response + + ###### # # This class creates an object to handle rate-limited API requests @@ -31,25 +65,25 @@ class ForbiddenError(HTTPError): class RateLimitedAPI: - def __init__(self, session: Optional[requests.Session] = None): + def __init__(self, session: Optional[ImpectSession] = None): """ Initializes a RateLimitedAPI object. Args: - session (requests.Session): The session object to use for the API calls. + session (ImpectSession): The session object to use for the API calls. """ - self.session = session or requests.Session() # use the provided session or create a new session + self.session = session or ImpectSession() # use the provided session or create a new session self.bucket = None # TokenBucket object to manage rate limit tokens # make a rate-limited API request def make_api_request_limited( self, url: str, method: str, data: Optional[Dict[str, str]] = None - ) -> requests.Response: + ) -> ImpectResponse: """ Executes an API call while applying the rate limit. Returns: - requests.Response: The response returned by the API. + ImpectResponse: The response returned by the API. """ # check if bucket is not initialized @@ -73,7 +107,6 @@ def make_api_request_limited( remaining=int(response.headers["RateLimit-Remaining"]) ) - # return response return response # check if a token is available @@ -102,12 +135,12 @@ def make_api_request_limited( def make_api_request( self, url: str, method: str, data: Optional[Dict[str, Any]] = None, max_retries: int = 3, retry_delay: int = 1 - ) -> requests.Response: + ) -> ImpectResponse: """ Executes an API call. Returns: - requests.Response: The response returned by the API. + ImpectResponse: The response returned by the API. """ # try API call for i in range(max_retries): @@ -205,31 +238,6 @@ def consumeToken(self): return True # return True to indicate successful token consumption -###### -# -# This function converts the response from an API call to a pandas dataframe, flattens it and fixes the column names -# -###### - - -def process_response(self: requests.Response, endpoint: str, raise_exception: bool = True) -> pd.DataFrame: - # validate and get data from response - result = validate_response(response=self, endpoint=endpoint, raise_exception=raise_exception) - - # convert to df - result = pd.json_normalize(result) - - # fix column names using regex - result = result.rename(columns=lambda x: re.sub(r"\.(.)", lambda y: y.group(1).upper(), x)) - - # return result - return result - - -# attach method to requests module -requests.Response.process_response = process_response - - ###### # # This function unnests the idMappings key from an API response @@ -301,7 +309,7 @@ def unnest_mappings_df(df: pd.DataFrame, mapping_col: str) -> pd.DataFrame: # define function to validate JSON response and return data -def validate_response(response: requests.Response, endpoint: str, raise_exception: bool = True) -> dict: +def validate_response(response: ImpectResponse, endpoint: str, raise_exception: bool = True) -> dict: # get data from response data = response.json()["data"] diff --git a/impectPy/player_iteration_averages.py b/impectPy/player_iteration_averages.py index 1c17621..945df84 100644 --- a/impectPy/player_iteration_averages.py +++ b/impectPy/player_iteration_averages.py @@ -90,12 +90,17 @@ def getPlayerIterationAveragesFromHost( f"squads/{squad_id}/player-kpis", method="GET" ).process_response( - endpoint="PlayerAverages" + endpoint="PlayerAverages", + raise_exception=False ).assign( iterationId=iteration, squadId=squad_id ) + # skip if empty repsonse + if len(averages_raw) == 0: + continue + # unnest scorings averages_raw = averages_raw.explode("kpis").reset_index(drop=True) diff --git a/impectPy/player_iteration_scores.py b/impectPy/player_iteration_scores.py index 8c4f7cb..804aaaf 100644 --- a/impectPy/player_iteration_scores.py +++ b/impectPy/player_iteration_scores.py @@ -81,7 +81,7 @@ def fetch_player_iteration_scores(connection, url): return connection.make_api_request_limited( url=url, method="GET" - ).process_response(endpoint="Player Match Scores") + ).process_response(endpoint="Player Match Scores", raise_exception=False) # get player scores if positions is None: @@ -99,7 +99,10 @@ def fetch_player_iteration_scores(connection, url): iterationId=iteration, squadId=squad_id ) - scores_list.append(scores) + + # check if response is empty + if len(scores) > 0: + scores_list.append(scores) scores_raw = pd.concat(scores_list).reset_index(drop=True).reset_index(drop=True) else: @@ -122,7 +125,10 @@ def fetch_player_iteration_scores(connection, url): squadId=squad_id, positions=position_string ) - scores_list.append(scores) + + # check if resonse is empty + if len(scores) > 0: + scores_list.append(scores) scores_raw = pd.concat(scores_list).reset_index(drop=True).reset_index(drop=True) # raise exception if no player played at given positions in entire iteration @@ -130,9 +136,10 @@ def fetch_player_iteration_scores(connection, url): raise Exception(f"No players played at given position in iteration {iteration}.") # print squads without players at given position - error_list = [str(squadId) for squadId in squad_ids if squadId not in scores_raw.squadId.to_list()] - if len(error_list) > 0: - print(f"No players played at positions {positions} for iteration {iteration} for following squads:\n\t{', '.join(error_list)}") + if positions is not None: + error_list = [str(squadId) for squadId in squad_ids if squadId not in scores_raw.squadId.to_list()] + if len(error_list) > 0: + print(f"No players played at positions {positions} for iteration {iteration} for following squads:\n\t{', '.join(error_list)}") # get players players = connection.make_api_request_limited(