diff --git a/clippercard/main.py b/clippercard/main.py index 13fe3cc..0b4cd8b 100644 --- a/clippercard/main.py +++ b/clippercard/main.py @@ -99,6 +99,9 @@ def _build_parser(): summary = subparsers.add_parser("summary", help="Show account summary") summary.add_argument("--debug", action="store_true", help="Enable debug logging") + summary.add_argument( + "--show-private", action="store_true", help="Show private information like card numbers (use with caution)" + ) auth_group = summary.add_argument_group("authentication") auth_group.add_argument( @@ -139,11 +142,14 @@ def main(): if args.command == "summary": if session.reused_cookies: print(f"Reusing saved cookies from {session.cookie_jar_path}") - print(clippercard.porcelain.tabular_output(session.profile_info, session.cards)) + print( + clippercard.porcelain.tabular_output( + session.profile_info, session.cards, show_private=args.show_private + ) + ) except (clippercard.client.ClipperCardError, ClipperCardCommandError, FileNotFoundError) as e: sys.exit(str(e)) if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) main() diff --git a/clippercard/parser.py b/clippercard/parser.py index 05245dc..75c6f65 100644 --- a/clippercard/parser.py +++ b/clippercard/parser.py @@ -20,10 +20,12 @@ """ import collections +from datetime import datetime import itertools import json import logging import re +from warnings import deprecated import bs4 @@ -158,6 +160,9 @@ def parse_login_form_fields(login_page_content): return fields +# Is this method still useful? Main client code now uses parse_dashboard_cards, +# while this method is 5 years out of date per git blame. +@deprecated("Only used in tests") def parse_cards(account_page_soup): """Parse the list of Clipper Cards registered to the profile""" card_info_divs = account_page_soup.find_all( @@ -354,6 +359,15 @@ def parse_dashboard_cards(dashboard_html_content): if balance is not None: products.append(CardProduct(name="BART", value=_cents_to_dollars(balance))) + passList = account.get("passList", []) + for pass_info in passList: + logger.debug("Parsing pass info: %s", pass_info.keys()) + pass_name = pass_info.get("passDescription", "Unknown Pass") + # NB: expirationDateTime is likely card validity, not pass expiration + # empirically it is ~100 years after pass activation + expiration = datetime.fromisoformat(pass_info.get("endDateTime", "Unknown Expiration")).date() + products.append(CardProduct(name="Pass", value=f"{pass_name}\n - Expires {expiration}")) + # Card features (currently empty, can extend later) features = [] diff --git a/clippercard/porcelain.py b/clippercard/porcelain.py index 3a493ad..8896406 100644 --- a/clippercard/porcelain.py +++ b/clippercard/porcelain.py @@ -20,6 +20,7 @@ """ from io import StringIO +import re from rich import box from rich.console import Console @@ -41,7 +42,27 @@ def _render_table(table): return buffer.getvalue().rstrip() -def tabular_output(user_profile, cards): +def _redact_private_info(label, value): + match label.lower(): + case "name": + return " ".join([part[:2] + "***" for part in value.split()]) + case "email": + local, domain = value.split("@") + return local[0] + "***@" + domain + case "mailing_address": + return "***" + case "phone": + re_match = re.match(r"(\+?\d{1,3})?([-.\s]?)[0-9-.\s]+(\d{2}-?\d{2})", value) + if re_match: + groups = re_match.groups() + redacted = f"{groups[0] + groups[1] if groups[0] else ''}***-***-{groups[2]}" + return redacted + case "serial_number": + return ("*" * (len(value) - 4)) + value[-4:] + return value + + +def tabular_output(user_profile, cards, show_private=False): """ Pretty prints a user profile and its associated cards and products. """ @@ -54,6 +75,8 @@ def tabular_output(user_profile, cards): for label, value in user_profile._asdict().items(): if value in (None, ""): continue + if not show_private: + value = _redact_private_info(label, value) profile_table.add_row(Text(str(label)), Text(str(value))) if profile_table.row_count: output_parts.append(_render_table(profile_table)) @@ -67,10 +90,13 @@ def tabular_output(user_profile, cards): card_table.add_column("Status", justify="left", no_wrap=True) card_table.add_column("Products", justify="left", no_wrap=True) for i, card in enumerate(cards, 1): + serial = card.serial_number + if not show_private: + serial = _redact_private_info("serial_number", serial) card_table.add_row( str(i), Text(str(card.nickname)), - Text(str(card.serial_number)), + Text(str(serial)), Text(str(card.type)), Text(str(card.status)), Text("\n".join(str(_) for _ in card.products + card.features)), diff --git a/tests/data/dashboard.html b/tests/data/dashboard.html index a6b721b..55f7edb 100644 --- a/tests/data/dashboard.html +++ b/tests/data/dashboard.html @@ -579,7 +579,63 @@