You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by ku...@apache.org on 2021/12/27 15:19:02 UTC
[submarine] branch master updated: SUBMARINE-1135. Implement CLI Config Command
This is an automated email from the ASF dual-hosted git repository.
kuanhsun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git
The following commit(s) were added to refs/heads/master by this push:
new e68cf27 SUBMARINE-1135. Implement CLI Config Command
e68cf27 is described below
commit e68cf27da85451a2c4d14eca366a0c125e939972
Author: atosystem <at...@hotmail.com>
AuthorDate: Sat Dec 25 12:35:22 2021 +0800
SUBMARINE-1135. Implement CLI Config Command
### What is this PR for?
Implement `submarine config` command to set *_port_* and *_hostname_* for API connection.
Usage:
`submarine config init` : init config file (restore to default)
`submarine config list` : list config content
`submarine config set <param> <value>` : set param = value in config
`submarine config get <param>` : get param
Examples:
1. Change API port to `8080`
`submarine config set connection.port 8080`
2. Change hostname to `127.0.0.1`
`submarine config set connection.hostname 127.0.0.1`
### What type of PR is it?
[Feature]
### Todos
None
### What is the Jira issue?
https://issues.apache.org/jira/projects/SUBMARINE/issues/SUBMARINE-1135
### How should this be tested?
python unit test is implemented
### Screenshots (if appropriate)
![](https://i.imgur.com/fk1MStN.png)
### Questions:
* Do the license files need updating? No
* Are there breaking changes for older versions? No
* Does this need new documentation? Yes
Author: atosystem <at...@hotmail.com>
Signed-off-by: kuanhsun <ku...@apache.org>
Closes #847 from atosystem/SUBMARINE-1135 and squashes the following commits:
e96646f3 [atosystem] SUBMARINE-1135. Add config init unit test
22beebbc [atosystem] SUBMARINE-1135. Change init connection.port
e49ab80c [atosystem] SUBMARINE-1135. Fix dependency for python3.7
bd63dd4d [atosystem] SUBMARINE-1135. Add rich in setup.py
1566e582 [atosystem] SUBMARINE-1135. Implement CLI Config and unit tests
---
.../style-check/python/mypy-requirements.txt | 2 +
submarine-sdk/pysubmarine/setup.py | 5 +
.../pysubmarine/submarine/cli/config/__init__.py | 20 ++--
.../submarine/cli/config/cli_config.yaml | 3 +
.../pysubmarine/submarine/cli/config/command.py | 82 +++++++++++++
.../pysubmarine/submarine/cli/config/config.py | 127 +++++++++++++++++++++
submarine-sdk/pysubmarine/submarine/cli/main.py | 12 ++
submarine-sdk/pysubmarine/tests/cli/test_config.py | 62 ++++++++++
8 files changed, 304 insertions(+), 9 deletions(-)
diff --git a/dev-support/style-check/python/mypy-requirements.txt b/dev-support/style-check/python/mypy-requirements.txt
index 369325a..41d5e73 100644
--- a/dev-support/style-check/python/mypy-requirements.txt
+++ b/dev-support/style-check/python/mypy-requirements.txt
@@ -18,4 +18,6 @@ types-requests==2.25.6
types-certifi==2020.4.0
types-six==1.16.1
types-python-dateutil==2.8.0
+types-dataclasses==0.6.1
+types-PyYAML==6.0.1
sqlalchemy[mypy]
diff --git a/submarine-sdk/pysubmarine/setup.py b/submarine-sdk/pysubmarine/setup.py
index 1e72068..720a561 100644
--- a/submarine-sdk/pysubmarine/setup.py
+++ b/submarine-sdk/pysubmarine/setup.py
@@ -26,6 +26,7 @@ setup(
long_description_content_type="text/markdown",
url="https://github.com/apache/submarine",
packages=find_packages(exclude=["tests", "tests.*"]),
+ package_data={"submarine.cli.config": ["cli_config.yaml"]},
install_requires=[
"six>=1.10.0",
"numpy==1.19.2",
@@ -41,6 +42,10 @@ setup(
"mlflow>=1.15.0",
"boto3>=1.17.58",
"click==8.0.3",
+ "rich==10.15.2",
+ "dacite==1.6.0",
+ "dataclasses>=0.6",
+ "pyaml==21.10.1",
],
extras_require={
"tf": ["tensorflow==1.15.0"],
diff --git a/dev-support/style-check/python/mypy-requirements.txt b/submarine-sdk/pysubmarine/submarine/cli/config/__init__.py
similarity index 62%
copy from dev-support/style-check/python/mypy-requirements.txt
copy to submarine-sdk/pysubmarine/submarine/cli/config/__init__.py
index 369325a..4103e81 100644
--- a/dev-support/style-check/python/mypy-requirements.txt
+++ b/submarine-sdk/pysubmarine/submarine/cli/config/__init__.py
@@ -1,11 +1,11 @@
# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements. See the NOTICE file distributed with
+# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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
+# the License. You may obtain a copy of the License at
#
-# http://www.apache.org/licenses/LICENSE-2.0
+# 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,
@@ -13,9 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-mypy==0.930
-types-requests==2.25.6
-types-certifi==2020.4.0
-types-six==1.16.1
-types-python-dateutil==2.8.0
-sqlalchemy[mypy]
+from submarine.cli.config.command import get_config, init_config, list_config, set_config
+
+__all__ = [
+ "list_config",
+ "get_config",
+ "set_config",
+ "init_config",
+]
diff --git a/submarine-sdk/pysubmarine/submarine/cli/config/cli_config.yaml b/submarine-sdk/pysubmarine/submarine/cli/config/cli_config.yaml
new file mode 100644
index 0000000..84dc9ed
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/config/cli_config.yaml
@@ -0,0 +1,3 @@
+connection:
+ hostname: "localhost"
+ port: 32080
diff --git a/submarine-sdk/pysubmarine/submarine/cli/config/command.py b/submarine-sdk/pysubmarine/submarine/cli/config/command.py
new file mode 100644
index 0000000..8927bc6
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/config/command.py
@@ -0,0 +1,82 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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 dataclasses import asdict
+from typing import Union
+
+import click
+from rich.console import Console
+from rich.json import JSON as richJSON
+from rich.panel import Panel
+
+from submarine.cli.config.config import initConfig, loadConfig, rgetattr, rsetattr, saveConfig
+
+
+@click.command("list")
+def list_config():
+ """List Submarine CLI Config"""
+ console = Console()
+ _config = loadConfig()
+ json_data = richJSON.from_data({**asdict(_config)})
+ console.print(Panel(json_data, title="SubmarineCliConfig"))
+
+
+@click.command("get")
+@click.argument("param")
+def get_config(param):
+ """Get Submarine CLI Config"""
+ _config = loadConfig()
+ try:
+ click.echo("{}={}".format(param, rgetattr(_config, param)))
+ except AttributeError as err:
+ click.echo(err)
+
+
+@click.command("set")
+@click.argument("param")
+@click.argument("value")
+def set_config(param, value):
+ """Set Submarine CLI Config"""
+ _config = loadConfig()
+ _paramField = rgetattr(_config, param)
+ # define types that can be cast from command line input
+ primitive = (int, str, bool)
+
+ def is_primitiveType(_type):
+ return _type in primitive
+
+ # cast type
+ if type(_paramField) == type(Union) and is_primitiveType(type(_paramField).__args__[0]):
+ value = type(_paramField).__args__[0](value)
+ elif is_primitiveType(type(_paramField)):
+ value = type(_paramField)(value)
+
+ try:
+ rsetattr(_config, param, value)
+ except TypeError as err:
+ click.echo(err)
+ saveConfig(_config)
+
+
+@click.command("init")
+def init_config():
+ """Init Submarine CLI Config"""
+ try:
+ initConfig()
+ click.echo("Submarine CLI Config initialized")
+ except AttributeError as err:
+ click.echo(err)
diff --git a/submarine-sdk/pysubmarine/submarine/cli/config/config.py b/submarine-sdk/pysubmarine/submarine/cli/config/config.py
new file mode 100644
index 0000000..dada3db
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/config/config.py
@@ -0,0 +1,127 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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 functools
+import os
+from dataclasses import asdict, dataclass, field
+from typing import Optional, Union
+
+import click
+import dacite
+import yaml
+
+CONFIG_YAML_PATH = os.path.join(os.path.dirname(__file__), "cli_config.yaml")
+
+
+@dataclass
+class BaseConfig:
+ def __setattr__(self, __name, __value) -> None:
+ """
+ Override __setattr__ for custom type checking
+ """
+ # ignore this line for mypy checking, since there is some errors for mypy to check dataclass
+ _field = self.__dataclass_fields__[__name] # type: ignore
+ if hasattr(_field.type, "__origin__") and _field.type.__origin__ == Union:
+ if not isinstance(__value, _field.type.__args__):
+ msg = (
+ "Field `{0.name}` is of type {1}, should be one of the type: {0.type.__args__}"
+ .format(_field, type(__value))
+ )
+ raise TypeError(msg)
+ else:
+ if not type(__value) == _field.type:
+ msg = "Field {0.name} is of type {1}, should be {0.type}".format(
+ _field, type(__value)
+ )
+ raise TypeError(msg)
+
+ super().__setattr__(__name, __value)
+
+
+@dataclass
+class ConnectionConfig(BaseConfig):
+ hostname: Optional[str] = field(
+ default="localhost",
+ metadata={"help": "Hostname for submarine CLI to connect"},
+ )
+
+ port: Optional[int] = field(
+ default=32080,
+ metadata={"help": "Port for submarine CLI to connect"},
+ )
+
+
+@dataclass
+class SubmarineCliConfig(BaseConfig):
+ connection: ConnectionConfig = field(
+ default_factory=lambda: ConnectionConfig(),
+ metadata={"help": "Port for submarine CLI to connect"},
+ )
+
+
+def rgetattr(obj, attr, *args):
+ """
+ Recursive get attr
+ Example:
+ rgetattr(obj,"a.b.c") is equivalent to obj.a.b.c
+ """
+
+ def _getattr(obj, attr):
+ return getattr(obj, attr, *args)
+
+ return functools.reduce(_getattr, [obj] + attr.split("."))
+
+
+def rsetattr(obj, attr, val):
+ """
+ Recursive set attr
+ Example:
+ rsetattr(obj,"a.b.c",val) is equivalent to obj.a.b.c = val
+ """
+ pre, _, post = attr.rpartition(".")
+ if pre:
+ _r = rgetattr(obj, pre)
+ return setattr(_r, post, val)
+ else:
+ return setattr(obj, post, val)
+
+
+def loadConfig(config_path: str = CONFIG_YAML_PATH) -> Optional[SubmarineCliConfig]:
+ with open(config_path, "r") as stream:
+ try:
+ parsed_yaml: dict = yaml.safe_load(stream)
+ return_config: SubmarineCliConfig = dacite.from_dict(
+ data_class=SubmarineCliConfig, data=parsed_yaml
+ )
+ return return_config
+ except yaml.YAMLError as exc:
+ click.echo("Error Reading Config")
+ click.echo(exc)
+ return None
+
+
+def saveConfig(config: SubmarineCliConfig, config_path: str = CONFIG_YAML_PATH):
+ with open(config_path, "w") as stream:
+ try:
+ yaml.safe_dump({**asdict(config)}, stream)
+ except yaml.YAMLError as exc:
+ click.echo("Error Saving Config")
+ click.echo(exc)
+
+
+def initConfig(config_path: str = CONFIG_YAML_PATH):
+ saveConfig(SubmarineCliConfig(), config_path)
diff --git a/submarine-sdk/pysubmarine/submarine/cli/main.py b/submarine-sdk/pysubmarine/submarine/cli/main.py
index b8a699b..2998f9a 100644
--- a/submarine-sdk/pysubmarine/submarine/cli/main.py
+++ b/submarine-sdk/pysubmarine/submarine/cli/main.py
@@ -17,6 +17,7 @@
import click
+from submarine.cli.config import command as config_cmd
from submarine.cli.environment import command as environment_cmd
from submarine.cli.experiment import command as experiment_cmd
from submarine.cli.notebook import command as notebook_cmd
@@ -49,6 +50,11 @@ def cmdgrp_sandbox():
pass
+@entry_point.group("config")
+def cmdgrp_config():
+ pass
+
+
# experiment
cmdgrp_list.add_command(experiment_cmd.list_experiment)
cmdgrp_get.add_command(experiment_cmd.get_experiment)
@@ -65,3 +71,9 @@ cmdgrp_delete.add_command(environment_cmd.delete_environment)
# sandbox
cmdgrp_sandbox.add_command(sandbox_cmd.start_sandbox)
cmdgrp_sandbox.add_command(sandbox_cmd.delete_sandbox)
+
+# config
+cmdgrp_config.add_command(config_cmd.set_config)
+cmdgrp_config.add_command(config_cmd.list_config)
+cmdgrp_config.add_command(config_cmd.get_config)
+cmdgrp_config.add_command(config_cmd.init_config)
diff --git a/submarine-sdk/pysubmarine/tests/cli/test_config.py b/submarine-sdk/pysubmarine/tests/cli/test_config.py
new file mode 100644
index 0000000..23563d4
--- /dev/null
+++ b/submarine-sdk/pysubmarine/tests/cli/test_config.py
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.testing import CliRunner
+
+from submarine.cli import main
+from submarine.cli.config.config import SubmarineCliConfig, initConfig, loadConfig
+
+
+def test_list_config():
+ initConfig()
+ runner = CliRunner()
+ result = runner.invoke(main.entry_point, ["config", "list"])
+ _config = loadConfig()
+ assert result.exit_code == 0
+ assert "SubmarineCliConfig" in result.output
+ assert '"hostname": "{}"'.format(_config.connection.hostname) in result.output
+ assert '"port": {}'.format(_config.connection.port) in result.output
+
+
+def test_init_config():
+ runner = CliRunner()
+ result = runner.invoke(main.entry_point, ["config", "init"])
+ result = runner.invoke(main.entry_point, ["config", "list"])
+ _default_config = SubmarineCliConfig()
+ assert result.exit_code == 0
+ assert '"hostname": "{}"'.format(_default_config.connection.hostname) in result.output
+ assert '"port": {}'.format(_default_config.connection.port) in result.output
+
+
+def test_get_set_experiment():
+ initConfig()
+ mock_hostname = "mockhost"
+ runner = CliRunner()
+ result = runner.invoke(main.entry_point, ["config", "get", "connection.hostname"])
+ assert result.exit_code == 0
+ _config = loadConfig()
+ assert "connection.hostname={}".format(_config.connection.hostname) in result.output
+
+ result = runner.invoke(
+ main.entry_point, ["config", "set", "connection.hostname", mock_hostname]
+ )
+ assert result.exit_code == 0
+
+ result = runner.invoke(main.entry_point, ["config", "get", "connection.hostname"])
+ assert result.exit_code == 0
+ _config = loadConfig()
+ assert "connection.hostname={}".format(mock_hostname) in result.output
+ assert mock_hostname == _config.connection.hostname
+ initConfig()
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org