From 02025a1caf2645421b60bead9f74f46c7f482c2a Mon Sep 17 00:00:00 2001 From: Luke Emery-Fertitta Date: Sun, 10 Dec 2023 09:59:43 -0800 Subject: [PATCH] Documet and release database command --- README.md | 22 +++++++-- pyproject.toml | 2 +- src/boardlib/__main__.py | 96 +++++++++++++++++++++++++++------------ src/boardlib/db/aurora.py | 5 ++ 4 files changed, 93 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8b2c1a7..7e6b975 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BoardLib 🧗‍♀️ -Utilities for interacting with (undocumented) climbing board APIs. Currently, the primary use case for this library is to retreive your logbook entries across multiple climbing boards in a unified format. There are also some APIs which could also be used for retrieving climb data, rankings, etc. Contributions are welcome for filling out additional API calls and use cases. +Utilities for interacting with (undocumented) climbing board APIs. ## Installation 🦺 @@ -8,9 +8,25 @@ Utilities for interacting with (undocumented) climbing board APIs. Currently, th ## Usage ⌨️ +### Databases 💾 + +To download the climb database for a given board: + +`boardlib database ` + +This will download a [sqlite](https://www.sqlite.org/index.html) database file to the given path and synchronize it with the latest available data. The database contains all of the publicly available climb data. Only the synchronization will be attempted if there is already a database at the given path, + +NOTE: The Moonboard is not currently supported for the database command. Contributions are welcome. + +#### Supported Boards 🛹 + +All [Aurora Climbing](https://auroraclimbing.com/) based boards (Kilter, Tension, etc.). + +### Logbooks 📚 + To download your logbook entries for a given board: -`boardlib --username= --output=.csv --grade-type="hueco"` +`boardlib logbook --username= --output=.csv --grade-type="hueco"` This outputs a CSV file with the following fields: @@ -34,7 +50,7 @@ moon2017,40,MOUNTAIN GOAT HARD,2021-07-13,V5,1, False See `boardlib --help` for a full list of supported board names and feature flags. -## Supported Boards 🛹 +#### Supported Boards 🛹 Currently all [Aurora Climbing](https://auroraclimbing.com/) based boards (Kilter, Tension, etc.) and all variations of the [Moonboard](https://moonboard.com/) should be supported. diff --git a/pyproject.toml b/pyproject.toml index 65ecbb9..3605762 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "boardlib" -version = "0.1.0" +version = "0.2.0" authors = [{ name = "Luke Emery-Fertitta", email = "lemeryfertitta@gmail.com" }] description = "Utilities for interacting with climbing board APIs" readme = "README.md" diff --git a/src/boardlib/__main__.py b/src/boardlib/__main__.py index 6e7f4f5..d446402 100644 --- a/src/boardlib/__main__.py +++ b/src/boardlib/__main__.py @@ -2,21 +2,15 @@ import csv import getpass import os +import pathlib import sys import boardlib.api.aurora import boardlib.api.moon +import boardlib.db.aurora -LOGBOOK_FIELDS = ( - "board", - "angle", - "name", - "date", - "grade", - "tries", - "is_mirror" -) +LOGBOOK_FIELDS = ("board", "angle", "name", "date", "grade", "tries", "is_mirror") def logbook_entries(board, username, password, grade_type="font"): @@ -42,21 +36,71 @@ def write_entries(output_file, entries, no_headers=False): writer.writerows(entries) -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( +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) + + 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) + print(f"Downloading database to {args.database_path}") + boardlib.db.aurora.download_database(args.board, args.database_path) + + print(f"Synchronizing database at {args.database_path}") + row_counts = boardlib.db.aurora.sync_shared_tables(args.board, args.database_path) + for table_name, row_count in row_counts.items(): + print(f"Synchronized {row_count} rows in {table_name}") + + +def add_database_parser(subparsers): + database_parser = subparsers.add_parser( + "database", help="Download and sync the database" + ) + database_parser.add_argument( + "board", + help="Board name", + choices=sorted(boardlib.api.aurora.HOST_BASES.keys()), + ) + database_parser.add_argument( + "database_path", + help=( + "Path for the database file. " + "If the file does not exist, the database will be downloaded to the given path and synchronized. " + "If it does exist, the database will just be synchronized", + ), + type=pathlib.Path, + ) + 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_parser.add_argument( "board", help="Board name", choices=sorted( boardlib.api.moon.BOARD_IDS.keys() | boardlib.api.aurora.HOST_BASES.keys() ), ) - parser.add_argument("-u", "--username", help="Username", required=True) - parser.add_argument("-o", "--output", help="Output file", required=False) - parser.add_argument( + logbook_parser.add_argument("-u", "--username", help="Username", required=True) + logbook_parser.add_argument("-o", "--output", help="Output file", required=False) + logbook_parser.add_argument( "--no-headers", help="Don't write headers", action="store_true", required=False ) - parser.add_argument( + logbook_parser.add_argument( "-g", "--grade-type", help="Grade type", @@ -64,20 +108,16 @@ def main(): default="font", required=False, ) - args = parser.parse_args() + logbook_parser.set_defaults(func=handle_logbook_command) - 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) - 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 main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + add_logbook_parser(subparsers) + add_database_parser(subparsers) + args = parser.parse_args() + args.func(args) if __name__ == "__main__": diff --git a/src/boardlib/db/aurora.py b/src/boardlib/db/aurora.py index f4fa004..199c5c1 100644 --- a/src/boardlib/db/aurora.py +++ b/src/boardlib/db/aurora.py @@ -49,6 +49,7 @@ def sync_shared_tables(board, database): :param board: The board to sync the database for. :param database: The sqlite3 database file to sync. + :returns: a mapping of synchronized table names to counts of inserted/updated/deleted rows. """ with sqlite3.connect(database) as connection: result = connection.execute( @@ -61,10 +62,14 @@ def sync_shared_tables(board, database): shared_sync_result = boardlib.api.aurora.shared_sync( board, tables=boardlib.api.aurora.SHARED_TABLES, shared_syncs=shared_syncs ) + row_counts = {} for table_name, rows in shared_sync_result["PUT"].items(): ROW_INSERTERS.get(table_name, insert_rows_default)( connection, table_name, rows ) + row_counts[table_name] = len(rows) + + return row_counts def insert_rows_default(connection, table_name, rows):