This is an automated email from the ASF dual-hosted git repository.

songjian pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new ec4ce2b  [python] Add config mechanism and cli subcommand config 
(#8585)
ec4ce2b is described below

commit ec4ce2b57313b2a38f4a36efac40a281a4f05f4e
Author: Jiajie Zhong <[email protected]>
AuthorDate: Tue Mar 1 11:16:44 2022 +0800

    [python] Add config mechanism and cli subcommand config (#8585)
    
    * [python] Add config mechanism and cli subcommand config
    
    * Add configuration.py mechanism for more easy change config
      and move some configs to it. It mechanism including
      configuration.py module and default_config.yaml file
    * Add `config` for cli subcommand allow users initialize, get,
      set configs
    
    close: #8344
    
    * Change setup.py format
---
 .../pydolphinscheduler/setup.py                    |   1 +
 .../src/pydolphinscheduler/cli/commands.py         |  44 +++++
 .../src/pydolphinscheduler/constants.py            |  20 --
 .../src/pydolphinscheduler/core/base_side.py       |   4 +-
 .../src/pydolphinscheduler/core/configuration.py   | 152 ++++++++++++++++
 .../pydolphinscheduler/core/default_config.yaml    |  43 +++++
 .../pydolphinscheduler/core/process_definition.py  |  27 ++-
 .../src/pydolphinscheduler/core/task.py            |   4 +-
 .../examples/task_dependent_example.py             |   6 +-
 .../src/pydolphinscheduler/exceptions.py           |   6 +
 .../src/pydolphinscheduler/java_gateway.py         |   7 +-
 .../src/pydolphinscheduler/side/project.py         |   6 +-
 .../src/pydolphinscheduler/side/queue.py           |   6 +-
 .../src/pydolphinscheduler/side/tenant.py          |   8 +-
 .../src/pydolphinscheduler/utils/path_dict.py      |  85 +++++++++
 .../pydolphinscheduler/tests/cli/test_config.py    | 201 +++++++++++++++++++++
 .../pydolphinscheduler/tests/cli/test_version.py   |   2 +-
 .../core/test_configuration.py}                    |  47 +++--
 .../core/test_default_config_yaml.py}              |  37 ++--
 .../tests/core/test_process_definition.py          |  42 ++---
 .../pydolphinscheduler/tests/testing/cli.py        |  10 +-
 .../pydolphinscheduler/tests/testing/constants.py  |   8 +
 .../pydolphinscheduler/tests/testing/path.py       |  13 +-
 .../tests/utils/test_path_dict.py                  | 201 +++++++++++++++++++++
 24 files changed, 842 insertions(+), 138 deletions(-)

