diff --git a/src/remote/configuration/__init__.py b/src/remote/configuration/__init__.py index d4596c8..0353bc9 100644 --- a/src/remote/configuration/__init__.py +++ b/src/remote/configuration/__init__.py @@ -8,7 +8,7 @@ class RemoteConfig: """Single remote connection description""" - # remote machine's hostname + # remote machine's hostname, with optional username@ prefix host: str # relative path to the working directory on remote machine starting from user home dir directory: Path diff --git a/src/remote/configuration/shared.py b/src/remote/configuration/shared.py index fe4fc73..f4a56f3 100644 --- a/src/remote/configuration/shared.py +++ b/src/remote/configuration/shared.py @@ -3,8 +3,8 @@ from pathlib import Path DEFAULT_REMOTE_ROOT = ".remotes" -HOST_REGEX = r"[-\w]+(\.[-\w]+)*" -PATH_REGEX = r"/?[-.\w\s]+(/[-.\w\s]+)*/?" +HOST_REGEX = r"([-\w]+@)?[-\w]+(\.[-\w]+)*" +PATH_REGEX = r"/?[-.\w\s][-.\w\s:]*(/[-.\w\s:]+)*/?" def hash_path(path: Path): diff --git a/src/remote/entrypoints.py b/src/remote/entrypoints.py index 1370d9e..d49812a 100644 --- a/src/remote/entrypoints.py +++ b/src/remote/entrypoints.py @@ -79,12 +79,14 @@ def _add_remote_host(config: WorkspaceConfig, connection: str): """Add a new remote host to the workspace config, check the connection, and save it if connection is ok :param config: the workspace config decription object - :param connection: connection string in format of 'host-name[:remote_dir]' + :param connection: connection string in format of '[user@]host-name[:remote_dir]' """ - parts = connection.split(":") - remote_host = parts[0] + remote_host, sep, path = connection.partition(":") config_medium = get_configuration_medium(config) - remote_dir = config_medium.generate_remote_directory(config) if len(parts) == 1 else Path(parts[1]) + if sep: + remote_dir = Path(path) + else: + remote_dir = config_medium.generate_remote_directory(config) added, index = config.add_remote_host(remote_host, remote_dir) if not added: @@ -108,7 +110,7 @@ def _add_remote_host(config: WorkspaceConfig, connection: str): @click.command(context_settings=DEFAULT_CONTEXT_SETTINGS) -@click.argument("connection", metavar="host-name[:remote_dir]", callback=validate_connection_string) +@click.argument("connection", metavar="[user@]host-name[:remote_dir]", callback=validate_connection_string) @log_exceptions def remote_add(connection: str): """Add one more host for remote connection to a config file""" @@ -118,7 +120,7 @@ def remote_add(connection: str): @click.command(context_settings=DEFAULT_CONTEXT_SETTINGS) -@click.argument("connection", metavar="host-name[:remote_dir]", callback=validate_connection_string) +@click.argument("connection", metavar="[user@]host-name[:remote_dir]", callback=validate_connection_string) @log_exceptions def remote_init(connection: str): """Initiate workspace for the remote execution in the current working directory""" diff --git a/test/test_entrypoints.py b/test/test_entrypoints.py index c54023b..f2d0d0f 100644 --- a/test/test_entrypoints.py +++ b/test/test_entrypoints.py @@ -27,6 +27,24 @@ TEST_DIR = ".remotes/my project" TEST_CONFIG = f"{TEST_HOST}:{shlex.quote(TEST_DIR)}" +TEST_HOSTS = [ + ("host", True), + ("host123", True), + ("host.domain.com", True), + ("ho-st.dom-ain.as1234", True), + ("ho-st.dom-ain.as1234:/home/dir", True), + ("ho-st.dom-ain.as1234:.home/dir.dir", True), + ("ho-st.dom-ain.as1234:.home/dir.dir/123/", True), + ("ho-st.dom-ain.as1234:.home/dir.dir/123/:something", True), + ("ho-st.dom-ain.as1234::/home/dir", False), + ("some_user@host", True), + ("some user@host", False), + ("some:user@host", False), + ("user@host:", False), + ("user@:/home/dir", False), + ("@host:/home/dir", False), +] + @contextmanager def cwd(path): @@ -76,20 +94,7 @@ def test_function(num): test_function(0) -@pytest.mark.parametrize( - "connection, is_valid", - [ - ("host", True), - ("host123", True), - ("host.domain.com", True), - ("ho-st.dom-ain.as1234", True), - ("ho-st.dom-ain.as1234:/home/dir", True), - ("ho-st.dom-ain.as1234:.home/dir.dir", True), - ("ho-st.dom-ain.as1234:.home/dir.dir/123/", True), - ("ho-st.dom-ain.as1234:.home/dir.dir/123/:something", False), - ("ho-st.dom-ain.as1234::/home/dir", False), - ], -) +@pytest.mark.parametrize("connection, is_valid", TEST_HOSTS) def test_validate_connection_string(connection, is_valid): if is_valid: entrypoints.validate_connection_string(None, None, connection) @@ -278,11 +283,12 @@ def test_remote_init_fails_if_workspace_is_already_initated(tmp_workspace): assert (tmp_workspace / CONFIG_FILE_NAME).read_text() == f"{TEST_CONFIG}\n" -def test_remote_init_fails_on_input_validation(tmp_path): +@pytest.mark.parametrize("connection", [connection for connection, is_valid in TEST_HOSTS if not is_valid]) +def test_remote_init_fails_on_input_validation(tmp_path, connection): runner = CliRunner() with cwd(tmp_path): - result = runner.invoke(entrypoints.remote_init, ["host:path:path"]) + result = runner.invoke(entrypoints.remote_init, [connection]) assert result.exit_code == 2 @@ -309,11 +315,12 @@ def test_remote_commands_fail_on_no_workspace(tmp_path): assert result.output == f"Cannot resolve the remote workspace in {tmp_path}\n" -def test_remote_add_fails_on_input_validation(tmp_path): +@pytest.mark.parametrize("connection", [connection for connection, is_valid in TEST_HOSTS if not is_valid]) +def test_remote_add_fails_on_input_validation(tmp_path, connection): runner = CliRunner() with cwd(tmp_path): - result = runner.invoke(entrypoints.remote_add, ["host:path:path"]) + result = runner.invoke(entrypoints.remote_add, [connection]) assert result.exit_code == 2