From 9dea99016b9bccd58bdef89ea02e1747995c2a5d Mon Sep 17 00:00:00 2001 From: Lauren Kaviak Date: Wed, 25 Feb 2026 17:27:19 -0600 Subject: [PATCH] base POC python --- .gitignore | 1 + python/__pycache__/config.cpython-313.pyc | Bin 0 -> 10280 bytes python/config.py | 138 ++++++++++++++++++++++ python/encrypted_file.txt | 1 + python/key.txt | 1 + python/plain_file.txt | 1 + python/sample.py | 24 ++++ readme.md | 7 ++ 8 files changed, 173 insertions(+) create mode 100644 .gitignore create mode 100644 python/__pycache__/config.cpython-313.pyc create mode 100644 python/config.py create mode 100644 python/encrypted_file.txt create mode 100644 python/key.txt create mode 100644 python/plain_file.txt create mode 100644 python/sample.py create mode 100644 readme.md 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 0000000000000000000000000000000000000000..42ecb6b340caf5dacfe934ad11cd7bcad1fd142e GIT binary patch literal 10280 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 Y{~clE8zm$@{gnW+_Od98+qgvj7fc1~{Qv*} literal 0 HcmV?d00001 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