diff --git a/dolphinscheduler-python/pydolphinscheduler/setup.py 
b/dolphinscheduler-python/pydolphinscheduler/setup.py
index 8be8d2a..2da8d2f 100644
--- a/dolphinscheduler-python/pydolphinscheduler/setup.py
+++ b/dolphinscheduler-python/pydolphinscheduler/setup.py
@@ -38,6 +38,7 @@ version = "2.0.4"
 prod = [
     "click>=8.0.0",
     "py4j~=0.10",
+    "pyyaml",
 ]
 
 build = [
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
index a430f45..5628799 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
@@ -21,6 +21,11 @@ import click
 from click import echo
 
 from pydolphinscheduler import __version__
+from pydolphinscheduler.core.configuration import (
+    get_single_config,
+    init_config_file,
+    set_single_config,
+)
 
 version_option_val = ["major", "minor", "micro"]
 
@@ -46,3 +51,42 @@ def version(part: str) -> None:
         echo(f"{__version__.split('.')[idx]}")
     else:
         echo(f"{__version__}")
+
+
[email protected]()
[email protected](
+    "--init",
+    "-i",
+    is_flag=True,
+    help="Initialize and create configuration file to 
`PYDOLPHINSCHEDULER_HOME`.",
+)
[email protected](
+    "--set",
+    "-s",
+    "setter",
+    multiple=True,
+    type=click.Tuple([str, str]),
+    help="Set specific setting to config file."
+    "Use multiple ``--set <KEY> <VAL>`` options to set multiple configs",
+)
[email protected](
+    "--get",
+    "-g",
+    "getter",
+    multiple=True,
+    type=str,
+    help="Get specific setting from config file."
+    "Use multiple ``--get <KEY>`` options to get multiple configs",
+)
+def config(getter, setter, init) -> None:
+    """Manage the configuration for pydolphinscheduler."""
+    if init:
+        init_config_file()
+    elif getter:
+        click.echo("The configuration query as below:\n")
+        configs_kv = [f"{key} = {get_single_config(key)}" for key in getter]
+        click.echo("\n".join(configs_kv))
+    elif setter:
+        for key, val in setter:
+            set_single_config(key, val)
+        click.echo("Set configuration done.")
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
index 65bf6c5..3992917 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/constants.py
@@ -25,22 +25,6 @@ class ProcessDefinitionReleaseState:
     OFFLINE: str = "OFFLINE"
 
 
-class ProcessDefinitionDefault:
-    """Constants default value for 
:class:`pydolphinscheduler.core.process_definition.ProcessDefinition`."""
-
-    PROJECT: str = "project-pydolphin"
-    TENANT: str = "tenant_pydolphin"
-    USER: str = "userPythonGateway"
-    # TODO simple set password same as username
-    USER_PWD: str = "userPythonGateway"
-    USER_EMAIL: str = "[email protected]"
-    USER_PHONE: str = "11111111111"
-    USER_STATE: int = 1
-    QUEUE: str = "queuePythonGateway"
-    WORKER_GROUP: str = "default"
-    TIME_ZONE: str = "Asia/Shanghai"
-
-
 class TaskPriority(str):
     """Constants for task priority."""
 
@@ -99,10 +83,6 @@ class JavaGatewayDefault(str):
 
     RESULT_DATA = "data"
 
-    SERVER_ADDRESS = "127.0.0.1"
-    SERVER_PORT = 25333
-    AUTO_CONVERT = True
-
 
 class Delimiter(str):
     """Constants for delimiter."""
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
index ed20d70..dca1f12 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/base_side.py
@@ -19,7 +19,7 @@
 
 from typing import Optional
 
-from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base import Base
 
 
@@ -34,7 +34,7 @@ class BaseSide(Base):
         cls,
         # TODO comment for avoiding cycle import
         # user: Optional[User] = ProcessDefinitionDefault.USER
-        user=ProcessDefinitionDefault.USER,
+        user=configuration.WORKFLOW_USER,
     ):
         """Create Base if not exists."""
         raise NotImplementedError
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
new file mode 100644
index 0000000..ec45876
--- /dev/null
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/configuration.py
@@ -0,0 +1,152 @@
+# 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.
+
+"""Configuration module for pydolphinscheduler."""
+
+import copy
+import os
+from pathlib import Path
+from typing import Any, Dict
+
+import yaml
+
+from pydolphinscheduler.exceptions import PyDSConfException, PyDSParamException
+from pydolphinscheduler.utils.path_dict import PathDict
+
+DEFAULT_CONFIG_PATH = 
Path(__file__).resolve().parent.joinpath("default_config.yaml")
+
+
+def get_config_file_path() -> Path:
+    """Get the path of pydolphinscheduler configuration file."""
+    pyds_home = os.environ.get("PYDOLPHINSCHEDULER_HOME", 
"~/pydolphinscheduler")
+    config_file_path = Path(pyds_home).joinpath("config.yaml").expanduser()
+    return config_file_path
+
+
+def read_yaml(path: str) -> Dict:
+    """Read configs dict from configuration file.
+
+    :param path: The path of configuration file.
+    """
+    with open(path, "r") as f:
+        return yaml.safe_load(f)
+
+
+def write_yaml(context: Dict, path: str) -> None:
+    """Write configs dict to configuration file.
+
+    :param context: The configs dict write to configuration file.
+    :param path: The path of configuration file.
+    """
+    parent = Path(path).parent
+    if not parent.exists():
+        parent.mkdir(parents=True)
+    with open(path, mode="w") as f:
+        f.write(yaml.dump(context))
+
+
+def default_yaml_config() -> Dict:
+    """Get default configs in ``DEFAULT_CONFIG_PATH``."""
+    with open(DEFAULT_CONFIG_PATH, "r") as f:
+        return yaml.safe_load(f)
+
+
+def _whether_exists_config() -> bool:
+    """Check whether config file already exists in 
:func:`get_config_file_path`."""
+    return True if get_config_file_path().exists() else False
+
+
+def get_all_configs():
+    """Get all configs from configuration file."""
+    exists = _whether_exists_config()
+    if exists:
+        return read_yaml(str(get_config_file_path()))
+    else:
+        return default_yaml_config()
+
+
+# Add configs as module variables to avoid read configuration multiple times 
when
+#  Get common configuration setting
+#  Set or get multiple configs in single time
+configs = get_all_configs()
+
+
+def init_config_file() -> None:
+    """Initialize configuration file to :func:`get_config_file_path`."""
+    if _whether_exists_config():
+        raise PyDSConfException(
+            "Initialize configuration false to avoid overwrite configure by 
accident, file already exists "
+            "in %s, if you wan to overwrite the exists configure please remove 
the exists file manually.",
+            str(get_config_file_path()),
+        )
+    write_yaml(context=default_yaml_config(), path=str(get_config_file_path()))
+
+
+def get_single_config(key: str) -> Any:
+    """Get single config to configuration file.
+
+    :param key: The config path want get.
+    """
+    global configs
+    config_path_dict = PathDict(configs)
+    if key not in config_path_dict:
+        raise PyDSParamException(
+            "Configuration path %s do not exists. Can not get configuration.", 
key
+        )
+    return config_path_dict.__getattr__(key)
+
+
+def set_single_config(key: str, value: Any) -> None:
+    """Change single config to configuration file.
+
+    :param key: The config path want change.
+    :param value: The new value want to set.
+    """
+    global configs
+    config_path_dict = PathDict(configs)
+    if key not in config_path_dict:
+        raise PyDSParamException(
+            "Configuration path %s do not exists. Can not set configuration.", 
key
+        )
+    config_path_dict.__setattr__(key, value)
+    write_yaml(context=dict(config_path_dict), 
path=str(get_config_file_path()))
+
+
+# Start Common Configuration Settings
+path_configs = PathDict(copy.deepcopy(configs))
+
+# Java Gateway Settings
+JAVA_GATEWAY_ADDRESS = str(getattr(path_configs, "java_gateway.address"))
+JAVA_GATEWAY_PORT = str(getattr(path_configs, "java_gateway.port"))
+JAVA_GATEWAY_AUTO_CONVERT = str(getattr(path_configs, 
"java_gateway.auto_convert"))
+
+# User Settings
+USER_NAME = str(getattr(path_configs, "default.user.name"))
+USER_PASSWORD = str(getattr(path_configs, "default.user.password"))
+USER_EMAIL = str(getattr(path_configs, "default.user.email"))
+USER_PHONE = str(getattr(path_configs, "default.user.phone"))
+USER_STATE = str(getattr(path_configs, "default.user.state"))
+
+# Workflow Settings
+WORKFLOW_PROJECT = str(getattr(path_configs, "default.workflow.project"))
+WORKFLOW_TENANT = str(getattr(path_configs, "default.workflow.tenant"))
+WORKFLOW_USER = str(getattr(path_configs, "default.workflow.user"))
+WORKFLOW_QUEUE = str(getattr(path_configs, "default.workflow.queue"))
+WORKFLOW_WORKER_GROUP = str(getattr(path_configs, 
"default.workflow.worker_group"))
+WORKFLOW_TIME_ZONE = str(getattr(path_configs, "default.workflow.time_zone"))
+
+# End Common Configuration Setting
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
new file mode 100644
index 0000000..45b1346
--- /dev/null
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/default_config.yaml
@@ -0,0 +1,43 @@
+# 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.
+
+java_gateway:
+  # The address of Python gateway server start. Set its value to `0.0.0.0` if 
your Python API run in different
+  # between Python gateway server. It could be be specific to other address 
like `127.0.0.1` or `localhost`
+  address: 127.0.0.1
+  # The port of Python gateway server start. Define which port you could 
connect to Python gateway server from
+  # Python API side.
+  port: 25333
+  
+  auto_convert: true
+
+default:
+  user:
+    name: userPythonGateway
+    # TODO simple set password same as username
+    password: userPythonGateway
+    email: [email protected]
+    tenant: tenant_pydolphin
+    phone: 11111111111
+    state: 1
+  workflow:
+    project: project-pydolphin
+    tenant: tenant_pydolphin
+    user: userPythonGateway
+    queue: queuePythonGateway
+    worker_group: default
+    time_zone: Asia/Shanghai
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
index 1c123fc..dd2b83a 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/process_definition.py
@@ -21,11 +21,8 @@ import json
 from datetime import datetime
 from typing import Any, Dict, List, Optional, Set
 
