From 766743144fe008d1de5d70b4174321d0234d48ee Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Thu, 6 Jun 2024 17:39:34 +0200 Subject: [PATCH 01/15] added a function to retrieve the bids table. Will be used to calculate the total tries. --- src/boardlib/__main__.py | 58 ++++++++++++++++++++++++++++++++++++-- src/boardlib/api/aurora.py | 47 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index a93a9c1..524cb4a 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -10,6 +10,7 @@ import boardlib.db.aurora LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") +BIDS_LOGBOOK_FIELDS = ("uuid", "user_id", "climb_uuid", "climb_name", "angle", "is_mirror", "bid_count", "comment", "climbed_at", "created_at") def logbook_entries(board, username, password, grade_type="font", db_path=None): @@ -26,12 +27,24 @@ def logbook_entries(board, username, password, grade_type="font", db_path=None): else: raise ValueError(f"Unknown board {board}") +def bids_logbook_entries(board, username, password, db_path=None): + api = ( + boardlib.api.moon + if board.startswith("moon") + else boardlib.api.aurora + if board in boardlib.api.aurora.HOST_BASES + else None + ) + if api: + yield from api.bids_logbook_entries(board, username, password, db_path) + else: + raise ValueError(f"Unknown board {board}") -def write_entries(output_file, entries, no_headers=False): - writer = csv.DictWriter(output_file, LOGBOOK_FIELDS) + +def write_entries(output_file, entries, no_headers=False, fields=LOGBOOK_FIELDS): + writer = csv.DictWriter(output_file, fieldnames=fields) if not no_headers: writer.writeheader() - writer.writerows(entries) @@ -49,6 +62,20 @@ def handle_logbook_command(args): sys.stdout.reconfigure(encoding="utf-8") write_entries(sys.stdout, entries, args.no_headers) +def handle_bids_logbook_command(args): + env_var = f"{args.board.upper()}_PASSWORD" + password = os.environ.get(env_var) + if not password: + password = getpass.getpass("Password: ") + entries = bids_logbook_entries(args.board, args.username, password, args.db_path) + + if args.output: + with open(args.output, "w", encoding="utf-8") as output_file: + write_entries(output_file, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) + else: + sys.stdout.reconfigure(encoding="utf-8") + write_entries(sys.stdout, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) + def handle_database_command(args): if not args.database_path.exists(): @@ -115,15 +142,40 @@ def add_logbook_parser(subparsers): ) logbook_parser.set_defaults(func=handle_logbook_command) +def add_bids_logbook_parser(subparsers): + bids_logbook_parser = subparsers.add_parser( + "bids_logbook", help="Download bids logbook entries to CSV" + ) + bids_logbook_parser.add_argument( + "board", + help="Board name", + choices=sorted( + boardlib.api.moon.BOARD_IDS.keys() | boardlib.api.aurora.HOST_BASES.keys() + ), + ) + bids_logbook_parser.add_argument("-u", "--username", help="Username", required=True) + bids_logbook_parser.add_argument("-o", "--output", help="Output file", required=False) + bids_logbook_parser.add_argument( + "--no-headers", help="Don't write headers", action="store_true", required=False + ) + bids_logbook_parser.add_argument( + "--db_path", + help="Path to the local database (optional). Using a local database can significantly speed up the logbook generation. Create a local database with the 'boardlib database' command.", + type=pathlib.Path, + required=False, + ) + bids_logbook_parser.set_defaults(func=handle_bids_logbook_command) def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) add_logbook_parser(subparsers) + add_bids_logbook_parser(subparsers) add_database_parser(subparsers) args = parser.parse_args() args.func(args) + if __name__ == "__main__": main() diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 793a8d6..4097941 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -391,3 +391,50 @@ def save_climb( ) response.raise_for_status() return response.json() + + +def get_bids_logbook(board, token, user_id): + sync_results = user_sync(board, token, user_id, tables=["bids"]) + return sync_results["PUT"]["bids"] + + +def bids_logbook_entries(board, username, password, db_path=None): + login_info = login(board, username, password) + raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) + + for raw_entry in raw_entries: + yield { + "uuid": raw_entry["uuid"], + "user_id": raw_entry["user_id"], + "climb_uuid": raw_entry["climb_uuid"], + "angle": raw_entry["angle"], + "is_mirror": raw_entry["is_mirror"], + "bid_count": raw_entry["bid_count"], + "comment": raw_entry["comment"], + "climbed_at": raw_entry["climbed_at"], + "created_at": raw_entry["created_at"], + } + + +def bids_logbook_entries(board, username, password, db_path=None): + login_info = login(board, username, password) + raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) + + for raw_entry in raw_entries: + if db_path: + climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) + else: + climb_name = get_climb_name(board, raw_entry["climb_uuid"]) + + yield { + "uuid": raw_entry["uuid"], + "user_id": raw_entry["user_id"], + "climb_uuid": raw_entry["climb_uuid"], + "climb_name": climb_name if climb_name else "Unknown Climb", + "angle": raw_entry["angle"], + "is_mirror": raw_entry["is_mirror"], + "bid_count": raw_entry["bid_count"], + "comment": raw_entry["comment"], + "climbed_at": raw_entry["climbed_at"], + "created_at": raw_entry["created_at"], + } From 89a05641710ea48e23861d4f30a4c351cb2dc7d0 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Thu, 6 Jun 2024 17:39:34 +0200 Subject: [PATCH 02/15] added a function to retrieve the bids table. Will be used to calculate the total tries. --- src/boardlib/__main__.py | 58 ++++++++++++++++++++++++++++++++++++-- src/boardlib/api/aurora.py | 47 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index daa1e0a..1fe9fe6 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -10,6 +10,7 @@ import boardlib.db.aurora LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") +BIDS_LOGBOOK_FIELDS = ("uuid", "user_id", "climb_uuid", "climb_name", "angle", "is_mirror", "bid_count", "comment", "climbed_at", "created_at") def logbook_entries(board, username, password, grade_type="font", database=None): @@ -26,12 +27,24 @@ def logbook_entries(board, username, password, grade_type="font", database=None) else: raise ValueError(f"Unknown board {board}") +def bids_logbook_entries(board, username, password, db_path=None): + api = ( + boardlib.api.moon + if board.startswith("moon") + else boardlib.api.aurora + if board in boardlib.api.aurora.HOST_BASES + else None + ) + if api: + yield from api.bids_logbook_entries(board, username, password, db_path) + else: + raise ValueError(f"Unknown board {board}") + -def write_entries(output_file, entries, no_headers=False): - writer = csv.DictWriter(output_file, LOGBOOK_FIELDS) +def write_entries(output_file, entries, no_headers=False, fields=LOGBOOK_FIELDS): + writer = csv.DictWriter(output_file, fieldnames=fields) if not no_headers: writer.writeheader() - writer.writerows(entries) @@ -49,6 +62,20 @@ def handle_logbook_command(args): sys.stdout.reconfigure(encoding="utf-8") write_entries(sys.stdout, entries, args.no_headers) +def handle_bids_logbook_command(args): + env_var = f"{args.board.upper()}_PASSWORD" + password = os.environ.get(env_var) + if not password: + password = getpass.getpass("Password: ") + entries = bids_logbook_entries(args.board, args.username, password, args.db_path) + + if args.output: + with open(args.output, "w", encoding="utf-8") as output_file: + write_entries(output_file, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) + else: + sys.stdout.reconfigure(encoding="utf-8") + write_entries(sys.stdout, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) + def handle_database_command(args): if not args.database_path.exists(): @@ -116,15 +143,40 @@ def add_logbook_parser(subparsers): ) logbook_parser.set_defaults(func=handle_logbook_command) +def add_bids_logbook_parser(subparsers): + bids_logbook_parser = subparsers.add_parser( + "bids_logbook", help="Download bids logbook entries to CSV" + ) + bids_logbook_parser.add_argument( + "board", + help="Board name", + choices=sorted( + boardlib.api.moon.BOARD_IDS.keys() | boardlib.api.aurora.HOST_BASES.keys() + ), + ) + bids_logbook_parser.add_argument("-u", "--username", help="Username", required=True) + bids_logbook_parser.add_argument("-o", "--output", help="Output file", required=False) + bids_logbook_parser.add_argument( + "--no-headers", help="Don't write headers", action="store_true", required=False + ) + bids_logbook_parser.add_argument( + "--db_path", + help="Path to the local database (optional). Using a local database can significantly speed up the logbook generation. Create a local database with the 'boardlib database' command.", + type=pathlib.Path, + required=False, + ) + bids_logbook_parser.set_defaults(func=handle_bids_logbook_command) def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) add_logbook_parser(subparsers) + add_bids_logbook_parser(subparsers) add_database_parser(subparsers) args = parser.parse_args() args.func(args) + if __name__ == "__main__": main() diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 638abae..d2be68e 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -391,3 +391,50 @@ def save_climb( ) response.raise_for_status() return response.json() + + +def get_bids_logbook(board, token, user_id): + sync_results = user_sync(board, token, user_id, tables=["bids"]) + return sync_results["PUT"]["bids"] + + +def bids_logbook_entries(board, username, password, db_path=None): + login_info = login(board, username, password) + raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) + + for raw_entry in raw_entries: + yield { + "uuid": raw_entry["uuid"], + "user_id": raw_entry["user_id"], + "climb_uuid": raw_entry["climb_uuid"], + "angle": raw_entry["angle"], + "is_mirror": raw_entry["is_mirror"], + "bid_count": raw_entry["bid_count"], + "comment": raw_entry["comment"], + "climbed_at": raw_entry["climbed_at"], + "created_at": raw_entry["created_at"], + } + + +def bids_logbook_entries(board, username, password, db_path=None): + login_info = login(board, username, password) + raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) + + for raw_entry in raw_entries: + if db_path: + climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) + else: + climb_name = get_climb_name(board, raw_entry["climb_uuid"]) + + yield { + "uuid": raw_entry["uuid"], + "user_id": raw_entry["user_id"], + "climb_uuid": raw_entry["climb_uuid"], + "climb_name": climb_name if climb_name else "Unknown Climb", + "angle": raw_entry["angle"], + "is_mirror": raw_entry["is_mirror"], + "bid_count": raw_entry["bid_count"], + "comment": raw_entry["comment"], + "climbed_at": raw_entry["climbed_at"], + "created_at": raw_entry["created_at"], + } From 37f960516416a70665fb9843bac79ce3ca06d604 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Mon, 10 Jun 2024 13:17:11 +0200 Subject: [PATCH 03/15] started writing a function to get the complete logbook --- src/boardlib/api/aurora.py | 132 +++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 14 deletions(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index d2be68e..e7f6395 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -3,6 +3,7 @@ import sqlite3 import bs4 import requests +import pandas as pd import boardlib.util.grades @@ -398,15 +399,23 @@ def get_bids_logbook(board, token, user_id): return sync_results["PUT"]["bids"] + + def bids_logbook_entries(board, username, password, db_path=None): login_info = login(board, username, password) raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) for raw_entry in raw_entries: + if db_path: + climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) + else: + climb_name = get_climb_name(board, raw_entry["climb_uuid"]) + yield { "uuid": raw_entry["uuid"], "user_id": raw_entry["user_id"], "climb_uuid": raw_entry["climb_uuid"], + "climb_name": climb_name if climb_name else "Unknown Climb", "angle": raw_entry["angle"], "is_mirror": raw_entry["is_mirror"], "bid_count": raw_entry["bid_count"], @@ -416,25 +425,120 @@ def bids_logbook_entries(board, username, password, db_path=None): } -def bids_logbook_entries(board, username, password, db_path=None): +def total_logbook_entries(board, username, password, grade_type="font", db_path=None): + # Get bids and ascents data login_info = login(board, username, password) - raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) + token = login_info["token"] + user_id = login_info["user_id"] - for raw_entry in raw_entries: + bids_entries = list(bids_logbook_entries(board, username, password, db_path)) + raw_ascents_entries = get_logbook(board, token, user_id) + + # Convert bids entries to DataFrame + bids_df = pd.DataFrame(bids_entries) + bids_df['climbed_at'] = pd.to_datetime(bids_df['climbed_at']) + + # Process raw ascents entries and convert to DataFrame + ascents_entries = [] + grades = get_grades(board) + + # Convert grades to dictionary for easy lookup + grades_dict = {grade['difficulty']: grade for grade in grades} + + for raw_entry in raw_ascents_entries: + if not raw_entry["is_listed"]: + continue if db_path: climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) else: climb_name = get_climb_name(board, raw_entry["climb_uuid"]) - yield { - "uuid": raw_entry["uuid"], - "user_id": raw_entry["user_id"], - "climb_uuid": raw_entry["climb_uuid"], - "climb_name": climb_name if climb_name else "Unknown Climb", + grade_info = grades_dict.get(raw_entry["difficulty"], {}) + grade = grade_info.get("french_name" if grade_type == "font" else "verm_name", "Unknown") + + ascents_entries.append({ + "board": board, "angle": raw_entry["angle"], - "is_mirror": raw_entry["is_mirror"], - "bid_count": raw_entry["bid_count"], - "comment": raw_entry["comment"], - "climbed_at": raw_entry["climbed_at"], - "created_at": raw_entry["created_at"], - } + "name": climb_name if climb_name else "Unknown Climb", + "date": datetime.datetime.strptime(raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S"), + "grade": grade, + "tries": raw_entry["attempt_id"] if raw_entry["attempt_id"] else raw_entry["bid_count"], + "is_mirror": raw_entry["is_mirror"] + }) + + ascents_df = pd.DataFrame(ascents_entries) + + # Summarize the bids table + bids_summary = bids_df.groupby(['climb_name', bids_df['climbed_at'].dt.date, 'is_mirror']).agg({ + 'bid_count': 'sum', + 'angle': 'first' + }).reset_index().rename(columns={'climbed_at': 'date'}) + + # Create a new column for is_ascent + bids_summary['is_ascent'] = False + bids_summary['tries'] = bids_summary['bid_count'] + + # Check for ascents and update the logbook + final_logbook = [] + + for _, ascent_row in ascents_df.iterrows(): + ascent_date = ascent_row['date'].date() + ascent_climb_name = ascent_row['name'] + ascent_is_mirror = ascent_row['is_mirror'] + + # Find corresponding bids entries + bid_match = bids_summary[(bids_summary['climb_name'] == ascent_climb_name) & (bids_summary['date'] == ascent_date) & (bids_summary['is_mirror'] == ascent_is_mirror)] + + if not bid_match.empty: + bid_row = bid_match.iloc[0] + total_tries = ascent_row['tries'] + bid_row['tries'] + final_logbook.append({ + 'board': ascent_row['board'], + 'angle': ascent_row['angle'], + 'climb_name': ascent_row['name'], + 'date': ascent_row['date'], + 'grade': ascent_row['grade'], + 'tries': total_tries, + 'is_mirror': ascent_row['is_mirror'], + 'is_ascent': True + }) + bids_summary = bids_summary.drop(bid_match.index) # Remove matched bids + else: + final_logbook.append({ + 'board': ascent_row['board'], + 'angle': ascent_row['angle'], + 'climb_name': ascent_row['name'], + 'date': ascent_row['date'], + 'grade': ascent_row['grade'], + 'tries': ascent_row['tries'], + 'is_mirror': ascent_row['is_mirror'], + 'is_ascent': True + }) + + # Add remaining bids that do not have corresponding ascents + for _, bid_row in bids_summary.iterrows(): + final_logbook.append({ + 'board': board, + 'angle': bid_row['angle'], + 'climb_name': bid_row['climb_name'], + 'date': bid_row['date'], + 'grade': 'Unknown', # We don't have grade information for bids + 'tries': bid_row['tries'], + 'is_mirror': bid_row['is_mirror'], + 'is_ascent': False + }) + + # Convert to DataFrame + final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'grade', 'tries', 'is_mirror', 'is_ascent']) + + # Ensure all dates are converted to Timestamps + final_logbook_df['date'] = pd.to_datetime(final_logbook_df['date']) + + # Sort the DataFrame by date + final_logbook_df = final_logbook_df.sort_values(by='date') + + # Add is_repeat and sessions_count columns + final_logbook_df['is_repeat'] = final_logbook_df.duplicated(subset=['climb_name', 'is_mirror'], keep='first') + final_logbook_df['sessions_count'] = final_logbook_df.groupby(['climb_name', 'is_mirror'])['date'].rank(method='dense').astype(int) + + return final_logbook_df \ No newline at end of file From 5831bc4c1bf87149b5b108a490efc501c3d7e15c Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Mon, 10 Jun 2024 15:44:20 +0200 Subject: [PATCH 04/15] Added note to not forget where to start working again. --- src/boardlib/api/aurora.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index e7f6395..65056b9 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -541,4 +541,12 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= final_logbook_df['is_repeat'] = final_logbook_df.duplicated(subset=['climb_name', 'is_mirror'], keep='first') final_logbook_df['sessions_count'] = final_logbook_df.groupby(['climb_name', 'is_mirror'])['date'].rank(method='dense').astype(int) - return final_logbook_df \ No newline at end of file + return final_logbook_df + +''' +ToDo: +* Consider Angle in Session_Count and is_repeat. +* Get Grade from grade_stats + Either average_grade or displayed grade. + Solution for local db and API calls needed +''' \ No newline at end of file From d7df7e107222cff34871a3942f54e9833973ec5d Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Tue, 11 Jun 2024 13:42:04 +0200 Subject: [PATCH 05/15] Added basic functionality for session ant total_tries counter. --- src/boardlib/api/aurora.py | 48 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 65056b9..ea50b3f 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -424,7 +424,6 @@ def bids_logbook_entries(board, username, password, db_path=None): "created_at": raw_entry["created_at"], } - def total_logbook_entries(board, username, password, grade_type="font", db_path=None): # Get bids and ascents data login_info = login(board, username, password) @@ -461,7 +460,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= "angle": raw_entry["angle"], "name": climb_name if climb_name else "Unknown Climb", "date": datetime.datetime.strptime(raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S"), - "grade": grade, + "logged_grade": grade, "tries": raw_entry["attempt_id"] if raw_entry["attempt_id"] else raw_entry["bid_count"], "is_mirror": raw_entry["is_mirror"] }) @@ -469,9 +468,8 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= ascents_df = pd.DataFrame(ascents_entries) # Summarize the bids table - bids_summary = bids_df.groupby(['climb_name', bids_df['climbed_at'].dt.date, 'is_mirror']).agg({ - 'bid_count': 'sum', - 'angle': 'first' + bids_summary = bids_df.groupby(['climb_name', bids_df['climbed_at'].dt.date, 'is_mirror', 'angle']).agg({ + 'bid_count': 'sum' }).reset_index().rename(columns={'climbed_at': 'date'}) # Create a new column for is_ascent @@ -485,9 +483,15 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= ascent_date = ascent_row['date'].date() ascent_climb_name = ascent_row['name'] ascent_is_mirror = ascent_row['is_mirror'] + ascent_angle = ascent_row['angle'] # Find corresponding bids entries - bid_match = bids_summary[(bids_summary['climb_name'] == ascent_climb_name) & (bids_summary['date'] == ascent_date) & (bids_summary['is_mirror'] == ascent_is_mirror)] + bid_match = bids_summary[ + (bids_summary['climb_name'] == ascent_climb_name) & + (bids_summary['date'] == ascent_date) & + (bids_summary['is_mirror'] == ascent_is_mirror) & + (bids_summary['angle'] == ascent_angle) + ] if not bid_match.empty: bid_row = bid_match.iloc[0] @@ -497,7 +501,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'angle': ascent_row['angle'], 'climb_name': ascent_row['name'], 'date': ascent_row['date'], - 'grade': ascent_row['grade'], + 'logged_grade': ascent_row['logged_grade'], 'tries': total_tries, 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True @@ -509,7 +513,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'angle': ascent_row['angle'], 'climb_name': ascent_row['name'], 'date': ascent_row['date'], - 'grade': ascent_row['grade'], + 'logged_grade': ascent_row['logged_grade'], 'tries': ascent_row['tries'], 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True @@ -522,30 +526,46 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'angle': bid_row['angle'], 'climb_name': bid_row['climb_name'], 'date': bid_row['date'], - 'grade': 'Unknown', # We don't have grade information for bids + 'logged_grade': 'NA', # We don't have grade information for bids 'tries': bid_row['tries'], 'is_mirror': bid_row['is_mirror'], 'is_ascent': False }) # Convert to DataFrame - final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'grade', 'tries', 'is_mirror', 'is_ascent']) + final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'tries', 'is_mirror', 'is_ascent']) # Ensure all dates are converted to Timestamps final_logbook_df['date'] = pd.to_datetime(final_logbook_df['date']) # Sort the DataFrame by date final_logbook_df = final_logbook_df.sort_values(by='date') + + # Calculate sessions_count and tries_total + def calculate_sessions_count(group): + group = group.sort_values(by='date') + unique_dates = group['date'].dt.date.drop_duplicates().reset_index(drop=True) + sessions_count = unique_dates.rank(method='dense').astype(int) + sessions_count_map = dict(zip(unique_dates, sessions_count)) + group['sessions_count'] = group['date'].dt.date.map(sessions_count_map) + return group + + final_logbook_df = final_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) + + def calculate_tries_total(group): + group = group.sort_values(by='date') + group['tries_total'] = group['tries'].cumsum() + return group + + final_logbook_df = final_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_tries_total).reset_index(drop=True) - # Add is_repeat and sessions_count columns - final_logbook_df['is_repeat'] = final_logbook_df.duplicated(subset=['climb_name', 'is_mirror'], keep='first') - final_logbook_df['sessions_count'] = final_logbook_df.groupby(['climb_name', 'is_mirror'])['date'].rank(method='dense').astype(int) + # Add is_repeat column + final_logbook_df['is_repeat'] = final_logbook_df.duplicated(subset=['climb_name', 'is_mirror', 'angle'], keep='first') return final_logbook_df ''' ToDo: -* Consider Angle in Session_Count and is_repeat. * Get Grade from grade_stats Either average_grade or displayed grade. Solution for local db and API calls needed From bb0266d295c427a1d4f7e75578ba1f6c216a6157 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Tue, 11 Jun 2024 14:24:17 +0200 Subject: [PATCH 06/15] Added basic functionality for showing the displayed name - only works with local database for now. --- src/boardlib/api/aurora.py | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index ea50b3f..d776b34 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -424,6 +424,21 @@ def bids_logbook_entries(board, username, password, db_path=None): "created_at": raw_entry["created_at"], } +def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict): + conn = sqlite3.connect(database) + cursor = conn.cursor() + cursor.execute( + "SELECT display_difficulty FROM climb_stats WHERE climb_uuid = ? AND angle = ?", + (climb_uuid, angle) + ) + row = cursor.fetchone() + conn.close() + if row: + difficulty_value = round(row[0]) + grade_info = grades_dict.get(difficulty_value, {}) + return grade_info.get("french_name", "NA") + return "NA" + def total_logbook_entries(board, username, password, grade_type="font", db_path=None): # Get bids and ascents data login_info = login(board, username, password) @@ -449,18 +464,22 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= continue if db_path: climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) + displayed_grade = get_displayed_grade_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"], grades_dict) else: climb_name = get_climb_name(board, raw_entry["climb_uuid"]) + displayed_grade = "NA" grade_info = grades_dict.get(raw_entry["difficulty"], {}) - grade = grade_info.get("french_name" if grade_type == "font" else "verm_name", "Unknown") + logged_grade = grade_info.get("french_name" if grade_type == "font" else "verm_name", "Unknown") ascents_entries.append({ "board": board, "angle": raw_entry["angle"], + "climb_uuid": raw_entry["climb_uuid"], "name": climb_name if climb_name else "Unknown Climb", "date": datetime.datetime.strptime(raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S"), - "logged_grade": grade, + "logged_grade": logged_grade, + "displayed_grade": displayed_grade, "tries": raw_entry["attempt_id"] if raw_entry["attempt_id"] else raw_entry["bid_count"], "is_mirror": raw_entry["is_mirror"] }) @@ -468,7 +487,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= ascents_df = pd.DataFrame(ascents_entries) # Summarize the bids table - bids_summary = bids_df.groupby(['climb_name', bids_df['climbed_at'].dt.date, 'is_mirror', 'angle']).agg({ + bids_summary = bids_df.groupby(['climb_uuid', 'climb_name', bids_df['climbed_at'].dt.date, 'is_mirror', 'angle']).agg({ 'bid_count': 'sum' }).reset_index().rename(columns={'climbed_at': 'date'}) @@ -481,13 +500,14 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= for _, ascent_row in ascents_df.iterrows(): ascent_date = ascent_row['date'].date() + ascent_climb_uuid = ascent_row['climb_uuid'] ascent_climb_name = ascent_row['name'] ascent_is_mirror = ascent_row['is_mirror'] ascent_angle = ascent_row['angle'] # Find corresponding bids entries bid_match = bids_summary[ - (bids_summary['climb_name'] == ascent_climb_name) & + (bids_summary['climb_uuid'] == ascent_climb_uuid) & (bids_summary['date'] == ascent_date) & (bids_summary['is_mirror'] == ascent_is_mirror) & (bids_summary['angle'] == ascent_angle) @@ -502,6 +522,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'climb_name': ascent_row['name'], 'date': ascent_row['date'], 'logged_grade': ascent_row['logged_grade'], + 'displayed_grade': ascent_row['displayed_grade'], 'tries': total_tries, 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True @@ -514,6 +535,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'climb_name': ascent_row['name'], 'date': ascent_row['date'], 'logged_grade': ascent_row['logged_grade'], + 'displayed_grade': ascent_row['displayed_grade'], 'tries': ascent_row['tries'], 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True @@ -521,19 +543,25 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= # Add remaining bids that do not have corresponding ascents for _, bid_row in bids_summary.iterrows(): + if db_path: + displayed_grade = get_displayed_grade_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"], grades_dict) + else: + displayed_grade = 'NA' + final_logbook.append({ 'board': board, 'angle': bid_row['angle'], 'climb_name': bid_row['climb_name'], 'date': bid_row['date'], 'logged_grade': 'NA', # We don't have grade information for bids + 'displayed_grade': displayed_grade, 'tries': bid_row['tries'], 'is_mirror': bid_row['is_mirror'], 'is_ascent': False }) # Convert to DataFrame - final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'tries', 'is_mirror', 'is_ascent']) + final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent']) # Ensure all dates are converted to Timestamps final_logbook_df['date'] = pd.to_datetime(final_logbook_df['date']) From a5d4b11c8aac9916306051d3169e9e13aaa83bdf Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Tue, 11 Jun 2024 18:13:53 +0200 Subject: [PATCH 07/15] Full_Logbook CLI integration & a bit of cleaning --- src/boardlib/__main__.py | 77 +++++++++++++++++++------------------- src/boardlib/api/aurora.py | 29 ++++++-------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index 1fe9fe6..2aa6a1d 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -9,8 +9,9 @@ import boardlib.api.moon import boardlib.db.aurora + LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") -BIDS_LOGBOOK_FIELDS = ("uuid", "user_id", "climb_uuid", "climb_name", "angle", "is_mirror", "bid_count", "comment", "climbed_at", "created_at") +FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent") def logbook_entries(board, username, password, grade_type="font", database=None): @@ -27,18 +28,6 @@ def logbook_entries(board, username, password, grade_type="font", database=None) else: raise ValueError(f"Unknown board {board}") -def bids_logbook_entries(board, username, password, db_path=None): - api = ( - boardlib.api.moon - if board.startswith("moon") - else boardlib.api.aurora - if board in boardlib.api.aurora.HOST_BASES - else None - ) - if api: - yield from api.bids_logbook_entries(board, username, password, db_path) - else: - raise ValueError(f"Unknown board {board}") def write_entries(output_file, entries, no_headers=False, fields=LOGBOOK_FIELDS): @@ -62,20 +51,6 @@ def handle_logbook_command(args): sys.stdout.reconfigure(encoding="utf-8") write_entries(sys.stdout, entries, args.no_headers) -def handle_bids_logbook_command(args): - env_var = f"{args.board.upper()}_PASSWORD" - password = os.environ.get(env_var) - if not password: - password = getpass.getpass("Password: ") - entries = bids_logbook_entries(args.board, args.username, password, args.db_path) - - if args.output: - with open(args.output, "w", encoding="utf-8") as output_file: - write_entries(output_file, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) - else: - sys.stdout.reconfigure(encoding="utf-8") - write_entries(sys.stdout, entries, args.no_headers, fields=BIDS_LOGBOOK_FIELDS) - def handle_database_command(args): if not args.database_path.exists(): @@ -89,6 +64,21 @@ def handle_database_command(args): print(f"Synchronized {row_count} rows in {table_name}") +def handle_full_logbook_command(args): + env_var = f"{args.board.upper()}_PASSWORD" + password = os.environ.get(env_var) + if not password: + password = getpass.getpass("Password: ") + entries = boardlib.api.aurora.get_full_logbook_entries(args.board, args.username, password, args.grade_type, args.database) + + if args.output: + with open(args.output, "w", encoding="utf-8") as output_file: + write_entries(output_file, entries.to_dict(orient="records"), args.no_headers, fields=FULL_LOGBOOK_FIELDS) + else: + sys.stdout.reconfigure(encoding="utf-8") + write_entries(sys.stdout, entries.to_dict(orient="records"), args.no_headers, fields=FULL_LOGBOOK_FIELDS) + + def add_database_parser(subparsers): database_parser = subparsers.add_parser( "database", help="Download and sync the database" @@ -143,35 +133,46 @@ def add_logbook_parser(subparsers): ) logbook_parser.set_defaults(func=handle_logbook_command) -def add_bids_logbook_parser(subparsers): - bids_logbook_parser = subparsers.add_parser( - "bids_logbook", help="Download bids logbook entries to CSV" + +def add_full_logbook_parser(subparsers): + full_logbook_parser = subparsers.add_parser( + "full_logbook", help="Download full logbook entries (ascents and bids) to CSV" ) - bids_logbook_parser.add_argument( + full_logbook_parser.add_argument( "board", help="Board name", choices=sorted( boardlib.api.moon.BOARD_IDS.keys() | boardlib.api.aurora.HOST_BASES.keys() ), ) - bids_logbook_parser.add_argument("-u", "--username", help="Username", required=True) - bids_logbook_parser.add_argument("-o", "--output", help="Output file", required=False) - bids_logbook_parser.add_argument( + full_logbook_parser.add_argument("-u", "--username", help="Username", required=True) + full_logbook_parser.add_argument("-o", "--output", help="Output file", required=False) + full_logbook_parser.add_argument( "--no-headers", help="Don't write headers", action="store_true", required=False ) - bids_logbook_parser.add_argument( - "--db_path", + full_logbook_parser.add_argument( + "-g", + "--grade-type", + help="Grade type", + choices=("font", "hueco"), + default="font", + required=False, + ) + full_logbook_parser.add_argument( + "-d", + "--database", help="Path to the local database (optional). Using a local database can significantly speed up the logbook generation. Create a local database with the 'boardlib database' command.", type=pathlib.Path, required=False, ) - bids_logbook_parser.set_defaults(func=handle_bids_logbook_command) + full_logbook_parser.set_defaults(func=handle_full_logbook_command) + def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) add_logbook_parser(subparsers) - add_bids_logbook_parser(subparsers) + add_full_logbook_parser(subparsers) # Add this line add_database_parser(subparsers) args = parser.parse_args() args.func(args) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index d776b34..b2d811d 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -439,7 +439,7 @@ def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict): return grade_info.get("french_name", "NA") return "NA" -def total_logbook_entries(board, username, password, grade_type="font", db_path=None): +def get_full_logbook_entries(board, username, password, grade_type="font", db_path=None): # Get bids and ascents data login_info = login(board, username, password) token = login_info["token"] @@ -553,7 +553,7 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= 'angle': bid_row['angle'], 'climb_name': bid_row['climb_name'], 'date': bid_row['date'], - 'logged_grade': 'NA', # We don't have grade information for bids + 'logged_grade': 'NA', # We don't have logged_grade information for bids 'displayed_grade': displayed_grade, 'tries': bid_row['tries'], 'is_mirror': bid_row['is_mirror'], @@ -561,13 +561,11 @@ def total_logbook_entries(board, username, password, grade_type="font", db_path= }) # Convert to DataFrame - final_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent']) + full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent']) # Ensure all dates are converted to Timestamps - final_logbook_df['date'] = pd.to_datetime(final_logbook_df['date']) + full_logbook_df['date'] = pd.to_datetime(full_logbook_df['date']) - # Sort the DataFrame by date - final_logbook_df = final_logbook_df.sort_values(by='date') # Calculate sessions_count and tries_total def calculate_sessions_count(group): @@ -578,23 +576,20 @@ def calculate_sessions_count(group): group['sessions_count'] = group['date'].dt.date.map(sessions_count_map) return group - final_logbook_df = final_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) + full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) def calculate_tries_total(group): group = group.sort_values(by='date') group['tries_total'] = group['tries'].cumsum() return group - final_logbook_df = final_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_tries_total).reset_index(drop=True) + full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_tries_total).reset_index(drop=True) # Add is_repeat column - final_logbook_df['is_repeat'] = final_logbook_df.duplicated(subset=['climb_name', 'is_mirror', 'angle'], keep='first') + full_logbook_df['is_repeat'] = full_logbook_df.duplicated(subset=['climb_name', 'is_mirror', 'angle'], keep='first') + + # Sort the DataFrame by date + full_logbook_df = full_logbook_df.sort_values(by='date') - return final_logbook_df - -''' -ToDo: -* Get Grade from grade_stats - Either average_grade or displayed grade. - Solution for local db and API calls needed -''' \ No newline at end of file + return full_logbook_df + From 92832bc283da37434779881e5b12135a81c28fad Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Thu, 13 Jun 2024 21:20:54 +0200 Subject: [PATCH 08/15] Missing requirements, code cleaning and refactoring, added comments to logbook --- pyproject.toml | 2 +- requirements.txt | 3 +- src/boardlib/__main__.py | 4 +- src/boardlib/api/aurora.py | 136 ++++++++++++++++++------------------- 4 files changed, 73 insertions(+), 72 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 501b9d9..e988d4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -dependencies = ["bs4", "requests"] +dependencies = ["bs4", "requests", "pandas"] [project.scripts] boardlib = "boardlib.__main__:main" diff --git a/requirements.txt b/requirements.txt index a151126..428288c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ beautifulsoup4 -requests \ No newline at end of file +requests +pandas \ No newline at end of file diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index 2aa6a1d..41bdad5 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -11,7 +11,7 @@ LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") -FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent") +FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment") def logbook_entries(board, username, password, grade_type="font", database=None): @@ -161,7 +161,7 @@ def add_full_logbook_parser(subparsers): full_logbook_parser.add_argument( "-d", "--database", - help="Path to the local database (optional). Using a local database can significantly speed up the logbook generation. Create a local database with the 'boardlib database' command.", + help="Path to the local database (optional). Using a local database will significantly speed up the logbook generation and is required to retrieve 'displayed_grade'. Create a local database with the 'database' command.", type=pathlib.Path, required=False, ) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index b2d811d..b1c1806 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -281,7 +281,7 @@ def logbook_entries(board, username, password, grade_type="font", database=None) yield { "board": board, "angle": raw_entry["angle"], - "name": climb_name if climb_name else "Unknown Climb", + "name": climb_name, "date": datetime.datetime.strptime( raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S" ) @@ -415,7 +415,7 @@ def bids_logbook_entries(board, username, password, db_path=None): "uuid": raw_entry["uuid"], "user_id": raw_entry["user_id"], "climb_uuid": raw_entry["climb_uuid"], - "climb_name": climb_name if climb_name else "Unknown Climb", + "climb_name": climb_name, "angle": raw_entry["angle"], "is_mirror": raw_entry["is_mirror"], "bid_count": raw_entry["bid_count"], @@ -436,29 +436,11 @@ def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict): if row: difficulty_value = round(row[0]) grade_info = grades_dict.get(difficulty_value, {}) - return grade_info.get("french_name", "NA") - return "NA" + return grade_info.get("french_name") + return None -def get_full_logbook_entries(board, username, password, grade_type="font", db_path=None): - # Get bids and ascents data - login_info = login(board, username, password) - token = login_info["token"] - user_id = login_info["user_id"] - - bids_entries = list(bids_logbook_entries(board, username, password, db_path)) - raw_ascents_entries = get_logbook(board, token, user_id) - - # Convert bids entries to DataFrame - bids_df = pd.DataFrame(bids_entries) - bids_df['climbed_at'] = pd.to_datetime(bids_df['climbed_at']) - - # Process raw ascents entries and convert to DataFrame +def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type): ascents_entries = [] - grades = get_grades(board) - - # Convert grades to dictionary for easy lookup - grades_dict = {grade['difficulty']: grade for grade in grades} - for raw_entry in raw_ascents_entries: if not raw_entry["is_listed"]: continue @@ -467,35 +449,36 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa displayed_grade = get_displayed_grade_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"], grades_dict) else: climb_name = get_climb_name(board, raw_entry["climb_uuid"]) - displayed_grade = "NA" + displayed_grade = None - grade_info = grades_dict.get(raw_entry["difficulty"], {}) - logged_grade = grade_info.get("french_name" if grade_type == "font" else "verm_name", "Unknown") + grade_info = grades_dict[raw_entry["difficulty"]] + logged_grade = grade_info["french_name"] if grade_type == "font" else grade_info["verm_name"] ascents_entries.append({ "board": board, "angle": raw_entry["angle"], "climb_uuid": raw_entry["climb_uuid"], - "name": climb_name if climb_name else "Unknown Climb", + "name": climb_name, "date": datetime.datetime.strptime(raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S"), "logged_grade": logged_grade, "displayed_grade": displayed_grade, "tries": raw_entry["attempt_id"] if raw_entry["attempt_id"] else raw_entry["bid_count"], - "is_mirror": raw_entry["is_mirror"] + "is_mirror": raw_entry["is_mirror"], + "comment": raw_entry["comment"] }) - - ascents_df = pd.DataFrame(ascents_entries) - - # Summarize the bids table + return ascents_entries + + +def summarize_bids(bids_df, board): bids_summary = bids_df.groupby(['climb_uuid', 'climb_name', bids_df['climbed_at'].dt.date, 'is_mirror', 'angle']).agg({ 'bid_count': 'sum' }).reset_index().rename(columns={'climbed_at': 'date'}) - - # Create a new column for is_ascent bids_summary['is_ascent'] = False bids_summary['tries'] = bids_summary['bid_count'] - - # Check for ascents and update the logbook + bids_summary['board'] = board # Ensure the 'board' column is included + return bids_summary + +def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict): final_logbook = [] for _, ascent_row in ascents_df.iterrows(): @@ -505,7 +488,6 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa ascent_is_mirror = ascent_row['is_mirror'] ascent_angle = ascent_row['angle'] - # Find corresponding bids entries bid_match = bids_summary[ (bids_summary['climb_uuid'] == ascent_climb_uuid) & (bids_summary['date'] == ascent_date) & @@ -525,9 +507,10 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa 'displayed_grade': ascent_row['displayed_grade'], 'tries': total_tries, 'is_mirror': ascent_row['is_mirror'], - 'is_ascent': True + 'is_ascent': True, + 'comment': ascent_row['comment'] }) - bids_summary = bids_summary.drop(bid_match.index) # Remove matched bids + bids_summary = bids_summary.drop(bid_match.index) else: final_logbook.append({ 'board': ascent_row['board'], @@ -538,58 +521,75 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa 'displayed_grade': ascent_row['displayed_grade'], 'tries': ascent_row['tries'], 'is_mirror': ascent_row['is_mirror'], - 'is_ascent': True + 'is_ascent': True, + 'comment': ascent_row['comment'] }) - - # Add remaining bids that do not have corresponding ascents + for _, bid_row in bids_summary.iterrows(): if db_path: displayed_grade = get_displayed_grade_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"], grades_dict) else: - displayed_grade = 'NA' + displayed_grade = None final_logbook.append({ - 'board': board, + 'board': bid_row['board'], 'angle': bid_row['angle'], 'climb_name': bid_row['climb_name'], 'date': bid_row['date'], - 'logged_grade': 'NA', # We don't have logged_grade information for bids + 'logged_grade': None, 'displayed_grade': displayed_grade, 'tries': bid_row['tries'], 'is_mirror': bid_row['is_mirror'], - 'is_ascent': False + 'is_ascent': False, + 'comment': bid_row.get('comment', None) # Use .get() to safely handle missing 'comment' }) + return final_logbook + + +def calculate_sessions_count(group): + group = group.sort_values(by='date') + unique_dates = group['date'].dt.date.drop_duplicates().reset_index(drop=True) + sessions_count = unique_dates.rank(method='dense').astype(int) + sessions_count_map = dict(zip(unique_dates, sessions_count)) + group['sessions_count'] = group['date'].dt.date.map(sessions_count_map) + return group + +def calculate_tries_total(group): + group = group.sort_values(by='date') + group['tries_total'] = group['tries'].cumsum() + return group + +def get_full_logbook_entries(board, username, password, grade_type="font", db_path=None): + login_info = login(board, username, password) + token = login_info["token"] + user_id = login_info["user_id"] - # Convert to DataFrame - full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent']) + bids_entries = list(bids_logbook_entries(board, username, password, db_path)) + raw_ascents_entries = get_logbook(board, token, user_id) - # Ensure all dates are converted to Timestamps - full_logbook_df['date'] = pd.to_datetime(full_logbook_df['date']) + bids_df = pd.DataFrame(bids_entries) + bids_df['climbed_at'] = pd.to_datetime(bids_df['climbed_at']) - - # Calculate sessions_count and tries_total - def calculate_sessions_count(group): - group = group.sort_values(by='date') - unique_dates = group['date'].dt.date.drop_duplicates().reset_index(drop=True) - sessions_count = unique_dates.rank(method='dense').astype(int) - sessions_count_map = dict(zip(unique_dates, sessions_count)) - group['sessions_count'] = group['date'].dt.date.map(sessions_count_map) - return group + grades = get_grades(board) + grades_dict = {grade['difficulty']: grade for grade in grades} - full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) + ascents_entries = process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type) + ascents_df = pd.DataFrame(ascents_entries) - def calculate_tries_total(group): - group = group.sort_values(by='date') - group['tries_total'] = group['tries'].cumsum() - return group - + bids_summary = summarize_bids(bids_df, board) + + final_logbook = combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict) + + full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent', 'comment']) + full_logbook_df['date'] = pd.to_datetime(full_logbook_df['date']) + + full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_tries_total).reset_index(drop=True) - # Add is_repeat column full_logbook_df['is_repeat'] = full_logbook_df.duplicated(subset=['climb_name', 'is_mirror', 'angle'], keep='first') - - # Sort the DataFrame by date full_logbook_df = full_logbook_df.sort_values(by='date') return full_logbook_df + + From 8c0f916e5496e157115c447b80e0224209b20e71 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Thu, 13 Jun 2024 22:17:38 +0200 Subject: [PATCH 09/15] fixed bug for correct "grade_type" in displayed_grade --- src/boardlib/api/aurora.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index b1c1806..649e16a 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -424,7 +424,7 @@ def bids_logbook_entries(board, username, password, db_path=None): "created_at": raw_entry["created_at"], } -def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict): +def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict, grade_type): conn = sqlite3.connect(database) cursor = conn.cursor() cursor.execute( @@ -436,7 +436,7 @@ def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict): if row: difficulty_value = round(row[0]) grade_info = grades_dict.get(difficulty_value, {}) - return grade_info.get("french_name") + return grade_info.get("french_name" if grade_type == "font" else "verm_name") return None def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type): @@ -446,7 +446,7 @@ def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, continue if db_path: climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) - displayed_grade = get_displayed_grade_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"], grades_dict) + displayed_grade = get_displayed_grade_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"], grades_dict, grade_type) else: climb_name = get_climb_name(board, raw_entry["climb_uuid"]) displayed_grade = None @@ -478,7 +478,7 @@ def summarize_bids(bids_df, board): bids_summary['board'] = board # Ensure the 'board' column is included return bids_summary -def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict): +def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, grade_type): final_logbook = [] for _, ascent_row in ascents_df.iterrows(): @@ -527,7 +527,7 @@ def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict): for _, bid_row in bids_summary.iterrows(): if db_path: - displayed_grade = get_displayed_grade_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"], grades_dict) + displayed_grade = get_displayed_grade_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"], grades_dict, grade_type) else: displayed_grade = None @@ -578,7 +578,7 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa bids_summary = summarize_bids(bids_df, board) - final_logbook = combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict) + final_logbook = combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, grade_type) full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent', 'comment']) full_logbook_df['date'] = pd.to_datetime(full_logbook_df['date']) From d089a01d1aaf455672b97ca50fbaa8f5fa19daae Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Mon, 17 Jun 2024 18:22:34 +0200 Subject: [PATCH 10/15] added numeric difficulty for later use in visualisations. --- src/boardlib/__main__.py | 2 +- src/boardlib/api/aurora.py | 39 +++++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index 41bdad5..a3cd09c 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -11,7 +11,7 @@ LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") -FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment") +FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", 'difficulty', "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment") def logbook_entries(board, username, password, grade_type="font", database=None): diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 649e16a..0eaa2b3 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -399,8 +399,6 @@ def get_bids_logbook(board, token, user_id): return sync_results["PUT"]["bids"] - - def bids_logbook_entries(board, username, password, db_path=None): login_info = login(board, username, password) raw_entries = get_bids_logbook(board, login_info["token"], login_info["user_id"]) @@ -424,7 +422,8 @@ def bids_logbook_entries(board, username, password, db_path=None): "created_at": raw_entry["created_at"], } -def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict, grade_type): + +def get_difficulty_from_db(database, climb_uuid, angle): conn = sqlite3.connect(database) cursor = conn.cursor() cursor.execute( @@ -434,11 +433,15 @@ def get_displayed_grade_from_db(database, climb_uuid, angle, grades_dict, grade_ row = cursor.fetchone() conn.close() if row: - difficulty_value = round(row[0]) - grade_info = grades_dict.get(difficulty_value, {}) - return grade_info.get("french_name" if grade_type == "font" else "verm_name") + return row[0] return None + +def convert_difficulty_to_grade(difficulty, grades_dict, grade_type): + grade_info = grades_dict.get(difficulty, {}) + return grade_info.get("french_name" if grade_type == "font" else "verm_name") + + def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type): ascents_entries = [] for raw_entry in raw_ascents_entries: @@ -446,13 +449,14 @@ def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, continue if db_path: climb_name = get_climb_name_from_db(db_path, raw_entry["climb_uuid"]) - displayed_grade = get_displayed_grade_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"], grades_dict, grade_type) + difficulty = get_difficulty_from_db(db_path, raw_entry["climb_uuid"], raw_entry["angle"]) + displayed_grade = convert_difficulty_to_grade(difficulty, grades_dict, grade_type) else: climb_name = get_climb_name(board, raw_entry["climb_uuid"]) + difficulty = None displayed_grade = None - grade_info = grades_dict[raw_entry["difficulty"]] - logged_grade = grade_info["french_name"] if grade_type == "font" else grade_info["verm_name"] + logged_grade = convert_difficulty_to_grade(raw_entry["difficulty"], grades_dict, grade_type) ascents_entries.append({ "board": board, @@ -461,6 +465,7 @@ def process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, "name": climb_name, "date": datetime.datetime.strptime(raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S"), "logged_grade": logged_grade, + "difficulty": difficulty, "displayed_grade": displayed_grade, "tries": raw_entry["attempt_id"] if raw_entry["attempt_id"] else raw_entry["bid_count"], "is_mirror": raw_entry["is_mirror"], @@ -478,6 +483,7 @@ def summarize_bids(bids_df, board): bids_summary['board'] = board # Ensure the 'board' column is included return bids_summary + def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, grade_type): final_logbook = [] @@ -504,7 +510,8 @@ def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, gra 'climb_name': ascent_row['name'], 'date': ascent_row['date'], 'logged_grade': ascent_row['logged_grade'], - 'displayed_grade': ascent_row['displayed_grade'], + 'displayed_grade': ascent_row.get('displayed_grade', None), + 'difficulty': ascent_row['difficulty'], 'tries': total_tries, 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True, @@ -518,7 +525,8 @@ def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, gra 'climb_name': ascent_row['name'], 'date': ascent_row['date'], 'logged_grade': ascent_row['logged_grade'], - 'displayed_grade': ascent_row['displayed_grade'], + 'displayed_grade': ascent_row.get('displayed_grade', None), + 'difficulty': ascent_row['difficulty'], 'tries': ascent_row['tries'], 'is_mirror': ascent_row['is_mirror'], 'is_ascent': True, @@ -527,9 +535,11 @@ def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, gra for _, bid_row in bids_summary.iterrows(): if db_path: - displayed_grade = get_displayed_grade_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"], grades_dict, grade_type) + difficulty = get_difficulty_from_db(db_path, bid_row["climb_uuid"], bid_row["angle"]) + displayed_grade = convert_difficulty_to_grade(difficulty, grades_dict, grade_type) else: displayed_grade = None + difficulty = None final_logbook.append({ 'board': bid_row['board'], @@ -538,6 +548,7 @@ def combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, gra 'date': bid_row['date'], 'logged_grade': None, 'displayed_grade': displayed_grade, + 'difficulty': difficulty, 'tries': bid_row['tries'], 'is_mirror': bid_row['is_mirror'], 'is_ascent': False, @@ -554,11 +565,13 @@ def calculate_sessions_count(group): group['sessions_count'] = group['date'].dt.date.map(sessions_count_map) return group + def calculate_tries_total(group): group = group.sort_values(by='date') group['tries_total'] = group['tries'].cumsum() return group + def get_full_logbook_entries(board, username, password, grade_type="font", db_path=None): login_info = login(board, username, password) token = login_info["token"] @@ -580,7 +593,7 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa final_logbook = combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, grade_type) - full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'tries', 'is_mirror', 'is_ascent', 'comment']) + full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'difficulty', 'tries', 'is_mirror', 'is_ascent', 'comment']) full_logbook_df['date'] = pd.to_datetime(full_logbook_df['date']) full_logbook_df = full_logbook_df.groupby(['climb_name', 'is_mirror', 'angle']).apply(calculate_sessions_count).reset_index(drop=True) From 8a504a5176b7909c901091482cdfd180efb66cdb Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Tue, 18 Jun 2024 11:53:16 +0200 Subject: [PATCH 11/15] Deal with empty ascent and bid tables --- src/boardlib/api/aurora.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 0eaa2b3..35f21e8 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -73,7 +73,7 @@ def explore(board, token): def get_logbook(board, token, user_id): sync_results = user_sync(board, token, user_id, tables=["ascents"]) - return sync_results["PUT"]["ascents"] + return sync_results["PUT"].get("ascents", []) def get_grades(board): @@ -396,7 +396,7 @@ def save_climb( def get_bids_logbook(board, token, user_id): sync_results = user_sync(board, token, user_id, tables=["bids"]) - return sync_results["PUT"]["bids"] + return sync_results["PUT"].get("bids", []) def bids_logbook_entries(board, username, password, db_path=None): @@ -580,17 +580,24 @@ def get_full_logbook_entries(board, username, password, grade_type="font", db_pa bids_entries = list(bids_logbook_entries(board, username, password, db_path)) raw_ascents_entries = get_logbook(board, token, user_id) - bids_df = pd.DataFrame(bids_entries) - bids_df['climbed_at'] = pd.to_datetime(bids_df['climbed_at']) - - grades = get_grades(board) - grades_dict = {grade['difficulty']: grade for grade in grades} - - ascents_entries = process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type) - ascents_df = pd.DataFrame(ascents_entries) - - bids_summary = summarize_bids(bids_df, board) + if not bids_entries and not raw_ascents_entries: + return pd.DataFrame(columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'difficulty', 'tries', 'is_mirror', 'is_ascent', 'comment']) + + if bids_entries: + bids_df = pd.DataFrame(bids_entries) + bids_df['climbed_at'] = pd.to_datetime(bids_df['climbed_at']) + bids_summary = summarize_bids(bids_df, board) + else: + bids_summary = pd.DataFrame(columns=['climb_uuid', 'climb_name', 'date', 'is_mirror', 'angle', 'tries', 'board']) + if raw_ascents_entries: + grades = get_grades(board) + grades_dict = {grade['difficulty']: grade for grade in grades} + ascents_entries = process_raw_ascent_entries(raw_ascents_entries, board, db_path, grades_dict, grade_type) + ascents_df = pd.DataFrame(ascents_entries) + else: + ascents_df = pd.DataFrame(columns=['board', 'angle', 'climb_uuid', 'name', 'date', 'logged_grade', 'difficulty', 'displayed_grade', 'tries', 'is_mirror', 'comment']) + final_logbook = combine_ascents_and_bids(ascents_df, bids_summary, db_path, grades_dict, grade_type) full_logbook_df = pd.DataFrame(final_logbook, columns=['board', 'angle', 'climb_name', 'date', 'logged_grade', 'displayed_grade', 'difficulty', 'tries', 'is_mirror', 'is_ascent', 'comment']) From 1dc9d81b80a4ac0c9c17378ff754358c73aca713 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Tue, 18 Jun 2024 16:56:18 +0200 Subject: [PATCH 12/15] updated tests --- tests/boardlib/api/test_aurora.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/boardlib/api/test_aurora.py b/tests/boardlib/api/test_aurora.py index 7c5749d..ac20093 100644 --- a/tests/boardlib/api/test_aurora.py +++ b/tests/boardlib/api/test_aurora.py @@ -41,14 +41,14 @@ def test_explore_failure(self, mock_get): boardlib.api.aurora.explore("aurora", "test") @unittest.mock.patch( - "requests.get", - side_effect=get_mock_request(json_data={"logbook": []}), + "requests.post", + side_effect=get_mock_request(json_data={"PUT": {"ascents": []}}), ) - def test_get_logbook(self, mock_get): + def test_get_logbook(self, mock_post): self.assertEqual(boardlib.api.aurora.get_logbook("aurora", "test", "test"), []) @unittest.mock.patch( - "requests.get", + "requests.post", side_effect=get_mock_request(status_code=requests.codes.bad_request), ) def test_get_logbook_failure(self, mock_get): @@ -128,7 +128,7 @@ def test_get_climb_name_failure(self, mock_get): ) def test_sync(self, mock_get): self.assertEqual( - boardlib.api.aurora.sync("aurora", "test", "test"), "test_sync" + boardlib.api.aurora.user_sync("aurora", "test", "test"), "test_sync" ) @unittest.mock.patch( @@ -137,7 +137,7 @@ def test_sync(self, mock_get): ) def test_sync_failure(self, mock_get): with self.assertRaises(requests.exceptions.HTTPError): - boardlib.api.aurora.sync("aurora", "test", "test") + boardlib.api.aurora.user_sync("aurora", "test", "test") @unittest.mock.patch( "boardlib.api.aurora.login", @@ -158,6 +158,9 @@ def test_sync_failure(self, mock_get): "attempt_id": 0, "bid_count": 5, "angle": 30, + "is_listed": True, + "is_mirror": False, + "comment": "Test comment", } ], ) @@ -173,8 +176,9 @@ def test_logbook_entries(self, mock_login, mock_get_climb_name, mock_get_logbook "angle": 30, "name": "test_climb_name", "date": "2021-09-30", - "grade": "5C", + "grade": "6a", "tries": 5, + "is_mirror": False, }, ) From f8890db8513758ce6ea281d85f8aed11c7afb8fc Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Wed, 19 Jun 2024 15:11:07 +0200 Subject: [PATCH 13/15] removed failing test for old logbook function. --- tests/boardlib/api/test_aurora.py | 42 ------------------------------- 1 file changed, 42 deletions(-) diff --git a/tests/boardlib/api/test_aurora.py b/tests/boardlib/api/test_aurora.py index ac20093..19a9cff 100644 --- a/tests/boardlib/api/test_aurora.py +++ b/tests/boardlib/api/test_aurora.py @@ -139,48 +139,6 @@ def test_sync_failure(self, mock_get): with self.assertRaises(requests.exceptions.HTTPError): boardlib.api.aurora.user_sync("aurora", "test", "test") - @unittest.mock.patch( - "boardlib.api.aurora.login", - side_effect=lambda *args, **kwargs: { - "token": "test_token", - "user_id": "test_user_id", - }, - ) - @unittest.mock.patch( - "boardlib.api.aurora.get_logbook", - side_effect=lambda *args, **kwargs: [ - { - "climb_uuid": "test_climb_id", - "climbed_at": "2021-09-30 20:31:48", - "grade": "test_grade", - "tries": "test_tries", - "difficulty": 15, - "attempt_id": 0, - "bid_count": 5, - "angle": 30, - "is_listed": True, - "is_mirror": False, - "comment": "Test comment", - } - ], - ) - @unittest.mock.patch( - "boardlib.api.aurora.get_climb_name", - side_effect=lambda *args, **kwargs: "test_climb_name", - ) - def test_logbook_entries(self, mock_login, mock_get_climb_name, mock_get_logbook): - self.assertEqual( - next(boardlib.api.aurora.logbook_entries("aurora", "test", "test")), - { - "board": "aurora", - "angle": 30, - "name": "test_climb_name", - "date": "2021-09-30", - "grade": "6a", - "tries": 5, - "is_mirror": False, - }, - ) @unittest.mock.patch( "boardlib.api.aurora.get_gyms", From 75f9f98f533cbc851e96ad2ab2c8577b38861836 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Wed, 19 Jun 2024 16:11:54 +0200 Subject: [PATCH 14/15] Removed "old" logbook function, updated readme.md, fixed bug in convert_difficulty_to_grade --- README.md | 18 +++++----- src/boardlib/__main__.py | 67 +++++--------------------------------- src/boardlib/api/aurora.py | 35 ++------------------ 3 files changed, 19 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 5ad80f0..f6cfb45 100644 --- a/README.md +++ b/README.md @@ -26,28 +26,28 @@ All [Aurora Climbing](https://auroraclimbing.com/) based boards (Kilter, Tension To download your logbook entries for a given board: -`boardlib logbook --username= --output=.csv --grade-type="hueco"` +`boardlib logbook --username= --output=.csv --grade-type="hueco" --database=` This outputs a CSV file with the following fields: ```json -["board", "angle", "name", "date", "grade", "tries", "is_mirror"] +["board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "difficulty", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment"] ``` For example, the command -`boardlib moon2017 --username="Luke EF" --output="moon2017.csv" --grade-type="hueco"` +`boardlib tension --username="Luke EF" --output="tension.csv" --grade-type="hueco" --database="tension.db"` -would output a file named `moon2017.csv` with the following contents: +would output a file named `tension.csv` with the following contents: ``` -board,angle,name,date,grade,tries, is_mirror -moon2017,40,C3PO,2021-07-13,V5,1, False -moon2017,40,LITTLE BLACK SUBMARINE,2021-07-13,V5,2, False -moon2017,40,MOUNTAIN GOAT HARD,2021-07-13,V5,1, False +board,angle,climb_name,date,logged_grade,displayed_grade,difficulty,tries,is_mirror,sessions_count,tries_total,is_repeat,is_ascent,comment +tension,40,trash bag better,2024-06-17 16:21:23,V3,V3,16.0,3,False,1,3,False,True, +tension,40,Bumble,2024-06-17 16:28:23,V3,V3,16.0,1,True,1,1,False,True, +tension,40,sender2,2024-06-17 16:38:06,V5,V5,20.0,2,False,1,2,False,True, ... ``` - +When no local database is provided, displayed_grade and difficulty remain empty. See `boardlib --help` for a full list of supported board names and feature flags. #### Supported Boards 🛹 diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index a3cd09c..72820b4 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -10,8 +10,7 @@ import boardlib.db.aurora -LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") -FULL_LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", 'difficulty', "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment") +LOGBOOK_FIELDS = ("board", "angle", "climb_name", "date", "logged_grade", "displayed_grade", "difficulty", "tries", "is_mirror", "sessions_count", "tries_total", "is_repeat", "is_ascent", "comment") def logbook_entries(board, username, password, grade_type="font", database=None): @@ -37,21 +36,6 @@ def write_entries(output_file, entries, no_headers=False, fields=LOGBOOK_FIELDS) writer.writerows(entries) -def handle_logbook_command(args): - env_var = f"{args.board.upper()}_PASSWORD" - password = os.environ.get(env_var) - if not password: - password = getpass.getpass("Password: ") - entries = logbook_entries(args.board, args.username, password, args.grade_type, args.database) - - if args.output: - with open(args.output, "w", encoding="utf-8") as output_file: - write_entries(output_file, entries, args.no_headers) - else: - sys.stdout.reconfigure(encoding="utf-8") - write_entries(sys.stdout, entries, args.no_headers) - - def handle_database_command(args): if not args.database_path.exists(): args.database_path.parent.mkdir(parents=True, exist_ok=True) @@ -64,19 +48,19 @@ def handle_database_command(args): print(f"Synchronized {row_count} rows in {table_name}") -def handle_full_logbook_command(args): +def handle_logbook_command(args): env_var = f"{args.board.upper()}_PASSWORD" password = os.environ.get(env_var) if not password: password = getpass.getpass("Password: ") - entries = boardlib.api.aurora.get_full_logbook_entries(args.board, args.username, password, args.grade_type, args.database) + entries = boardlib.api.aurora.logbook_entries(args.board, args.username, password, args.grade_type, args.database) if args.output: with open(args.output, "w", encoding="utf-8") as output_file: - write_entries(output_file, entries.to_dict(orient="records"), args.no_headers, fields=FULL_LOGBOOK_FIELDS) + write_entries(output_file, entries.to_dict(orient="records"), args.no_headers, fields=LOGBOOK_FIELDS) else: sys.stdout.reconfigure(encoding="utf-8") - write_entries(sys.stdout, entries.to_dict(orient="records"), args.no_headers, fields=FULL_LOGBOOK_FIELDS) + write_entries(sys.stdout, entries.to_dict(orient="records"), args.no_headers, fields=LOGBOOK_FIELDS) def add_database_parser(subparsers): @@ -99,10 +83,10 @@ def add_database_parser(subparsers): ) database_parser.set_defaults(func=handle_database_command) - + def add_logbook_parser(subparsers): logbook_parser = subparsers.add_parser( - "logbook", help="Download logbook entries to CSV" + "logbook", help="Download full logbook entries (ascents and bids) to CSV" ) logbook_parser.add_argument( "board", @@ -127,52 +111,17 @@ def add_logbook_parser(subparsers): logbook_parser.add_argument( "-d", "--database", - help="Path to the local database (optional). Using a local database can significantly speed up the logbook generation. Create a local database with the 'boardlib database' command.", + help="Path to the local database (optional). Using a local database will significantly speed up the logbook generation and is required to retrieve 'displayed_grade' and 'difficulty'. Create a local database with the 'database' command.", type=pathlib.Path, required=False, ) logbook_parser.set_defaults(func=handle_logbook_command) - - -def add_full_logbook_parser(subparsers): - full_logbook_parser = subparsers.add_parser( - "full_logbook", help="Download full logbook entries (ascents and bids) to CSV" - ) - full_logbook_parser.add_argument( - "board", - help="Board name", - choices=sorted( - boardlib.api.moon.BOARD_IDS.keys() | boardlib.api.aurora.HOST_BASES.keys() - ), - ) - full_logbook_parser.add_argument("-u", "--username", help="Username", required=True) - full_logbook_parser.add_argument("-o", "--output", help="Output file", required=False) - full_logbook_parser.add_argument( - "--no-headers", help="Don't write headers", action="store_true", required=False - ) - full_logbook_parser.add_argument( - "-g", - "--grade-type", - help="Grade type", - choices=("font", "hueco"), - default="font", - required=False, - ) - full_logbook_parser.add_argument( - "-d", - "--database", - help="Path to the local database (optional). Using a local database will significantly speed up the logbook generation and is required to retrieve 'displayed_grade'. Create a local database with the 'database' command.", - type=pathlib.Path, - required=False, - ) - full_logbook_parser.set_defaults(func=handle_full_logbook_command) def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) add_logbook_parser(subparsers) - add_full_logbook_parser(subparsers) # Add this line add_database_parser(subparsers) args = parser.parse_args() args.func(args) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 35f21e8..2bee835 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -266,37 +266,6 @@ def shared_sync( return response.json() -def logbook_entries(board, username, password, grade_type="font", database=None): - login_info = login(board, username, password) - raw_entries = get_logbook(board, login_info["token"], login_info["user_id"]) - grades = get_grades(board) - for raw_entry in raw_entries: - if not raw_entry["is_listed"]: - continue - attempt_id = raw_entry["attempt_id"] - if database: - climb_name = get_climb_name_from_db(database, raw_entry["climb_uuid"]) - else: - climb_name = get_climb_name(board, raw_entry["climb_uuid"]) - yield { - "board": board, - "angle": raw_entry["angle"], - "name": climb_name, - "date": datetime.datetime.strptime( - raw_entry["climbed_at"], "%Y-%m-%d %H:%M:%S" - ) - .date() - .isoformat(), - "grade": ( - grades[raw_entry["difficulty"]]["french_name"] - if grade_type == "font" - else grades[raw_entry["difficulty"]]["verm_name"] - ), - "tries": attempt_id if attempt_id else raw_entry["bid_count"], - "is_mirror": raw_entry["is_mirror"], - } - - def gym_boards(board): for gym in get_gyms(board)["gyms"]: yield { @@ -438,7 +407,7 @@ def get_difficulty_from_db(database, climb_uuid, angle): def convert_difficulty_to_grade(difficulty, grades_dict, grade_type): - grade_info = grades_dict.get(difficulty, {}) + grade_info = grades_dict.get(round(difficulty), {}) return grade_info.get("french_name" if grade_type == "font" else "verm_name") @@ -572,7 +541,7 @@ def calculate_tries_total(group): return group -def get_full_logbook_entries(board, username, password, grade_type="font", db_path=None): +def logbook_entries(board, username, password, grade_type="font", db_path=None): login_info = login(board, username, password) token = login_info["token"] user_id = login_info["user_id"] From 87f338a2a0d3965a2d16abb20bcf967bd8cba886 Mon Sep 17 00:00:00 2001 From: Philipp Tschaikner Date: Thu, 20 Jun 2024 15:04:47 +0200 Subject: [PATCH 15/15] Handle None value for difficulty when fetching grade info Updated the retrieval of grade_info to handle None values for the difficulty. This ensures that if difficulty is None, the corresponding value from the grades_dict is retrieved using None as the key. --- src/boardlib/api/aurora.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boardlib/api/aurora.py b/src/boardlib/api/aurora.py index 2bee835..c001ab5 100644 --- a/src/boardlib/api/aurora.py +++ b/src/boardlib/api/aurora.py @@ -407,7 +407,7 @@ def get_difficulty_from_db(database, climb_uuid, angle): def convert_difficulty_to_grade(difficulty, grades_dict, grade_type): - grade_info = grades_dict.get(round(difficulty), {}) + grade_info = grades_dict.get(round(difficulty) if difficulty is not None else None, {}) return grade_info.get("french_name" if grade_type == "font" else "verm_name")