diff --git a/src/zinolib/config/__init__.py b/src/zinolib/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/zinolib/config/tcl.py b/src/zinolib/config/tcl.py new file mode 100644 index 0000000..2f38c23 --- /dev/null +++ b/src/zinolib/config/tcl.py @@ -0,0 +1,157 @@ +from pathlib import Path +import re + +from .utils import find_config_file + + +TCL_FILENAMES = [".ritz.tcl", "ritz.tcl"] + + +def load(filename): + """Get the contents of a .ritz.tcl config file + + .ritz.tcl is formatted as a tcl file. + """ + path = Path(filename).expanduser() + with path.open("r") as f: + # normalize lineends just in case + lines = [line.strip() for line in f.readlines()] + text = "\n".join(lines) + return text + + +def parse(text): + """ + Parse the text of a .ritz.tcl config file + + .ritz.tcl is formatted as a tcl file. + + A config-file with the following contents: + + set Secret 0123456789 + set User admin + set Server example.org + set Port 8001 + + global Sortby + set Sortby "upd-rev" + + set _Secret(dev-server) 0123456789 + set _User(dev-server) admin + set _Server(dev-server) example.com + set _Port(dev-server) 8001 + + Results in the following dict: + + { + "default": { + "Secret": "0123456789", + "User": "admin", + "Server": "example.org", + "Port": "8001", + "Sortby": "upd-rev", + }, + "dev-server": { + "Secret": "0123456789", + "User": "admin", + "Server": "example.com", + "Port": "8001", + }, + } + """ + lines = text.split("\n") + config = {} + for line in lines: + _set = re.findall(r"^\s?set _?([a-zA-Z0-9]+)(?:\((.*)\))? (.*)$", line) + if _set: + group = _set[0][1] if _set[0][1] != "" else "default" + key = _set[0][0] + value = _set[0][2] + + if group not in config: + config[group] = {} + + config[group][key] = value + return config + + +def normalize(tcl_config_dict): + """ + Standardize on snake-case key-names and break out global options + + Usage:: + + > config_dict = Normalizer.normalise(tcl_config_dict) + + A config-dict with the following contents:: + + { + "default": { + "Secret": "0123456789", + "User": "admin", + "Server": "example.org", + "Port": "8001", + "Sortby": "upd-rev", + }, + "dev-server": { + "Secret": "0123456789", + "User": "admin", + "Server": "example.com", + "Port": "8001", + }, + } + + Results in a dict with the following contents:: + + { + "connections": { + "default": { + "secret": "0123456789", + "username": "admin", + "server": "example.org", + "port": "8001", + }, + "dev_server": { + "secret": "0123456789", + "username": "admin", + "server": "example.com", + "port": "8001", + }, + }, + "globals": { + "sort_by": "upd-rev", + }, + } + """ + KEYMAP = {"Sortby": "sort_by", "User": "username"} + CONNECTION_KEYS = set(("username", "secret", "server", "port")) + connections = {} + globals = {} + for name in tcl_config_dict: + connection = {} + for key, value in tcl_config_dict[name].items(): + key = KEYMAP.get(key, key.lower()) + key = "_".join(key.split("-")) + if key not in CONNECTION_KEYS: + globals[key] = value + else: + connection[key] = value + name = "_".join(name.split("-")) + connections[name] = connection + return {"globals": globals, "connections": connections} + + +def generate_potential_filenames(): + for filename in TCL_FILENAMES: + filename = find_config_file(filename) + if filename: + return filename + + +# legacy +def parse_tcl_config(filename=None): + if filename is None: + filename = generate_potential_filenames() + contents = load(filename) + config = parse(contents) + return config diff --git a/src/zinolib/config/utils.py b/src/zinolib/config/utils.py new file mode 100644 index 0000000..bb2d5f3 --- /dev/null +++ b/src/zinolib/config/utils.py @@ -0,0 +1,29 @@ +from os import environ +from pathlib import Path + + +DEFAULT_XDG_CONFIG_HOME = Path.home() / '.config' +XDG_CONFIG_HOME = environ.get('XDG_CONFIG_HOME', DEFAULT_XDG_CONFIG_HOME) +VISIBLE_LOCATIONS = [XDG_CONFIG_HOME, Path('/usr/local/etc'), Path('/etc')] +INVISIBLE_LOCATIONS = [Path.cwd(), Path.home()] +CONFIG_DIRECTORIES = INVISIBLE_LOCATIONS + VISIBLE_LOCATIONS + + +def find_config_file(filename): + """ + Look for filename in CONFIG_DIRECTORIES in order + + If the file isn't found in any of them, raise FileNotFoundError + """ + tried = [] + for directory in CONFIG_DIRECTORIES: + if directory in INVISIBLE_LOCATIONS: + used_filename = f'.{filename}' + else: + used_filename = filename + path = directory / used_filename + tried.append(path) + if path.is_file(): + return path + tried_paths = [str(path) for path in tried] + raise FileNotFoundError(f"Looked for config in {tried_paths}, none found") diff --git a/src/zinolib/ritz.py b/src/zinolib/ritz.py index d9e0bf8..6998555 100644 --- a/src/zinolib/ritz.py +++ b/src/zinolib/ritz.py @@ -82,12 +82,11 @@ from datetime import datetime, timedelta import errno from time import mktime -import re -from os.path import expanduser from typing import NamedTuple import codecs import select +from .config.tcl import parse_tcl_config from .utils import windows_codepage_cp1252, generate_authtoken @@ -170,29 +169,6 @@ def _decode_history(logarray): return ret -# def parse_tcl_config(filename: str | Path): -def parse_tcl_config(filename): - """Parse a .ritz.tcl config file - - Used to fetch a users connection information to connect to zino - .ritz.tcl is formatted as a tcl file. - """ - config = {} - with open(expanduser(filename), "r") as f: - for line in f.readlines(): - _set = re.findall(r"^\s?set _?([a-zA-Z0-9]+)(?:\((.*)\))? (.*)$", line) - if _set: - group = _set[0][1] if _set[0][1] != "" else "default" - key = _set[0][0] - value = _set[0][2] - - if group not in config: - config[group] = {} - - config[group][key] = value - return config - - class Case: """Zino case element diff --git a/tests/test_config_tcl.py b/tests/test_config_tcl.py new file mode 100644 index 0000000..313048b --- /dev/null +++ b/tests/test_config_tcl.py @@ -0,0 +1,98 @@ +import os +from pathlib import Path +from tempfile import mkstemp +from unittest import TestCase + +from zinolib.config.tcl import parse_tcl_config, normalize, parse + + +RITZ_CONFIG = """ + set Secret 0123456789 + set User admin + set Server example.org + set Port 8001 + + global Sortby + set Sortby "upd-rev" + + set _Secret(dev-server) 0123456789 + set _User(dev-server) admin + set _Server(dev-server) example.com + set _Port(dev-server) 8001 +""" + + +def clean_config(configtext): + config = "\n".join(line.strip() for line in configtext.split("\n")) + return config + + +def make_configfile(text): + config = clean_config(text) + fd, filename = mkstemp(text=True, suffix=".tcl") + os.write(fd, bytes(config, encoding="ascii")) + return filename + + +def delete_configfile(filename): + Path(filename).unlink(missing_ok=True) + + +class ParseTclConfigTest(TestCase): + def test_parse_tcl_config_missing_config_file(self): + with self.assertRaises(FileNotFoundError): + tcl_config_dict = parse_tcl_config("cvfgdh vghj vbjhk") + + def test_parse_tcl_config_empty_config_file(self): + filename = make_configfile("") + tcl_config_dict = parse_tcl_config(filename) + self.assertEqual(tcl_config_dict, {}) + delete_configfile(filename) + + def test_parse_tcl_config_golden_path(self): + filename = make_configfile(RITZ_CONFIG) + tcl_config_dict = parse_tcl_config(filename) + expected = { + "default": { + "Port": "8001", + "Secret": "0123456789", + "Server": "example.org", + "Sortby": '"upd-rev"', + "User": "admin", + }, + "dev-server": { + "Port": "8001", + "Secret": "0123456789", + "Server": "example.com", + "User": "admin", + }, + } + self.assertEqual(tcl_config_dict, expected) + delete_configfile(filename) + + +class ParseNormalizeTest(TestCase): + def test_normalize_empty_dict(self): + expected = {"globals": {}, "connections": {}} + self.assertEqual(normalize({}), expected) + + def test_normalize_golden_path(self): + tcl_config_dict = parse(clean_config(RITZ_CONFIG)) + expected = { + "globals": {"sort_by": '"upd-rev"'}, + "connections": { + "default": { + "secret": "0123456789", + "username": "admin", + "server": "example.org", + "port": "8001", + }, + "dev_server": { + "secret": "0123456789", + "username": "admin", + "server": "example.com", + "port": "8001", + }, + }, + } + self.assertEqual(normalize(tcl_config_dict), expected) diff --git a/tests/test_config_utils.py b/tests/test_config_utils.py new file mode 100644 index 0000000..638cfe6 --- /dev/null +++ b/tests/test_config_utils.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path +from tempfile import mkstemp, mkdtemp +from unittest import TestCase + +from zinolib.config.utils import find_config_file + + +def make_file(): + fd, filename = mkstemp(dir=str(Path.cwd())) + os.write(fd, b"") + return filename + + +def delete_file(filename): + Path(filename).unlink(missing_ok=True) + + +class FindConfigFileTest(TestCase): + def test_find_config_file_missing_config_file(self): + with self.assertRaises(FileNotFoundError): + find_config_file("bcekjyfbu eyxxgyikyvub iysbiucbcsiu") + + def test_find_config_file_golden_path(self): + filename = make_file() + found_filename = find_config_file(filename) + delete_file(filename) + self.assertEqual(Path.cwd() / filename, found_filename) + + def test_find_config_file_unusuable_file(self): + with self.assertRaises(FileNotFoundError): + filename = mkdtemp(dir=str(Path.cwd())) + found_filename = find_config_file(filename) + delete_file(filename)