-from pydolphinscheduler.constants import (
-    ProcessDefinitionDefault,
-    ProcessDefinitionReleaseState,
-    TaskType,
-)
+from pydolphinscheduler.constants import ProcessDefinitionReleaseState, 
TaskType
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base import Base
 from pydolphinscheduler.exceptions import PyDSParamException, 
PyDSTaskNoFoundException
 from pydolphinscheduler.java_gateway import launch_gateway
@@ -90,12 +87,12 @@ class ProcessDefinition(Base):
         schedule: Optional[str] = None,
         start_time: Optional[str] = None,
         end_time: Optional[str] = None,
-        timezone: Optional[str] = ProcessDefinitionDefault.TIME_ZONE,
-        user: Optional[str] = ProcessDefinitionDefault.USER,
-        project: Optional[str] = ProcessDefinitionDefault.PROJECT,
-        tenant: Optional[str] = ProcessDefinitionDefault.TENANT,
-        queue: Optional[str] = ProcessDefinitionDefault.QUEUE,
-        worker_group: Optional[str] = ProcessDefinitionDefault.WORKER_GROUP,
+        timezone: Optional[str] = configuration.WORKFLOW_TIME_ZONE,
+        user: Optional[str] = configuration.WORKFLOW_USER,
+        project: Optional[str] = configuration.WORKFLOW_PROJECT,
+        tenant: Optional[str] = configuration.WORKFLOW_TENANT,
+        queue: Optional[str] = configuration.WORKFLOW_QUEUE,
+        worker_group: Optional[str] = configuration.WORKFLOW_WORKER_GROUP,
         timeout: Optional[int] = 0,
         release_state: Optional[str] = ProcessDefinitionReleaseState.ONLINE,
         param: Optional[Dict] = None,
@@ -153,12 +150,12 @@ class ProcessDefinition(Base):
         """
         return User(
             self._user,
-            ProcessDefinitionDefault.USER_PWD,
-            ProcessDefinitionDefault.USER_EMAIL,
-            ProcessDefinitionDefault.USER_PHONE,
+            configuration.USER_PASSWORD,
+            configuration.USER_EMAIL,
+            configuration.USER_PHONE,
             self._tenant,
             self._queue,
-            ProcessDefinitionDefault.USER_STATE,
+            configuration.USER_STATE,
         )
 
     @staticmethod
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
index 693f508..9bc8d45 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/core/task.py
@@ -22,11 +22,11 @@ from typing import Dict, List, Optional, Sequence, Set, 
Tuple, Union
 
 from pydolphinscheduler.constants import (
     Delimiter,
-    ProcessDefinitionDefault,
     TaskFlag,
     TaskPriority,
     TaskTimeoutFlag,
 )
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base import Base
 from pydolphinscheduler.core.process_definition import (
     ProcessDefinition,
@@ -104,7 +104,7 @@ class Task(Base):
         description: Optional[str] = None,
         flag: Optional[str] = TaskFlag.YES,
         task_priority: Optional[str] = TaskPriority.MEDIUM,
-        worker_group: Optional[str] = ProcessDefinitionDefault.WORKER_GROUP,
+        worker_group: Optional[str] = configuration.WORKFLOW_WORKER_GROUP,
         delay_time: Optional[int] = 0,
         fail_retry_times: Optional[int] = 0,
         fail_retry_interval: Optional[int] = 1,
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/examples/task_dependent_example.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/examples/task_dependent_example.py
index ae19d95..88d6ea2 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/examples/task_dependent_example.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/examples/task_dependent_example.py
@@ -35,7 +35,7 @@ task_dependent:
 
 task_dependent(this task dependent on task_dependent_external.task_1 and 
task_dependent_external.task_2).
 """
-from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.process_definition import ProcessDefinition
 from pydolphinscheduler.tasks.dependent import And, Dependent, DependentItem, 
Or
 from pydolphinscheduler.tasks.shell import Shell
