From 52a6fe5baf5cb45948f3e8f5e58e7e895345c0b2 Mon Sep 17 00:00:00 2001 From: SanjeevLakhwani Date: Tue, 18 Nov 2025 18:08:17 -0500 Subject: [PATCH 1/4] feat(bentoctl): add global CLI access with shell autocompletions Enable running bentoctl from any directory via a global wrapper script. Add argcomplete integration for dynamic tab completions in bash/zsh. - Add bin/bentoctl wrapper that handles BENTO_HOME and venv activation - Integrate argcomplete into entry.py for dynamic completions - Add static zsh completion script as fallback - Add documentation for global installation --- bin/bentoctl | 39 +++++++++ completions/_bentoctl | 176 +++++++++++++++++++++++++++++++++++++++++ docs/global-install.md | 137 ++++++++++++++++++++++++++++++++ py_bentoctl/entry.py | 3 + requirements.txt | 1 + 5 files changed, 356 insertions(+) create mode 100755 bin/bentoctl create mode 100644 completions/_bentoctl create mode 100644 docs/global-install.md diff --git a/bin/bentoctl b/bin/bentoctl new file mode 100755 index 00000000..2749dd6b --- /dev/null +++ b/bin/bentoctl @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# bentoctl - Global wrapper to run bentoctl from anywhere +# +# This script allows you to invoke bentoctl from any directory. +# It changes to the BENTO_HOME directory and runs the actual bentoctl.bash script. + +# Determine the Bento installation directory +# If BENTO_HOME is set, use it; otherwise, use the script's parent directory +if [[ -z "${BENTO_HOME}" ]]; then + # Get the directory where this script is located (resolving symlinks) + SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" + BENTO_HOME="$(dirname "$(dirname "$SCRIPT_PATH")")" +fi + +export BENTO_HOME + +# Validate that the directory exists +if [[ ! -d "${BENTO_HOME}" ]]; then + echo "Error: BENTO_HOME directory not found: ${BENTO_HOME}" >&2 + echo "Set BENTO_HOME environment variable to your bento installation path." >&2 + exit 1 +fi + +# Validate that bentoctl.bash exists +if [[ ! -f "${BENTO_HOME}/bentoctl.bash" ]]; then + echo "Error: bentoctl.bash not found in ${BENTO_HOME}" >&2 + exit 1 +fi + +# Change to the Bento directory and execute the script +cd "${BENTO_HOME}" || exit 1 + +# Activate virtual environment if it exists +if [[ -f "${BENTO_HOME}/env/bin/activate" ]]; then + source "${BENTO_HOME}/env/bin/activate" +fi + +# Execute the actual bentoctl script with all arguments +exec ./bentoctl.bash "$@" diff --git a/completions/_bentoctl b/completions/_bentoctl new file mode 100644 index 00000000..e243eb00 --- /dev/null +++ b/completions/_bentoctl @@ -0,0 +1,176 @@ +#compdef bentoctl + +# Zsh completion script for bentoctl +# Place this file in a directory in your $fpath (e.g., ~/.zsh/completions/) +# Or source it directly in your .zshrc + +_bentoctl() { + local curcontext="$curcontext" state line + typeset -A opt_args + + local -a commands + commands=( + # Initialization commands + 'init-dirs:Initialize directories for BentoV2 structure' + 'init-auth:Configure authentication with Keycloak' + 'init-certs:Initialize SSL certificates for gateway domains' + 'init-docker:Initialize Docker configuration and networks' + 'init-web:Initialize web app (public or private) files' + 'init-all:Initialize certs, directories, Docker networks, and web portals' + 'init-config:Initialize configuration files for specific services' + 'init-cbioportal:Initialize cBioPortal if enabled' + + # Database commands + 'pg-dump:Dump contents of all Postgres databases to a directory' + 'pg-load:Load contents of all Postgres databases from a directory' + + # Utility commands + 'convert-pheno:Convert a Phenopacket V1 JSON document to V2' + 'conv:Convert a Phenopacket V1 JSON document to V2' + + # Service commands + 'run:Run Bento services' + 'start:Run Bento services' + 'up:Run Bento services' + 'stop:Stop Bento services' + 'down:Stop Bento services' + 'restart:Restart Bento services' + 'clean:Clean services' + 'work-on:Work on a service in local development mode' + 'dev:Work on a service in local development mode' + 'develop:Work on a service in local development mode' + 'local:Work on a service in local development mode' + 'prebuilt:Switch a service back to prebuilt mode' + 'pre-built:Switch a service back to prebuilt mode' + 'prod:Switch a service back to prebuilt mode' + 'mode:See service production/development mode' + 'state:See service production/development mode' + 'status:See service production/development mode' + 'pull:Pull the image for a specific service' + 'shell:Run a shell inside a running service container' + 'sh:Run a shell inside a running service container' + 'run-as-shell:Run a shell inside a stopped service container' + 'logs:Check logs for a service' + 'compose-config:Generate Compose config YAML' + ) + + # Common services for service-based commands + local -a all_services + all_services=( + 'all:All services' + 'aggregation:Federation/aggregation service' + 'auth:Authentication service' + 'auth-db:Authentication database' + 'authz:Authorization service' + 'authz-db:Authorization database' + 'beacon:Beacon service' + 'cbioportal:cBioPortal service' + 'drs:Data Repository Service' + 'drop-box:Drop box service' + 'event-relay:Event relay service' + 'gateway:Gateway/proxy service' + 'gohan:Gohan services (api + elasticsearch)' + 'gohan-api:Gohan API service' + 'gohan-elasticsearch:Gohan Elasticsearch' + 'katsu:Metadata service' + 'katsu-db:Metadata database' + 'notification:Notification service' + 'public:Public frontend' + 'redis:Redis cache' + 'reference:Reference service' + 'reference-db:Reference database' + 'service-registry:Service registry' + 'web:Web frontend' + 'wes:Workflow Execution Service' + ) + + # Services that can be worked on (have repositories) + local -a workable_services + workable_services=( + 'aggregation:Federation/aggregation service' + 'authz:Authorization service' + 'beacon:Beacon service' + 'drs:Data Repository Service' + 'drop-box:Drop box service' + 'event-relay:Event relay service' + 'gateway:Gateway service' + 'gohan-api:Gohan API service' + 'katsu:Metadata service' + 'notification:Notification service' + 'public:Public frontend' + 'reference:Reference service' + 'service-registry:Service registry' + 'web:Web frontend' + 'wes:Workflow Execution Service' + ) + + _arguments -C \ + '(-d --debug)'{-d,--debug}'[Enable remote Python debugging]' \ + '1: :->command' \ + '*:: :->args' + + case $state in + command) + _describe -t commands 'bentoctl command' commands + ;; + args) + case $line[1] in + run|start|up|stop|down|restart|clean|logs|pull|mode|state|status) + _arguments \ + '1: :->service' \ + '(-p --pull)'{-p,--pull}'[Pull the service image first]' \ + '(-f --follow)'{-f,--follow}'[Follow logs]' + if [[ $state == service ]]; then + _describe -t services 'service' all_services + fi + ;; + work-on|dev|develop|local) + _arguments '1: :->service' + if [[ $state == service ]]; then + _describe -t services 'service' workable_services + fi + ;; + prebuilt|pre-built|prod) + _arguments '1: :->service' + if [[ $state == service ]]; then + _describe -t services 'service' all_services + fi + ;; + shell|sh|run-as-shell) + _arguments \ + '1: :->service' \ + '(-s --shell)'{-s,--shell}'[Shell to use]: :(/bin/bash /bin/sh)' + if [[ $state == service ]]; then + _describe -t services 'service' all_services + fi + ;; + init-web) + _arguments \ + '1: :(public private)' \ + '(-f --force)'{-f,--force}'[Overwrite existing files]' + ;; + init-config) + _arguments \ + '1: :(katsu beacon beacon-network)' \ + '(-f --force)'{-f,--force}'[Overwrite existing config]' + ;; + init-certs) + _arguments '(-f --force)'{-f,--force}'[Remove and recreate certificates]' + ;; + pg-dump|pg-load) + _arguments '1:directory:_directories' + ;; + convert-pheno|conv) + _arguments \ + '1:source file:_files -g "*.json"' \ + '2:target file:_files -g "*.json"' + ;; + compose-config) + _arguments '--services[List services seen by Compose]' + ;; + esac + ;; + esac +} + +_bentoctl "$@" diff --git a/docs/global-install.md b/docs/global-install.md new file mode 100644 index 00000000..85d9e606 --- /dev/null +++ b/docs/global-install.md @@ -0,0 +1,137 @@ +# Global Installation of bentoctl + +This guide explains how to install `bentoctl` so it can be run from any directory with shell autocompletions. + +## Prerequisites + +- Python 3.7+ +- A Bento installation at `/path/to/bento` +- Virtual environment with dependencies installed + +## Installation Steps + +### 1. Install Python Dependencies + +```bash +cd /path/to/bento +source env/bin/activate +pip install -r requirements.txt +``` + +### 2. Create Global Symlink + +Choose one of the following locations: + +**User-local (recommended, no sudo required):** +```bash +mkdir -p ~/.local/bin +ln -sf /path/to/bento/bin/bentoctl ~/.local/bin/bentoctl +``` + +**System-wide:** +```bash +sudo ln -sf /path/to/bento/bin/bentoctl /usr/local/bin/bentoctl +``` + +### 3. Ensure PATH is Set + +Add to your shell configuration file (`~/.zshrc` or `~/.bashrc`): + +```bash +export PATH="$HOME/.local/bin:$PATH" +``` + +### 4. Set Up Shell Completions + +#### Zsh (with argcomplete - recommended) + +Add to `~/.zshrc`: + +```zsh +# bentoctl completions +autoload -U bashcompinit && bashcompinit +eval "$(register-python-argcomplete bentoctl)" +``` + +#### Zsh (with static completions - alternative) + +Add to `~/.zshrc`: + +```zsh +# Add completions directory to fpath +fpath=(/path/to/bento/completions $fpath) +autoload -Uz compinit && compinit +``` + +#### Bash + +Add to `~/.bashrc`: + +```bash +eval "$(register-python-argcomplete bentoctl)" +``` + +### 5. Reload Shell + +```bash +source ~/.zshrc # or ~/.bashrc +``` + +## Usage + +Once installed, you can run `bentoctl` from any directory: + +```bash +# Run from anywhere +bentoctl run all +bentoctl logs katsu -f +bentoctl mode + +# Tab completion works for commands and services +bentoctl # Shows all commands +bentoctl run # Shows all services +bentoctl logs - # Shows flags +``` + +## Multiple Installations + +If you have multiple Bento installations (e.g., dev and staging), you can set `BENTO_HOME` to switch between them: + +```bash +# Use a specific installation +BENTO_HOME=/path/to/staging bentoctl run all + +# Or create aliases +alias bentoctl-dev='BENTO_HOME=/path/to/dev bentoctl' +alias bentoctl-staging='BENTO_HOME=/path/to/staging bentoctl' +``` + +## Troubleshooting + +### Command not found + +Ensure `~/.local/bin` is in your PATH: +```bash +echo $PATH | grep -q ".local/bin" && echo "OK" || echo "Add ~/.local/bin to PATH" +``` + +### Completions not working + +1. Verify argcomplete is installed: + ```bash + pip show argcomplete + ``` + +2. For zsh, ensure bashcompinit is loaded before the eval command. + +3. Try regenerating completions: + ```bash + source ~/.zshrc + ``` + +### Wrong Bento directory + +The wrapper script auto-detects `BENTO_HOME` from the symlink location. To override: +```bash +export BENTO_HOME=/path/to/your/bento +``` diff --git a/py_bentoctl/entry.py b/py_bentoctl/entry.py index a465f6f8..9f6dfc45 100644 --- a/py_bentoctl/entry.py +++ b/py_bentoctl/entry.py @@ -3,6 +3,8 @@ import subprocess import sys +import argcomplete + from abc import ABC, abstractmethod from pathlib import Path @@ -402,6 +404,7 @@ def _add_subparser(arg: str, help_text: str, subcommand: Type[SubCommand], alias _add_subparser("logs", "Check logs for a service.", Logs) _add_subparser("compose-config", "Generate Compose config YAML.", ComposeConfig) + argcomplete.autocomplete(parser) p_args = parser.parse_args(args) if not getattr(p_args, "func", None): diff --git a/requirements.txt b/requirements.txt index 802dde28..c06a09f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ tqdm==4.67.1 typing_extensions==4.15.0 urllib3==2.5.0 websocket-client==1.9.0 +argcomplete==3.5.3 From dc44ac950af62abf6b1245a65e9b6d12f6dc0f6c Mon Sep 17 00:00:00 2001 From: SanjeevLakhwani Date: Wed, 19 Nov 2025 13:57:00 -0500 Subject: [PATCH 2/4] docs: add bash completion script and expand installation docs - Add static bash completion script for non-argcomplete setups - Expand bash installation instructions in global-install.md --- completions/bentoctl.bash | 72 +++++++++++++++++++++++++++++++++++++++ docs/global-install.md | 27 +++++++++++---- 2 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 completions/bentoctl.bash diff --git a/completions/bentoctl.bash b/completions/bentoctl.bash new file mode 100644 index 00000000..b794f708 --- /dev/null +++ b/completions/bentoctl.bash @@ -0,0 +1,72 @@ +# Bash completion script for bentoctl +# Place this file in /etc/bash_completion.d/ or ~/.local/share/bash-completion/completions/ + +_bentoctl_completions() { + local cur prev words cword + _init_completion || return + + local commands="init-dirs init-auth init-certs init-docker init-web init-all init-config init-cbioportal pg-dump pg-load convert-pheno conv run start up stop down restart clean work-on dev develop local prebuilt pre-built prod mode state status pull shell sh run-as-shell logs compose-config" + + # All services (for most commands) + local all_services="all aggregation auth auth-db authz authz-db beacon cbioportal drs drop-box event-relay gateway gohan gohan-api gohan-elasticsearch katsu katsu-db notification public redis reference reference-db service-registry web wes" + + # Services that can be worked on (have repositories) + local workable_services="aggregation authz beacon drs drop-box event-relay gateway gohan-api katsu notification public reference service-registry web wes" + + if [[ ${cword} -eq 1 ]]; then + COMPREPLY=($(compgen -W "${commands}" -- "${cur}")) + return + fi + + case "${prev}" in + run|start|up|stop|down|restart|clean|logs|pull|mode|state|status) + COMPREPLY=($(compgen -W "${all_services}" -- "${cur}")) + ;; + work-on|dev|develop|local) + COMPREPLY=($(compgen -W "${workable_services}" -- "${cur}")) + ;; + prebuilt|pre-built|prod) + COMPREPLY=($(compgen -W "${all_services}" -- "${cur}")) + ;; + shell|sh|run-as-shell) + COMPREPLY=($(compgen -W "${all_services}" -- "${cur}")) + ;; + init-web) + COMPREPLY=($(compgen -W "public private" -- "${cur}")) + ;; + init-config) + COMPREPLY=($(compgen -W "katsu beacon beacon-network" -- "${cur}")) + ;; + pg-dump|pg-load) + _filedir -d + ;; + convert-pheno|conv) + _filedir + ;; + -s|--shell) + COMPREPLY=($(compgen -W "/bin/bash /bin/sh" -- "${cur}")) + ;; + *) + # Handle flags + case "${words[1]}" in + run|start|up|restart) + COMPREPLY=($(compgen -W "-p --pull" -- "${cur}")) + ;; + logs) + COMPREPLY=($(compgen -W "-f --follow" -- "${cur}")) + ;; + init-certs|init-web|init-config) + COMPREPLY=($(compgen -W "-f --force" -- "${cur}")) + ;; + shell|sh|run-as-shell) + COMPREPLY=($(compgen -W "-s --shell" -- "${cur}")) + ;; + compose-config) + COMPREPLY=($(compgen -W "--services" -- "${cur}")) + ;; + esac + ;; + esac +} + +complete -F _bentoctl_completions bentoctl diff --git a/docs/global-install.md b/docs/global-install.md index 85d9e606..e2898e25 100644 --- a/docs/global-install.md +++ b/docs/global-install.md @@ -2,12 +2,6 @@ This guide explains how to install `bentoctl` so it can be run from any directory with shell autocompletions. -## Prerequisites - -- Python 3.7+ -- A Bento installation at `/path/to/bento` -- Virtual environment with dependencies installed - ## Installation Steps ### 1. Install Python Dependencies @@ -23,12 +17,14 @@ pip install -r requirements.txt Choose one of the following locations: **User-local (recommended, no sudo required):** + ```bash mkdir -p ~/.local/bin ln -sf /path/to/bento/bin/bentoctl ~/.local/bin/bentoctl ``` **System-wide:** + ```bash sudo ln -sf /path/to/bento/bin/bentoctl /usr/local/bin/bentoctl ``` @@ -63,14 +59,28 @@ fpath=(/path/to/bento/completions $fpath) autoload -Uz compinit && compinit ``` -#### Bash +#### Bash (with argcomplete - recommended) Add to `~/.bashrc`: ```bash +# bentoctl completions eval "$(register-python-argcomplete bentoctl)" ``` +#### Bash (with static completions - alternative) + +Copy the completion script to the bash-completion directory: + +```bash +# System-wide +sudo cp /path/to/bento/completions/bentoctl.bash /etc/bash_completion.d/bentoctl + +# Or user-local +mkdir -p ~/.local/share/bash-completion/completions +cp /path/to/bento/completions/bentoctl.bash ~/.local/share/bash-completion/completions/bentoctl +``` + ### 5. Reload Shell ```bash @@ -111,6 +121,7 @@ alias bentoctl-staging='BENTO_HOME=/path/to/staging bentoctl' ### Command not found Ensure `~/.local/bin` is in your PATH: + ```bash echo $PATH | grep -q ".local/bin" && echo "OK" || echo "Add ~/.local/bin to PATH" ``` @@ -118,6 +129,7 @@ echo $PATH | grep -q ".local/bin" && echo "OK" || echo "Add ~/.local/bin to PATH ### Completions not working 1. Verify argcomplete is installed: + ```bash pip show argcomplete ``` @@ -132,6 +144,7 @@ echo $PATH | grep -q ".local/bin" && echo "OK" || echo "Add ~/.local/bin to PATH ### Wrong Bento directory The wrapper script auto-detects `BENTO_HOME` from the symlink location. To override: + ```bash export BENTO_HOME=/path/to/your/bento ``` From 0b01d06b8c6c9183df98e83fec157e047b157985 Mon Sep 17 00:00:00 2001 From: SanjeevLakhwani Date: Wed, 19 Nov 2025 14:05:49 -0500 Subject: [PATCH 3/4] perf(bentoctl): optimize imports for faster tab completions Defer all heavy module imports until execution time using lazy loading. This allows argcomplete to generate completions without loading the config module (which parses docker-compose files). - Add lazy import functions for all helper modules - Use static service lists for argument choices during completion - Import modules only when their functions are actually called --- py_bentoctl/entry.py | 174 ++++++++++++++++++++++++++++++++----------- 1 file changed, 129 insertions(+), 45 deletions(-) diff --git a/py_bentoctl/entry.py b/py_bentoctl/entry.py index 9f6dfc45..8b341642 100644 --- a/py_bentoctl/entry.py +++ b/py_bentoctl/entry.py @@ -1,5 +1,6 @@ from __future__ import annotations import argparse +import os import subprocess import sys @@ -7,16 +8,91 @@ from abc import ABC, abstractmethod from pathlib import Path +from typing import Optional, Tuple, Type -from .auth_helper import init_auth -from . import config as c -from . import db_helpers as dh -from . import feature_helpers as fh -from . import services as s -from . import other_helpers as oh -from . import utils as u +# Lazy imports - these modules are heavy and slow down tab completion +# They will be imported on first use in the functions that need them +_config = None +_db_helpers = None +_feature_helpers = None +_services = None +_other_helpers = None +_utils = None +_auth_helper = None -from typing import Optional, Tuple, Type + +def _get_config(): + global _config + if _config is None: + from . import config as c + _config = c + return _config + + +def _get_db_helpers(): + global _db_helpers + if _db_helpers is None: + from . import db_helpers as dh + _db_helpers = dh + return _db_helpers + + +def _get_feature_helpers(): + global _feature_helpers + if _feature_helpers is None: + from . import feature_helpers as fh + _feature_helpers = fh + return _feature_helpers + + +def _get_services(): + global _services + if _services is None: + from . import services as s + _services = s + return _services + + +def _get_other_helpers(): + global _other_helpers + if _other_helpers is None: + from . import other_helpers as oh + _other_helpers = oh + return _other_helpers + + +def _get_utils(): + global _utils + if _utils is None: + from . import utils as u + _utils = u + return _utils + + +def _get_auth_helper(): + global _auth_helper + if _auth_helper is None: + from .auth_helper import init_auth + _auth_helper = init_auth + return _auth_helper + + +# Static service lists for fast completions (these don't require loading config) +# These are used during tab completion to avoid loading the heavy config module +STATIC_ALL_SERVICES = ( + "all", "aggregation", "auth", "auth-db", "authz", "authz-db", "beacon", + "cbioportal", "drs", "drop-box", "event-relay", "gateway", "gohan", + "gohan-api", "gohan-elasticsearch", "katsu", "katsu-db", "notification", + "public", "redis", "reference", "reference-db", "service-registry", "web", "wes" +) + +STATIC_WORKABLE_SERVICES = ( + "aggregation", "authz", "beacon", "drs", "drop-box", "event-relay", + "gateway", "gohan-api", "katsu", "notification", "public", "reference", + "service-registry", "web", "wes" +) + +STATIC_MULTI_SERVICE_PREFIXES = ("gohan",) class SubCommand(ABC): @@ -35,7 +111,7 @@ class InitAuth(SubCommand): @staticmethod def exec(args): - init_auth(docker_client=u.get_docker_client()) + _get_auth_helper()(docker_client=_get_utils().get_docker_client()) class Run(SubCommand): @@ -43,17 +119,19 @@ class Run(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, choices=c.DOCKER_COMPOSE_SERVICES_PLUS_ALL, + "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, help="Service to run, or 'all' to run everything.") sp.add_argument("--pull", "-p", action="store_true", help="Try to pull the corresponding service image first.") @staticmethod def exec(args): + c = _get_config() + s = _get_services() if args.pull: s.pull_service(args.service) if c.BENTO_FEATURE_CBIOPORTAL.enabled: - fh.init_cbioportal() + _get_feature_helpers().init_cbioportal() s.run_service(args.service) @@ -63,12 +141,12 @@ class Stop(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, choices=c.DOCKER_COMPOSE_SERVICES_PLUS_ALL, + "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, help="Service to stop, or 'all' to stop everything.") @staticmethod def exec(args): - s.stop_service(args.service) + _get_services().stop_service(args.service) class Restart(SubCommand): @@ -76,7 +154,7 @@ class Restart(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, choices=c.DOCKER_COMPOSE_SERVICES_PLUS_ALL, + "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, help="Service to restart, or 'all' to restart everything.") sp.add_argument( "--pull", "-p", action="store_true", @@ -84,6 +162,7 @@ def add_args(sp): @staticmethod def exec(args): + s = _get_services() if args.pull: s.pull_service(args.service) s.restart_service(args.service) @@ -94,23 +173,23 @@ class Clean(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, choices=c.DOCKER_COMPOSE_SERVICES_PLUS_ALL, + "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, help="Service to clean, or 'all' to clean everything.") @staticmethod def exec(args): - s.clean_service(args.service) + _get_services().clean_service(args.service) class WorkOn(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("service", type=str, choices=tuple(c.BENTO_SERVICES_DATA.keys()), help="Service to work on.") + sp.add_argument("service", type=str, choices=STATIC_WORKABLE_SERVICES, help="Service to work on.") @staticmethod def exec(args): - s.work_on_service(args.service) + _get_services().work_on_service(args.service) class Prebuilt(SubCommand): @@ -118,12 +197,12 @@ class Prebuilt(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, choices=c.BENTO_SERVICES_COMPOSE_IDS_PLUS_ALL, + "service", type=str, choices=STATIC_ALL_SERVICES, help="Service to switch to pre-built mode (will use an entirely pre-built image with code).") @staticmethod def exec(args): - s.prod_service(args.service) + _get_services().prod_service(args.service) class Mode(SubCommand): @@ -131,13 +210,13 @@ class Mode(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, - choices=c.BENTO_SERVICES_COMPOSE_IDS_PLUS_ALL, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Displays the current mode of the service(s).") @staticmethod def exec(args): - s.mode_service(args.service) + _get_services().mode_service(args.service) class Pull(SubCommand): @@ -145,20 +224,22 @@ class Pull(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, - choices=(*c.DOCKER_COMPOSE_SERVICES, *c.MULTI_SERVICE_PREFIXES, c.SERVICE_LITERAL_ALL), + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to pull image for.") @staticmethod def exec(args): - s.pull_service(args.service) + _get_services().pull_service(args.service) class Shell(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("service", type=str, choices=c.DOCKER_COMPOSE_SERVICES, help="Service to enter a shell for.") + # Exclude "all" from shell choices - can only shell into one service + shell_services = tuple(s for s in STATIC_ALL_SERVICES if s != "all") + sp.add_argument("service", type=str, choices=shell_services, help="Service to enter a shell for.") sp.add_argument( "--shell", "-s", default="/bin/bash", type=str, choices=("/bin/bash", "/bin/sh"), @@ -166,14 +247,16 @@ def add_args(sp): @staticmethod def exec(args): - s.enter_shell_for_service(args.service, args.shell) + _get_services().enter_shell_for_service(args.service, args.shell) class RunShell(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("service", type=str, choices=c.DOCKER_COMPOSE_SERVICES, help="Service to run a shell for.") + # Exclude "all" from shell choices - can only shell into one service + shell_services = tuple(s for s in STATIC_ALL_SERVICES if s != "all") + sp.add_argument("service", type=str, choices=shell_services, help="Service to run a shell for.") sp.add_argument( "--shell", "-s", default="/bin/bash", type=str, choices=("/bin/bash", "/bin/sh"), @@ -181,7 +264,7 @@ def add_args(sp): @staticmethod def exec(args): - s.run_as_shell_for_service(args.service, args.shell) + _get_services().run_as_shell_for_service(args.service, args.shell) class Logs(SubCommand): @@ -189,7 +272,7 @@ class Logs(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default=c.SERVICE_LITERAL_ALL, choices=c.DOCKER_COMPOSE_SERVICES_PLUS_ALL, + "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, help="Service to check logs of.") sp.add_argument( "--follow", "-f", action="store_true", @@ -197,7 +280,7 @@ def add_args(sp): @staticmethod def exec(args): - s.logs_service(args.service, args.follow) + _get_services().logs_service(args.service, args.follow) class ComposeConfig(SubCommand): @@ -208,7 +291,7 @@ def add_args(sp): @staticmethod def exec(args): - s.compose_config(args.services) + _get_services().compose_config(args.services) # Other helpers @@ -217,7 +300,7 @@ class InitDirs(SubCommand): @staticmethod def exec(args): - oh.init_dirs() + _get_other_helpers().init_dirs() class InitCerts(SubCommand): @@ -230,14 +313,14 @@ def add_args(sp): @staticmethod def exec(args): - oh.init_self_signed_certs(args.force) + _get_other_helpers().init_self_signed_certs(args.force) class InitDocker(SubCommand): @staticmethod def exec(args): - oh.init_docker(client=u.get_docker_client()) + _get_other_helpers().init_docker(client=_get_utils().get_docker_client()) class InitWeb(SubCommand): @@ -252,14 +335,14 @@ def add_args(sp): @staticmethod def exec(args): - oh.init_web(args.service, args.force) + _get_other_helpers().init_web(args.service, args.force) class InitCBioPortal(SubCommand): @staticmethod def exec(args): - fh.init_cbioportal() + _get_feature_helpers().init_cbioportal() class InitAll(SubCommand): @@ -270,13 +353,14 @@ def add_args(sp): @staticmethod def exec(args): + oh = _get_other_helpers() oh.init_self_signed_certs(force=False) oh.init_dirs() - oh.init_docker(client=u.get_docker_client()) + oh.init_docker(client=_get_utils().get_docker_client()) oh.init_web("private", force=False) oh.init_web("public", force=False) - if c.BENTO_FEATURE_CBIOPORTAL.enabled: - fh.init_cbioportal() + if _get_config().BENTO_FEATURE_CBIOPORTAL.enabled: + _get_feature_helpers().init_cbioportal() class InitConfig(SubCommand): @@ -295,7 +379,7 @@ def add_args(sp): @staticmethod def exec(args): - oh.init_config(args.service, args.force) + _get_other_helpers().init_config(args.service, args.force) class PgDump(SubCommand): @@ -306,7 +390,7 @@ def add_args(sp): @staticmethod def exec(args): - dh.pg_dump(args.dir) + _get_db_helpers().pg_dump(args.dir) class PgLoad(SubCommand): @@ -317,7 +401,7 @@ def add_args(sp): @staticmethod def exec(args): - dh.pg_load(args.dir) + _get_db_helpers().pg_load(args.dir) class ConvertPhenopacket(SubCommand): @@ -333,7 +417,7 @@ def exec(args): # import asyncio # TODO: re-convert to async # asyncio.run(oh.convert_phenopacket_file(args.source, args.target)) - oh.convert_phenopacket_file(args.source, args.target) + _get_other_helpers().convert_phenopacket_file(args.source, args.target) def main(args: Optional[list[str]] = None) -> int: From 80f0794c5be3348b1863984f43da4c0883dff970 Mon Sep 17 00:00:00 2001 From: SanjeevLakhwani Date: Wed, 19 Nov 2025 15:58:05 -0500 Subject: [PATCH 4/4] style(bentoctl): fix flake8 lint issues - Remove unused import 'os' - Break long lines to comply with 79 char limit - Shorten help text where needed --- py_bentoctl/entry.py | 150 +++++++++++++++++++++++++++++-------------- 1 file changed, 101 insertions(+), 49 deletions(-) diff --git a/py_bentoctl/entry.py b/py_bentoctl/entry.py index 8b341642..e0823618 100644 --- a/py_bentoctl/entry.py +++ b/py_bentoctl/entry.py @@ -1,6 +1,5 @@ from __future__ import annotations import argparse -import os import subprocess import sys @@ -77,13 +76,15 @@ def _get_auth_helper(): return _auth_helper -# Static service lists for fast completions (these don't require loading config) +# Static service lists for fast completions +# (these don't require loading config) # These are used during tab completion to avoid loading the heavy config module STATIC_ALL_SERVICES = ( "all", "aggregation", "auth", "auth-db", "authz", "authz-db", "beacon", "cbioportal", "drs", "drop-box", "event-relay", "gateway", "gohan", "gohan-api", "gohan-elasticsearch", "katsu", "katsu-db", "notification", - "public", "redis", "reference", "reference-db", "service-registry", "web", "wes" + "public", "redis", "reference", "reference-db", "service-registry", + "web", "wes" ) STATIC_WORKABLE_SERVICES = ( @@ -119,9 +120,12 @@ class Run(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to run, or 'all' to run everything.") - sp.add_argument("--pull", "-p", action="store_true", help="Try to pull the corresponding service image first.") + sp.add_argument( + "--pull", "-p", action="store_true", + help="Try to pull the corresponding service image first.") @staticmethod def exec(args): @@ -141,7 +145,8 @@ class Stop(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to stop, or 'all' to stop everything.") @staticmethod @@ -154,7 +159,8 @@ class Restart(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to restart, or 'all' to restart everything.") sp.add_argument( "--pull", "-p", action="store_true", @@ -173,7 +179,8 @@ class Clean(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to clean, or 'all' to clean everything.") @staticmethod @@ -185,7 +192,9 @@ class WorkOn(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("service", type=str, choices=STATIC_WORKABLE_SERVICES, help="Service to work on.") + sp.add_argument( + "service", type=str, choices=STATIC_WORKABLE_SERVICES, + help="Service to work on.") @staticmethod def exec(args): @@ -198,7 +207,7 @@ class Prebuilt(SubCommand): def add_args(sp): sp.add_argument( "service", type=str, choices=STATIC_ALL_SERVICES, - help="Service to switch to pre-built mode (will use an entirely pre-built image with code).") + help="Service to switch to pre-built mode.") @staticmethod def exec(args): @@ -238,8 +247,11 @@ class Shell(SubCommand): @staticmethod def add_args(sp): # Exclude "all" from shell choices - can only shell into one service - shell_services = tuple(s for s in STATIC_ALL_SERVICES if s != "all") - sp.add_argument("service", type=str, choices=shell_services, help="Service to enter a shell for.") + shell_services = tuple( + s for s in STATIC_ALL_SERVICES if s != "all") + sp.add_argument( + "service", type=str, choices=shell_services, + help="Service to enter a shell for.") sp.add_argument( "--shell", "-s", default="/bin/bash", type=str, choices=("/bin/bash", "/bin/sh"), @@ -255,8 +267,11 @@ class RunShell(SubCommand): @staticmethod def add_args(sp): # Exclude "all" from shell choices - can only shell into one service - shell_services = tuple(s for s in STATIC_ALL_SERVICES if s != "all") - sp.add_argument("service", type=str, choices=shell_services, help="Service to run a shell for.") + shell_services = tuple( + s for s in STATIC_ALL_SERVICES if s != "all") + sp.add_argument( + "service", type=str, choices=shell_services, + help="Service to run a shell for.") sp.add_argument( "--shell", "-s", default="/bin/bash", type=str, choices=("/bin/bash", "/bin/sh"), @@ -272,11 +287,12 @@ class Logs(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, nargs="?", default="all", choices=STATIC_ALL_SERVICES, + "service", type=str, nargs="?", default="all", + choices=STATIC_ALL_SERVICES, help="Service to check logs of.") sp.add_argument( "--follow", "-f", action="store_true", - help="Whether to follow the logs (keep them open and show new entries).") + help="Follow logs (keep open and show new entries).") @staticmethod def exec(args): @@ -287,7 +303,9 @@ class ComposeConfig(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("--services", action="store_true", help="List services seen by Compose.") + sp.add_argument( + "--services", action="store_true", + help="List services seen by Compose.") @staticmethod def exec(args): @@ -309,7 +327,7 @@ class InitCerts(SubCommand): def add_args(sp): sp.add_argument( "--force", "-f", action="store_true", - help="Removes all previously created certs and keys before creating new ones.") + help="Remove existing certs and keys before creating new ones.") @staticmethod def exec(args): @@ -320,7 +338,8 @@ class InitDocker(SubCommand): @staticmethod def exec(args): - _get_other_helpers().init_docker(client=_get_utils().get_docker_client()) + client = _get_utils().get_docker_client() + _get_other_helpers().init_docker(client=client) class InitWeb(SubCommand): @@ -328,7 +347,8 @@ class InitWeb(SubCommand): @staticmethod def add_args(sp): sp.add_argument( - "service", type=str, choices=["public", "private"], help="Prepares the web applications for deployment.") + "service", type=str, choices=["public", "private"], + help="Prepares the web applications for deployment.") sp.add_argument( "--force", "-f", action="store_true", help="Overwrites any existing branding/translation/etc.") @@ -386,7 +406,9 @@ class PgDump(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("dir", type=Path, help="Path to a new directory for the database dump files.") + sp.add_argument( + "dir", type=Path, + help="Path to a new directory for the database dump files.") @staticmethod def exec(args): @@ -397,7 +419,9 @@ class PgLoad(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("dir", type=Path, help="Path to a directory to load the database dump files from.") + sp.add_argument( + "dir", type=Path, + help="Path to a directory to load database dump files from.") @staticmethod def exec(args): @@ -408,9 +432,12 @@ class ConvertPhenopacket(SubCommand): @staticmethod def add_args(sp): - sp.add_argument("source", type=str, help="Source Phenopackets V1 JSON document to convert") - sp.add_argument("target", nargs="?", default=None, type=str, - help="Optional target file path where to place the converted Phenopackets") + sp.add_argument( + "source", type=str, + help="Source Phenopackets V1 JSON document to convert") + sp.add_argument( + "target", nargs="?", default=None, type=str, + help="Target file path for converted Phenopackets") @staticmethod def exec(args): @@ -435,58 +462,83 @@ def main(args: Optional[list[str]] = None) -> int: print("break on this line") parser = argparse.ArgumentParser( - description="Tools for managing a Bento deployment (development or production).", + description="Tools for managing a Bento deployment.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--debug", "-d", action="store_true") subparsers = parser.add_subparsers() - def _add_subparser(arg: str, help_text: str, subcommand: Type[SubCommand], aliases: Tuple[str, ...] = ()): - subparser = subparsers.add_parser(arg, help=help_text, aliases=aliases) + def _add_subparser( + arg: str, + help_text: str, + subcommand: Type[SubCommand], + aliases: Tuple[str, ...] = () + ): + subparser = subparsers.add_parser( + arg, help=help_text, aliases=aliases) subparser.set_defaults(func=subcommand.exec) subcommand.add_args(subparser) # Generic initialization helpers - _add_subparser("init-dirs", "Initialize directories for BentoV2 structure.", InitDirs) - _add_subparser("init-auth", "Configure authentication for BentoV2 with a local Keycloak instance.", InitAuth) - _add_subparser("init-certs", "Initialize ssl certificates for BentoV2 gateway domains.", InitCerts) - _add_subparser("init-docker", "Initialize Docker configuration & networks.", InitDocker) - _add_subparser("init-web", "Init web app (public or private) files", InitWeb) + _add_subparser( + "init-dirs", "Initialize directories for BentoV2.", InitDirs) + _add_subparser( + "init-auth", "Configure authentication with Keycloak.", InitAuth) + _add_subparser( + "init-certs", "Initialize SSL certificates for gateway.", InitCerts) + _add_subparser( + "init-docker", "Initialize Docker config & networks.", InitDocker) + _add_subparser( + "init-web", "Init web app (public or private) files", InitWeb) _add_subparser( "init-all", - "Initialize certs, directories, Docker networks, secrets, and web portals. DOES NOT initialize Keycloak.", + "Initialize certs, dirs, Docker, web. NOT Keycloak.", InitAll) - _add_subparser("init-config", "Initialize configuration files for specific services.", InitConfig) + _add_subparser( + "init-config", "Initialize config files for services.", InitConfig) # Feature-specific initialization commands - _add_subparser("init-cbioportal", "Initialize cBioPortal if enabled", InitCBioPortal) + _add_subparser( + "init-cbioportal", "Initialize cBioPortal if enabled", InitCBioPortal) # Database commands # - Postgres: - _add_subparser("pg-dump", "Dump contents of all Postgres database containers to a directory.", PgDump) - _add_subparser("pg-load", "Load contents of all Postgres database containers from a directory.", PgLoad) + _add_subparser( + "pg-dump", "Dump Postgres databases to directory.", PgDump) + _add_subparser( + "pg-load", "Load Postgres databases from directory.", PgLoad) # Other commands - _add_subparser("convert-pheno", - "Convert a Phenopacket V1 JSON document to V2", ConvertPhenopacket, aliases=("conv",)) + _add_subparser( + "convert-pheno", "Convert Phenopacket V1 to V2", + ConvertPhenopacket, aliases=("conv",)) # Service commands - _add_subparser("run", "Run Bento services.", Run, aliases=("start", "up")) - _add_subparser("stop", "Stop Bento services.", Stop, aliases=("down",)) + _add_subparser( + "run", "Run Bento services.", Run, aliases=("start", "up")) + _add_subparser( + "stop", "Stop Bento services.", Stop, aliases=("down",)) _add_subparser("restart", "Restart Bento services.", Restart) _add_subparser("clean", "Clean services.", Clean) _add_subparser( - "work-on", "Work on a specific service in a local development mode.", WorkOn, + "work-on", "Work on service in local dev mode.", WorkOn, aliases=("dev", "develop", "local")) - _add_subparser("prebuilt", "Switch a service back to prebuilt mode.", Prebuilt, aliases=("pre-built", "prod")) _add_subparser( - "mode", "See if a service (or which services) are in production/development mode.", Mode, + "prebuilt", "Switch service to prebuilt mode.", Prebuilt, + aliases=("pre-built", "prod")) + _add_subparser( + "mode", "See service mode (local/prebuilt).", Mode, aliases=("state", "status")) - _add_subparser("pull", "Pull the image for a specific service.", Pull) - _add_subparser("shell", "Run a shell inside an already-running service container.", Shell, aliases=("sh",)) - _add_subparser("run-as-shell", "Run a shell inside a stopped service container.", RunShell) + _add_subparser( + "pull", "Pull the image for a service.", Pull) + _add_subparser( + "shell", "Shell into running service container.", Shell, + aliases=("sh",)) + _add_subparser( + "run-as-shell", "Shell into stopped service container.", RunShell) _add_subparser("logs", "Check logs for a service.", Logs) - _add_subparser("compose-config", "Generate Compose config YAML.", ComposeConfig) + _add_subparser( + "compose-config", "Generate Compose config YAML.", ComposeConfig) argcomplete.autocomplete(parser) p_args = parser.parse_args(args)