From c6bc4a75d8269fad9cffaeb91e0472d26946b4c5 Mon Sep 17 00:00:00 2001 From: Shiv 70 Date: Wed, 21 Oct 2026 17:15:39 +0530 Subject: [PATCH 1/3] Add Base64 encode and decode commands --- fixit/base64utils.py | 20 ++++++++++++++++++++ fixit/cli.py | 25 ++++++++++++++++++++++++- tests/test_base64.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 fixit/base64utils.py create mode 100644 tests/test_base64.py diff --git a/fixit/base64utils.py b/fixit/base64utils.py new file mode 100644 index 0000000..1880528 --- /dev/null +++ b/fixit/base64utils.py @@ -0,0 +1,20 @@ +"""Base64 text encoding and decoding utilities.""" + +import base64 +import binascii + + +def encode_text(text: str) -> str: + """Encode UTF-8 text as a Base64 string.""" + encoded = base64.b64encode(text.encode("utf-8")) + return encoded.decode("ascii") + + +def decode_text(text: str) -> str: + """Decode a Base64 string into UTF-8 text.""" + try: + encoded = text.encode("ascii") + decoded = base64.b64decode(encoded, validate=True) + return decoded.decode("utf-8") + except (binascii.Error, UnicodeEncodeError, UnicodeDecodeError) as exc: + raise ValueError("Invalid Base64 input.") from exc diff --git a/fixit/cli.py b/fixit/cli.py index b5ecd6d..aa83092 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -3,7 +3,15 @@ import sys import uuid -from fixit import __version__, convert, jsonfmt, password, textutils, wordcount +from fixit import ( + __version__, + base64utils, + convert, + jsonfmt, + password, + textutils, + wordcount, +) CONVERSIONS = { "c2f": convert.celsius_to_fahrenheit, @@ -40,6 +48,16 @@ def build_parser() -> argparse.ArgumentParser: rw = sub.add_parser("reverse", help="reverse the words in a sentence") rw.add_argument("text") + + b64 = sub.add_parser("base64", help="encode or decode Base64 text") + b64_sub = b64.add_subparsers(dest="base64_action", required=True) + + b64_encode = b64_sub.add_parser("encode", help="encode text as Base64") + b64_encode.add_argument("text") + + b64_decode = b64_sub.add_parser("decode", help="decode Base64 text") + b64_decode.add_argument("text") + return parser @@ -63,6 +81,11 @@ def main(argv=None) -> int: print(textutils.is_palindrome(args.text)) elif args.command == "reverse": print(textutils.reverse_words(args.text)) + elif args.command == "base64": + if args.base64_action == "encode": + print(base64utils.encode_text(args.text)) + else: + print(base64utils.decode_text(args.text)) return 0 except ValueError as e: print(str(e), file=sys.stderr) diff --git a/tests/test_base64.py b/tests/test_base64.py new file mode 100644 index 0000000..958d87a --- /dev/null +++ b/tests/test_base64.py @@ -0,0 +1,43 @@ +import pytest + +from fixit import base64utils, cli + + +def test_encode_text(): + assert base64utils.encode_text("hello") == "aGVsbG8=" + + +def test_decode_text(): + assert base64utils.decode_text("aGVsbG8=") == "hello" + + +def test_encode_unicode_text(): + text = "\u0928\u092e\u0938\u094d\u0924\u0947" + assert base64utils.decode_text(base64utils.encode_text(text)) == text + + +def test_empty_text(): + assert base64utils.encode_text("") == "" + assert base64utils.decode_text("") == "" + + +@pytest.mark.parametrize("value", ["%%%", "aGVsbG8", "////"]) +def test_invalid_base64_or_non_utf8_text(value): + with pytest.raises(ValueError, match="Invalid Base64 input"): + base64utils.decode_text(value) + + +def test_cli_encode(capsys): + assert cli.main(["base64", "encode", "hello"]) == 0 + assert capsys.readouterr().out == "aGVsbG8=\n" + + +def test_cli_decode(capsys): + assert cli.main(["base64", "decode", "aGVsbG8="]) == 0 + assert capsys.readouterr().out == "hello\n" + + +def test_cli_invalid_base64(capsys): + assert cli.main(["base64", "decode", "%%%"]) == 1 + captured = capsys.readouterr() + assert "Invalid Base64 input" in captured.err From 90af268a9dcda8161ed7390e2cd445924710da76 Mon Sep 17 00:00:00 2001 From: Shiv 70 Date: Fri, 23 Oct 2026 08:43:33 +0530 Subject: [PATCH 2/3] Restore command dispatch table in CLI --- fixit/cli.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/fixit/cli.py b/fixit/cli.py index 870bb06..80b2d22 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -131,6 +131,7 @@ def build_parser() -> argparse.ArgumentParser: return parser + def main(argv=None) -> int: """Run a fixit command and return its exit status. @@ -139,28 +140,13 @@ def main(argv=None) -> int: """ try: args = build_parser().parse_args(argv) - if args.command == "wc": - run_wc(args) - elif args.command == "convert": - print(CONVERSIONS[args.kind](args.value)) - elif args.command == "password": - print(password.generate(args.length, use_symbols=args.symbols)) - elif args.command == "uuid": - for _ in range(args.count): - print(uuid.uuid4()) - elif args.command == "json": - print(jsonfmt.format_json(args.raw)) - elif args.command == "palindrome": - print(textutils.is_palindrome(args.text)) - elif args.command == "reverse": - print(textutils.reverse_words(args.text)) - elif args.command == "base64": - run_base64(args) + COMMAND_TABLE[args.command](args) return 0 except ValueError as e: print(str(e), file=sys.stderr) return 1 + if __name__ == "__main__": sys.exit(main()) From c0c29cd3e44237ac78848463c16e84a4aec3474d Mon Sep 17 00:00:00 2001 From: Shiv 70 Date: Fri, 23 Oct 2026 08:48:42 +0530 Subject: [PATCH 3/3] Merge upstream main and preserve Base64 commands --- fixit/cli.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/fixit/cli.py b/fixit/cli.py index 80b2d22..7a36396 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -1,3 +1,4 @@ + """Command line entry point: `fixit `.""" import argparse import sys @@ -14,6 +15,7 @@ wordcount, ) + CONVERSIONS = { "c2f": convert.celsius_to_fahrenheit, "f2c": convert.fahrenheit_to_celsius, @@ -21,6 +23,7 @@ "mi2km": convert.miles_to_km, } + def run_wc(args): if args.file is not None: try: @@ -38,6 +41,7 @@ def run_wc(args): f"chars={wordcount.count_chars(text)}" ) + def run_convert(args): try: result = CONVERSIONS[args.kind](args.value) @@ -45,31 +49,42 @@ def run_convert(args): except KeyError: raise ValueError(f"Error: unknown conversion '{args.kind}'") + def run_password(args): print(password.generate(args.length, use_symbols=args.symbols)) + def run_uuid(args): for _ in range(args.count): print(uuid.uuid4()) + def run_json(args): try: print(jsonfmt.format_json(args.raw)) except ValueError as e: raise ValueError(f"Error: invalid JSON - {e}") + def run_palindrome(args): print(textutils.is_palindrome(args.text)) + def run_reverse(args): print(textutils.reverse_words(args.text)) + def run_base64(args): if args.base64_action == "encode": print(base64utils.encode_text(args.text)) else: print(base64utils.decode_text(args.text)) + +def run_slugify(args): + print(textutils.slugify(args.text)) + + COMMAND_TABLE = { "wc": run_wc, "convert": run_convert, @@ -79,8 +94,10 @@ def run_base64(args): "palindrome": run_palindrome, "reverse": run_reverse, "base64": run_base64, + "slugify": run_slugify, } + def build_parser() -> argparse.ArgumentParser: """Build the argument parser for the fixit command-line interface. @@ -89,11 +106,17 @@ def build_parser() -> argparse.ArgumentParser: args = parser.parse_args(["wc", "hello world"]) assert args.command == "wc" """ - parser = argparse.ArgumentParser(prog="fixit", description="A tiny toolbox to practice open source.") - parser.add_argument("--version", action="version", version=f"fixit {__version__}") + parser = argparse.ArgumentParser( + prog="fixit", + description="A tiny toolbox to practice open source.", + ) + parser.add_argument( + "--version", + action="version", + version=f"fixit {__version__}", + ) sub = parser.add_subparsers(dest="command", required=True) - wc = sub.add_parser("wc", help="count words, lines and characters") wc.add_argument("text", nargs="?") wc.add_argument("--file", type=Path, help="read text from a file") @@ -102,7 +125,6 @@ def build_parser() -> argparse.ArgumentParser: cv.add_argument("value", type=float) cv.add_argument("kind", choices=sorted(CONVERSIONS)) - pw = sub.add_parser("password", help="generate a password") pw.add_argument("--length", type=int, default=12) pw.add_argument("--symbols", action="store_true") @@ -128,8 +150,10 @@ def build_parser() -> argparse.ArgumentParser: b64_decode = b64_sub.add_parser("decode", help="decode Base64 text") b64_decode.add_argument("text") - return parser + slug = sub.add_parser("slugify", help="convert text to a URL-friendly slug") + slug.add_argument("text") + return parser def main(argv=None) -> int: @@ -147,6 +171,5 @@ def main(argv=None) -> int: return 1 - if __name__ == "__main__": sys.exit(main())