@@ -58,12 +58,12 @@ with ProcessDefinition(
         dependence=And(
             Or(
                 DependentItem(
-                    project_name=ProcessDefinitionDefault.PROJECT,
+                    project_name=configuration.WORKFLOW_PROJECT,
                     process_definition_name="task_dependent_external",
                     dependent_task_name="task_1",
                 ),
                 DependentItem(
-                    project_name=ProcessDefinitionDefault.PROJECT,
+                    project_name=configuration.WORKFLOW_PROJECT,
                     process_definition_name="task_dependent_external",
                     dependent_task_name="task_2",
                 ),
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
index 745ef3e..1cdc254 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
@@ -44,3 +44,9 @@ class PyDSJavaGatewayException(PyDSBaseException):
 
 class PyDSProcessDefinitionNotAssignException(PyDSBaseException):
     """Exception for pydolphinscheduler process definition not assign error."""
+
+
+class PyDSConfException(PyDSBaseException):
+    """Exception for pydolphinscheduler configuration error."""
+
+    pass
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
index 2876ed5..8560638 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/java_gateway.py
@@ -23,6 +23,7 @@ from py4j.java_collections import JavaMap
 from py4j.java_gateway import GatewayParameters, JavaGateway
 
 from pydolphinscheduler.constants import JavaGatewayDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.exceptions import PyDSJavaGatewayException
 
 
@@ -38,9 +39,9 @@ def launch_gateway(
     This is why automatic conversion is disabled by default.
     """
     gateway_parameters = GatewayParameters(
-        address=address or JavaGatewayDefault.SERVER_ADDRESS,
-        port=port or JavaGatewayDefault.SERVER_PORT,
-        auto_convert=auto_convert or JavaGatewayDefault.AUTO_CONVERT,
+        address=address or configuration.JAVA_GATEWAY_ADDRESS,
+        port=port or configuration.JAVA_GATEWAY_PORT,
+        auto_convert=auto_convert or configuration.JAVA_GATEWAY_AUTO_CONVERT,
     )
     gateway = JavaGateway(gateway_parameters=gateway_parameters)
     return gateway
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
index 02382ca..b568cb4 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/project.py
@@ -19,7 +19,7 @@
 
 from typing import Optional
 
-from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base_side import BaseSide
 from pydolphinscheduler.java_gateway import launch_gateway
 
@@ -29,12 +29,12 @@ class Project(BaseSide):
 
     def __init__(
         self,
-        name: str = ProcessDefinitionDefault.PROJECT,
+        name: str = configuration.WORKFLOW_PROJECT,
         description: Optional[str] = None,
     ):
         super().__init__(name, description)
 
-    def create_if_not_exists(self, user=ProcessDefinitionDefault.USER) -> None:
+    def create_if_not_exists(self, user=configuration.USER_NAME) -> None:
         """Create Project if not exists."""
         gateway = launch_gateway()
         gateway.entry_point.createProject(user, self.name, self.description)
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
index 9d6664e..e7c68e1 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/queue.py
@@ -19,7 +19,7 @@
 
 from typing import Optional
 
-from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base_side import BaseSide
 from pydolphinscheduler.java_gateway import gateway_result_checker, 
launch_gateway
 
@@ -29,12 +29,12 @@ class Queue(BaseSide):
 
     def __init__(
         self,
-        name: str = ProcessDefinitionDefault.QUEUE,
+        name: str = configuration.WORKFLOW_QUEUE,
         description: Optional[str] = "",
     ):
         super().__init__(name, description)
 
-    def create_if_not_exists(self, user=ProcessDefinitionDefault.USER) -> None:
+    def create_if_not_exists(self, user=configuration.USER_NAME) -> None:
         """Create Queue if not exists."""
         gateway = launch_gateway()
         # Here we set Queue.name and Queue.queueName same as self.name
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
index 508c033..6aaabfe 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/side/tenant.py
@@ -19,7 +19,7 @@
 
 from typing import Optional
 
-from pydolphinscheduler.constants import ProcessDefinitionDefault
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.base_side import BaseSide
 from pydolphinscheduler.java_gateway import launch_gateway
 
@@ -29,15 +29,15 @@ class Tenant(BaseSide):
 
     def __init__(
         self,
-        name: str = ProcessDefinitionDefault.TENANT,
-        queue: str = ProcessDefinitionDefault.QUEUE,
+        name: str = configuration.WORKFLOW_TENANT,
+        queue: str = configuration.WORKFLOW_QUEUE,
         description: Optional[str] = None,
     ):
         super().__init__(name, description)
         self.queue = queue
 
     def create_if_not_exists(
-        self, queue_name: str, user=ProcessDefinitionDefault.USER
+        self, queue_name: str, user=configuration.USER_NAME
     ) -> None:
         """Create Tenant if not exists."""
         gateway = launch_gateway()
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
new file mode 100644
index 0000000..cf836c9
--- /dev/null
+++ 
b/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/utils/path_dict.py
@@ -0,0 +1,85 @@
+# 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.
+
+"""Path dict allow users access value by key chain, like 
`var.key1.key2.key3`."""
+
+
+# TODO maybe we should rewrite it by `collections.abc.MutableMapping` later,
+#  according to https://stackoverflow.com/q/3387691/7152658
+class PathDict(dict):
+    """Path dict allow users access value by key chain, like 
`var.key1.key2.key3`."""
+
+    def __init__(self, original=None):
+        super().__init__()
+        if original is None:
+            pass
+        elif isinstance(original, dict):
+            for key in original:
+                self.__setitem__(key, original[key])
+        else:
+            raise TypeError(
+                "Parameter original expected dict type but get %s", 
type(original)
+            )
+
+    def __getitem__(self, key):
+        if "." not in key:
+            # try:
+            return dict.__getitem__(self, key)
+        # except KeyError:
+        #     # cPickle would get error when key without value pairs, in this 
case we just skip it
+        #     return
+        my_key, rest_of_key = key.split(".", 1)
+        target = dict.__getitem__(self, my_key)
+        if not isinstance(target, PathDict):
+            raise KeyError(
+                'Cannot get "%s" to (%s) as sub-key of "%s".'
+                % (rest_of_key, repr(target), my_key)
+            )
+        return target[rest_of_key]
+
+    def __setitem__(self, key, value):
+        if "." in key:
+            my_key, rest_of_key = key.split(".", 1)
+            target = self.setdefault(my_key, PathDict())
+            if not isinstance(target, PathDict):
+                raise KeyError(
+                    'Cannot set "%s" from (%s) as sub-key of "%s"'
+                    % (rest_of_key, repr(target), my_key)
+                )
+            target[rest_of_key] = value
+        else:
+            if isinstance(value, dict) and not isinstance(value, PathDict):
+                value = PathDict(value)
+            dict.__setitem__(self, key, value)
+
+    def __contains__(self, key):
+        if "." not in key:
+            return dict.__contains__(self, key)
+        my_key, rest_of_key = key.split(".", 1)
+        target = dict.__getitem__(self, my_key)
+        if not isinstance(target, PathDict):
+            return False
+        return rest_of_key in target
+
+    def setdefault(self, key, default):
+        """Overwrite method dict.setdefault."""
+        if key not in self:
+            self[key] = default
+        return self[key]
+
+    __setattr__ = __setitem__
+    __getattr__ = __getitem__
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py
new file mode 100644
index 0000000..7d5f88b
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_config.py
@@ -0,0 +1,201 @@
+# 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.
+
+"""Test command line interface subcommand `config`."""
+
+import os
+from pathlib import Path
+
+import pytest
+
+from pydolphinscheduler.cli.commands import cli
+from pydolphinscheduler.core.configuration import get_config_file_path
+from tests.testing.cli import CliTestWrapper
+from tests.testing.constants import DEV_MODE
+
+default_config_path = "~/pydolphinscheduler"
+config_file = "config.yaml"
+
+
[email protected]
+def delete_tmp_config_file():
+    """Util for deleting temp configuration file after test finish."""
+    yield
+    config_file_path = get_config_file_path()
+    config_file_path.unlink()
+
+
[email protected](
+    DEV_MODE,
+    reason="Avoid delete ~/pydolphinscheduler/config.yaml by accident when 
test locally.",
+)
[email protected](
+    "home",
+    [
+        None,
+        "/tmp/pydolphinscheduler",
+        "/tmp/test_abc",
+    ],
+)
+def test_config_init(delete_tmp_config_file, home):
+    """Test command line interface `config --init`."""
+    if home:
+        os.environ["PYDOLPHINSCHEDULER_HOME"] = home
+        config_path = home
+    else:
+        config_path = default_config_path
+
+    path = Path(config_path).joinpath(config_file).expanduser()
+    assert not path.exists()
+
+    cli_test = CliTestWrapper(cli, ["config", "--init"])
+    cli_test.assert_success()
+
+    assert path.exists()
+    # TODO We have a bug here, yaml dump do not support comment
+    # with path.open(mode="r") as cli_crt, open(path_default_config_yaml, "r") 
as src:
+    #     assert src.read() == cli_crt.read()
+
+
[email protected](
+    "key, expect",
+    [
+        # We test each key in one single section
+        ("java_gateway.address", "127.0.0.1"),
+        ("default.user.name", "userPythonGateway"),
+        ("default.workflow.project", "project-pydolphin"),
+    ],
+)
+def test_config_get(delete_tmp_config_file, key: str, expect: str):
+    """Test command line interface `config --get XXX`."""
+    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    cli_test = CliTestWrapper(cli, ["config", "--init"])
+    cli_test.assert_success()
+
+    cli_test = CliTestWrapper(cli, ["config", "--get", key])
+    cli_test.assert_success(output=f"{key} = {expect}", fuzzy=True)
+
+
[email protected](
+    "keys, expects",
+    [
+        # We test mix section keys
+        (("java_gateway.address", "java_gateway.port"), ("127.0.0.1", 
"25333")),
+        (
+            ("java_gateway.auto_convert", "default.user.tenant"),
+            ("True", "tenant_pydolphin"),
+        ),
+        (
+            (
+                "java_gateway.port",
+                "default.user.state",
+                "default.workflow.worker_group",
+            ),
+            ("25333", "1", "default"),
+        ),
+    ],
+)
+def test_config_get_multiple(delete_tmp_config_file, keys: str, expects: str):
+    """Test command line interface `config --get KEY1 --get KEY2 ...`."""
+    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    cli_test = CliTestWrapper(cli, ["config", "--init"])
+    cli_test.assert_success()
+
+    get_args = ["config"]
+    for key in keys:
+        get_args.append("--get")
+        get_args.append(key)
+    cli_test = CliTestWrapper(cli, get_args)
+
+    for idx, expect in enumerate(expects):
+        cli_test.assert_success(output=f"{keys[idx]} = {expect}", fuzzy=True)
+
+
+# TODO fix command `config --set KEY VAL`
[email protected](reason="We still have bug in command `config --set KEY VAL`")
[email protected](
+    "key, value",
+    [
+        # We test each key in one single section
+        ("java_gateway.address", "127.1.1.1"),
+        ("default.user.name", "editUserPythonGateway"),
+        ("default.workflow.project", "edit-project-pydolphin"),
+    ],
+)
+def test_config_set(delete_tmp_config_file, key: str, value: str):
+    """Test command line interface `config --set KEY VALUE`."""
+    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    cli_test = CliTestWrapper(cli, ["config", "--init"])
+    cli_test.assert_success()
+
+    # Make sure value do not exists first
+    cli_test = CliTestWrapper(cli, ["config", "--get", key])
+    assert f"{key} = {value}" not in cli_test.result.output
+
+    cli_test = CliTestWrapper(cli, ["config", "--set", key, value])
+    cli_test.assert_success()
+
+    cli_test = CliTestWrapper(cli, ["config", "--get", key])
+    assert f"{key} = {value}" in cli_test.result.output
+
+
+# TODO do not skip `config --set KEY1 VAL1 --set KEY2 VAL2`
[email protected](
+    reason="We still have bug in command `config --set KEY1 VAL1 --set KEY2 
VAL2`"
+)
[email protected](
+    "keys, values",
+    [
+        # We test each key in mixture section
+        (("java_gateway.address", "java_gateway.port"), ("127.1.1.1", 
"25444")),
+        (
+            ("java_gateway.auto_convert", "default.user.tenant"),
+            ("False", "edit_tenant_pydolphin"),
+        ),
+        (
+            (
+                "java_gateway.port",
+                "default.user.state",
+                "default.workflow.worker_group",
+            ),
+            ("25555", "0", "not-default"),
+        ),
+    ],
+)
+def test_config_set_multiple(delete_tmp_config_file, keys: str, values: str):
+    """Test command line interface `config --set KEY1 VAL1 --set KEY2 VAL2`."""
+    os.environ["PYDOLPHINSCHEDULER_HOME"] = "/tmp/pydolphinscheduler"
+    cli_test = CliTestWrapper(cli, ["config", "--init"])
+    cli_test.assert_success()
+
+    set_args = ["config"]
+    for idx, key in enumerate(keys):
+        # Make sure values do not exists first
+        cli_test = CliTestWrapper(cli, ["config", "--get", key])
+        assert f"{key} = {values[idx]}" not in cli_test.result.output
+
+        set_args.append("--set")
+        set_args.append(key)
+        set_args.append(values[idx])
+
+    cli_test = CliTestWrapper(cli, set_args)
+    cli_test.assert_success()
+
+    for idx, key in enumerate(keys):
+        # Make sure values exists after `config --set` run
+        cli_test = CliTestWrapper(cli, ["config", "--get", key])
+        assert f"{key} = {values[idx]}" in cli_test.result.output
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py
index df17759..f0dcb0e 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/cli/test_version.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Test command line interface subcommand version."""
+"""Test command line interface subcommand `version`."""
 
 import pytest
 
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
 b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
similarity index 51%
copy from 
dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
copy to 
dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
index a430f45..b055cd1 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/cli/commands.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_configuration.py
@@ -15,34 +15,31 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Commands line interface's command of pydolphinscheduler."""
+"""Test class :mod:`pydolphinscheduler.core.configuration`' method."""
 
-import click
-from click import echo
+import os
+from pathlib import Path
 
-from pydolphinscheduler import __version__
+import pytest
 
-version_option_val = ["major", "minor", "micro"]
+from pydolphinscheduler.core import configuration
 
 
[email protected]()
-def cli():
-    """Apache DolphinScheduler Python API's command line interface."""
-
-
[email protected]()
[email protected](
-    "--part",
-    "-p",
-    required=False,
-    type=click.Choice(version_option_val, case_sensitive=False),
-    multiple=False,
-    help="The part of version your want to get.",
[email protected](
+    "env, expect",
+    [
+        (None, "~/pydolphinscheduler"),
+        ("/tmp/pydolphinscheduler", "/tmp/pydolphinscheduler"),
+        ("/tmp/test_abc", "/tmp/test_abc"),
+    ],
 )
-def version(part: str) -> None:
-    """Show current version of pydolphinscheduler."""
-    if part:
-        idx = version_option_val.index(part)
-        echo(f"{__version__.split('.')[idx]}")
-    else:
-        echo(f"{__version__}")
+def test_get_config_file_path(env, expect):
+    """Test get config file path method."""
+    # Avoid env setting by other tests
+    os.environ.pop("PYDOLPHINSCHEDULER_HOME", None)
+    if env:
+        os.environ["PYDOLPHINSCHEDULER_HOME"] = env
+    assert (
+        Path(expect).joinpath("config.yaml").expanduser()
+        == configuration.get_config_file_path()
+    )
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
similarity index 52%
copy from 
dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
copy to 
dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
index 745ef3e..050cc52 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/src/pydolphinscheduler/exceptions.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_default_config_yaml.py
@@ -15,32 +15,25 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Exceptions for pydolphinscheduler."""
+"""Test default config file."""
 
+from typing import Dict
 
-class PyDSBaseException(Exception):
-    """Base exception for pydolphinscheduler."""
+import yaml
 
-    pass
+from tests.testing.path import path_default_config_yaml
 
 
-class PyDSParamException(PyDSBaseException):
-    """Exception for pydolphinscheduler parameter verify error."""
+def nested_key_check(test_dict: Dict) -> None:
+    """Test whether default configuration file exists specific character."""
+    for key, val in test_dict.items():
+        assert "." not in key, f"There is not allowed special character in key 
`{key}`."
+        if isinstance(val, dict):
+            nested_key_check(val)
 
-    pass
 
-
-class PyDSTaskNoFoundException(PyDSBaseException):
-    """Exception for pydolphinscheduler workflow task no found error."""
-
-    pass
-
-
-class PyDSJavaGatewayException(PyDSBaseException):
-    """Exception for pydolphinscheduler Java gateway error."""
-
-    pass
-
-
-class PyDSProcessDefinitionNotAssignException(PyDSBaseException):
-    """Exception for pydolphinscheduler process definition not assign error."""
+def test_key_without_dot_delimiter():
+    """Test wrapper of whether default configuration file exists specific 
character."""
+    with open(path_default_config_yaml, "r") as f:
+        default_config = yaml.safe_load(f)
+        nested_key_check(default_config)
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
index f51338d..655b7fd 100644
--- 
a/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
+++ 
b/dolphinscheduler-python/pydolphinscheduler/tests/core/test_process_definition.py
@@ -24,10 +24,8 @@ from unittest.mock import patch
 import pytest
 from freezegun import freeze_time
 
-from pydolphinscheduler.constants import (
-    ProcessDefinitionDefault,
-    ProcessDefinitionReleaseState,
-)
+from pydolphinscheduler.constants import ProcessDefinitionReleaseState
+from pydolphinscheduler.core import configuration
 from pydolphinscheduler.core.process_definition import ProcessDefinition
 from pydolphinscheduler.exceptions import PyDSParamException
 from pydolphinscheduler.side import Project, Tenant, User
@@ -51,22 +49,22 @@ def test_process_definition_key_attr(func):
 @pytest.mark.parametrize(
     "name,value",
     [
-        ("timezone", ProcessDefinitionDefault.TIME_ZONE),
-        ("project", Project(ProcessDefinitionDefault.PROJECT)),
-        ("tenant", Tenant(ProcessDefinitionDefault.TENANT)),
+        ("timezone", configuration.WORKFLOW_TIME_ZONE),
+        ("project", Project(configuration.WORKFLOW_PROJECT)),
+        ("tenant", Tenant(configuration.WORKFLOW_TENANT)),
         (
             "user",
             User(
-                ProcessDefinitionDefault.USER,
-                ProcessDefinitionDefault.USER_PWD,
-                ProcessDefinitionDefault.USER_EMAIL,
-                ProcessDefinitionDefault.USER_PHONE,
-                ProcessDefinitionDefault.TENANT,
-                ProcessDefinitionDefault.QUEUE,
-                ProcessDefinitionDefault.USER_STATE,
+                configuration.USER_NAME,
+                configuration.USER_PASSWORD,
+                configuration.USER_EMAIL,
+                configuration.USER_PHONE,
+                configuration.WORKFLOW_TENANT,
+                configuration.WORKFLOW_QUEUE,
+                configuration.USER_STATE,
             ),
         ),
-        ("worker_group", ProcessDefinitionDefault.WORKER_GROUP),
+        ("worker_group", configuration.WORKFLOW_WORKER_GROUP),
         ("release_state", ProcessDefinitionReleaseState.ONLINE),
     ],
 )
@@ -233,9 +231,9 @@ def test_process_definition_get_define_without_task():
     expect = {
         "name": TEST_PROCESS_DEFINITION_NAME,
         "description": None,
-        "project": ProcessDefinitionDefault.PROJECT,
-        "tenant": ProcessDefinitionDefault.TENANT,
-        "workerGroup": ProcessDefinitionDefault.WORKER_GROUP,
+        "project": configuration.WORKFLOW_PROJECT,
+        "tenant": configuration.WORKFLOW_TENANT,
+        "workerGroup": configuration.WORKFLOW_WORKER_GROUP,
         "timeout": 0,
         "releaseState": ProcessDefinitionReleaseState.ONLINE,
         "param": None,
@@ -318,8 +316,8 @@ def test_process_definition_simple_separate():
 def test_set_process_definition_user_attr(user_attrs):
     """Test user with correct attributes if we specific assigned to process 
definition object."""
     default_value = {
-        "tenant": ProcessDefinitionDefault.TENANT,
-        "queue": ProcessDefinitionDefault.QUEUE,
+        "tenant": configuration.WORKFLOW_TENANT,
+        "queue": configuration.WORKFLOW_QUEUE,
     }
     with ProcessDefinition(TEST_PROCESS_DEFINITION_NAME, **user_attrs) as pd:
         user = pd.user
@@ -407,13 +405,13 @@ def test_schedule_json_start_and_end_time(start_time, 
end_time, expect_date):
         "crontab": schedule,
         "startTime": expect_date["start_time"],
         "endTime": expect_date["end_time"],
-        "timezoneId": ProcessDefinitionDefault.TIME_ZONE,
+        "timezoneId": configuration.WORKFLOW_TIME_ZONE,
     }
     with ProcessDefinition(
         TEST_PROCESS_DEFINITION_NAME,
         schedule=schedule,
         start_time=start_time,
         end_time=end_time,
-        timezone=ProcessDefinitionDefault.TIME_ZONE,
+        timezone=configuration.WORKFLOW_TIME_ZONE,
     ) as pd:
         assert pd.schedule_json == expect
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py
index 1585920..0d2c1d1 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/cli.py
@@ -18,17 +18,14 @@
 """Utils of command line test."""
 
 
-import os
-
 from click.testing import CliRunner
 
+from tests.testing.constants import DEV_MODE
+
 
 class CliTestWrapper:
     """Wrap command click CliRunner.invoke."""
 
-    _dev_mode_env_name = "PY_DOLPHINSCHEDULER_DEV_MODE"
-    _dev_mode_true_val = {"true", "t", "1"}
-
     def __init__(self, *args, **kwargs):
         runner = CliRunner()
         self.result = runner.invoke(*args, **kwargs)
@@ -55,8 +52,7 @@ class CliTestWrapper:
         It read variable named `PY_DOLPHINSCHEDULER_DEV_MODE` from env, when 
it set to `true` or `t` or `1`
         will print result output when class :class:`CliTestWrapper` is 
initialization.
         """
-        dev_mode = str(os.getenv(self._dev_mode_env_name))
-        if dev_mode.strip().lower() in self._dev_mode_true_val:
+        if DEV_MODE:
             print(f"\n{self.result.output}\n")
 
     def assert_success(self, output: str = None, fuzzy: bool = False):
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
index 1552192..dcc32a6 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/constants.py
@@ -17,6 +17,8 @@
 
 """Constants variables for test module."""
 
+import os
+
 # Record some task without example in directory `example`. Some of them maybe 
can not write example,
 # but most of them just without adding by mistake, and we should add it later.
 task_without_example = {
@@ -26,3 +28,9 @@ task_without_example = {
     "python",
     "procedure",
 }
+
+# whether in dev mode, if true we will add or remove some tests. Or make be 
and more detail infos when
+# test failed.
+DEV_MODE = str(
+    os.environ.get("PY_DOLPHINSCHEDULER_DEV_MODE", False)
+).strip().lower() in {"true", "t", "1"}
diff --git a/dolphinscheduler-python/pydolphinscheduler/tests/testing/path.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/testing/path.py
index 2e75be2..d1e520b 100644
--- a/dolphinscheduler-python/pydolphinscheduler/tests/testing/path.py
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/testing/path.py
@@ -20,13 +20,14 @@
 from pathlib import Path
 from typing import Any, Generator
 
-path_code_tasks = Path(__file__).parent.parent.parent.joinpath(
-    "src", "pydolphinscheduler", "tasks"
-)
-path_example = Path(__file__).parent.parent.parent.joinpath(
-    "src", "pydolphinscheduler", "examples"
+project_root = Path(__file__).parent.parent.parent
+
+path_code_tasks = project_root.joinpath("src", "pydolphinscheduler", "tasks")
+path_example = project_root.joinpath("src", "pydolphinscheduler", "examples")
+path_doc_tasks = project_root.joinpath("docs", "source", "tasks")
+path_default_config_yaml = project_root.joinpath(
+    "src", "pydolphinscheduler", "core", "default_config.yaml"
 )
-path_doc_tasks = Path(__file__).parent.parent.parent.joinpath("docs", 
"source", "tasks")
 
 
 def get_all_examples() -> Generator[Path, Any, None]:
diff --git 
a/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py 
b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py
new file mode 100644
index 0000000..92e4b2f
--- /dev/null
+++ b/dolphinscheduler-python/pydolphinscheduler/tests/utils/test_path_dict.py
@@ -0,0 +1,201 @@
+# 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.
+
+"""Test utils.path_dict module."""
+
+import copy
+from typing import Dict, Tuple
+
+import pytest
+
+from pydolphinscheduler.utils.path_dict import PathDict
+
+src_dict_list = [
+    # dict with one single level
+    {"a": 1},
+    # dict with two levels, with same nested keys 'b'
+    {"a": 1, "b": 2, "c": {"d": 3}, "e": {"b": 4}},
+    # dict with three levels, with same nested keys 'b'
+    {"a": 1, "b": 2, "c": {"d": 3}, "e": {"b": {"b": 4}, "f": 5}},
+    # dict with specific key container
+    {
+        "a": 1,
+        "a-b": 2,
+    },
+]
+
+
[email protected]("org", src_dict_list)
+def test_val_between_dict_and_path_dict(org: Dict):
+    """Test path dict equal to original dict."""
+    path_dict = PathDict(org)
+    assert org == dict(path_dict)
+
+
+def test_path_dict_basic_attr_access():
+    """Test basic behavior of path dict.
+
+    Including add by attribute, with simple, nested dict, and specific key 
dict.
+    """
+    expect = copy.deepcopy(src_dict_list[2])
+    path_dict = PathDict(expect)
+
+    # Add node with one level
+    val = 3
+    path_dict.f = val
+    expect.update({"f": val})
+    assert expect == path_dict
+
+    # Add node with multiple level
+    val = {"abc": 123}
+    path_dict.e.g = val
+    expect.update({"e": {"b": {"b": 4}, "f": 5, "g": val}})
+    assert expect == path_dict
+
+    # Specific key
+    expect = copy.deepcopy(src_dict_list[3])
+    path_dict = PathDict(expect)
+    assert 1 == path_dict.a
+    assert 2 == getattr(path_dict, "a-b")
+
+
[email protected](
+    "org, exists, not_exists",
+    [
+        (
+            src_dict_list[0],
+            ("a"),
+            ("b", "a.b"),
+        ),
+        (
+            src_dict_list[1],
+            ("a", "b", "c", "e", "c.d", "e.b"),
+            ("a.b", "c.e", "b.c", "b.e"),
+        ),
+        (
+            src_dict_list[2],
+            ("a", "b", "c", "e", "c.d", "e.b", "e.b.b", "e.b.b", "e.f"),
+            ("a.b", "c.e", "b.c", "b.e", "b.b.f", "b.f"),
+        ),
+    ],
+)
+def test_path_dict_attr(org: Dict, exists: Tuple, not_exists: Tuple):
+    """Test properties' integrity of path dict."""
+    path_dict = PathDict(org)
+    assert all([hasattr(path_dict, path) for path in exists])
+    # assert not any([hasattr(path_dict, path) for path in not_exists])
+
+
[email protected](
+    "org, path_get",
+    [
+        (
+            src_dict_list[0],
+            {"a": 1},
+        ),
+        (
+            src_dict_list[1],
+            {
+                "a": 1,
+                "b": 2,
+                "c": {"d": 3},
+                "c.d": 3,
+                "e": {"b": 4},
+                "e.b": 4,
+            },
+        ),
+        (
+            src_dict_list[2],
+            {
+                "a": 1,
+                "b": 2,
+                "c": {"d": 3},
+                "c.d": 3,
+                "e": {"b": {"b": 4}, "f": 5},
+                "e.b": {"b": 4},
+                "e.b.b": 4,
+                "e.f": 5,
+            },
+        ),
+    ],
+)
+def test_path_dict_get(org: Dict, path_get: Dict):
+    """Test path dict getter function."""
+    path_dict = PathDict(org)
+    assert all([path_get[path] == path_dict.__getattr__(path) for path in 
path_get])
+
+
[email protected](
+    "org, path_set, expect",
+    [
+        # Add not exists node
+        (
+            src_dict_list[0],
+            {"b": 2},
+            {
+                "a": 1,
+                "b": 2,
+            },
+        ),
+        # Overwrite exists node with different type of value
+        (
+            src_dict_list[0],
+            {"a": "b"},
+            {"a": "b"},
+        ),
+        # Add multiple not exists node with variable types of value
+        (
+            src_dict_list[0],
+            {
+                "b.c.d": 123,
+                "b.c.e": "a",
+                "b.f": {"g": 23, "h": "bc", "i": {"j": "k"}},
+            },
+            {
+                "a": 1,
+                "b": {
+                    "c": {
+                        "d": 123,
+                        "e": "a",
+                    },
+                    "f": {"g": 23, "h": "bc", "i": {"j": "k"}},
+                },
+            },
+        ),
+        # Test complex original data
+        (
+            src_dict_list[2],
+            {
+                "g": 12,
+                "c.h": 34,
+            },
+            {
+                "a": 1,
+                "b": 2,
+                "g": 12,
+                "c": {"d": 3, "h": 34},
+                "e": {"b": {"b": 4}, "f": 5},
+            },
+        ),
+    ],
+)
+def test_path_dict_set(org: Dict, path_set: Dict, expect: Dict):
+    """Test path dict setter function."""
+    path_dict = PathDict(org)
+    for path in path_set:
+        path_dict.__setattr__(path, path_set[path])
+    assert expect == path_dict

Reply via email to