diff --git a/riocli/auth/login.py b/riocli/auth/login.py index ae846c34..e7cf121c 100644 --- a/riocli/auth/login.py +++ b/riocli/auth/login.py @@ -15,7 +15,7 @@ from click_help_colors import HelpColorsCommand from riocli.auth.util import select_project, get_token -from riocli.config import Configuration +from riocli.utils.context import get_root_context @click.command( @@ -27,26 +27,26 @@ help='Email of the Rapyuta.io account') @click.option('--password', prompt='Password', hide_input=True, help='Password for the Rapyuta.io account') -def login(email: str, password: str): +@click.pass_context +def login(ctx: click.Context, email: str, password: str): """ Log into the Rapyuta.io account using the CLI. This is required to use most of the functionalities of the CLI. """ - config = Configuration() - config.data['email_id'] = email - config.data['password'] = password - - config.data['auth_token'] = get_token(email, password) + ctx = get_root_context(ctx) + ctx.obj.data['email_id'] = email + ctx.obj.data['password'] = password + ctx.obj.data['auth_token'] = get_token(email, password) # Save if the file does not already exist - if not config.exists: + if not ctx.obj.exists: click.echo('Logged in successfully!') - config.save() + ctx.obj.save() else: click.echo("[Warning] rio already has a config file present") click.confirm('Do you want to override the config', abort=True) - select_project(config) - config.save() + select_project(ctx.obj) + ctx.obj.save() click.echo('Logged in successfully!') diff --git a/riocli/auth/logout.py b/riocli/auth/logout.py index fcc495da..120616d7 100644 --- a/riocli/auth/logout.py +++ b/riocli/auth/logout.py @@ -17,19 +17,19 @@ @click.command() -def logout(): +@click.pass_context +def logout(ctx: click.Context): """ Log out from the Rapyuta.io account using the CLI. """ - config = Configuration() - if not config.exists: + if not ctx.obj.exists: return - config.data.pop('auth_token', None) - config.data.pop('password', None) - config.data.pop('email_id', None) - config.data.pop('project_id', None) - config.save() + ctx.obj.data.pop('auth_token', None) + ctx.obj.data.pop('password', None) + ctx.obj.data.pop('email_id', None) + ctx.obj.data.pop('project_id', None) + ctx.obj.save() click.secho('Logged out successfully!', fg='green') diff --git a/riocli/auth/refresh_token.py b/riocli/auth/refresh_token.py index e4c33ce3..0822b06e 100644 --- a/riocli/auth/refresh_token.py +++ b/riocli/auth/refresh_token.py @@ -19,18 +19,18 @@ @click.command() -def refresh_token(): +@click.pass_context +def refresh_token(ctx: click.Context): """ Refreshes the authentication Token after it expires """ - config = Configuration() - email = config.data.get('email_id', None) - password = config.data.get('password', None) - if not config.exists or not email or not password: + email = ctx.obj.data.get('email_id', None) + password = ctx.obj.data.get('password', None) + if not ctx.obj.exists or not email or not password: raise LoggedOut - config.data['auth_token'] = get_token(email, password) + ctx.obj.data['auth_token'] = get_token(email, password) - config.save() + ctx.obj.save() click.echo('Token refreshed successfully!') diff --git a/riocli/auth/staging.py b/riocli/auth/staging.py index 9e151347..54855103 100644 --- a/riocli/auth/staging.py +++ b/riocli/auth/staging.py @@ -16,6 +16,7 @@ from riocli.auth.login import select_project from riocli.auth.util import get_token from riocli.config import Configuration +from riocli.utils.context import get_root_context _STAGING_ENVIRONMENT_SUBDOMAIN = "apps.okd4v2.okd4beta.rapyuta.io" _NAMED_ENVIRONMENTS = ["v11", "v12", "v13", "v14", "v15", "qa"] @@ -23,30 +24,31 @@ @click.command('environment', hidden=True) @click.argument('name', type=str) -def environment(name: str): +@click.pass_context +def environment(ctx: click.Context, name: str): """ Sets the Rapyuta.io environment to use (Internal use) """ - config = Configuration() + ctx = get_root_context(ctx) if name == 'ga': - config.data.pop('environment', None) - config.data.pop('catalog_host', None) - config.data.pop('core_api_host', None) - config.data.pop('rip_host', None) + ctx.obj.data.pop('environment', None) + ctx.obj.data.pop('catalog_host', None) + ctx.obj.data.pop('core_api_host', None) + ctx.obj.data.pop('rip_host', None) else: - _configure_environment(config, name) + _configure_environment(ctx.obj, name) - config.data.pop('project_id', None) - email = config.data.get('email_id', None) - password = config.data.get('password', None) - config.save() + ctx.obj.data.pop('project_id', None) + email = ctx.obj.data.get('email_id', None) + password = ctx.obj.data.get('password', None) + ctx.obj.save() - config.data['auth_token'] = get_token(email, password) + ctx.obj.data['auth_token'] = get_token(email, password) - select_project(config) - config.save() + select_project(ctx.obj) + ctx.obj.save() def _validate_environment(name: str) -> bool: diff --git a/riocli/auth/status.py b/riocli/auth/status.py index 8d345d0f..613e564d 100644 --- a/riocli/auth/status.py +++ b/riocli/auth/status.py @@ -17,15 +17,15 @@ @click.command() -def status(): +@click.pass_context +def status(ctx: click.Context): """ Shows the Login status of the CLI """ - config = Configuration() - if not config.exists: + if not ctx.obj.exists: click.secho('Logged out 🔒', fg='red') exit(1) - if 'auth_token' in config.data: + if 'auth_token' in ctx.obj.data: click.secho('Logged in 🎉', fg='green') diff --git a/riocli/auth/util.py b/riocli/auth/util.py index a20f966a..2472d053 100644 --- a/riocli/auth/util.py +++ b/riocli/auth/util.py @@ -36,6 +36,7 @@ def select_project(config: Configuration) -> str: choice = show_selection(project_map, header='Select the project to activate') config.data['project_id'] = choice + config.data['project_name'] = project_map[choice] def get_token(email: str, password: str) -> str: diff --git a/riocli/bootstrap.py b/riocli/bootstrap.py index e237ccef..443e8bb3 100644 --- a/riocli/bootstrap.py +++ b/riocli/bootstrap.py @@ -17,14 +17,15 @@ import click import rapyuta_io.version +from click import Context from click_help_colors import HelpColorsGroup from click_plugins import with_plugins -from click_repl import register_repl from pkg_resources import iter_entry_points from riocli.auth import auth from riocli.build import build from riocli.completion import completion +from riocli.config import Configuration from riocli.deployment import deployment from riocli.device import device from riocli.marketplace import marketplace @@ -33,6 +34,7 @@ from riocli.project import project from riocli.rosbag import rosbag from riocli.secret import secret +from riocli.shell import shell, deprecated_repl from riocli.static_route import static_route @@ -40,14 +42,15 @@ @click.group( invoke_without_command=False, cls=HelpColorsGroup, - help_headers_color='yellow', - help_options_color='green', + help_headers_color="yellow", + help_options_color="green", ) -def cli(): - pass +@click.pass_context +def cli(ctx: Context, config: str = None): + ctx.obj = Configuration(filepath=config) -@cli.command('help') +@cli.command("help") @click.pass_context def cli_help(ctx): """ @@ -61,7 +64,7 @@ def version(): """ Version of the CLI/SDK """ - click.echo("rio {} / SDK {}".format(__version__, rapyuta_io.VERSIONSTR)) + click.echo("rio {} / SDK {}".format(__version__, rapyuta_io.__version__)) return @@ -77,4 +80,5 @@ def version(): cli.add_command(network) cli.add_command(completion) cli.add_command(marketplace) -register_repl(cli) +cli.add_command(shell) +cli.add_command(deprecated_repl) diff --git a/riocli/project/select.py b/riocli/project/select.py index aa8a2c41..256beb3c 100644 --- a/riocli/project/select.py +++ b/riocli/project/select.py @@ -13,18 +13,20 @@ # limitations under the License. import click -from riocli.config import Configuration from riocli.project.util import name_to_guid +from riocli.utils.context import get_root_context @click.command('select') @click.argument('project-name', type=str) @name_to_guid -def select_project(project_name: str, project_guid: str) -> None: +@click.pass_context +def select_project(ctx: click.Context, project_name: str, project_guid: str) -> None: """ Sets the given project in the CLI context. All other resources use this project to act upon. """ - config = Configuration() - config.data['project_id'] = project_guid - config.save() + ctx = get_root_context(ctx) + ctx.obj.data['project_id'] = project_guid + ctx.obj.data['project_name'] = project_name + ctx.obj.save() click.secho('Project {} ({}) is selected!'.format(project_name, project_guid), fg='green') diff --git a/riocli/shell/__init__.py b/riocli/shell/__init__.py new file mode 100644 index 00000000..7a173489 --- /dev/null +++ b/riocli/shell/__init__.py @@ -0,0 +1,68 @@ +# Copyright 2022 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import click +from click_help_colors import HelpColorsCommand +from click_repl import repl +from prompt_toolkit.history import FileHistory, ThreadedHistory + +from riocli.config import Configuration +from riocli.shell.prompt import prompt_callback + + +@click.command( + cls=HelpColorsCommand, + help_headers_color='yellow', + help_options_color='green', +) +@click.pass_context +def shell(ctx: click.Context): + """ + Interactive Shell for Rapyuta.io + """ + start_shell(ctx) + + +@click.command( + 'repl', + cls=HelpColorsCommand, + help_headers_color='yellow', + help_options_color='green', + hidden=True +) +@click.pass_context +def deprecated_repl(ctx: click.Context): + """ + [Deprecated] Use "rio shell" instead + """ + start_shell(ctx) + + +def start_shell(ctx: click.Context): + prompt_config = _parse_config(ctx.obj) + repl(click.get_current_context(), prompt_kwargs=prompt_config) + + +def _parse_config(config: Configuration) -> dict: + history_path = os.path.join(click.get_app_dir(config.APP_NAME), "history") + default_prompt_kwargs = { + 'history': ThreadedHistory(FileHistory(history_path)), + 'message': prompt_callback, + 'enable_suspend': True + } + + shell_config = config.data.get('shell', {}) + + return {**default_prompt_kwargs, **shell_config} diff --git a/riocli/shell/prompt.py b/riocli/shell/prompt.py new file mode 100644 index 00000000..f8c122c3 --- /dev/null +++ b/riocli/shell/prompt.py @@ -0,0 +1,19 @@ +# Copyright 2022 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import click + + +@click.pass_context +def prompt_callback(ctx: click.Context) -> str: + return '{} > '.format(ctx.obj.data['project_name']) diff --git a/riocli/utils/context.py b/riocli/utils/context.py new file mode 100644 index 00000000..3e7787ca --- /dev/null +++ b/riocli/utils/context.py @@ -0,0 +1,27 @@ +# Copyright 2022 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from click import Context + + +def get_root_context(ctx: Context) -> Context: + """ + get_root_context figures out the top-level Context from the given context by walking down the linked-list. + + https://click.palletsprojects.com/en/8.0.x/complex/#contexts + """ + while True: + if ctx.parent is None: + return ctx + + ctx = ctx.parent