From 74589380a778df9a3715e8aee1b40f8a6a6e071d Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 11 Aug 2023 16:53:39 -0700 Subject: [PATCH 1/6] Adds functions to import and export user configuration with CLI entrypoints --- .github/workflows/unit_tests.yml | 19 +-- neon_core/cli.py | 27 +++ neon_core/util/device_utils.py | 70 ++++++++ neon_core/util/diagnostic_utils.py | 1 + test/test_config_export/config/neon.yaml | 1 + .../config/skills/skill-test.neon/skill.json | 3 + .../config/skills/skill-test.neon/test.file | 0 test/test_config_import/neon_export.zip | Bin 0 -> 750 bytes test/test_diagnostic_utils.py | 132 --------------- test/{test_skill_utils.py => test_util.py} | 157 +++++++++++++++++- 10 files changed, 260 insertions(+), 150 deletions(-) create mode 100644 neon_core/util/device_utils.py create mode 100644 test/test_config_export/config/neon.yaml create mode 100644 test/test_config_export/config/skills/skill-test.neon/skill.json create mode 100644 test/test_config_import/config/skills/skill-test.neon/test.file create mode 100644 test/test_config_import/neon_export.zip delete mode 100644 test/test_diagnostic_utils.py rename test/{test_skill_utils.py => test_util.py} (64%) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 965567d8e..e6e1ec785 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -33,27 +33,16 @@ jobs: env: GITHUB_TOKEN: ${{secrets.neon_token}} - - name: Test Skill Utils + - name: Test Util run: | - pytest test/test_skill_utils.py --doctest-modules --junitxml=tests/skill-utils-test-results.xml + pytest test/test_util.py --doctest-modules --junitxml=tests/util-test-results.xml env: GITHUB_TOKEN: ${{secrets.neon_token}} - - name: Upload Skill Utils test results + - name: Upload Util test results uses: actions/upload-artifact@v2 with: name: skill-utils-test-results - path: tests/skill-utils-test-results.xml - - - name: Test Diagnostic Utils - run: | - pytest test/test_diagnostic_utils.py --doctest-modules --junitxml=tests/diagnostic-utils-test-results.xml - env: - GITHUB_TOKEN: ${{secrets.neon_token}} - - name: Upload Diagnostic Utils test results - uses: actions/upload-artifact@v2 - with: - name: diagnostic-utils-test-results - path: tests/diagnostic-utils-test-results.xml + path: tests/util-test-results.xml unit_tests: strategy: diff --git a/neon_core/cli.py b/neon_core/cli.py index d71010d14..b888280c7 100644 --- a/neon_core/cli.py +++ b/neon_core/cli.py @@ -95,6 +95,33 @@ def install_skill_requirements(skill_dir): click.echo(e) +@neon_core_cli.command(help="Export Core Configuration") +@click.argument("output_directory") +def export_configuration(output_directory): + from neon_core.util.device_utils import export_user_config + from neon_utils.configuration_utils import init_config_dir + try: + init_config_dir() + output = export_user_config(output_directory) + click.echo(f"Exported configuration to: {output}") + except Exception as e: + click.echo(e) + + +@neon_core_cli.command(help="Import Core Configuration") +@click.argument("exported_configuration") +def export_configuration(exported_configuration): + from neon_core.util.device_utils import import_user_config + from neon_utils.configuration_utils import init_config_dir + try: + init_config_dir() + config_path = import_user_config(exported_configuration) + click.echo(f"Imported configuration to: {config_path}. Services may " + f"need to be restarted to load these changes") + except Exception as e: + click.echo(e) + + @neon_core_cli.command(help="Start Neon Skills module") @click.option("--install-skills", "-i", default=None, help="Path to local skills for which to install dependencies") diff --git a/neon_core/util/device_utils.py b/neon_core/util/device_utils.py new file mode 100644 index 000000000..805ad7e0f --- /dev/null +++ b/neon_core/util/device_utils.py @@ -0,0 +1,70 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework +# All trademark and other rights reserved by their respective owners +# Copyright 2008-2022 Neongecko.com Inc. +# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds, +# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo +# BSD-3 License +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import shutil + +from os import makedirs +from os.path import expanduser, isdir, exists, join, isfile +from ovos_utils.xdg_utils import xdg_config_home + + +def export_user_config(output_path: str, config_path: str = None) -> str: + """ + Export user configuration to an archive for backup/migration + @param output_path: Directory to write output archive to + @param config_path: Configuration path to export (else use XDG) + @return: Path to generated output file + """ + output_path = join(expanduser(output_path), "neon_export") + if exists(output_path) and not isdir(output_path): + raise FileExistsError(f"Expected output directory but got file: " + f"{output_path}") + if exists(f"{output_path}.zip"): + raise FileExistsError(f"Export already exists: {output_path}.zip") + makedirs(output_path, exist_ok=True) + config_path = config_path or join(xdg_config_home(), "neon") + shutil.copytree(config_path, join(output_path, "neon_export")) + output_file = shutil.make_archive(output_path, "zip", config_path, + config_path) + shutil.rmtree(output_path) + return output_file + + +def import_user_config(input_file: str, config_path: str = None) -> str: + """ + Export user configuration to an archive for backup/migration + @param input_file: Exported configuration archive to import + @param config_path: Configuration path to import to (else use XDG) + @return: Path configuration was imported to + """ + input_file = expanduser(input_file) + if not isfile(input_file): + raise FileNotFoundError(f"Invalid input file: {input_file}") + config_path = config_path or join(xdg_config_home(), "neon") + shutil.unpack_archive(input_file, config_path, "zip") + return config_path diff --git a/neon_core/util/diagnostic_utils.py b/neon_core/util/diagnostic_utils.py index 0dec0ccfc..a5dda2744 100644 --- a/neon_core/util/diagnostic_utils.py +++ b/neon_core/util/diagnostic_utils.py @@ -120,6 +120,7 @@ def send_diagnostics(allow_logs=True, allow_transcripts=True, allow_config=True) return data +# TODO: Deprecate method def cli_send_diags(): """ CLI Entry Point to Send Diagnostics diff --git a/test/test_config_export/config/neon.yaml b/test/test_config_export/config/neon.yaml new file mode 100644 index 000000000..7fa901fba --- /dev/null +++ b/test/test_config_export/config/neon.yaml @@ -0,0 +1 @@ +test: true diff --git a/test/test_config_export/config/skills/skill-test.neon/skill.json b/test/test_config_export/config/skills/skill-test.neon/skill.json new file mode 100644 index 000000000..937c4d4cc --- /dev/null +++ b/test/test_config_export/config/skills/skill-test.neon/skill.json @@ -0,0 +1,3 @@ +{ + "test_skill_key": "value" +} \ No newline at end of file diff --git a/test/test_config_import/config/skills/skill-test.neon/test.file b/test/test_config_import/config/skills/skill-test.neon/test.file new file mode 100644 index 000000000..e69de29bb diff --git a/test/test_config_import/neon_export.zip b/test/test_config_import/neon_export.zip new file mode 100644 index 0000000000000000000000000000000000000000..eda581467748a03fe6920235a4093256b6c9957d GIT binary patch literal 750 zcmWIWW@Zs#0D;+!+~Hsbl<)`A8Tq-X`YG|b$=P|C=@}*Z0hP%aiAA{qMfq8&$tA`5 z@p{hrMXCCJsrh+eMoDUMi9U#kPtMOv%S?|?ttiMZD$$4V^aJ3Q%|x?|9cWo`c4kga zFgsI_($LiMeagguF!% None: - os.makedirs(cls.config_dir, exist_ok=True) - - os.environ["NEON_CONFIG_PATH"] = cls.config_dir - os.environ["XDG_CONFIG_HOME"] = cls.config_dir - test_dir = os.path.join(os.path.dirname(__file__), "diagnostic_files") - from neon_core.configuration import patch_config - patch_config({"log_dir": test_dir}) - - @classmethod - def tearDownClass(cls) -> None: - if os.getenv("NEON_CONFIG_PATH"): - os.environ.pop("NEON_CONFIG_PATH") - shutil.rmtree(cls.config_dir) - - def setUp(self) -> None: - self.report_metric.reset_mock() - neon_utils.metrics_utils.report_metric = self.report_metric - - def test_send_diagnostics_default(self): - from neon_core.util.diagnostic_utils import send_diagnostics - send_diagnostics() - self.report_metric.assert_called_once() - args = self.report_metric.call_args - self.assertEqual(args.args, ("diagnostics",)) - data = args.kwargs - self.assertIsInstance(data, dict) - self.assertIsInstance(data["host"], str) - self.assertIsInstance(data["configurations"], str) - self.assertIsInstance(data["logs"], str) - # self.assertIsInstance(data["transcripts"], str) - - def test_send_diagnostics_no_extras(self): - from neon_core.util.diagnostic_utils import send_diagnostics - send_diagnostics(False, False, False) - self.report_metric.assert_called_once() - args = self.report_metric.call_args - self.assertEqual(args.args, ("diagnostics",)) - data = args.kwargs - self.assertIsInstance(data, dict) - self.assertIsInstance(data["host"], str) - self.assertIsNone(data["configurations"]) - self.assertIsNone(data["logs"]) - self.assertIsNone(data["transcripts"]) - - def test_send_diagnostics_allow_logs(self): - from neon_core.util.diagnostic_utils import send_diagnostics - send_diagnostics(True, False, False) - self.report_metric.assert_called_once() - args = self.report_metric.call_args - self.assertEqual(args.args, ("diagnostics",)) - data = args.kwargs - self.assertIsInstance(data, dict) - self.assertIsInstance(data["host"], str) - self.assertIsNone(data["configurations"]) - self.assertIsInstance(data["logs"], str) - self.assertIsNone(data["transcripts"]) - - def test_send_diagnostics_allow_transcripts(self): - from neon_core.util.diagnostic_utils import send_diagnostics - send_diagnostics(False, True, False) - self.report_metric.assert_called_once() - args = self.report_metric.call_args - self.assertEqual(args.args, ("diagnostics",)) - data = args.kwargs - self.assertIsInstance(data, dict) - self.assertIsInstance(data["host"], str) - self.assertIsNone(data["configurations"]) - self.assertIsNone(data["logs"]) - # self.assertIsInstance(data["transcripts"], str) - - def test_send_diagnostics_allow_config(self): - from neon_core.util.diagnostic_utils import send_diagnostics - send_diagnostics(False, False, True) - self.report_metric.assert_called_once() - args = self.report_metric.call_args - self.assertEqual(args.args, ("diagnostics",)) - data = args.kwargs - self.assertIsInstance(data, dict) - self.assertIsInstance(data["host"], str) - self.assertIsInstance(data["configurations"], str) - self.assertIsNone(data["logs"]) - self.assertIsNone(data["transcripts"]) - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_skill_utils.py b/test/test_util.py similarity index 64% rename from test/test_skill_utils.py rename to test/test_util.py index 68a613993..7d29f6e9b 100644 --- a/test/test_skill_utils.py +++ b/test/test_util.py @@ -26,17 +26,20 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import importlib -import json import os import shutil import sys import unittest +from os.path import join, dirname, isfile, isdir + +import neon_utils.metrics_utils + +from mock import Mock -from mock.mock import Mock sys.path.append(os.path.dirname(os.path.dirname(__file__))) + TEST_SKILLS_NO_AUTH = [ "https://github.com/NeonGeckoCom/alerts.neon/tree/dev", "https://github.com/NeonGeckoCom/caffeinewiz.neon/tree/dev", @@ -134,6 +137,7 @@ def test_get_neon_skills_data(self): normalize_github_url(neon_skills[skill]["url"])) def test_install_local_skills(self): + import importlib import ovos_skills_manager.requirements import neon_core.util.skill_utils importlib.reload(neon_core.util.skill_utils) @@ -245,5 +249,152 @@ def test_skill_class_patches(self): # Class added in ovos-workwhop 0.0.12 pass + +class DiagnosticUtilsTests(unittest.TestCase): + config_dir = os.path.join(os.path.dirname(__file__), "test_config") + report_metric = Mock() + + @classmethod + def setUpClass(cls) -> None: + os.makedirs(cls.config_dir, exist_ok=True) + + os.environ["NEON_CONFIG_PATH"] = cls.config_dir + os.environ["XDG_CONFIG_HOME"] = cls.config_dir + test_dir = os.path.join(os.path.dirname(__file__), "diagnostic_files") + from neon_core.configuration import patch_config + patch_config({"log_dir": test_dir}) + + @classmethod + def tearDownClass(cls) -> None: + if os.getenv("NEON_CONFIG_PATH"): + os.environ.pop("NEON_CONFIG_PATH") + shutil.rmtree(cls.config_dir) + + def setUp(self) -> None: + self.report_metric.reset_mock() + neon_utils.metrics_utils.report_metric = self.report_metric + + def test_send_diagnostics_default(self): + from neon_core.util.diagnostic_utils import send_diagnostics + send_diagnostics() + self.report_metric.assert_called_once() + args = self.report_metric.call_args + self.assertEqual(args.args, ("diagnostics",)) + data = args.kwargs + self.assertIsInstance(data, dict) + self.assertIsInstance(data["host"], str) + self.assertIsInstance(data["configurations"], str) + self.assertIsInstance(data["logs"], str) + # self.assertIsInstance(data["transcripts"], str) + + def test_send_diagnostics_no_extras(self): + from neon_core.util.diagnostic_utils import send_diagnostics + send_diagnostics(False, False, False) + self.report_metric.assert_called_once() + args = self.report_metric.call_args + self.assertEqual(args.args, ("diagnostics",)) + data = args.kwargs + self.assertIsInstance(data, dict) + self.assertIsInstance(data["host"], str) + self.assertIsNone(data["configurations"]) + self.assertIsNone(data["logs"]) + self.assertIsNone(data["transcripts"]) + + def test_send_diagnostics_allow_logs(self): + from neon_core.util.diagnostic_utils import send_diagnostics + send_diagnostics(True, False, False) + self.report_metric.assert_called_once() + args = self.report_metric.call_args + self.assertEqual(args.args, ("diagnostics",)) + data = args.kwargs + self.assertIsInstance(data, dict) + self.assertIsInstance(data["host"], str) + self.assertIsNone(data["configurations"]) + self.assertIsInstance(data["logs"], str) + self.assertIsNone(data["transcripts"]) + + def test_send_diagnostics_allow_transcripts(self): + from neon_core.util.diagnostic_utils import send_diagnostics + send_diagnostics(False, True, False) + self.report_metric.assert_called_once() + args = self.report_metric.call_args + self.assertEqual(args.args, ("diagnostics",)) + data = args.kwargs + self.assertIsInstance(data, dict) + self.assertIsInstance(data["host"], str) + self.assertIsNone(data["configurations"]) + self.assertIsNone(data["logs"]) + # self.assertIsInstance(data["transcripts"], str) + + def test_send_diagnostics_allow_config(self): + from neon_core.util.diagnostic_utils import send_diagnostics + send_diagnostics(False, False, True) + self.report_metric.assert_called_once() + args = self.report_metric.call_args + self.assertEqual(args.args, ("diagnostics",)) + data = args.kwargs + self.assertIsInstance(data, dict) + self.assertIsInstance(data["host"], str) + self.assertIsInstance(data["configurations"], str) + self.assertIsNone(data["logs"]) + self.assertIsNone(data["transcripts"]) + + +class DeviceUtilsTests(unittest.TestCase): + def test_export_user_config(self): + from neon_core.util.device_utils import export_user_config + output_path = join(dirname(__file__), "test_config_export") + config_path = join(output_path, "config") + output_file = export_user_config(output_path, config_path) + self.assertTrue(isfile(output_file)) + self.assertEqual(dirname(output_file), output_path) + + # Validate exported contents + extract_path = join(output_path, "exported") + shutil.unpack_archive(output_file, extract_path) + self.assertTrue(isdir(extract_path)) + self.assertTrue(isfile(join(extract_path, "skills", + "skill-test.neon", "skill.json"))) + self.assertTrue(isfile(join(extract_path, "neon.yaml"))) + + # Validate export file exists + with self.assertRaises(FileExistsError): + export_user_config(output_path, config_path) + + # Cleanup test files + shutil.rmtree(extract_path) + # os.remove(output_file) + + def test_import_user_config(self): + from neon_core.util.device_utils import import_user_config + valid_import_directory = join(dirname(__file__), "test_config_import", + "config") + valid_import_file = join(dirname(__file__), "test_config_import", + "neon_export.zip") + # Write test config to be overwritten + with open(join(valid_import_directory, "neon.yaml"), "w+") as f: + f.write("success: False\n") + + # Validate imported contents + imported = import_user_config(valid_import_file, valid_import_directory) + self.assertEqual(imported, valid_import_directory) + self.assertTrue(isfile(join(valid_import_directory, "neon.yaml"))) + self.assertTrue(isfile(join(valid_import_directory, "skills", "skill-test.neon", "test.file"))) + self.assertTrue(isfile(join(valid_import_directory, "skills", "skill-test.neon", "skill.json"))) + self.assertTrue(isfile(join(valid_import_directory, "neon.yaml"))) + with open(join(valid_import_directory, "neon.yaml")) as f: + contents = f.read() + self.assertEqual(contents, "test: true\n") + + # Validate import file exists + with self.assertRaises(FileNotFoundError): + import_user_config(valid_import_directory) + + # Cleanup + os.remove(join(valid_import_directory, "neon.yaml")) + os.remove(join(valid_import_directory, "skills", "skill-test.neon", + "skill.json")) + + if __name__ == '__main__': unittest.main() From 0536ae19e0d184b0e5f6de7085208aac32f2607f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 11 Aug 2023 17:44:31 -0700 Subject: [PATCH 2/6] Replace test case cleanup --- test/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index 7d29f6e9b..ccba27b68 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -363,7 +363,7 @@ def test_export_user_config(self): # Cleanup test files shutil.rmtree(extract_path) - # os.remove(output_file) + os.remove(output_file) def test_import_user_config(self): from neon_core.util.device_utils import import_user_config From 0ee2d13136ea7fb35d41d9a286f3b92bc3a6e37f Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 11 Aug 2023 18:57:31 -0700 Subject: [PATCH 3/6] Troubleshoot unit test failure --- test/test_util.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_util.py b/test/test_util.py index ccba27b68..457532e6a 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -354,7 +354,8 @@ def test_export_user_config(self): shutil.unpack_archive(output_file, extract_path) self.assertTrue(isdir(extract_path)) self.assertTrue(isfile(join(extract_path, "skills", - "skill-test.neon", "skill.json"))) + "skill-test.neon", "skill.json")), + repr(dir(extract_path))) self.assertTrue(isfile(join(extract_path, "neon.yaml"))) # Validate export file exists @@ -379,8 +380,10 @@ def test_import_user_config(self): imported = import_user_config(valid_import_file, valid_import_directory) self.assertEqual(imported, valid_import_directory) self.assertTrue(isfile(join(valid_import_directory, "neon.yaml"))) - self.assertTrue(isfile(join(valid_import_directory, "skills", "skill-test.neon", "test.file"))) - self.assertTrue(isfile(join(valid_import_directory, "skills", "skill-test.neon", "skill.json"))) + self.assertTrue(isfile(join(valid_import_directory, "skills", + "skill-test.neon", "test.file"))) + self.assertTrue(isfile(join(valid_import_directory, "skills", + "skill-test.neon", "skill.json"))) self.assertTrue(isfile(join(valid_import_directory, "neon.yaml"))) with open(join(valid_import_directory, "neon.yaml")) as f: contents = f.read() From 8284d0a9cb1ce20a0178e1dc1f85dd5d0e9c93a5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Fri, 11 Aug 2023 19:21:09 -0700 Subject: [PATCH 4/6] Troubleshoot unit test failure --- test/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index 457532e6a..e3691dadc 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -355,7 +355,7 @@ def test_export_user_config(self): self.assertTrue(isdir(extract_path)) self.assertTrue(isfile(join(extract_path, "skills", "skill-test.neon", "skill.json")), - repr(dir(extract_path))) + repr(os.listdir(extract_path))) self.assertTrue(isfile(join(extract_path, "neon.yaml"))) # Validate export file exists From abd2f258b31d776eaf7a72f8b2d6835b6f8e3bb1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 14 Aug 2023 09:09:59 -0700 Subject: [PATCH 5/6] Troubleshoot unit test failure --- test/test_util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_util.py b/test/test_util.py index e3691dadc..88e0636ab 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -357,6 +357,9 @@ def test_export_user_config(self): "skill-test.neon", "skill.json")), repr(os.listdir(extract_path))) self.assertTrue(isfile(join(extract_path, "neon.yaml"))) + with open(join(extract_path, "neon.yaml")) as f: + contents = f.read() + self.assertEqual(contents, "test: true\n") # Validate export file exists with self.assertRaises(FileExistsError): From a9986c71a7c10923edfb4dd94e75af6375f00667 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Mon, 14 Aug 2023 09:41:43 -0700 Subject: [PATCH 6/6] Troubleshoot unit test failure --- neon_core/util/device_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neon_core/util/device_utils.py b/neon_core/util/device_utils.py index 805ad7e0f..efcaae92c 100644 --- a/neon_core/util/device_utils.py +++ b/neon_core/util/device_utils.py @@ -46,9 +46,8 @@ def export_user_config(output_path: str, config_path: str = None) -> str: f"{output_path}") if exists(f"{output_path}.zip"): raise FileExistsError(f"Export already exists: {output_path}.zip") - makedirs(output_path, exist_ok=True) config_path = config_path or join(xdg_config_home(), "neon") - shutil.copytree(config_path, join(output_path, "neon_export")) + shutil.copytree(config_path, output_path) output_file = shutil.make_archive(output_path, "zip", config_path, config_path) shutil.rmtree(output_path)