-
Notifications
You must be signed in to change notification settings - Fork 0
Envoy CLI and Cleanup Command #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import os | ||
| import argparse | ||
|
|
||
| from .cli.status import STATUS_ARGS | ||
| from .cli.cleanup import CLEANUP_ARGS | ||
|
|
||
| from . import connect | ||
| from .exceptions import CommandError | ||
|
|
||
| from dotenv import load_dotenv | ||
|
|
||
|
|
||
| # Global arguments for all commands | ||
| load_dotenv() | ||
| GLOBAL_ARGS = { | ||
| ("-u", "--url"): { | ||
| "help": "the url for the admin API of your Envoy node", | ||
| "default": os.environ.get("ENVOY_URL", None), | ||
| "type": str, | ||
| "metavar": "URL", | ||
| }, | ||
| ("-c", "--client-id"): { | ||
| "help": "the client id of the API key to use for authentication", | ||
| "default": os.environ.get("ENVOY_CLIENT_ID", None), | ||
| "type": str, | ||
| "metavar": "ID", | ||
| }, | ||
| ("-s", "--secret"): { | ||
| "help": "the client secret of the API key to use for authentication", | ||
| "default": os.environ.get("ENVOY_CLIENT_SECRET", None), | ||
| "type": str, | ||
| "metavar": "SECRET", | ||
| }, | ||
| ("-t", "--timeout"): { | ||
| "help": "the number of seconds to wait for a response before timing out", | ||
| "default": 10.0, | ||
| "type": float, | ||
| "metavar": "SEC", | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| # Define the CLI commands and arguments | ||
| COMMANDS = { | ||
| "status": STATUS_ARGS, | ||
| "cleanup": CLEANUP_ARGS, | ||
| } | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| prog="envoy", | ||
| description="API client for TRISA Envoy nodes and common commands", | ||
| epilog="Report bugs to https://github.com/trisacrypto/pyenvoy/issues", | ||
| ) | ||
|
|
||
| # Add global arguments to the parser | ||
| for pargs, kwargs in GLOBAL_ARGS.items(): | ||
| if isinstance(pargs, str): | ||
| pargs = (pargs,) | ||
| parser.add_argument(*pargs, **kwargs) | ||
|
|
||
| # Add subparsers for each command | ||
| subparsers = parser.add_subparsers(title="commands", required=True) | ||
| for cmd, cargs in COMMANDS.items(): | ||
| subparser = subparsers.add_parser(cmd, description=cargs["description"]) | ||
| for pargs, kwargs in cargs.get("args", {}).items(): | ||
| if isinstance(pargs, str): | ||
| pargs = (pargs,) | ||
| subparser.add_argument(*pargs, **kwargs) | ||
| subparser.set_defaults(func=cargs["func"]) | ||
|
|
||
| try: | ||
| args = parser.parse_args() | ||
| if args.url is None or args.client_id is None or args.secret is None: | ||
| parser.error( | ||
| "missing required client configuration: url, client id, and/or secret" | ||
| ) | ||
|
|
||
| client = connect( | ||
| url=args.url, | ||
| client_id=args.client_id, | ||
| client_secret=args.secret, | ||
| timeout=args.timeout, | ||
| ) | ||
|
|
||
| args.func(client, args) | ||
| except CommandError as e: | ||
| parser.error(str(e)) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| """ | ||
| This module implements the `cleanup` command for the `envoy` CLI. | ||
| """ | ||
| import re | ||
|
|
||
| from .prompt import confirm | ||
| from ..exceptions import CommandError, NotFound | ||
|
|
||
|
|
||
| def is_ulid(value: str) -> bool: | ||
| return re.match(r'^[0-9A-HJ-NP-TV-Z]{26}$', value) is not None | ||
|
|
||
|
|
||
| def cleanup(client, args): | ||
| # Get Counterparty ID and Confirm Counterparty Selection | ||
| counterparty_id = None | ||
| if is_ulid(args.counterparty): | ||
| try: | ||
| counterparty = client.counterparties.detail(args.counterparty) | ||
| counterparty_id = counterparty["id"] | ||
| except NotFound: | ||
| raise CommandError(f"counterparty '{args.counterparty}' not found") | ||
| else: | ||
| counterparty = client.counterparties.search(args.counterparty) | ||
| if len(counterparty) == 0: | ||
| raise CommandError(f"counterparty '{args.counterparty}' not found") | ||
| else: | ||
| counterparty = counterparty[0] | ||
| counterparty_id = counterparty["id"] | ||
|
|
||
| if not args.yes and not confirm(f"continue with {counterparty['name']} ({counterparty_id})?"): | ||
| raise CommandError("could not identify counterparty") | ||
|
|
||
| counterparty_id = counterparty["id"] | ||
|
|
||
| # Get transactions and confirm transaction selection | ||
| # TODO: use `/v1/counterparties/{counterparty_id}/transfers` when implemented | ||
| # TODO: handle pagination when implemented | ||
| transactions = client.transactions.list( | ||
| params={ | ||
| "status": args.status, | ||
| } | ||
| ) | ||
|
|
||
| # Filter transactions by counterparty and status | ||
| transactions = [ | ||
| tx for tx in transactions | ||
| if tx["counterparty_id"] == counterparty_id and tx["status"] == args.status | ||
| ] | ||
|
|
||
| # Confirm moving forward with the operation | ||
| method = "archive" if not args.delete else "delete" | ||
| if not args.yes and not confirm(f"{method} {len(transactions)} {args.status} transactions for {counterparty['name']} ({counterparty_id})?"): | ||
| raise CommandError(f"{method} operation cancelled") | ||
|
|
||
| for tx in transactions: | ||
| if args.dry_run: | ||
| print(" ".join([ | ||
| f"dry run: {method} transfer {tx['id']} (status {tx['status']})", | ||
| f"with {tx['counterparty']}: {tx['amount']} {tx['virtual_asset']}", | ||
| f"from {tx['originator_address']} to {tx['beneficiary_address']}" | ||
| ])) | ||
| elif args.delete: | ||
| client.transactions.delete(tx["id"]) | ||
| else: | ||
| tx = client.transactions.detail(tx["id"]) | ||
| tx.archive() | ||
|
|
||
|
|
||
| CLEANUP_ARGS = { | ||
| "description": "batch archive or delete transactions by counterparty and status", | ||
| "func": cleanup, | ||
| "args": { | ||
| ("-s", "--status"): { | ||
| "help": "the status of the transactions to cleanup", | ||
| "default": "review", | ||
| "type": str, | ||
| "choices": [ | ||
| "draft", "pending", "repair", "review", | ||
| "rejected", "accepted", "completed" | ||
| ], | ||
| }, | ||
| ("-c", "--counterparty"): { | ||
| "help": "the counterparty name or ID of the transactions to cleanup", | ||
| "type": str, | ||
| "required": True, | ||
| }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conflicting short flags between global and cleanup argsMedium Severity The cleanup subparser defines Additional Locations (1) |
||
| ("-D", "--delete"): { | ||
| "help": "delete the transactions instead of archiving them", | ||
| "action": "store_true", | ||
| "default": False, | ||
| }, | ||
| ("-d", "--dry-run"): { | ||
| "help": "do not perform the operation, print the affected transactions", | ||
| "action": "store_true", | ||
| "default": False, | ||
| }, | ||
| ("-y", "--yes"): { | ||
| "help": "answer yes to all confirmation prompts", | ||
| "action": "store_true", | ||
| "default": False, | ||
| }, | ||
| }, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from ..exceptions import CommandError | ||
|
|
||
|
|
||
| def confirm(prompt) -> bool: | ||
| for _ in range(3): | ||
| response = input(f"{prompt} [y/N]: ").strip().lower() | ||
| if response in {'yes', 'y'}: | ||
| return True | ||
| elif response in {'no', 'n'}: | ||
| return False | ||
| else: | ||
| print("Invalid response. Please enter 'y'/'yes' or 'n'/'no'.") | ||
|
|
||
| raise CommandError("could not confirm operation") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| """ | ||
| This module implements the `status` command for the `envoy` CLI. | ||
| """ | ||
|
|
||
| import json | ||
|
|
||
|
|
||
| def status(client, args): | ||
| status = client.status() | ||
|
|
||
| if args.output == "json": | ||
| print(json.dumps(status, indent=2)) | ||
| elif args.output == "text": | ||
| print(f"Status: {status['status']}") | ||
| print(f"Uptime: {status['uptime']}") | ||
| print(f"Version: {status['version']}") | ||
| else: | ||
| print(status["status"]) | ||
|
|
||
|
|
||
| STATUS_ARGS = { | ||
| "description": "reports the status of your Envoy node", | ||
| "func": status, | ||
| "args": { | ||
| ("-o", "--output"): { | ||
| "help": "the output format to use", | ||
| "default": "json", | ||
| "type": str, | ||
| "choices": ["json", "text"], | ||
| }, | ||
| }, | ||
| } |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
__init__.pybreaks CLI package distributionHigh Severity
The new
envoy/cli/directory has no__init__.pyfile. Sincesetup.pyusesfind_packages()(notfind_namespace_packages()), setuptools requires__init__.pyto recognize a directory as a package. Without it,envoy.cliand its modules (cleanup,status,prompt) won't be included in the pip-installed distribution, causing the CLI entry point (envoy=envoy.__main__:main) to fail with an import error at runtime.Additional Locations (1)
setup.py#L122-L123