From ee8627e0da7eaa39c2b3e928196315e236da7c19 Mon Sep 17 00:00:00 2001 From: dtomlinson Date: Sun, 12 Jan 2020 23:31:04 +0000 Subject: [PATCH] initial commit --- .gitignore | 129 +++++++++++++ LICENSE | 21 +++ README.md | 33 ++++ poetry.lock | 356 +++++++++++++++++++++++++++++++++++ pyproject.toml | 28 +++ src/panaetius/__header__.py | 1 + src/panaetius/__init__.py | 6 + src/panaetius/__version__.py | 1 + src/panaetius/config.py | 206 ++++++++++++++++++++ src/panaetius/config_inst.py | 11 ++ src/panaetius/db.py | 181 ++++++++++++++++++ src/panaetius/header.py | 24 +++ src/panaetius/library.py | 114 +++++++++++ src/panaetius/logging.py | 54 ++++++ tests/__header__.py | 1 + tests/__init__.py | 0 tests/test.py | 31 +++ tests/test_panaetius.py | 5 + 18 files changed, 1202 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/panaetius/__header__.py create mode 100644 src/panaetius/__init__.py create mode 100644 src/panaetius/__version__.py create mode 100644 src/panaetius/config.py create mode 100644 src/panaetius/config_inst.py create mode 100644 src/panaetius/db.py create mode 100644 src/panaetius/header.py create mode 100644 src/panaetius/library.py create mode 100644 src/panaetius/logging.py create mode 100644 tests/__header__.py create mode 100644 tests/__init__.py create mode 100644 tests/test.py create mode 100644 tests/test_panaetius.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..890250c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 dtomlinson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccd7563 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Author + +Daniel Tomlinson (dtomlinson@panaetius.co.uk) + +# Requires + +`>= python3.7` + +# Python requirements + +- toml = "^0.10.0" +- pylite = "^0.1.0" + +# Documentation + +_soon_ + +# Installation + +_soon_ + +# Easy Way + +## Python + +### From pip + +### From local wheel + +### From source + +# Example Usage + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..84305f5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,356 @@ +[[package]] +category = "dev" +description = "Atomic file writes." +name = "atomicwrites" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.3.0" + +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.3.0" + +[package.extras] +azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] +dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] +docs = ["sphinx", "zope.interface"] +tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] + +[[package]] +category = "dev" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +name = "autopep8" +optional = false +python-versions = "*" +version = "1.4.4" + +[package.dependencies] +pycodestyle = ">=2.4.0" + +[[package]] +category = "dev" +description = "Cross-platform colored terminal text." +marker = "sys_platform == \"win32\"" +name = "colorama" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.4.3" + +[[package]] +category = "dev" +description = "Clean single-source support for Python 3 and 2" +name = "future" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "0.18.2" + +[[package]] +category = "dev" +description = "Read metadata from Python packages" +marker = "python_version < \"3.8\"" +name = "importlib-metadata" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +version = "1.4.0" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "rst.linker"] +testing = ["packaging", "importlib-resources"] + +[[package]] +category = "dev" +description = "An autocompletion tool for Python that can be used for text editors." +name = "jedi" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.15.2" + +[package.dependencies] +parso = ">=0.5.2" + +[package.extras] +testing = ["colorama (0.4.1)", "docopt", "pytest (>=3.9.0,<5.0.0)"] + +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + +[[package]] +category = "dev" +description = "More routines for operating on iterables, beyond itertools" +name = "more-itertools" +optional = false +python-versions = ">=3.5" +version = "8.0.2" + +[[package]] +category = "dev" +description = "A Python Parser" +name = "parso" +optional = false +python-versions = "*" +version = "0.5.2" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + +[[package]] +category = "dev" +description = "plugin and hook calling mechanisms for python" +name = "pluggy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "0.13.1" + +[package.dependencies] +[package.dependencies.importlib-metadata] +python = "<3.8" +version = ">=0.12" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +category = "dev" +description = "A full-screen, console-based Python debugger" +name = "pudb" +optional = false +python-versions = "*" +version = "2019.2" + +[package.dependencies] +pygments = ">=1.0" +urwid = ">=1.1.1" + +[[package]] +category = "dev" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "py" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.8.1" + +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.5.0" + +[[package]] +category = "dev" +description = "Python docstring style checker" +name = "pydocstyle" +optional = false +python-versions = ">=3.5" +version = "5.0.2" + +[package.dependencies] +snowballstemmer = "*" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.1" + +[[package]] +category = "dev" +description = "Pygments is a syntax highlighting package written in Python." +name = "pygments" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.5.2" + +[[package]] +category = "main" +description = "Intract with sqlite3 in python as simple as it can be." +name = "pylite" +optional = false +python-versions = "*" +version = "0.1.0" + +[[package]] +category = "dev" +description = "pytest: simple powerful testing with Python" +name = "pytest" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.10.1" + +[package.dependencies] +atomicwrites = ">=1.0" +attrs = ">=17.4.0" +colorama = "*" +more-itertools = ">=4.0.0" +pluggy = ">=0.7" +py = ">=1.5.0" +setuptools = "*" +six = ">=1.10.0" + +[[package]] +category = "dev" +description = "JSON RPC 2.0 server library" +name = "python-jsonrpc-server" +optional = false +python-versions = "*" +version = "0.3.2" + +[package.dependencies] +future = ">=0.14.0" +ujson = "<=1.35" + +[package.extras] +test = ["versioneer", "pylint", "pycodestyle", "pyflakes", "pytest", "mock", "pytest-cov", "coverage"] + +[[package]] +category = "dev" +description = "Python Language Server for the Language Server Protocol" +name = "python-language-server" +optional = false +python-versions = "*" +version = "0.31.4" + +[package.dependencies] +jedi = ">=0.14.1,<0.16" +pluggy = "*" +python-jsonrpc-server = ">=0.3.2" +ujson = "<=1.35" + +[package.extras] +all = ["autopep8", "flake8", "mccabe", "pycodestyle", "pydocstyle (>=2.0.0)", "pyflakes (>=1.6.0)", "pylint", "rope (>=0.10.5)", "yapf"] +autopep8 = ["autopep8"] +flake8 = ["flake8"] +mccabe = ["mccabe"] +pycodestyle = ["pycodestyle"] +pydocstyle = ["pydocstyle (>=2.0.0)"] +pyflakes = ["pyflakes (>=1.6.0)"] +pylint = ["pylint"] +rope = ["rope (>0.10.5)"] +test = ["versioneer", "pylint", "pytest", "mock", "pytest-cov", "coverage", "numpy", "pandas", "matplotlib", "pyqt5"] +yapf = ["yapf"] + +[[package]] +category = "dev" +description = "a python refactoring library..." +name = "rope" +optional = false +python-versions = "*" +version = "0.16.0" + +[package.extras] +dev = ["pytest"] + +[[package]] +category = "dev" +description = "Python 2 and 3 compatibility utilities" +name = "six" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "1.13.0" + +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "main" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + +[[package]] +category = "dev" +description = "Ultra fast JSON encoder and decoder for Python" +marker = "platform_system != \"Windows\"" +name = "ujson" +optional = false +python-versions = "*" +version = "1.35" + +[[package]] +category = "dev" +description = "A full-featured console (xterm et al.) user interface library" +name = "urwid" +optional = false +python-versions = "*" +version = "2.1.0" + +[[package]] +category = "dev" +description = "A formatter for Python code." +name = "yapf" +optional = false +python-versions = "*" +version = "0.29.0" + +[[package]] +category = "dev" +description = "Backport of pathlib-compatible object wrapper for zip files" +marker = "python_version < \"3.8\"" +name = "zipp" +optional = false +python-versions = ">=2.7" +version = "0.6.0" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pathlib2", "contextlib2", "unittest2"] + +[metadata] +content-hash = "7a4d46761d7e6a0219a916d84155c28090cb74e6dce33d70dfc57305c9f9278b" +python-versions = "^3.7" + +[metadata.hashes] +atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"] +attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"] +autopep8 = ["4d8eec30cc81bc5617dbf1218201d770dc35629363547f17577c61683ccfb3ee"] +colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"] +future = ["b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"] +importlib-metadata = ["bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", "f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"] +jedi = ["1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064", "e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807"] +mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] +more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"] +parso = ["55cf25df1a35fd88b878715874d2c4dc1ad3f0eebd1e0266a67e1f55efccfbe1", "5c1f7791de6bd5dbbeac8db0ef5594b36799de198b3f7f7014643b0c5536b9d3"] +pluggy = ["15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"] +pudb = ["e8f0ea01b134d802872184b05bffc82af29a1eb2f9374a277434b932d68f58dc"] +py = ["5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", "c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"] +pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] +pydocstyle = ["da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", "f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5"] +pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] +pygments = ["2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"] +pylite = ["e338d20d3f8f72dd84d1e58f2fd6dba008d593e0cfacfb5fbdd5a297b830628e", "eb46f5beb1f2102672fd4355c013ac2feebc0df284d65f7711f2041a0a410141"] +pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"] +python-jsonrpc-server = ["05bcf26eac4c98c96afec266acdf563d8f454e12612da9a3f9aabb66c46daf35"] +python-language-server = ["68d1a5ed20714e45ee417348ae46de45ab4ed32c6c02ad147cbb9d7ea5293adb"] +rope = ["52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203", "ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad", "d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"] +six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"] +snowballstemmer = ["209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", "df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"] +toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] +ujson = ["f66073e5506e91d204ab0c614a148d5aa938bdbf104751be66f8ad7a222f5f86"] +urwid = ["0896f36060beb6bf3801cb554303fef336a79661401797551ba106d23ab4cd86"] +yapf = ["712e23c468506bf12cadd10169f852572ecc61b266258422d45aaf4ad7ef43de", "cad8a272c6001b3401de3278238fdc54997b6c2e56baa751788915f879a52fca"] +zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4e3f76e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "panaetius" +version = "1.0" +description = "Module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly logger instance." +license = "MIT" +authors = ["dtomlinson "] +readme = "READEME.md" + +[tool.poetry.dependencies] +python = "^3.7" +toml = "^0.10.0" +pylite = "^0.1.0" + +[tool.poetry.dev-dependencies] +pytest = "^3.0" +autopep8 = "^1.4" +pudb = "^2019.2" +McCabe = "^0.6.1" +YAPF = "^0.29.0" +pydocstyle = "^5.0" +Pyflakes = "^2.1" +Rope = "^0.16.0" +python-language-server = "^0.31.4" +pycodestyle = "^2.5" + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/src/panaetius/__header__.py b/src/panaetius/__header__.py new file mode 100644 index 0000000..17c5fdb --- /dev/null +++ b/src/panaetius/__header__.py @@ -0,0 +1 @@ +__header__ = 'panaetius_test' diff --git a/src/panaetius/__init__.py b/src/panaetius/__init__.py new file mode 100644 index 0000000..11348a3 --- /dev/null +++ b/src/panaetius/__init__.py @@ -0,0 +1,6 @@ +from panaetius.config_inst import CONFIG +from .config import Config +from .library import set_config +from panaetius.header import __header__ +import panaetius.logging +from panaetius.logging import logger as logger diff --git a/src/panaetius/__version__.py b/src/panaetius/__version__.py new file mode 100644 index 0000000..7e49527 --- /dev/null +++ b/src/panaetius/__version__.py @@ -0,0 +1 @@ +__version__ = '1.0' diff --git a/src/panaetius/config.py b/src/panaetius/config.py new file mode 100644 index 0000000..63d282d --- /dev/null +++ b/src/panaetius/config.py @@ -0,0 +1,206 @@ +from typing import Callable, Union +import os +import toml + +from panaetius.library import export +from panaetius.header import __header__ +from panaetius.db import Mask + +# __all__ = ['Config'] + + +@export +class Config: + + """Handles the config options for the module and stores config variables + to be shared. + + Attributes + ---------- + config_file : dict + Contains the config options. See + :meth:`~panaetius.config.Config.read_config` + for the data structure. + deferred_messages : list + A list containing the messages to be logged once the logger has been + instantiated. + Mask : panaetius.db.Mask + Class to mask values in a config file. + module_name : str + A string representing the module name. This is added in front of all + envrionment variables and is the title of the `config.toml`. + path : str + Path to config file + + Parameters + ---------- + path : str + Path to config file + """ + + def __init__(self, path: str) -> None: + """ + See :class:`~panaetius.config.Config` for parameters. + """ + self.path = os.path.expanduser(path) + self.deferred_messages = [] + self.config_file = self.read_config(path) + self.module_name = __header__.lower() + self.Mask = Mask + + def read_config(self, path: str, write: bool = False) -> Union[dict, None]: + """Reads the toml config file from `path` if it exists. + + Parameters + ---------- + path : str + Path to config file. Should not contain `config.toml` + + Example: ``path = '~/.config/panaetius'`` + + Returns + ------- + Union[dict, None] + Returns a dict if the file is found else returns nothing. + + The dict contains a key for each header. Each key corresponds to a + dictionary containing a key, value pair for each config under + that header. + + Example:: + + [panaetius] + + [panaetius.foo] + foo = bar + + Returns a dict: + + ``{'panaetius' : {foo: {'foo': 'bar'}}}`` + """ + + path += 'config.toml' if path[-1] == '/' else '/config.toml' + path = os.path.expanduser(path) + if not write: + try: + with open(path, 'r+') as config_file: + config_file = toml.load(config_file) + self.defer_log(f'Config file found at {path}') + return config_file + except FileNotFoundError: + self.defer_log(f'Config file not found at {path}') + pass + else: + try: + with open(path, 'w+') as config_file: + config_file = toml.load(config_file) + self.defer_log(f'Config file found at {path}') + return config_file + except FileNotFoundError: + self.defer_log(f'Config file not found at {path}') + pass + + def get( + self, + key: str, + default: str = None, + cast: Callable = None, + mask: bool = False, + ) -> Union[str, None]: + """Retrives the config variable from either the `config.toml` or an + environment variable. Will default to the default value if nothing + is found + + Parameters + ---------- + key : str + Key to the configuration variable. Should be in the form + `panaetius.variable` or `panaetius.header.variable`. + When loaded, it will be accessable at + `Config.panaetius_variable` or + `Config.panaetius_header_variable`. + default : str, optional + The default value if nothing is found. Defaults to `None`. + cast : Callable, optional + The type of the variable. E.g `int` or `float`. Should reference + the type object and not as string. Defaults to `None`. + + Returns + ------- + Any + Will return the config variable if found, or the default. + """ + env_key = f"{__header__.upper()}_{key.upper().replace('.', '_')}" + + try: + # look in the config.toml + if len(key.split('.')) == 2: + # look for subsections + # print(mask) + if mask: + # print('mask', key) + value = self.Mask( + self.path, self.config_file, key + ).get_value() + else: + # print('no-mask') + section, name = key.lower().split('.') + value = self.config_file[self.module_name][section][name] + self.defer_log(f'{env_key} found in config.toml') + else: + # print('valueerror') + # look under top level module __header__ + # key = f'{self.module_name}.key' + if mask: + # key = f'{__header__}.{key}' + # print(f'mask key={key}') + value = self.Mask( + self.path, self.config_file, key + ).get_value() + else: + name = key.lower() + value = self.config_file[self.module_name][name] + self.defer_log(f'{env_key} found in config.toml') + # finally: + try: + # return if found in config.toml + return cast(value) if cast else value + except UnboundLocalError: + # pass if nothing was found + # print('unbound error') + pass + except KeyError: + # print('key error') + self.defer_log(f'{env_key} not found in config.toml') + except TypeError: + # print('type error') + self.defer_log(f'{env_key} not found in config.toml') + + # look for an environment variable + value = os.environ.get(env_key) + + if value is not None: + self.defer_log(f'{env_key} found in an environment variable') + else: + # fall back to default + self.defer_log(f'{env_key} not found in an environment variable.') + value = default + self.defer_log(f'{env_key} set to default {default}') + return cast(value) if cast else value + + def defer_log(self, msg: str) -> None: + """Populates a list `Config.deferred_messages` with all the events to + be passed to the logger later if required. + + Parameters + ---------- + msg : str + The message to be logged. + """ + self.deferred_messages.append(msg) + + def reset_log(self) -> None: + """Empties the list `Config.deferred_messages`. + """ + del self.deferred_messages + self.deferred_messages = [] diff --git a/src/panaetius/config_inst.py b/src/panaetius/config_inst.py new file mode 100644 index 0000000..61d90c3 --- /dev/null +++ b/src/panaetius/config_inst.py @@ -0,0 +1,11 @@ +import os + +from panaetius.header import __header__ +from panaetius.config import Config + + +DEFAULT_CONFIG_PATH = f'~/.config/{__header__.lower()}' +CONFIG_PATH = os.environ.get( + f'{__header__.upper()}_CONFIG_PATH', DEFAULT_CONFIG_PATH +) +CONFIG = Config(CONFIG_PATH) diff --git a/src/panaetius/db.py b/src/panaetius/db.py new file mode 100644 index 0000000..3eae95a --- /dev/null +++ b/src/panaetius/db.py @@ -0,0 +1,181 @@ +from os import path, urandom +import hashlib +from typing import Tuple +import toml +import io + +from pylite.simplite import Pylite + +from panaetius.header import __header__ as __header__ +import panaetius + + +class Mask: + @property + def hash(self): + try: + if not self._hash_exists: + pass + except AttributeError: + self._hash = hashlib.pbkdf2_hmac( + 'sha256', + self.entry[self.name].encode('utf-8'), + self.salt, + 100000, + dklen=12, + ) + self._hash_exists = True + finally: + return self._hash + + @property + def salt(self): + self._salt = urandom(32) + return self._salt + + @staticmethod + def as_string(obj: bytes) -> str: + return bytes.hex(obj) + + @staticmethod + def fromhex(obj: str) -> bytes: + return bytes.fromhex(obj) + + @staticmethod + def _from_key(config_var) -> Tuple[str, str]: + try: + header, name = config_var.split('.') + except ValueError: + header = '' + name = config_var + return (header, name) + + def __init__( + self, config_path: str, config_contents: dict, config_var: str + ): + self.table: str = __header__ + self.config_path = config_path + self.config_contents = config_contents + self.config_var = config_var.replace('.', '_') + self.header = self._from_key(config_var)[0] + self.name = self._from_key(config_var)[1] + try: + # If value is under a subsection + self.entry = self.config_contents[self.table][self.header] + except KeyError: + # If value is under the main header + self.entry = self.config_contents[self.table] + + def _get_database_file(self): + self.database = self.config_path + self.database += ( + f'.{self.table}.db' + if self.config_path[-1] == '/' + else f'/.{self.table}.db' + ) + self.database = path.expanduser(self.database) + return self + + def _open_database(self): + self.database = Pylite(self.database) + + def _get_table(self): + tables = [i[0] for i in self.database.get_tables()] + if self.table not in tables: + # panaetius.logger.debug( + # 'Table not present in the database;' + # f'creating the table {self.table} now' + # ) + self.database.add_table( + f'{self.table}', + Name='text', + Hash='text', + Salt='text', + Value='text', + ) + else: + # panaetius.logger.debug('Table already exists in the database') + pass + self.table_name = self.table + + def _check_entries(self): + var = self.database.get_items(self.table, f'Name="{self.config_var}"') + if len(var) == 0: + return False + else: + return True + + def _insert_entries(self): + self.database.insert( + self.table, + self.config_var, + self.as_string(self.hash), + self.as_string(self.salt), + self.entry[self.name], + ) + + def update_entries_in_db(self): + self.database.remove(self.table, f'Name="{self.config_var}"') + self._insert_entries() + + def run_query(self, query: str): + cur = self.database.db.cursor() + cur.execute(query) + self.database.db.commit() + self.result = cur.fetchall() + return self + + def get_all_items(self, where_clause: str = None): + if where_clause is not None: + self.result = self.database.get_items(self.table, where_clause) + else: + self.result = self.database.get_items(self.table) + return self + + def process(self): + if not self._check_entries(): + # panaetius.logger.debug('does not exist') + self._insert_entries() + self._update_entries_in_config() + self.get_all_items() + # panaetius.logger.debug(f'returning: {self.result[0][3]}') + return self.entry[self.name] + else: + self.get_all_items(f'Name="{self.config_var}"') + if self.result[0][1] == self.entry[self.name]: + # panaetius.logger.debug('exists and hash matches') + # panaetius.logger.debug(f'returning: {self.result[0][3]}') + return self.result + else: + # panaetius.logger.debug('exists and hash doesnt match') + # panaetius.logger.debug( + # f'file_hash={self.entry[self.name]}, {self.result[0][1]}' + # ) + self.update_entries_in_db() + self._update_entries_in_config() + self.get_all_items(f'Name="{self.config_var}"') + # panaetius.logger.debug(f'returning: {self.result[0][3]}') + return self.entry[self.name] + + def _open_config_file(self) -> io.TextIOWrapper: + self.config_path += ( + '/config.toml' if self.config_path[-1] != '/' else 'config.toml' + ) + c = open(path.expanduser(self.config_path), 'w') + return c + + def _update_entries_in_config(self): + self.entry.update({self.name: self.as_string(self.hash)}) + # panaetius.logger.debug(self.config_contents) + # panaetius.logger.debug(self.entry) + c = self._open_config_file() + toml.dump(self.config_contents, c) + c.close() + + def get_value(self): + # print(f'key in db {self.config_var}') + self._get_database_file() + self._open_database() + self._get_table() + self.process() + return self.result[0][3] diff --git a/src/panaetius/header.py b/src/panaetius/header.py new file mode 100644 index 0000000..ec9687b --- /dev/null +++ b/src/panaetius/header.py @@ -0,0 +1,24 @@ +import os +from importlib import util + +__path = os.getcwd() + +try: + __spec = util.spec_from_file_location( + '__header__', f'{os.getcwd()}/__header__.py' + ) + __header__ = util.module_from_spec(__spec) + __spec.loader.exec_module(__header__) + __header__ = __header__.__header__ +except FileNotFoundError: + venv = os.environ.get('VIRTUAL_ENV').split('/')[-1] + if venv is not None: + __header__ = venv + else: + raise FileNotFoundError( + f'Cannot find a __header__.py file in {os.getcwd()} containing the' + ' __header__ value of your project name and you are not working' + ' from a virtual environment. Either make sure this file ' + 'exists and the value is set or create and work from a virtual ' + 'environment and try again.' + ) diff --git a/src/panaetius/library.py b/src/panaetius/library.py new file mode 100644 index 0000000..039363b --- /dev/null +++ b/src/panaetius/library.py @@ -0,0 +1,114 @@ +from __future__ import annotations +import sys +from typing import Any, TypeVar, Type, TYPE_CHECKING, Union, List +import ast + + +if TYPE_CHECKING: + import logging + + +config_inst_t = TypeVar('config_inst_t', bound='panaetius.config.Config') + + +def export(fn: callable) -> callable: + mod = sys.modules[fn.__module__] + if hasattr(mod, '__all__'): + mod.__all__.append(fn.__name__) + else: + mod.__all__ = [fn.__name__] + return fn + + +def set_config( + config_inst: Type[config_inst_t], + key: str, + default: str = None, + cast: Any = None, + check: Union[None, List] = None, + mask: bool = False, +) -> None: + """Sets the config variable on the instance of a class. + + Parameters + ---------- + config_inst : Type[config_inst_t] + Instance of the :class:`~panaetius.config.Config` class. + key : str + The key referencing the config variable. + default : str, optional + The default value. + mask : bool, optional + Boolean to indiciate if a value in the `config.toml` should be masked. + If this is set to True then the first time the variable is read from + the config file the value will be replaced with a hash. Any time that + value is then read the hash will be compared to the one stored and if + they match the true value will be returned. This is stored in a sqlite + `.db` next to the config file and is hidden by default. If the hash + provided doesn't match the default behaviour is to update the `.db` + with the new value and hash the value again. If you delete the + database file then you will need to set the value again in the + `config.toml`. + cast : Any, optional + The type of the variable. + check : Union[None, List], optional + Type of object to check against. This is useful if you want to use TOML + to define a list, but want to make sure that a string representation + of a list will be loaded properly if it set as an environment variable. + + Example: + + `config.toml` has the following attribute set: + ``` + [package.users] + auth = ['user1', 'user2'] + ``` + + If set as an environment variable you can pass this list as a string + and set check=list: + + Environment variable: + PACKAGE_USERS_AUTH = "['user1', 'user2']" + + Usage in code: + ```python + set_config(CONFIG, 'users.auth', check=list) + ``` + """ + config_var = key.lower().replace('.', '_') + if check is None: + setattr( + config_inst, config_var, config_inst.get(key, default, cast, mask) + ) + else: + if type(config_inst.get(key, default, cast, mask)) is not check: + if check is list: + var = ast.literal_eval( + config_inst.get(key, default, cast, mask) + ) + setattr(config_inst, config_var, var) + else: + setattr( + config_inst, + config_var, + config_inst.get(key, default, cast, mask), + ) + + +# Create function to print cached logged messages and reset +def process_cached_logs( + config_inst: Type[config_inst_t], logger: logging.Logger +): + """Prints the cached messages from :class:`~panaetius.config.Config` + and resets the cache. + + Parameters + ---------- + config_inst : Type[config_inst_t] + Instance of :class:`~panaetius.config.Config`. + logger : logging.Logger + Instance of the logger. + """ + for msg in config_inst.deferred_messages: + logger.info(msg) + config_inst.reset_log() diff --git a/src/panaetius/logging.py b/src/panaetius/logging.py new file mode 100644 index 0000000..4236665 --- /dev/null +++ b/src/panaetius/logging.py @@ -0,0 +1,54 @@ +import logging +from logging.handlers import RotatingFileHandler +import os +import sys + +import panaetius +from panaetius import CONFIG as CONFIG +from panaetius import __header__ as __header__ +from panaetius import set_config as set_config + + +panaetius.set_config(CONFIG, 'logging.path') +panaetius.set_config( + CONFIG, + 'logging.format', + '{\n\t"time": "%(asctime)s",\n\t"file_name": "%(filename)s",' + '\n\t"module": "%(module)s",\n\t"function":"%(funcName)s",\n\t' + '"line_number": "%(lineno)s",\n\t"logging_level":' + '"%(levelname)s",\n\t"message": "%(message)s"\n}', + cast=str, +) +set_config(CONFIG, 'logging.level', 'INFO') + +# Logging Configuration +logger = logging.getLogger(__header__) +loghandler_sys = logging.StreamHandler(sys.stdout) + +# Checking if log path is set +if CONFIG.logging_path: + CONFIG.logging_path += ( + f'{__header__}.log' + if CONFIG.logging_path[-1] == '/' + else f'/{__header__}.log' + ) + # Set default log file options + set_config(CONFIG, 'logging.backup_count', 3, int) + set_config(CONFIG, 'logging.rotate_bytes', 512000, int) + + # Configure file handler + loghandler_file = RotatingFileHandler( + os.path.expanduser(CONFIG.logging_path), + 'a', + CONFIG.logging_rotate_bytes, + CONFIG.logging_backup_count, + ) + + # Add to file formatter + loghandler_file.setFormatter(logging.Formatter(CONFIG.logging_format)) + logger.addHandler(loghandler_file) + +# Configure and add to stdout formatter +loghandler_sys.setFormatter(logging.Formatter(CONFIG.logging_format)) +logger.addHandler(loghandler_sys) +logger.setLevel(CONFIG.logging_level) diff --git a/tests/__header__.py b/tests/__header__.py new file mode 100644 index 0000000..17c5fdb --- /dev/null +++ b/tests/__header__.py @@ -0,0 +1 @@ +__header__ = 'panaetius_test' diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..632d743 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,31 @@ +import panaetius + +# from panaetius import CONFIG as CONFIG +# from panaetius import logger as logger + +print(panaetius.__header__) + +panaetius.set_config(panaetius.CONFIG, 'logging.level') + +# print(panaetius.CONFIG.logging_format) +print(panaetius.CONFIG.logging_path) +print(panaetius.config_inst.CONFIG_PATH) + +# panaetius.logger.info('test event') + + +panaetius.logger.info('setting foo.bar value') +panaetius.set_config(panaetius.CONFIG, 'foo.bar', mask=True) + +panaetius.logger.info(f'foo.bar set to {panaetius.CONFIG.foo_bar}') + +# print((panaetius.CONFIG.path)) +# print(panaetius.CONFIG.logging_level) + +panaetius.set_config(panaetius.CONFIG, 'test', mask=True) +panaetius.logger.info(f'test_root={panaetius.CONFIG.test}') + +print(panaetius.CONFIG.config_file) + +# for i in panaetius.CONFIG.deferred_messages: +# panaetius.logger.debug(i) diff --git a/tests/test_panaetius.py b/tests/test_panaetius.py new file mode 100644 index 0000000..caeeb82 --- /dev/null +++ b/tests/test_panaetius.py @@ -0,0 +1,5 @@ +from panaetius import __version__ + + +def test_version(): + assert __version__ == '0.1.0'