commit 9dea99016b9bccd58bdef89ea02e1747995c2a5d Author: Lauren Kaviak Date: Wed Feb 25 17:27:19 2026 -0600 base POC python diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eacdb77 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +python/.venv \ No newline at end of file diff --git a/python/__pycache__/config.cpython-313.pyc b/python/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..42ecb6b Binary files /dev/null and b/python/__pycache__/config.cpython-313.pyc differ diff --git a/python/config.py b/python/config.py new file mode 100644 index 0000000..2899167 --- /dev/null +++ b/python/config.py @@ -0,0 +1,138 @@ +import yaml +import yaql +from os import getenv, getcwd, putenv +from os.path import exists,isfile,isdir +from typing import Any + +class FileReference: + """ + !FileReference should simply refer to a file that exists. + The file is not loaded as part of the configuration. + """ + yaml_tag = '!FileReference' + yaml_loader = yaml.SafeLoader + yaml_dumper = yaml.SafeDumper + filename: str = None + def __init__(self, filename: str): + self.filename = filename + @property + def scalar_value(self): + return self.filename + def __repr__(self): + fn = self.filename + exists = isfile(fn) + return ("(reference to '{}')" if exists else "(invalid reference to '{}')").format(fn) + +class FileData(yaml.YAMLObject): + """ + !FileData should refer to a file that exists. + The file's contents are then loaded as a string + and available for reference. + """ + yaml_tag = '!FileData' + yaml_loader = yaml.SafeLoader + yaml_dumper = yaml.SafeDumper + filename: str = None + data: Any = None + def __init__(self, filename: str): + self.filename = filename + @property + def scalar_value(self): + return self.filename + def read_data(self): + if self.data is not None: return self.data + with open(self.filename,'r') as file: + self.data = file.read() + return self.data + def calculate_data_hash(self): + # to-do: actually hash the data + return self.data[:10] if isinstance(self.data,str) else 'DATA HASH' + def __repr__(self): + hashdata = self.calculate_data_hash() + return '(filename:{filename:}, data hash: {hashdata:})'.format(filename=self.filename,hashdata=hashdata) + +class EncryptedFileData(FileData): + yaml_tag = '!EncryptedFileData' + yaml_loader = yaml.SafeLoader + yaml_dumper = yaml.SafeDumper + def __init__(self, filename: str, key: str | FileData): + self.filename = filename + self.key = key + def key_data(self): + if isinstance(self.key, str): return self.key + if isinstance(self.key, FileData): + return self.key.read_data() + def decrypt(self, data, key): + return data + def read_data(self): + base_data = super().read_data() + key_data = self.key_data() + return self.decrypt(base_data, key_data) + def __repr__(self): + return '(encrypted file: {:})'.format(self.filename) + +def generic_representer(dumper, data): + return dumper.represent_scalar(type(data).yaml_tag, data.scalar_value) + +def filedata_constructor(loader, node): + filename = loader.construct_scalar(node) + return FileData(filename) + +def fileref_constructor(loader, node): + filename = loader.construct_scalar(node) + return FileReference(filename) + +yaml.add_representer(FileData, generic_representer) +yaml.add_constructor(FileData.yaml_tag, filedata_constructor, yaml.SafeLoader) +yaml.add_representer(FileReference, generic_representer) +yaml.add_constructor(FileReference.yaml_tag, fileref_constructor, yaml.SafeLoader) + +class Configuration: + appname: str = '' + document: Any = None + engine: Any = None + def __init__(self): + self.engine = yaql.factory.YaqlFactory().create() + @property + def specified_configfile_variable_name(self): + return self.appname+"_CONFIG_FILE" + def find_config_file(self) -> str: + specified_file = getenv(self.specified_configfile_variable_name) + if specified_file is not None: + return specified_file + home = getenv('HOME') + pwd = getcwd() + populate_template = lambda base:base.format(appname=self.appname,pwd=pwd,home=home) + paths = map(populate_template,[ + "{pwd:}/.config.yaml", + "{home:}/.config/{appname:}/.config.yaml", + "/etc/{appname:}/.config.yaml" + ]) + for path in paths: + if isfile(path): + return path + def specify_config_file(self, fn): + putenv(self.specified_configfile_variable_name, fn) + def load_data(self, config_source=None): + if isinstance(config_source, str): + self.document = yaml.safe_load(config_source) + return + filename_to_load = self.find_config_file() + with open(filename_to_load,'r') as config_file: + self.document = yaml.safe_load(filename_to_load) + def save_config(self): + filename_to_load = self.find_config_file() + with open(filename_to_load,'w') as config_file: + yaml.safe_dump(self.document, config_file, default_flow_style=False) + def dump_config(self): + return yaml.safe_dump(self.document) + def query(self,query): + expression = self.engine(query) + return expression.evaluate(data=self.document) + +def query_property(query, unpack_filedata=True, null_invalid_file_references=True): + def run_query(config): + results = config.query(query) + if not (unpack_filedata or null_invalid_file_references): + return results + return property(run_query) \ No newline at end of file diff --git a/python/encrypted_file.txt b/python/encrypted_file.txt new file mode 100644 index 0000000..9e56060 --- /dev/null +++ b/python/encrypted_file.txt @@ -0,0 +1 @@ +SOON MAY THE WEATHER MAN COME \ No newline at end of file diff --git a/python/key.txt b/python/key.txt new file mode 100644 index 0000000..caecd95 --- /dev/null +++ b/python/key.txt @@ -0,0 +1 @@ +ThereOnceWasAShip \ No newline at end of file diff --git a/python/plain_file.txt b/python/plain_file.txt new file mode 100644 index 0000000..2fa015a --- /dev/null +++ b/python/plain_file.txt @@ -0,0 +1 @@ +WE'LL TAKE OUR LEAVE AND GO \ No newline at end of file diff --git a/python/sample.py b/python/sample.py new file mode 100644 index 0000000..ce825e2 --- /dev/null +++ b/python/sample.py @@ -0,0 +1,24 @@ +from config import Configuration, query_property + +class SampleConfiguration(Configuration): + plain_file = query_property("$.plain") + reference = query_property("$.working_reference") + broken_reference = query_property("$.broken_reference") + encrypted_file = query_property("$.encrypted_file") + + +sample_config=""" +plain: !FileData plain_file.txt +working_reference: !FileReference plain_file.txt +broken_reference: !FileReference fake_file.txt +encrypted_file: !EncryptedFileData + filename: encrypted_file.txt + key: !FileReference key.txt +""" + +config = SampleConfiguration() +config.load_data(config_source=sample_config) + +from pprint import pprint + +pprint([config.reference, config.broken_reference, config.plain_file.read_data(),config.encrypted_file.read_data()]) \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4a81d85 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +# Priorities: + +1. Runtime Settings +1. `$($(appname))_CONFIG_FILE` +1. `$PWD/.config.yaml` +1. `$HOME/.config/$appname/.config.yaml` +1. `/etc/$appname/.config.yaml` \ No newline at end of file