diff --git a/mycroft/client/text/text_client.py b/mycroft/client/text/text_client.py index 70f0f3a52d25..e3cf49ce1c52 100644 --- a/mycroft/client/text/text_client.py +++ b/mycroft/client/text/text_client.py @@ -32,8 +32,8 @@ from mycroft.messagebus.client import MessageBusClient from mycroft.messagebus.message import Message from mycroft.util.log import LOG -from mycroft.configuration import Configuration, BASE_FOLDER -from mycroft.configuration.ovos import is_using_xdg +from mycroft.configuration import Configuration +from ovos_utils.configuration import is_using_xdg, get_xdg_base import locale @@ -187,7 +187,7 @@ def load_settings(): # Check XDG_CONFIG_DIR if config_file is None: - for conf_dir in xdg.BaseDirectory.load_config_paths(BASE_FOLDER): + for conf_dir in xdg.BaseDirectory.load_config_paths(get_xdg_base()): xdg_file = os.path.join(conf_dir, filename) if os.path.isfile(xdg_file): config_file = xdg_file @@ -230,7 +230,7 @@ def save_settings(): config_file = path else: config_file = os.path.join(xdg.BaseDirectory.xdg_config_home, - BASE_FOLDER, filename) + get_xdg_base(), filename) with io.open(config_file, 'w') as f: f.write(str(json.dumps(config, ensure_ascii=False))) diff --git a/mycroft/configuration/__init__.py b/mycroft/configuration/__init__.py index 7534802ae71d..246b4cdcd879 100644 --- a/mycroft/configuration/__init__.py +++ b/mycroft/configuration/__init__.py @@ -16,6 +16,5 @@ from mycroft.configuration.locale import set_default_lf_lang, setup_locale, \ set_default_tz, set_default_lang, get_default_tz, get_default_lang, \ get_config_tz, get_primary_lang_code, load_languages, load_language -from mycroft.configuration.locations import SYSTEM_CONFIG, USER_CONFIG, \ - get_xdg_config_locations, BASE_FOLDER, DEFAULT_CONFIG -from mycroft.configuration.ovos import is_using_xdg, get_ovos_config +from mycroft.configuration.locations import SYSTEM_CONFIG, USER_CONFIG, DEFAULT_CONFIG +from ovos_utils.configuration import get_ovos_config, is_using_xdg, get_xdg_config_locations diff --git a/mycroft/configuration/config.py b/mycroft/configuration/config.py index 384f6e17d213..3bf403cedf28 100644 --- a/mycroft/configuration/config.py +++ b/mycroft/configuration/config.py @@ -20,7 +20,7 @@ from requests import RequestException from mycroft.configuration.locations import * -from mycroft.configuration.ovos import is_using_xdg +from ovos_utils.configuration import is_using_xdg, get_xdg_config_locations from mycroft.util import camel_case_split from mycroft.util.json_helper import load_commented_json, merge_dict from mycroft.util.log import LOG @@ -196,7 +196,7 @@ def _log_old_location_deprecation(old_user_config=OLD_USER_CONFIG): LOG.warning(" Note that this location is deprecated and will" + " not be used in the future") LOG.warning(" Please move it to " + join(xdg.BaseDirectory.xdg_config_home, - BASE_FOLDER)) + get_xdg_base())) def _get_system_constraints(): diff --git a/mycroft/configuration/locations.py b/mycroft/configuration/locations.py index e94bccc1d202..4329ea9e6525 100644 --- a/mycroft/configuration/locations.py +++ b/mycroft/configuration/locations.py @@ -12,40 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -from time import sleep from os.path import join, dirname, expanduser, exists +from time import sleep import xdg.BaseDirectory -from mycroft.configuration.ovos import get_ovos_config +from ovos_utils.configuration import get_ovos_config, get_xdg_config_locations # check for path overrides _ovos_cfg = get_ovos_config() -BASE_FOLDER = _ovos_cfg["base_folder"] -CONFIG_FILE_NAME = _ovos_cfg["config_filename"] -DEFAULT_CONFIG = _ovos_cfg["default_config_path"] or \ - join(dirname(__file__), CONFIG_FILE_NAME) +DEFAULT_CONFIG = _ovos_cfg["default_config_path"] or \ + join(dirname(__file__), "mycroft.conf") SYSTEM_CONFIG = os.environ.get('MYCROFT_SYSTEM_CONFIG', - f'/etc/{BASE_FOLDER}/{CONFIG_FILE_NAME}') + f'/etc/{_ovos_cfg["base_folder"]}/{_ovos_cfg["config_filename"]}') # TODO: remove in 22.02 # Make sure we support the old location still # Deprecated and will be removed eventually -OLD_USER_CONFIG = join(expanduser('~'), '.' + BASE_FOLDER, CONFIG_FILE_NAME) -USER_CONFIG = join(xdg.BaseDirectory.xdg_config_home, BASE_FOLDER, CONFIG_FILE_NAME) +OLD_USER_CONFIG = join(expanduser('~'), '.' + _ovos_cfg["base_folder"], _ovos_cfg["config_filename"]) +USER_CONFIG = join(xdg.BaseDirectory.xdg_config_home, _ovos_cfg["base_folder"], _ovos_cfg["config_filename"]) REMOTE_CONFIG = "mycroft.ai" WEB_CONFIG_CACHE = os.environ.get('MYCROFT_WEB_CACHE') or \ - join(xdg.BaseDirectory.xdg_config_home, BASE_FOLDER, 'web_cache.json') - - -def get_xdg_config_locations(): - # This includes both the user config and - # /etc/xdg/mycroft/mycroft.conf - xdg_paths = list(reversed( - [join(p, CONFIG_FILE_NAME) - for p in xdg.BaseDirectory.load_config_paths(BASE_FOLDER)] - )) - return xdg_paths + join(xdg.BaseDirectory.xdg_config_home, _ovos_cfg["base_folder"], 'web_cache.json') def __ensure_folder_exists(path): diff --git a/mycroft/configuration/ovos.py b/mycroft/configuration/ovos.py index 4395f17f6452..96a9bff6cd51 100644 --- a/mycroft/configuration/ovos.py +++ b/mycroft/configuration/ovos.py @@ -62,60 +62,4 @@ } } """ -from importlib.util import find_spec -from os.path import isfile, dirname, join - -import xdg.BaseDirectory -from mycroft.util.json_helper import load_commented_json, merge_dict - - -def get_ovos_config(): - config = {"xdg": True, - "base_folder": "mycroft", - "config_filename": "mycroft.conf", - "default_config_path": join(dirname(__file__), "mycroft.conf")} - - try: - if isfile("/etc/OpenVoiceOS/ovos.conf"): - config = merge_dict(config, - load_commented_json( - "/etc/OpenVoiceOS/ovos.conf")) - elif isfile("/etc/mycroft/ovos.conf"): - config = merge_dict(config, - load_commented_json("/etc/mycroft/ovos.conf")) - except: - # tolerate bad json TODO proper exception (?) - pass - - # This includes both the user config and - # /etc/xdg/OpenVoiceOS/ovos.conf - for p in xdg.BaseDirectory.load_config_paths("OpenVoiceOS"): - if isfile(join(p, "ovos.conf")): - try: - xdg_cfg = load_commented_json(join(p, "ovos.conf")) - config = merge_dict(config, xdg_cfg) - except: - # tolerate bad json TODO proper exception (?) - pass - - # let's check for derivatives specific configs - # the assumption is that these cores are exclusive to each other, - # this will never find more than one override - # TODO this works if using dedicated .venvs what about system installs? - cores = config.get("module_overrides") or {} - for k in cores: - if find_spec(k): - config = merge_dict(config, cores[k]) - break - else: - subcores = config.get("submodule_mappings") or {} - for k in subcores: - if find_spec(k): - config = merge_dict(config, cores[subcores[k]]) - break - - return config - - -def is_using_xdg(): - return get_ovos_config().get("xdg", True) +from ovos_utils.configuration import get_ovos_config, is_using_xdg diff --git a/mycroft/filesystem/__init__.py b/mycroft/filesystem/__init__.py index 2c5d75b98a4e..2b0fe44541dd 100644 --- a/mycroft/filesystem/__init__.py +++ b/mycroft/filesystem/__init__.py @@ -15,8 +15,8 @@ import os import shutil from os.path import join, expanduser, isdir -import mycroft.configuration import xdg.BaseDirectory +from ovos_utils.configuration import is_using_xdg, get_xdg_base class FileSystemAccess: @@ -35,11 +35,11 @@ def __init_path(path): if not isinstance(path, str) or len(path) == 0: raise ValueError("path must be initialized as a non empty string") - path = expanduser(f'~/.{mycroft.configuration.BASE_FOLDER}/{path}') + path = expanduser(f'~/.{get_xdg_base()}/{path}') - if mycroft.configuration.is_using_xdg(): + if is_using_xdg(): xdg_path = expanduser( - f'{xdg.BaseDirectory.xdg_config_home}/{mycroft.configuration.BASE_FOLDER}/{path}') + f'{xdg.BaseDirectory.xdg_config_home}/{get_xdg_base()}/{path}') # Migrate from the old location if it still exists if isdir(path) and not isdir(xdg_path): shutil.move(path, xdg_path) diff --git a/mycroft/lock/__init__.py b/mycroft/lock/__init__.py index b672f5e8a312..949c4fff1ca6 100644 --- a/mycroft/lock/__init__.py +++ b/mycroft/lock/__init__.py @@ -14,7 +14,7 @@ # from signal import getsignal, signal, SIGKILL, SIGINT, SIGTERM, \ SIG_DFL, default_int_handler, SIG_IGN # signals -from mycroft.configuration import BASE_FOLDER +from ovos_utils.configuration import get_xdg_base import os # Operating System functions # @@ -98,7 +98,7 @@ class Lock: # python 3+ 'class Lock' # # Class constants - DIRECTORY = get_temp_path(BASE_FOLDER) + DIRECTORY = get_temp_path(get_xdg_base()) FILE = '/{}.pid' # diff --git a/mycroft/skills/event_scheduler.py b/mycroft/skills/event_scheduler.py index 0189d7fef47d..b98323081178 100644 --- a/mycroft/skills/event_scheduler.py +++ b/mycroft/skills/event_scheduler.py @@ -23,8 +23,8 @@ from os.path import isfile, join, expanduser import xdg.BaseDirectory -from mycroft.configuration import Configuration, BASE_FOLDER -from mycroft.configuration.ovos import is_using_xdg +from mycroft.configuration import Configuration +from ovos_utils.configuration import is_using_xdg, get_xdg_base from mycroft.messagebus.message import Message from mycroft.util.log import LOG from mycroft.skills.mycroft_skill.event_container import EventContainer, \ @@ -67,13 +67,13 @@ def __init__(self, bus, schedule_file='schedule.json', autostart=True): self.is_running = True core_conf = Configuration.get(remote=False) - data_dir = core_conf.get('data_dir', xdg.BaseDirectory.save_data_path(BASE_FOLDER)) + data_dir = core_conf.get('data_dir', xdg.BaseDirectory.save_data_path(get_xdg_base())) old_schedule_path = join(expanduser(data_dir), schedule_file) self.schedule_file = old_schedule_path if is_using_xdg(): new_schedule_path = join( - xdg.BaseDirectory.load_first_config(BASE_FOLDER), schedule_file) + xdg.BaseDirectory.load_first_config(get_xdg_base()), schedule_file) if isfile(old_schedule_path): shutil.move(old_schedule_path, new_schedule_path) self.schedule_file = new_schedule_path diff --git a/mycroft/skills/msm_wrapper.py b/mycroft/skills/msm_wrapper.py index 9bf405934eb1..dbf7af7f5f70 100644 --- a/mycroft/skills/msm_wrapper.py +++ b/mycroft/skills/msm_wrapper.py @@ -27,14 +27,15 @@ from mycroft.configuration import Configuration from mycroft.configuration.ovos import is_using_xdg from combo_lock import ComboLock -from mycroft.util.log import LOG -from mycroft.util.file_utils import get_temp_path -from mycroft.configuration import BASE_FOLDER from mock_msm import \ MycroftSkillsManager as MockMSM, \ SkillRepo as MockSkillRepo +from ovos_utils.configuration import get_xdg_base +from mycroft.util.file_utils import get_temp_path +from mycroft.util.log import LOG + try: from msm.exceptions import MsmException from msm import MycroftSkillsManager, SkillRepo @@ -70,16 +71,16 @@ def get_skills_directory(conf=None): # if xdg is disabled, ignore it! elif not is_using_xdg(): # old style mycroft-core skills path definition - data_dir = conf.get("data_dir") or "/opt/" + BASE_FOLDER + data_dir = conf.get("data_dir") or "/opt/" + get_xdg_base() folder = conf["skills"].get("msm", {}).get("directory", "skills") skills_folder = path.join(data_dir, folder) else: - skills_folder = xdg.BaseDirectory.save_data_path(BASE_FOLDER + '/skills') + skills_folder = xdg.BaseDirectory.save_data_path(get_xdg_base() + '/skills') # create folder if needed try: makedirs(skills_folder, exist_ok=True) except PermissionError: # old style /opt/mycroft/skills not available - skills_folder = xdg.BaseDirectory.save_data_path(BASE_FOLDER + '/skills') + skills_folder = xdg.BaseDirectory.save_data_path(get_xdg_base() + '/skills') makedirs(skills_folder, exist_ok=True) return path.expanduser(skills_folder) @@ -109,7 +110,7 @@ def build_msm_config(device_config: dict) -> MsmConfig: msm_config = device_config['skills'].get('msm', {}) msm_repo_config = msm_config.get('repo', {}) enclosure_config = device_config.get('enclosure', {}) - data_dir = path.expanduser(device_config.get('data_dir', xdg.BaseDirectory.save_data_path(BASE_FOLDER))) + data_dir = path.expanduser(device_config.get('data_dir', xdg.BaseDirectory.save_data_path(get_xdg_base()))) skills_dir = get_skills_directory(device_config) old_skills_dir = path.join(data_dir, msm_config.get('directory', "skills")) diff --git a/mycroft/skills/mycroft_skill/mycroft_skill.py b/mycroft/skills/mycroft_skill/mycroft_skill.py index 064f13b5f1f9..9c2616bf6a94 100644 --- a/mycroft/skills/mycroft_skill/mycroft_skill.py +++ b/mycroft/skills/mycroft_skill/mycroft_skill.py @@ -33,7 +33,8 @@ from mycroft.audio import wait_while_speaking from ovos_utils.enclosure.api import EnclosureAPI from mycroft.gui import SkillGUI -from mycroft.configuration import Configuration, BASE_FOLDER +from ovos_utils.configuration import is_using_xdg, get_xdg_base +from mycroft.configuration import Configuration from mycroft.dialog import load_dialogs from mycroft.filesystem import FileSystemAccess from mycroft.messagebus.message import Message, dig_for_message @@ -189,7 +190,7 @@ def _init_settings(self): # Otherwise save to XDG_CONFIG_DIR if not self.settings_write_path.joinpath('settings.json').exists(): self.settings_write_path = Path(xdg.BaseDirectory.save_config_path( - BASE_FOLDER, 'skills', basename(self.root_dir))) + get_xdg_base(), 'skills', basename(self.root_dir))) # To not break existing setups, # read from skill directory if the settings file exists there @@ -197,7 +198,7 @@ def _init_settings(self): # Then, check XDG_CONFIG_DIR if not settings_read_path.joinpath('settings.json').exists(): - for path in xdg.BaseDirectory.load_config_paths(BASE_FOLDER, 'skills', + for path in xdg.BaseDirectory.load_config_paths(get_xdg_base(), 'skills', basename(self.root_dir)): path = Path(path) # If there is a settings file here, use it diff --git a/mycroft/skills/settings.py b/mycroft/skills/settings.py index c484eee3ad5a..f056bef12f7d 100644 --- a/mycroft/skills/settings.py +++ b/mycroft/skills/settings.py @@ -66,7 +66,8 @@ import yaml from mycroft.api import DeviceApi, is_paired, is_backend_disabled -from mycroft.configuration import Configuration, BASE_FOLDER +from mycroft.configuration import Configuration +from ovos_utils.configuration import get_xdg_base from mycroft.messagebus.message import Message from mycroft.util import camel_case_split from mycroft.util.log import LOG @@ -249,7 +250,7 @@ def upload(self): LOG.debug('settingsmeta.json not uploaded - device is not paired') if not synced and not self._stopped: - self.upload_timer = Timer(ONE_MINUTE, self.upload) + self.upload_timer = Timer(60, self.upload) self.upload_timer.daemon = True self.upload_timer.start() @@ -308,7 +309,7 @@ def _issue_api_call(self): # Path to remote cache -REMOTE_CACHE = Path(xdg_cache_home, BASE_FOLDER, 'remote_skill_settings.json') +REMOTE_CACHE = Path(xdg_cache_home, get_xdg_base(), 'remote_skill_settings.json') def load_remote_settings_cache(): @@ -401,7 +402,7 @@ def download(self, message=None): self.download_timer.cancel() if self.continue_downloading: - self.download_timer = Timer(ONE_MINUTE, self.download) + self.download_timer = Timer(60, self.download) self.download_timer.daemon = True self.download_timer.start() diff --git a/mycroft/skills/skill_manager.py b/mycroft/skills/skill_manager.py index 23b26bd813e5..2ccef5372470 100644 --- a/mycroft/skills/skill_manager.py +++ b/mycroft/skills/skill_manager.py @@ -21,7 +21,8 @@ from mycroft.api import is_paired from mycroft.enclosure.api import EnclosureAPI -from mycroft.configuration import Configuration, BASE_FOLDER +from mycroft.configuration import Configuration +from ovos_utils.configuration import get_xdg_base from mycroft.messagebus.message import Message from mycroft.util.log import LOG from mycroft.util import connected @@ -123,7 +124,7 @@ def _get_skill_locations(conf=None): # they should be considered applets rather than full applications skill_locations = list(reversed( [os.path.join(p, "skills") - for p in xdg.BaseDirectory.load_data_paths(BASE_FOLDER)] + for p in xdg.BaseDirectory.load_data_paths(get_xdg_base())] )) # load the old hardcoded /opt/mycroft/skills folder diff --git a/mycroft/skills/skill_updater.py b/mycroft/skills/skill_updater.py index bd435b51bd1b..cf9253ba8b88 100644 --- a/mycroft/skills/skill_updater.py +++ b/mycroft/skills/skill_updater.py @@ -20,8 +20,8 @@ import xdg.BaseDirectory from mycroft.api import DeviceApi, is_paired -from mycroft.configuration import Configuration, BASE_FOLDER -from mycroft.configuration.ovos import is_using_xdg +from mycroft.configuration import Configuration +from ovos_utils.configuration import get_xdg_base, is_using_xdg from mycroft.util import connected from combo_lock import ComboLock from mycroft.util.log import LOG @@ -99,10 +99,10 @@ def installed_skills_file_path(self): else: if not is_using_xdg(): self._installed_skills_file_path = os.path.expanduser( - f'~/.{BASE_FOLDER}/.mycroft-skills') + f'~/.{get_xdg_base()}/.mycroft-skills') else: self._installed_skills_file_path = os.path.join( - xdg.BaseDirectory.xdg_data_home, BASE_FOLDER, '.mycroft-skills') + xdg.BaseDirectory.xdg_data_home, get_xdg_base(), '.mycroft-skills') return self._installed_skills_file_path diff --git a/mycroft/util/file_utils.py b/mycroft/util/file_utils.py index e3cdf4baccf5..283aeacf6f6b 100644 --- a/mycroft/util/file_utils.py +++ b/mycroft/util/file_utils.py @@ -21,6 +21,7 @@ import os import xdg.BaseDirectory from ovos_utils.file_utils import get_temp_path +from ovos_utils.configuration import is_using_xdg, get_xdg_base import mycroft.configuration from mycroft.util.log import LOG # do not delete these imports, here for backwards compat! @@ -62,20 +63,20 @@ def resolve_resource_file(res_name): return res_name # Now look for XDG_DATA_DIRS - for path in xdg.BaseDirectory.load_data_paths(mycroft.configuration.BASE_FOLDER): + for path in xdg.BaseDirectory.load_data_paths(get_xdg_base()): filename = os.path.join(path, res_name) if os.path.isfile(filename): return filename # Now look in the old user location filename = os.path.join(os.path.expanduser('~'), - f'.{mycroft.configuration.BASE_FOLDER}', + f'.{get_xdg_base()}', res_name) if os.path.isfile(filename): return filename # Next look for /opt/mycroft/res/res_name - data_dir = config.get('data_dir', xdg.BaseDirectory.save_data_path(mycroft.configuration.BASE_FOLDER)) + data_dir = config.get('data_dir', xdg.BaseDirectory.save_data_path(get_xdg_base())) res_dir = os.path.join(data_dir, 'res') filename = os.path.expanduser(os.path.join(res_dir, res_name)) if os.path.isfile(filename): @@ -152,12 +153,12 @@ def get_cache_directory(domain=None): config = mycroft.configuration.Configuration.get(remote=False) directory = config.get("cache_path") if not directory: - if not mycroft.configuration.is_using_xdg(): + if not is_using_xdg(): # If not defined, use /tmp/mycroft/cache - directory = get_temp_path(mycroft.configuration.BASE_FOLDER, 'cache') + directory = get_temp_path(get_xdg_base(), 'cache') else: directory = os.path.join(xdg.BaseDirectory.xdg_data_home, - mycroft.configuration.BASE_FOLDER, "cache") + get_xdg_base(), "cache") return ensure_directory_exists(directory, domain) diff --git a/requirements/minimal.txt b/requirements/minimal.txt index 983ceaa456ee..1ad516f7cfb3 100644 --- a/requirements/minimal.txt +++ b/requirements/minimal.txt @@ -5,4 +5,5 @@ pyxdg~=0.26 mycroft-messagebus-client~=0.9.1,!=0.9.2,!=0.9.3 psutil~=5.6.6 combo-lock~=0.2 +ovos-utils~=0.0.14a4 ovos-plugin-manager~=0.0.3a1 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f9d7ca625693..4747a5596fc5 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -8,6 +8,7 @@ python-dateutil~=2.6.0 combo-lock~=0.2 PyYAML~=5.4 +ovos-utils~=0.0.14a4 ovos-plugin-manager~=0.0.3a1 ovos-tts-plugin-mimic~=0.2.3 ovos-tts-plugin-mimic2~=0.1.2 diff --git a/test/unittests/lock/test_lock.py b/test/unittests/lock/test_lock.py index 05849dd2cb5d..7c909b51ab7f 100644 --- a/test/unittests/lock/test_lock.py +++ b/test/unittests/lock/test_lock.py @@ -22,33 +22,33 @@ from mycroft.lock import Lock from mycroft.util.file_utils import get_temp_path -from mycroft.configuration import BASE_FOLDER +from ovos_utils.configuration import get_xdg_base class TestLock(unittest.TestCase): def setUp(self): - if exists(get_temp_path(BASE_FOLDER)): - rmtree(get_temp_path(BASE_FOLDER)) + if exists(get_temp_path(get_xdg_base())): + rmtree(get_temp_path(get_xdg_base())) def test_create_lock(self): l1 = Lock('test') self.assertTrue( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) def test_delete_lock(self): l1 = Lock('test') self.assertTrue( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) l1.delete() self.assertFalse( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) @patch('os.kill') def test_existing_lock(self, mock_kill): """ Test that an existing lock will kill the old pid. """ l1 = Lock('test') self.assertTrue( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) l2 = Lock('test2') self.assertFalse(mock_kill.called) l2 = Lock('test') @@ -57,13 +57,13 @@ def test_existing_lock(self, mock_kill): def test_keyboard_interrupt(self): l1 = Lock('test') self.assertTrue( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) try: os.kill(os.getpid(), signal.SIGINT) except KeyboardInterrupt: pass self.assertFalse( - isfile(get_temp_path(BASE_FOLDER, 'test.pid'))) + isfile(get_temp_path(get_xdg_base(), 'test.pid'))) if __name__ == '__main__':