Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 43 additions & 35 deletions impectPy/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]

Expand Down
7 changes: 6 additions & 1 deletion impectPy/player_iteration_averages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 13 additions & 6 deletions impectPy/player_iteration_scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -122,17 +125,21 @@ 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
if len(scores_raw) == 0:
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(
Expand Down