Browse Source
* [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 format3.0.0/version-upgrade
Jiajie Zhong
3 years ago
committed by
GitHub
24 changed files with 889 additions and 91 deletions
@ -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 |
@ -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: userPythonGateway@dolphinscheduler.com |
||||||
|
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 |
@ -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__ |
@ -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" |
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture |
||||||
|
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() |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif( |
||||||
|
DEV_MODE, |
||||||
|
reason="Avoid delete ~/pydolphinscheduler/config.yaml by accident when test locally.", |
||||||
|
) |
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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() |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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) |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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` |
||||||
|
@pytest.mark.skip(reason="We still have bug in command `config --set KEY VAL`") |
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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` |
||||||
|
@pytest.mark.skip( |
||||||
|
reason="We still have bug in command `config --set KEY1 VAL1 --set KEY2 VAL2`" |
||||||
|
) |
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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 |
@ -0,0 +1,45 @@ |
|||||||
|
# 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 class :mod:`pydolphinscheduler.core.configuration`' method.""" |
||||||
|
|
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
import pytest |
||||||
|
|
||||||
|
from pydolphinscheduler.core import configuration |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"env, expect", |
||||||
|
[ |
||||||
|
(None, "~/pydolphinscheduler"), |
||||||
|
("/tmp/pydolphinscheduler", "/tmp/pydolphinscheduler"), |
||||||
|
("/tmp/test_abc", "/tmp/test_abc"), |
||||||
|
], |
||||||
|
) |
||||||
|
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() |
||||||
|
) |
@ -0,0 +1,39 @@ |
|||||||
|
# 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 default config file.""" |
||||||
|
|
||||||
|
from typing import Dict |
||||||
|
|
||||||
|
import yaml |
||||||
|
|
||||||
|
from tests.testing.path import path_default_config_yaml |
||||||
|
|
||||||
|
|
||||||
|
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) |
||||||
|
|
||||||
|
|
||||||
|
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) |
@ -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, |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("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") |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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]) |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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]) |
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize( |
||||||
|
"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 |
Loading…
Reference in new issue