27 Commits

Author SHA1 Message Date
dtomlinson91
03be9558f8 chore: release 2.3.3 (#2)
* linting

* adding py.typed

* adding pyyaml types

* workaround #1

* updating latest

* updating dev dependencies

* adding isort mypy safety

* chore: prepare release 2.3.3
2021-11-20 22:09:08 +00:00
bd1aa09b4c Merge branch 'develop' 2021-11-14 09:12:31 +00:00
b0d635eb04 bumping to v2.3.2 2021-11-14 09:12:26 +00:00
1d72b976a4 removing old docs, adding requirements 2021-11-14 08:53:11 +00:00
4f93519c41 Merge branch 'develop' 2021-11-14 08:33:29 +00:00
3a2a8951a7 updating docstrings, adding duty, updating README 2021-11-14 08:33:22 +00:00
89655d46ae patching to 2.2.2 2021-10-23 21:10:11 +01:00
bbc580424c Merge branch 'develop' 2021-10-23 21:08:37 +01:00
8add8aaefd Merge branch 'feature/skip_header_in_config_init' into develop 2021-10-23 21:08:27 +01:00
1af790f01a updating tests 2021-10-23 21:08:16 +01:00
485ab9ef09 change abc to raise NotImplementedError for tests 2021-10-23 21:08:07 +01:00
844a2f6f3f changing env var to use strings without extra quotes 2021-10-23 21:07:31 +01:00
2092245dad adding skip header directory option 2021-10-23 21:07:07 +01:00
16f753fdf3 updating todo 2021-10-23 21:05:36 +01:00
9f1caf79ff patch - v2.2.1 2021-10-23 05:06:10 +01:00
70911f98b0 add expand_user to Config 2021-10-23 05:05:46 +01:00
441a26127f remove DS_Store 2021-10-23 05:05:35 +01:00
9cc6f2483d bumping to 2.2.0 2021-10-22 22:45:13 +01:00
6e24f9d70b adding Squash to __init__.py 2021-10-22 22:44:50 +01:00
8c18d01f05 bumping version to 2.1 2021-10-20 22:29:19 +01:00
d7700c4863 adding squasher utility 2021-10-20 22:29:08 +01:00
948bc65e76 removing old files 2021-10-20 22:26:46 +01:00
a0627a0922 Merge branch 'feature/toml_to_yaml' into develop 2021-10-19 21:46:49 +01:00
525107ad63 adding a config.yml instead of config.toml 2021-10-19 21:46:41 +01:00
d604179cbf updating todos 2021-10-18 02:36:27 +01:00
31fe9b1afc removing old source code 2021-10-18 02:32:15 +01:00
78b86967e7 Merge branch 'feature/rewrite' into develop 2021-10-18 02:31:35 +01:00
53 changed files with 1143 additions and 1516 deletions

BIN
.DS_Store vendored

Binary file not shown.

7
.coveragerc Normal file
View File

@@ -0,0 +1,7 @@
[report]
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain if tests don't hit defensive assertion code:
raise NotImplementedError

3
.gitignore vendored
View File

@@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
# custom
.DS_Store

86
README.md Normal file
View File

@@ -0,0 +1,86 @@
# Panaetius
This package provides:
- Functionality to read user variables from a `config.yml` or environment variables.
- A convenient default logging formatter printing `json` that can save to disk and rotate.
- Utility functions.
## Config
### options
#### skip_header_init
If `skip_header_init=True` then the `config_path` will not use the `header_variable` as the
sub-directory in the `config_path`.
E.g
`CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True)`
Will look in `~/tembo/config/config.yml`.
If `skip_header_init=False` then would look in `~/tembo/config/tembo/config.yml`.
### Module
Convenient to place in a package/sub-package `__init__.py`.
See Tembo for an example: <https://github.com/tembo-pages/tembo-core/blob/main/tembo/cli/__init__.py>
Example snippet to use in a module:
```python
"""Subpackage that contains the CLI application."""
import os
from typing import Any
import panaetius
from panaetius.exceptions import LoggingDirectoryDoesNotExistException
if (config_path := os.environ.get("TEMBO_CONFIG")) is not None:
CONFIG: Any = panaetius.Config("tembo", config_path, skip_header_init=True)
else:
CONFIG = panaetius.Config(
"tembo", "~/tembo/.config", skip_header_init=True
)
panaetius.set_config(CONFIG, "base_path", "~/tembo")
panaetius.set_config(CONFIG, "template_path", "~/tembo/.templates")
panaetius.set_config(CONFIG, "scopes", {})
panaetius.set_config(CONFIG, "logging.level", "DEBUG")
panaetius.set_config(CONFIG, "logging.path")
try:
logger = panaetius.set_logger(
CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level)
)
except LoggingDirectoryDoesNotExistException:
_LOGGING_PATH = CONFIG.logging_path
CONFIG.logging_path = ""
logger = panaetius.set_logger(
CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level)
)
logger.warning("Logging directory %s does not exist", _LOGGING_PATH)
```
This means in `./tembo/cli/cli.py` you can
```python
import tembo.cli
# access the CONFIG instance + variables from the config.yml
tembo.cli.CONFIG
```
## Utility Functions
### Squasher
Squashes a json object or Python dictionary into a single level dictionary.

View File

@@ -1,33 +0,0 @@
# 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

View File

@@ -1,62 +0,0 @@
Author
=======
Daniel Tomlinson (dtomlinson@panaetius.co.uk)
Requires
=========
`>= python3.7`
Python requirements
====================
- toml = "^0.10.0"
- pylite = "^0.1.0"
Documentation
==============
Read the documentation on `read the docs`_.
.. _read the docs: https://panaetius.readthedocs.io/en/latest/introduction.html
Installation
==============
You can install ``panaetius`` the following ways:
Python
-------
.. Attention:: You should install in a python virtual environment
From pypi using pip
~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
pip install panaetius
From local wheel
~~~~~~~~~~~~~~~~~
Download the latest verion from the `releases`_ page.
.. _releases: https://github.com/dtomlinson91/panaetius/releases
Install with pip:
.. code-block:: bash
pip install -U panaetius-1.0.2-py3-none-any.whl
From source
~~~~~~~~~~~~
Clone the repo and install using ``setup.py``:
.. code-block:: bash
python setup.py

3
TODO
View File

@@ -1,3 +0,0 @@
Todo:
☐ Item

View File

@@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -1,87 +0,0 @@
@import url("css/theme.css");
.modname {
font-size: 0.8em;
opacity: 0.4;
}
.modname::before {
content: '- ';
}
.title {
font-weight: bold;
font-size: 1.2em;
background-color: #eee;
display: block;
padding: 1px 5px;
border-left: 2px solid #ddd;
}
/*colour of the text in the toc*/
.wy-menu-vertical header, .wy-menu-vertical p.caption{
color: #b26d46;;
}
/*colour of the top left header*/
.wy-side-nav-search{
background-color: #31465a;
}
/*colours of the class definitions*/
.rst-content dl:not(.docutils) dt{
background: #e2d7d1;
color: #0b2852;
border-top: solid 3px #31465a;
}
/*colour of the link in the class defintions*/
.rst-content .viewcode-link, .rst-content .viewcode-back{
color: #4b674a;
}
/*colour of the function definitions*/
.rst-content dl:not(.docutils) dl dt{
border-left: solid 3px #31465a;
background: #e2d7d1;
color: #0b2852;
}
/*colour of the link in the function definitions*/
.rst-content .viewcode-link, .rst-content .viewcode-back{
color: #4b674a;
}
/*edit the width of the body*/
.wy-nav-content{
max-width: 1200px;
}
/*code example blocks*/
.rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre{
background: #b4bec8;
color: black;
/*border-style: solid;*/
/*border-width: thin;*/
}
/*colour of inline code blocks using ``*/
.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal{
color: #b26d46;
}
/* Change code blocks font and bump up font size slightly (normally 12px)*/
.rst-content pre.literal-block, .rst-content div[class^="highlight"] pre, .rst-content .linenodiv pre {
font-family: 'Inconsolata', monospace !important;
font-size: 14px !important;
white-space: pre-wrap;
}
/* Change code descriptions and literal blocks (inline code via ``) to match the normal font size being used in the sphinx_RTD_theme text (normally 14px)*/
.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname, code.docutils {
font-family: 'Inconsolata', monospace !important;
font-size: 14px !important;
}
/*variables text*/
dl.class > dd > table.docutils.field-list tbody tr.field-odd.field th.field-name::before{
content: '(Class Attributes) ';
}

View File

@@ -1,17 +0,0 @@
Version history
================
1.1
----
- Adding overwrite to ``__header__`` functionality. See the Configuration documentation page on how to configure.
1.0.2
------
- Minor fixes and documentation updates.
1.0
--------
- Initial release.

View File

@@ -1,95 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import panaetius
from panaetius.__version__ import __version__ as version
import sphinx_rtd_theme
# -- Project information -----------------------------------------------------
project = 'panaetius'
copyright = '2019, Daniel Tomlinson'
author = 'Daniel Tomlinson'
# The full version, including alpha/beta/rc tags
release = version
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx.ext.todo',
]
# -- Napoleon Settings -----------------------------------------------------
napoleon_google_docstring = False
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = False
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = True
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_use_keyword = True
autodoc_member_order = 'bysource'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The master toctree document.
master_doc = 'index'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_static_path = ['_static']
html_context = {'css_files': ['_static/custom.css']}
html_theme_options = {
'collapse_navigation': True,
'display_version': True,
'prev_next_buttons_location': 'both',
#'navigation_depth': 3,
}
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# Enable todo
todo_include_todos = True

View File

@@ -1,68 +0,0 @@
Configuration
=============
panaetius is fairly easy to configure. There are just a couple of options to be aware of.
Manual configuration of ``Config`` instance
--------------------------------------------
Configuring with a ``__header__.py`` is deprecated. Manually set this value.
Use the following snippet to configure (in ``__init__.py``):
.. code-block:: python
import panaetius
from panaetius.config import Config
CONFIG = Config(path="~/.config/island-code-extractor", header="island-code-extractor")
panaetius.set_config(CONFIG, "reddit.secret")
Access this in your code by importing the ``CONFIG`` instance from your module:
.. code-block:: python
from island_code_extractor import CONFIG
from island_code_extractor import panaetius
CONFIG.reddit_output_path
panaetius.logger.info("Using logger")
__header__.py
-------------
You should set a ``__header__.py`` next to your script or module.
This ``__header__.py`` should contain a ``__header__`` variable that sets the name of your project/script.
E.g a ``__header__.py`` for the module ``plex_posters`` would look like:
.. code-block:: python
__header__ = 'plex_posters'
Your config file can then be created at ``~/.config/__header__/config.toml``.
Your environment variables can be created with:
.. code-block:: bash
HEADER_FOO = "bar"
HEADER_SUBSECTION_FOO = "bar"
The headers of the toml file would look like:
.. code-block:: toml
[__header__]
foo = bar
[__header__.subsection]
foo = bar
If you are writing a script, simply place this ``__header__.py`` along side your script. Panaetius will pick this up when the script is ran.
If you are writing a module, you can either place the ``__header__.py`` alongside the script that uses your module. If this is not possible, panaetius will set the default ``__header__`` variable to the name of the virtualenv that the script is activated from.
If neither of the above aren't possible (say your script is running in a lambda on AWS), then ``__header__`` will be set to the default of ``panaetius``.

View File

@@ -1,5 +0,0 @@
.. role:: modname
:class: modname
.. role:: title
:class: title

View File

@@ -1,3 +0,0 @@
Table of Contents
=================
.. include:: toc.rst

View File

@@ -1,154 +0,0 @@
Introduction
=============
.. image:: https://img.shields.io/readthedocs/panaetius?style=for-the-badge :target: https://panaetius.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://img.shields.io/github/v/tag/dtomlinson91/panaetius?style=for-the-badge :alt: GitHub tag (latest by date)
.. image:: https://img.shields.io/github/commit-activity/m/dtomlinson91/panaetius?style=for-the-badge :alt: GitHub commit activity
.. image:: https://img.shields.io/github/issues/dtomlinson91/panaetius?style=for-the-badge :alt: GitHub issues
.. image:: https://img.shields.io/github/license/dtomlinson91/panaetius?style=for-the-badge :alt: GitHubtbc
Python module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly formatted logger instance.
Usage
------
Setting a config file
~~~~~~~~~~~~~~~~~~~~~~
The main functionality of ``panaetius`` is using a config file to store variables.
Your ``config.toml`` can be created and found in ``~/.config/__header__/config.toml`` where ``__header__`` is equal to the variable configured/set. `See how to configure`_ this variable in the configuration section of panaetius.
.. _See how to configure: https://panaetius.readthedocs.io/en/latest/configuration.html#header-py
Setting values in a config.toml/environment variables
#######################################################
A ``config.toml`` can be created in the default folder for the module. In this example this would be found in ``~/.config/example_module/config.toml``.
An example ``config.toml`` could look like:
.. code-block:: toml
[example_module]
test = "a6cbf36649b029f3618a0cc1"
[example_module.logging]
path = "~/.config/example_module"
level = "DEBUG"
[example_module.foo]
bar = "6b3b96815218960ceaf7cceb"
These are equivalent to the environment variables:
.. code-block:: bash
EXAMPLE_MODULE_TEST
EXAMPLE_MODULE_LOGGING_PATH
EXAMPLE_MODULE_LOGGING_LEVEL
EXAMPLE_MODULE_FOO_BAR
.. Attention::
Environment variables take precedent over the ``config.toml``. If both are set then the environment variable will be used.
You can overwrite the ``config.toml`` location by setting the environment variable:
.. code-block:: bash
DEFAULT_CONFIG_PATH = "~/path/to/config"
Setting values in your code
############################
Values in a ``config.toml`` or from an environment variable need to be set in your work in order for you to use them. You can do this easily by
- importing panaetius.
- using the :func:`~panaetius.library.set_config` function.
E.g your script could contain:
.. code-block:: python
import panaetius
panaetius.set_config(panaetius.CONFIG, 'logging.path')
.. Note::
The ``key`` attribute in :func:`~panaetius.library.set_config` is specified as a string, with the hirearchy in the config file split with a ``.``
.. Important::
The default value for a variable defined using :func:`~panaetius.library.set_config` is ``None``. See the documentation of this function to see all the options available.
Accessing values
#################
You can then access the result of this variable later in your code:
.. code-block:: python
panaetius.CONFIG.logging_path
Logging
~~~~~~~~
In order to save to disk, you need to specify a path for the log file in the config file/environment variable. There is no need to register this with :func:`~panaetius.library.set_config` as ``panaetius`` will do this automatically.
There are other options available for you to configure a logger. These are (including the default values which can be overwritten):
.. code-block:: toml
[example_module.logging]
backup_count = 3
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}"
level = "INFO" # Level should be in CAPS
rotate_bytes = 512000
You can use the logger in your code by:
.. code-block:: python
panaetius.logger.info('some log message')
which gives an output of:
.. code-block:: json
{
"time": "2020-01-13 23:07:17,913",
"file_name": "test.py",
"module": "test",
"function":"<module>",
"line_number": "33",
"logging_level":"INFO",
"message": "some logging message"
}
Importing and using the api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See `panaetius api page`_ on how to use and import the module.
.. _panaetius api page: https://panaetius.readthedocs.io/en/latest/modules/panaetius.html
Configuration
---------------
See `configuration page`_ on how to configure ``panaetius``.
.. _configuration page: https://panaetius.readthedocs.io/en/latest/configuration.html

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.config :modname:`panaetius.config`
---------------------------------------------
.. automodule:: panaetius.config
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.config_inst :modname:`panaetius.config_inst`
--------------------------------------------------------
.. automodule:: panaetius.config_inst
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.db :modname:`panaetius.db`
-------------------------------------
.. automodule:: panaetius.db
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.header :modname:`panaetius.header`
---------------------------------------------
.. automodule:: panaetius.header
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.library :modname:`panaetius.library`
------------------------------------------------
.. automodule:: panaetius.library
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,9 +0,0 @@
.. include:: ../global.rst
panaetius.logging :modname:`panaetius.logging`
----------------------------------------------
.. automodule:: panaetius.logging
:members:
:undoc-members:
:show-inheritance:

View File

@@ -1,66 +0,0 @@
.. include:: ../global.rst
*********
panaetius
*********
API
===
The following is availble by importing the module:
.. code-block:: python
import panaetius
panaetius.CONFIG
----------------
:obj:`panaetius.CONFIG` provides an instance of :class:`panaetius.config.Config`
panaetius.set_config()
-----------------------
Conveniently provides :func:`panaetius.library.set_config`
Use in your module/script with:
.. code-block:: python
panaetius.set_config(panaetius.CONFIG, 'aws.secret_key', str, mask=True)
panaetius.CONFIG.aws_secret_key
-------------------------------
Conveniently provides access to all attributes that have been declared with :func:`panaetius.library.set_config`:
.. code-block:: python
my_secret_key = panaetius.CONFIG.aws_secret_key
panaetius.logger
-----------------
:obj:`panaetius.logger` provides a logger instance already formatted with a nice json output.
.. code-block:: python
panaetius.logger.info('some logging message')
This gives a logger output of:
.. code-block:: json
{
"time": "2020-01-13 23:07:17,913",
"file_name": "test.py",
"module": "test",
"function":"<module>",
"line_number": "33",
"logging_level":"INFO",
"message": "some logging message"
}

View File

@@ -1,27 +0,0 @@
.. toctree::
:maxdepth: 1
:caption: Overview
:titlesonly:
introduction
configuration
changelog
.. toctree::
:maxdepth: 4
:caption: Modules
:titlesonly:
modules/panaetius.rst
.. toctree::
:maxdepth: 4
:caption: Submodules
:titlesonly:
modules/panaetius.config.rst
modules/panaetius.config_inst.rst
modules/panaetius.db.rst
modules/panaetius.header.rst
modules/panaetius.library.rst
modules/panaetius.logging.rst

321
duties.py Normal file
View File

@@ -0,0 +1,321 @@
from __future__ import annotations
import importlib
import os
import pathlib
import re
import shutil
import sys
from io import StringIO
from duty import duty
PACKAGE_NAME = "panaetius"
@duty
def update_deps(ctx, dry: bool = False):
"""
Update the dependencies using Poetry.
Args:
ctx: The context instance (passed automatically).
dry (bool, optional) = If True will update the `poetry.lock` without updating the
dependencies themselves. Defaults to False.
Example:
`duty update_deps dry=False`
"""
dry_run = "--dry-run" if dry else ""
ctx.run(
["poetry", "update", dry_run],
title=f"Updating poetry deps {dry_run}",
)
@duty
def test(ctx):
"""
Run tests using pytest.
Args:
ctx: The context instance (passed automatically).
"""
pytest_results = ctx.run(["pytest", "-v"], pty=True)
print(pytest_results)
@duty
def coverage(ctx):
"""
Generate a coverage report and save to XML and HTML.
Args:
ctx: The context instance (passed automatically).
Example:
`duty coverage`
"""
ctx.run(["coverage", "run", "--source", PACKAGE_NAME, "-m", "pytest"])
res = ctx.run(["coverage", "report"], pty=True)
print(res)
ctx.run(["coverage", "html"])
ctx.run(["coverage", "xml"])
@duty
def version(ctx, bump: str = "patch"):
"""
Bump the version using Poetry and update _version.py.
Args:
ctx: The context instance (passed automatically).
bump (str, optional) = poetry version flag. Available options are:
patch, minor, major, prepatch, preminor, premajor, prerelease.
Defaults to patch.
Example:
`duty version bump=major`
"""
# bump with poetry
result = ctx.run(["poetry", "version", bump])
new_version = re.search(r"(?:.*)(?:\s)(\d+\.\d+\.\d+)$", result)
print(new_version.group(0))
# update _version.py
version_file = pathlib.Path(PACKAGE_NAME) / "_version.py"
with version_file.open("w", encoding="utf-8") as version_file:
version_file.write(
f'"""Module containing the version of {PACKAGE_NAME}."""\n\n' + f'__version__ = "{new_version.group(1)}"\n'
)
print(f"Bumped _version.py to {new_version.group(1)}")
@duty
def build(ctx):
"""
Build with poetry and extract the setup.py and copy to project root.
Args:
ctx: The context instance (passed automatically).
Example:
`duty build`
"""
repo_root = pathlib.Path(".")
# build with poetry
result = ctx.run(["poetry", "build"])
print(result)
# extract the setup.py from the tar
extracted_tar = re.search(r"(?:.*)(?:Built\s)(.*)", result)
tar_file = pathlib.Path(f"./dist/{extracted_tar.group(1)}")
shutil.unpack_archive(tar_file, tar_file.parents[0])
# copy setup.py to repo root
extracted_path = tar_file.parents[0] / os.path.splitext(tar_file.stem)[0]
setup_py = extracted_path / "setup.py"
shutil.copyfile(setup_py, (repo_root / "setup.py"))
# cleanup
shutil.rmtree(extracted_path)
@duty
def export(ctx):
"""
Export the dependencies to a requirements.txt file.
Args:
ctx: The context instance (passed automatically).
Example:
`duty export`
"""
requirements_content = ctx.run(
[
"poetry",
"export",
"-f",
"requirements.txt",
"--without-hashes",
]
)
requirements_dev_content = ctx.run(
[
"poetry",
"export",
"-f",
"requirements.txt",
"--without-hashes",
"--dev",
]
)
requirements = pathlib.Path(".") / "requirements.txt"
requirements_dev = pathlib.Path(".") / "requirements_dev.txt"
with requirements.open("w", encoding="utf-8") as req:
req.write(requirements_content)
with requirements_dev.open("w", encoding="utf-8") as req:
req.write(requirements_dev_content)
@duty
def publish(ctx, password: str):
"""
Publish the package to pypi.org.
Args:
ctx: The context instance (passed automatically).
password (str): pypi.org password.
Example:
`duty publish password=$my_password`
"""
dist_dir = pathlib.Path(".") / "dist"
rm_result = rm_tree(dist_dir)
print(rm_result)
publish_result = ctx.run(["poetry", "publish", "-u", "dtomlinson", "-p", password, "--build"])
print(publish_result)
@duty(silent=True)
def clean(ctx):
"""
Delete temporary files.
Args:
ctx: The context instance (passed automatically).
"""
ctx.run("rm -rf .mypy_cache")
ctx.run("rm -rf .pytest_cache")
ctx.run("rm -rf tests/.pytest_cache")
ctx.run("rm -rf build")
ctx.run("rm -rf dist")
ctx.run("rm -rf pip-wheel-metadata")
ctx.run("rm -rf site")
ctx.run("rm -rf coverage.xml")
ctx.run("rm -rf pytest.xml")
ctx.run("rm -rf htmlcov")
ctx.run("find . -iname '.coverage*' -not -name .coveragerc | xargs rm -rf")
ctx.run("find . -type d -name __pycache__ | xargs rm -rf")
ctx.run("find . -name '*.rej' -delete")
@duty
def format(ctx):
"""
Format code using Black and isort.
Args:
ctx: The context instance (passed automatically).
"""
res = ctx.run(["black", "--line-length=99", PACKAGE_NAME], pty=True, title="Running Black")
print(res)
res = ctx.run(["isort", PACKAGE_NAME])
print(res)
@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"])
def check(ctx):
"""
Check the code quality, check types, check documentation builds and check dependencies for vulnerabilities.
Args:
ctx: The context instance (passed automatically).
"""
@duty
def check_code_quality(ctx):
"""
Check the code quality using prospector.
Args:
ctx: The context instance (passed automatically).
"""
ctx.run(["prospector", PACKAGE_NAME], pty=True, title="Checking code quality with prospector")
@duty
def check_types(ctx):
"""
Check the types using mypy.
Args:
ctx: The context instance (passed automatically).
"""
ctx.run(["mypy", PACKAGE_NAME], pty=True, title="Checking types with MyPy")
@duty
def check_docs(ctx):
"""
Check the documentation builds successfully.
Args:
ctx: The context instance (passed automatically).
"""
ctx.run(["mkdocs", "build"], title="Building documentation")
@duty
def check_dependencies(ctx):
"""
Check dependencies with safety for vulnerabilities.
Args:
ctx: The context instance (passed automatically).
"""
for module in sys.modules:
if module.startswith("safety.") or module == "safety":
del sys.modules[module]
importlib.invalidate_caches()
from safety import safety
from safety.formatter import report
from safety.util import read_requirements
requirements = ctx.run(
"poetry export --dev --without-hashes",
title="Exporting dependencies as requirements",
allow_overrides=False,
)
def check_vulns():
packages = list(read_requirements(StringIO(requirements)))
vulns = safety.check(packages=packages, ignore_ids="41002", key="", db_mirror="", cached=False, proxy={})
output_report = report(vulns=vulns, full=True, checked_packages=len(packages))
print(vulns)
if vulns:
print(output_report)
ctx.run(
check_vulns,
stdin=requirements,
title="Checking dependencies",
pty=True,
)
def rm_tree(directory: pathlib.Path):
"""
Recursively delete a directory and all its contents.
Args:
directory (pathlib.Path): The directory to delete.
"""
for child in directory.glob("*"):
if child.is_file():
child.unlink()
else:
rm_tree(child)
directory.rmdir()

View File

@@ -1,6 +0,0 @@
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

View File

@@ -1 +0,0 @@
__version__ = '1.0.2'

View File

@@ -1,178 +0,0 @@
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']
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, header: str = __header__) -> None:
"""
See :class:`~panaetius.config.Config` for parameters.
"""
self.path = os.path.expanduser(path)
self.header = header
self.deferred_messages = []
self.config_file = self.read_config(path)
self.module_name = self.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.
"""
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}')
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}')
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"{self.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 self.header
# key = f'{self.module_name}.key'
if mask:
# key = f'{self.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.replace("-", "_"))
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 = []

View File

@@ -1,9 +0,0 @@
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)

View File

@@ -1,256 +0,0 @@
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:
"""Class to handle masking sensitive values in a config file
Attributes
----------
config_contents : dict
A dict containing the contents of the config file.
config_path : str
The path to the config file.
config_var : str
The key corresponding to the config entry.
database : Pylite
A Pylite instance for the datbase.
entry : str
The result from the config file. Could either be a hash or the raw
value.
header : str
The __header__ which denotes where the config file is stored.
name : str
The key of the entry in the config file.
result : str
The value of the entry in the config file.
table_name : str
The sqlite table name. Defaults to the __header__ value.
"""
@property
def hash(self):
"""Property to determine the hash of a config entry.
Returns
-------
bytes
The hash as a bytes object.
"""
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):
"""Property to detemine a random salt to use in creation of the hash.
Returns
-------
bytes
The salt as a bytes object.
"""
self._salt = urandom(32)
return self._salt
@staticmethod
def as_string(obj: bytes) -> str:
"""Static method to return a string from a bytes object.
Parameters
----------
obj : bytes
Bytes object to be converted to a string.
Returns
-------
str
The bytes object as a string.
"""
return bytes.hex(obj)
@staticmethod
def fromhex(obj: str) -> bytes:
"""Static method to create a bytes object from a string.
Parameters
----------
obj : str
String object to be converted to bytes.
Returns
-------
bytes
The string object as 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
):
"""Summary
See :class:`~Mask` for parameters.
"""
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):
"""Get the true value from the database if it exists, create if it'
' doesn't exist or update if the hash has changed.
Returns
-------
str
The result from the database.
"""
# 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]

View File

@@ -1,26 +0,0 @@
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:
try:
venv = os.environ.get('VIRTUAL_ENV').split('/')[-1]
__header__ = venv
except AttributeError:
print(
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. \n The __header__ value has been '
'set to the default of panaetius.'
)
__header__ = 'panaetius'

View File

@@ -1,112 +0,0 @@
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 :code:`check=list`::
Environment variable:
PACKAGE_USERS_AUTH = "['user1', 'user2']"
Usage in code::
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()

View File

@@ -1,54 +0,0 @@
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)

View File

@@ -1,5 +1,7 @@
"""
panaetius - a utility library to read variables and provide convenient logging.
Panaetius package.
A utility library to read variables and provide convenient logging.
Author: Daniel Tomlinson (dtomlinson@panaetius.co.uk)
"""

3
panaetius/_version.py Normal file
View File

@@ -0,0 +1,3 @@
"""Module containing the version of panaetius."""
__version__ = "2.3.3"

View File

@@ -1,8 +1,8 @@
"""
Access variables from a config file or an environment variable.
Config module to access variables from a config file or an environment variable.
This module defines the `Config` class to interact and read variables from either a
`config.toml` or an environment variable.
`config.yml` or an environment variable.
"""
from __future__ import annotations
@@ -12,33 +12,53 @@ import os
import pathlib
from typing import Any
import toml
# import toml
import yaml
from panaetius.exceptions import KeyErrorTooDeepException, InvalidPythonException
from panaetius.exceptions import KeyErrorTooDeepException
class Config:
"""The configuration class to access variables."""
"""
A configuration class to access user variables.
def __init__(self, header_variable: str, config_path: str = "") -> None:
Args:
header_variable (str): the `header` variable.
config_path (str|None=None): the path where the header directory is stored.
skip_header_init (bool=False): if True will not use a header subdirectory in the
`config_path`.
"""
def __init__(
self,
header_variable: str,
config_path: str | None = None,
skip_header_init: bool = False,
) -> None:
"""
Create a Config object to set and access variables.
Args:
header_variable (str): Your header variable name.
config_path (str, optional): The path where the header directory is stored.
Defaults to `~/.config`.
Defaults to None on initialisation.
skip_header_init (bool, optional): If True will not use a header
subdirectory in the `config_path`. Defaults to False.
Examples:
`config_path` defaults to None on initialisation but will be set to `~/.config`.
Example:
A header of `data_analysis` with a config_path of `~/myapps` will define
a config file in `~/myapps/data_analysis/config.toml`.
a config file in `~/myapps/data_analysis/config.yml`.
"""
self.header_variable = header_variable
self.config_path = (
pathlib.Path(config_path)
if config_path
pathlib.Path(config_path).expanduser()
if config_path is not None
else pathlib.Path.home() / ".config"
)
self.skip_header_init = skip_header_init
self._missing_config = self._check_config_file_exists()
# default logging options
@@ -49,15 +69,21 @@ class Config:
@property
def config(self) -> dict:
"""
Return the contents of the config file. If missing returns an empty dictionary.
Return the contents of the config file.
If no config file is specified then this returns an empty dictionary.
Returns:
dict: The contents of the `.toml` loaded as a python dictionary.
dict: The contents of the config `.yml` loaded as a python dictionary.
"""
config_file_location = self.config_path / self.header_variable / "config.toml"
if self.skip_header_init:
config_file_location = self.config_path / "config.yml"
else:
config_file_location = self.config_path / self.header_variable / "config.yml"
try:
with open(config_file_location, "r", encoding="utf-8") as config_file:
return dict(toml.load(config_file))
# return dict(toml.load(config_file))
return dict(yaml.load(stream=config_file, Loader=yaml.SafeLoader))
except FileNotFoundError:
return {}
@@ -67,8 +93,8 @@ class Config:
The key can either be one (`value`) or two (`data.value`) levels deep.
A key of (`value`) (with a header of `data_analysis`) would refer to a
`config.toml` of:
A key of `value` (with a header of `data_analysis`) would refer to a
`config.yml` of:
```
[data_analysis]
@@ -77,7 +103,7 @@ class Config:
or an environment variable of `DATA_ANALYSIS_VALUE="'some value'"`.
A key of (`data.value`) would refer to a `config.toml` of:
A key of `data.value` would refer to a `config.yml` of:
```
[data_analysis.data]
value = "some value"
@@ -101,7 +127,10 @@ class Config:
return self._get_env_value(env_key, default)
def _check_config_file_exists(self) -> bool:
config_file_location = self.config_path / self.header_variable / "config.toml"
if self.skip_header_init is False:
config_file_location = self.config_path / self.header_variable / "config.yml"
else:
config_file_location = self.config_path / "config.yml"
try:
with open(config_file_location, "r", encoding="utf-8"):
return False
@@ -163,7 +192,8 @@ class Config:
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
raise InvalidPythonException(f"{value} is not valid Python.") # noqa
# string without spaces: ValueError, with spaces; SyntaxError
return value
def __load_default_value(self, default: Any) -> Any: # noqa
return default

View File

@@ -1,13 +1,13 @@
"""Exceptions for the module."""
"""Module that defines custom exceptions for Panetius."""
class KeyErrorTooDeepException(Exception):
pass
"""Raised if the keys in the config.yml are nested too deeply."""
class LoggingDirectoryDoesNotExistException(Exception):
pass
"""Raised if the logging directory does not exist."""
class InvalidPythonException(Exception):
pass
"""Raised if the environement variable Python type is invalid."""

View File

@@ -63,11 +63,17 @@ def set_logger(config_inst: Config, logging_format_inst: LoggingData) -> logging
# configure file handler
if config_inst.logging_path is not None:
if not config_inst.skip_header_init:
logging_file = (
pathlib.Path(config_inst.logging_path)
/ config_inst.header_variable
/ f"{config_inst.header_variable}.log"
).expanduser()
else:
logging_file = (
pathlib.Path(config_inst.logging_path)
/ f"{config_inst.header_variable}.log"
).expanduser()
if not logging_file.parents[0].exists():
raise LoggingDirectoryDoesNotExistException()
@@ -98,11 +104,16 @@ class LoggingData(metaclass=ABCMeta):
@property
@abstractmethod
def format(self) -> str:
pass
raise NotImplementedError
@property
@abstractmethod
def logging_level(self) -> str:
raise NotImplementedError
@abstractmethod
def __init__(self, logging_level: str):
self.logging_level = logging_level
raise NotImplementedError
class SimpleLogger(LoggingData):
@@ -113,8 +124,12 @@ class SimpleLogger(LoggingData):
'"%(levelname)s",\n\t"message": "%(message)s"\n}',
)
@property
def logging_level(self) -> str:
return self._logging_level
def __init__(self, logging_level: str = "INFO"):
self.logging_level = logging_level
self._logging_level = logging_level
class AdvancedLogger(LoggingData):
@@ -127,8 +142,12 @@ class AdvancedLogger(LoggingData):
'"%(levelname)s",\n\t"message": "%(message)s"\n}',
)
@property
def logging_level(self) -> str:
return self._logging_level
def __init__(self, logging_level: str = "INFO"):
self.logging_level = logging_level
self._logging_level = logging_level
class CustomLogger(LoggingData):
@@ -136,6 +155,10 @@ class CustomLogger(LoggingData):
def format(self) -> str:
return str(self._format)
@property
def logging_level(self) -> str:
return self._logging_level
def __init__(self, logging_format: str, logging_level: str = "INFO"):
self.logging_level = logging_level
self._logging_level = logging_level
self._format = logging_format

0
panaetius/py.typed Normal file
View File

View File

@@ -0,0 +1,3 @@
"""Sub-package which defines general utility functions."""
from panaetius.utilities.squasher import Squash

View File

@@ -0,0 +1,64 @@
"""Sub-module that defines squashing json objects into a single json object."""
from __future__ import annotations
from copy import deepcopy
import itertools
from typing import Iterator, Tuple
class Squash:
"""Squash a json object or Python dictionary into a single level dictionary."""
def __init__(self, data: dict) -> None:
"""
Create a Squash object to squash data into a single level dictionary.
Args:
data (dict): [description]
Example:
squashed_data = Squash(my_data)
squashed_data.as_dict
"""
self.data = data
@property
def as_dict(self) -> dict:
"""
Return the squashed data as a dictionary.
Returns:
dict: The original data squashed as a dict.
"""
return self._squash()
@staticmethod
def _unpack_dict(
key: str, value: dict | list | str
) -> Iterator[Tuple[str, dict | list | str]]:
if isinstance(value, dict):
for sub_key, sub_value in value.items():
temporary_key = f"{key}_{sub_key}"
yield temporary_key, sub_value
elif isinstance(value, list):
for index, sub_value in enumerate(value):
temporary_key = f"{key}_{index}"
yield temporary_key, sub_value
else:
yield key, value
def _squash(self) -> dict:
result = deepcopy(self.data)
while True:
result = dict(
itertools.chain.from_iterable(
itertools.starmap(self._unpack_dict, result.items())
)
)
if not any(
isinstance(value, dict) for value in result.values()
) and not any(isinstance(value, list) for value in result.values()):
break
return result

324
poetry.lock generated
View File

@@ -1,3 +1,18 @@
[[package]]
name = "ansimarkup"
version = "1.5.0"
description = "Produce colored terminal text with an xml-like markup"
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
colorama = "*"
[package.extras]
devel = ["bumpversion (>=0.5.2)", "check-manifest (>=0.35)", "readme-renderer (>=16.0)", "flake8", "pep8-naming"]
tests = ["tox (>=2.6.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.3.1)"]
[[package]]
name = "astroid"
version = "2.8.2"
@@ -49,6 +64,45 @@ PyYAML = ">=5.3.1"
six = ">=1.10.0"
stevedore = ">=1.20.0"
[[package]]
name = "cached-property"
version = "1.5.2"
description = "A decorator for caching properties in classes."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "certifi"
version = "2021.10.8"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "charset-normalizer"
version = "2.0.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "click"
version = "8.0.3"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "colorama"
version = "0.4.4"
@@ -76,6 +130,34 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "dparse"
version = "0.5.1"
description = "A parser for Python dependency files"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
packaging = "*"
pyyaml = "*"
toml = "*"
[package.extras]
pipenv = ["pipenv"]
[[package]]
name = "duty"
version = "0.7.0"
description = "A simple task runner."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cached-property = {version = ">=1.5,<2.0", markers = "python_version < \"3.8\""}
failprint = ">=0.8,<1.0"
[[package]]
name = "execnet"
version = "1.9.0"
@@ -87,6 +169,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
testing = ["pre-commit"]
[[package]]
name = "failprint"
version = "0.8.0"
description = "Run a command, print its output only if it fails."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
ansimarkup = ">=1.4,<2.0"
jinja2 = ">=2.11,<4"
ptyprocess = {version = ">=0.6,<1.0", markers = "sys_platform != \"win32\""}
[[package]]
name = "flake8"
version = "2.3.0"
@@ -134,6 +229,14 @@ python-versions = ">=3.7"
gitdb = ">=4.0.1,<5"
typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
optional = false
python-versions = ">=3.5"
[[package]]
name = "importlib-metadata"
version = "4.8.1"
@@ -161,7 +264,7 @@ python-versions = "*"
[[package]]
name = "isort"
version = "5.9.3"
version = "5.10.1"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
@@ -173,6 +276,20 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "jinja2"
version = "3.0.3"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "lazy-object-proxy"
version = "1.6.0"
@@ -181,6 +298,14 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mccabe"
version = "0.6.1"
@@ -315,6 +440,14 @@ with_mypy = ["mypy (>=0.600)"]
with_pyroma = ["pyroma (>=2.4)"]
with_vulture = ["vulture (>=1.5)"]
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "py"
version = "1.10.0"
@@ -421,14 +554,6 @@ python-versions = "*"
[package.dependencies]
pylint = ">=1.7"
[[package]]
name = "pylite"
version = "0.1.0"
description = "Intract with sqlite3 in python as simple as it can be."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyparsing"
version = "2.4.7"
@@ -504,10 +629,28 @@ testing = ["filelock"]
name = "pyyaml"
version = "6.0"
description = "YAML parser and emitter for Python"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "requests"
version = "2.26.0"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "requirements-detector"
version = "0.7"
@@ -519,6 +662,20 @@ python-versions = "*"
[package.dependencies]
astroid = ">=1.4"
[[package]]
name = "safety"
version = "1.10.3"
description = "Checks installed dependencies for known vulnerabilities."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
Click = ">=6.0"
dparse = ">=0.5.1"
packaging = "*"
requests = "*"
[[package]]
name = "setoptconf-tmp"
version = "0.3.1"
@@ -582,6 +739,14 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "types-pyyaml"
version = "6.0.1"
description = "Typing stubs for PyYAML"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "types-toml"
version = "0.10.1"
@@ -598,6 +763,19 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.7"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "wrapt"
version = "1.12.1"
@@ -621,9 +799,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "468d1aa5e0c440262f6041ad859358a84ef32462941aa6f3ba71838a52cc1ced"
content-hash = "e9da2be8225df95a41d2027f5c9cdaa7f744beff549d915d9922d7ddead8dc54"
[metadata.files]
ansimarkup = [
{file = "ansimarkup-1.5.0-py2.py3-none-any.whl", hash = "sha256:3146ca74af5f69e48a9c3d41b31085c0d6378f803edeb364856d37c11a684acf"},
{file = "ansimarkup-1.5.0.tar.gz", hash = "sha256:96c65d75bbed07d3dcbda8dbede8c2252c984f90d0ca07434b88a6bbf345fad3"},
]
astroid = [
{file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"},
{file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"},
@@ -640,6 +822,22 @@ bandit = [
{file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
{file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
]
cached-property = [
{file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"},
{file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"},
]
certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
{file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
]
click = [
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
@@ -683,10 +881,22 @@ dodgy = [
{file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"},
{file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"},
]
dparse = [
{file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"},
{file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"},
]
duty = [
{file = "duty-0.7.0-py3-none-any.whl", hash = "sha256:45068baf1639f16464aa40e9d8f698f0ae09408368fe53a34e9bfe6993dfd743"},
{file = "duty-0.7.0.tar.gz", hash = "sha256:5ebfd4640ab41e3058f1d8433f74228d60c9a808def1784e65319ef1899a9d15"},
]
execnet = [
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
failprint = [
{file = "failprint-0.8.0-py3-none-any.whl", hash = "sha256:a8215a7aca5ce687116b995cd3a9667180f222ab88c4328a5007d2fa0b5c0f78"},
{file = "failprint-0.8.0.tar.gz", hash = "sha256:4633b52f9395bf042ad996c96cd7819a94b2021833030dd1eb692ebbd86b89a1"},
]
flake8 = [
{file = "flake8-2.3.0-py2.py3-none-any.whl", hash = "sha256:c99cc9716d6655d9c8bcb1e77632b8615bf0abd282d7abd9f5c2148cad7fc669"},
{file = "flake8-2.3.0.tar.gz", hash = "sha256:5ee1a43ccd0716d6061521eec6937c983efa027793013e572712c4da55c7c83e"},
@@ -703,6 +913,10 @@ gitpython = [
{file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
{file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
@@ -712,8 +926,12 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
{file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"},
@@ -739,6 +957,62 @@ lazy-object-proxy = [
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@@ -800,6 +1074,10 @@ prospector = [
{file = "prospector-1.5.1-py3-none-any.whl", hash = "sha256:47f8ff3fd36ae276967eb392ca20b300a7bdea66c0d0252250a4d89a6c03ab15"},
{file = "prospector-1.5.1.tar.gz", hash = "sha256:851c2892cd615cfee91fd27cfaf7a5061d14daf2853aa8f012e927b98f919578"},
]
ptyprocess = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
@@ -834,10 +1112,6 @@ pylint-plugin-utils = [
{file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"},
{file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"},
]
pylite = [
{file = "pylite-0.1.0-py3-none-any.whl", hash = "sha256:eb46f5beb1f2102672fd4355c013ac2feebc0df284d65f7711f2041a0a410141"},
{file = "pylite-0.1.0.tar.gz", hash = "sha256:e338d20d3f8f72dd84d1e58f2fd6dba008d593e0cfacfb5fbdd5a297b830628e"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@@ -893,9 +1167,17 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
]
requests = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
requirements-detector = [
{file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"},
]
safety = [
{file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"},
{file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"},
]
setoptconf-tmp = [
{file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"},
{file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"},
@@ -952,6 +1234,10 @@ typed-ast = [
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
types-pyyaml = [
{file = "types-PyYAML-6.0.1.tar.gz", hash = "sha256:2e27b0118ca4248a646101c5c318dc02e4ca2866d6bc42e84045dbb851555a76"},
{file = "types_PyYAML-6.0.1-py3-none-any.whl", hash = "sha256:d5b318269652e809b5c30a5fe666c50159ab80bfd41cd6bafe655bf20b29fcba"},
]
types-toml = [
{file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"},
{file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"},
@@ -961,6 +1247,10 @@ typing-extensions = [
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
urllib3 = [
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
]
wrapt = [
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
]

View File

@@ -1,10 +1,10 @@
[tool.poetry]
name = "panaetius"
version = "1.1"
version = "2.3.3"
description = "Python module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly formatted logger instance."
license = "MIT"
authors = ["dtomlinson <dtomlinson@panaetius.co.uk>"]
readme = "./README.rst"
readme = "./README.md"
homepage = "https://github.com/dtomlinson91/panaetius"
repository = "https://github.com/dtomlinson91/panaetius"
documentation = "https://panaetius.readthedocs.io/en/latest/introduction.html"
@@ -25,7 +25,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.7"
toml = "^0.10.0"
pylite = "^0.1.0"
PyYAML = "^6.0"
[tool.poetry.dev-dependencies]
prospector = {extras = ["with_bandit", "with_mypy"], version = "^1.5.1"}
@@ -34,6 +34,24 @@ pytest = "^6.2.5"
pytest-datadir = "^1.3.1"
pytest-xdist = "^2.4.0"
coverage = "^6.0.2"
duty = "^0.7.0"
types-PyYAML = "^6.0.1"
isort = "^5.10.1"
mypy = "^0.910"
safety = "^1.10.3"
[tool.black]
line-length = 120
[tool.isort]
line-length = 120
not_skip = "__init__.py"
multi_line_output = 3
force_single_line = false
balanced_wrapping = true
default_section = "THIRDPARTY"
known_first_party = "duty"
include_trailing_comma = true
[build-system]
requires = ["poetry>=0.12"]

View File

@@ -1,7 +1,2 @@
pylite==0.1.0 \
--hash=sha256:e338d20d3f8f72dd84d1e58f2fd6dba008d593e0cfacfb5fbdd5a297b830628e \
--hash=sha256:eb46f5beb1f2102672fd4355c013ac2feebc0df284d65f7711f2041a0a410141
toml==0.10.0 \
--hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c \
--hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \
--hash=sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3
pyyaml==6.0; python_version >= "3.6"
toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")

61
requirements_dev.txt Normal file
View File

@@ -0,0 +1,61 @@
ansimarkup==1.5.0; python_version >= "3.6"
astroid==2.8.2; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1"
atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
bandit==1.7.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5"
cached-property==1.5.2; python_version < "3.8" and python_version >= "3.6"
colorama==0.4.4; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.1" and python_version < "4.0" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
coverage==6.0.2; python_version >= "3.6"
dodgy==0.2.1; python_full_version >= "3.6.1" and python_version < "4.0"
duty==0.7.0; python_version >= "3.6"
execnet==1.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
failprint==0.8.0; python_version >= "3.6"
flake8-polyfill==1.0.2; python_full_version >= "3.6.1" and python_version < "4.0"
flake8==2.3.0; python_full_version >= "3.6.1" and python_version < "4.0"
gitdb==4.0.7; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7"
gitpython==3.1.24; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7"
importlib-metadata==4.8.1; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") and python_full_version >= "3.6.1"
iniconfig==1.1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
isort==5.9.3; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
jinja2==3.0.3; python_version >= "3.6"
lazy-object-proxy==1.6.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1"
markupsafe==2.0.1; python_version >= "3.6"
mccabe==0.6.1; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
mypy-extensions==0.4.3; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5"
mypy==0.910; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5"
packaging==21.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
pbr==5.6.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
pep8-naming==0.10.0; python_full_version >= "3.6.1" and python_version < "4.0"
pep8==1.7.1; python_full_version >= "3.6.1" and python_version < "4.0"
platformdirs==2.4.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1"
pluggy==1.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
prospector==1.5.1; python_full_version >= "3.6.1" and python_version < "4.0"
ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.6"
py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pycodestyle==2.8.0; python_full_version >= "3.6.1" and python_version < "4.0"
pydocstyle==6.1.1; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
pyflakes==2.3.1; python_full_version >= "3.6.1" and python_version < "4.0"
pylint-celery==0.3; python_full_version >= "3.6.1" and python_version < "4.0"
pylint-django==2.4.4; python_full_version >= "3.6.1" and python_version < "4.0"
pylint-flask==0.6; python_full_version >= "3.6.1" and python_version < "4.0"
pylint-plugin-utils==0.6; python_full_version >= "3.6.1" and python_version < "4.0"
pylint==2.11.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1"
pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6"
pytest-datadir==1.3.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
pytest-forked==1.3.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
pytest-xdist==2.4.0; python_version >= "3.6"
pytest==6.2.5; python_version >= "3.6"
pyyaml==6.0; python_version >= "3.6"
requirements-detector==0.7; python_full_version >= "3.6.1" and python_version < "4.0"
setoptconf-tmp==0.3.1; python_full_version >= "3.6.1" and python_version < "4.0"
six==1.16.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5"
smmap==4.0.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7"
snowballstemmer==2.1.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
stevedore==3.4.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0")
typed-ast==1.4.3; python_full_version >= "3.6.1" and python_version < "3.8" and python_version >= "3.6" and implementation_name == "cpython"
types-pyyaml==6.0.1
types-toml==0.10.1
typing-extensions==3.10.0.2; python_full_version >= "3.6.1" and python_version < "3.8" and python_version >= "3.7"
wrapt==1.12.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1"
zipp==3.6.0; python_version < "3.8" and python_version >= "3.6"

View File

@@ -1,70 +0,0 @@
Coding:
No Config File:
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25)
needs to be lower case in the toml, need a check for this
Config File:
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25)
needs to be lower case in the toml, need a check for this
Logging:
✔ Create SimpleLogger, AdvancedLogger, CustomLogger classes @done(21-10-16 16:22)
should simply have the different logging strings to output
should both specify whether to save to file or not
✔ Logging path should take by default the config path unless overwritten? @done(21-10-16 23:49)
Errors:
✔ Check logging path + config path are valid, if not raise error. @done(21-10-18 00:04)
✔ Add tests for these. @done(21-10-18 00:04)
✔ Check for a key > 2 levels, raise custom error, write test @done(21-10-17 23:30)
Linting:
✔ Check all functions and annotations. @done(21-10-18 01:07)
Docstrings:
✔ Write the docstrings for public functions/methods. @done(21-10-18 02:29)
Functionality:
✔ When both a config file and a env var is found, use the env var. @done(21-10-18 00:38)
Documentation:
☐ Rewrite documentation using `mkdocs` and using `.md`. @2h
Misc:
☐ Use the python runner to build the docs & run the tests (including coverage html)
coverage run -m pytest && coverage report && coverage html
☐ document this in trilium
Tests:
Bugfixes:
✔ If loading from a default, don't covert to TOML @done(21-10-17 20:33)
✔ Env Vars should be given as python objects @done(21-10-17 20:33)
The string or node provided may only consist of the following Python literal structures: strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None.
use ast.literal_eval()
https://docs.python.org/3/library/ast.html#ast.literal_eval
__init__:
✔ Test default config path set to "~/.config" @done(21-10-17 17:25)
✔ Test config path is set when passed in @done(21-10-17 17:25)
config property:
✔ Check testing config file is returned as dict @done(21-10-17 17:25)
✔ Check _self.missing_config and empty dict is returned @done(21-10-17 17:25)
get_value:
config_file:
✔ Arrays & tables loaded correctly from config file @done(21-10-17 20:34)
✔ test when key length is 1 the value is returned @done(21-10-17 18:55)
✔ test when key length is 2 the value is returned @done(21-10-17 18:55)
✔ test when key not found and no env var default is loaded @done(21-10-17 19:01)
✔ test bool's are properly converted @done(21-10-17 19:01)
✔ test when key not found and env var is set value is loaded @done(21-10-17 20:43)
env_var:
✔ check if env key is missing the default is read in @done(21-10-17 20:55)
✔ check if env key is present the values are read in @done(21-10-17 22:24)
✔ parametrise a test to read in values form env vars and they're set correctly @done(21-10-17 22:24)
✔ test that the env var is valid python @done(21-10-18 01:03)
library:
✔ test set_config works @done(21-10-17 23:29)

View File

@@ -1,27 +1,25 @@
# -*- coding: utf-8 -*-
from distutils.core import setup
package_dir = \
{'': 'src'}
from setuptools import setup
packages = \
['panaetius']
['panaetius', 'panaetius.utilities']
package_data = \
{'': ['*']}
install_requires = \
['pylite>=0.1.0,<0.2.0', 'toml>=0.10.0,<0.11.0']
['PyYAML>=6.0,<7.0', 'toml>=0.10.0,<0.11.0']
setup_kwargs = {
'name': 'panaetius',
'version': '1.0.2',
'description': 'Python module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly logger instance.',
'long_description': 'Author\n=======\n\nDaniel Tomlinson (dtomlinson@panaetius.co.uk)\n\nRequires\n=========\n\n`>= python3.7`\n\nPython requirements\n====================\n\n- toml = "^0.10.0"\n- pylite = "^0.1.0"\n\nDocumentation\n==============\n\nRead the documentation on `read the docs`_.\n\n.. _read the docs: https://panaetius.readthedocs.io/en/latest/introduction.html\n\nInstallation\n==============\n\nYou can install ..:obj:`panaetius`\n\nEasy Way\n=========\n\nPython\n-------\n\nFrom pip\n~~~~~~~~~\n\nFrom local wheel\n~~~~~~~~~~~~~~~~~\n\nFrom source\n~~~~~~~~~~~~\n\nExample Usage\n==============\n\n',
'version': '2.3.3',
'description': 'Python module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly formatted logger instance.',
'long_description': '# Panaetius\n\nThis package provides:\n\n- Functionality to read user variables from a `config.yml` or environment variables.\n- A convenient default logging formatter printing `json` that can save to disk and rotate.\n- Utility functions.\n\n## Config\n\n### options\n\n#### skip_header_init\n\nIf `skip_header_init=True` then the `config_path` will not use the `header_variable` as the\nsub-directory in the `config_path`.\n\nE.g\n\n`CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True)`\n\nWill look in `~/tembo/config/config.yml`.\n\nIf `skip_header_init=False` then would look in `~/tembo/config/tembo/config.yml`.\n\n### Module\n\nConvenient to place in a package/sub-package `__init__.py`.\n\nSee Tembo for an example: <https://github.com/tembo-pages/tembo-core/blob/main/tembo/cli/__init__.py>\n\nExample snippet to use in a module:\n\n```python\n"""Subpackage that contains the CLI application."""\n\nimport os\nfrom typing import Any\n\nimport panaetius\nfrom panaetius.exceptions import LoggingDirectoryDoesNotExistException\n\n\nif (config_path := os.environ.get("TEMBO_CONFIG")) is not None:\n CONFIG: Any = panaetius.Config("tembo", config_path, skip_header_init=True)\nelse:\n CONFIG = panaetius.Config(\n "tembo", "~/tembo/.config", skip_header_init=True\n )\n\n\npanaetius.set_config(CONFIG, "base_path", "~/tembo")\npanaetius.set_config(CONFIG, "template_path", "~/tembo/.templates")\npanaetius.set_config(CONFIG, "scopes", {})\npanaetius.set_config(CONFIG, "logging.level", "DEBUG")\npanaetius.set_config(CONFIG, "logging.path")\n\ntry:\n logger = panaetius.set_logger(\n CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level)\n )\nexcept LoggingDirectoryDoesNotExistException:\n _LOGGING_PATH = CONFIG.logging_path\n CONFIG.logging_path = ""\n logger = panaetius.set_logger(\n CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level)\n )\n logger.warning("Logging directory %s does not exist", _LOGGING_PATH)\n\n```\n\nThis means in `./tembo/cli/cli.py` you can\n\n```python\nimport tembo.cli\n\n# access the CONFIG instance + variables from the config.yml\ntembo.cli.CONFIG\n```\n\n\n## Utility Functions\n\n### Squasher\n\nSquashes a json object or Python dictionary into a single level dictionary.\n',
'author': 'dtomlinson',
'author_email': 'dtomlinson@panaetius.co.uk',
'maintainer': None,
'maintainer_email': None,
'url': 'https://github.com/dtomlinson91/panaetius',
'package_dir': package_dir,
'packages': packages,
'package_data': package_data,
'install_requires': install_requires,

View File

@@ -0,0 +1,9 @@
panaetius_testing:
some_top_string: some_top_value
second:
some_second_string: some_second_value
some_second_int: 1
some_second_float: 1.0
some_second_list: ["some", "second", "value"]
some_second_table: { "first": ["some", "first", "value"] }
some_second_table_bools: { "bool": [true, false] }

View File

@@ -0,0 +1,9 @@
panaetius_testing:
some_top_string: some_top_value
second:
some_second_string: some_second_value
some_second_int: 1
some_second_float: 1.0
some_second_list: ["some", "second", "value"]
some_second_table: { "first": ["some", "first", "value"] }
some_second_table_bools: { "bool": [true, false] }

View File

@@ -29,6 +29,17 @@ def test_user_config_path_set(header, shared_datadir):
assert str(config.config_path) == config_path
def test_user_config_path_without_header_dir_set(header, shared_datadir):
# arrange
config_path = str(shared_datadir / "without_header")
# act
config = panaetius.Config(header, config_path, skip_header_init=True)
# assert
assert str(config.config_path) == config_path
# test config files
@@ -44,6 +55,18 @@ def test_config_file_exists(header, shared_datadir):
assert config._missing_config is False
def test_config_file_without_header_dir_exists(header, shared_datadir):
# arrange
config_path = str(shared_datadir / "without_header")
# act
config = panaetius.Config(header, config_path, skip_header_init=True)
_ = config.config
# assert
assert config._missing_config is False
def test_config_file_contents_read_success(
header, shared_datadir, testing_config_contents
):
@@ -106,7 +129,7 @@ def test_get_value_from_key(
def test_get_value_environment_var_override(header, shared_datadir):
# arrange
os.environ[f"{header.upper()}_SOME_TOP_STRING"] = '"some_overridden_value"'
os.environ[f"{header.upper()}_SOME_TOP_STRING"] = "some_overridden_value"
config_path = str(shared_datadir / "without_logging")
config = panaetius.Config(header, config_path)
panaetius.set_config(config, "some_top_string")
@@ -158,7 +181,7 @@ def test_get_value_missing_key_from_default(header, shared_datadir):
def test_get_value_missing_key_from_env(header, shared_datadir):
# arrange
os.environ[f"{header.upper()}_MISSING_KEY"] = '"some missing key"'
os.environ[f"{header.upper()}_MISSING_KEY"] = "some missing key"
config_path = str(shared_datadir / "without_logging")
config = panaetius.Config(header, config_path)
@@ -205,7 +228,7 @@ def test_missing_config_read_from_default(header, shared_datadir):
@pytest.mark.parametrize(
"env_value,expected_value",
[
('"a missing string"', "a missing string"),
("a missing string", "a missing string"),
("1", 1),
("1.0", 1.0),
("True", True),
@@ -237,6 +260,7 @@ def test_missing_config_read_from_env_var(
del os.environ[f"{header.upper()}_MISSING_KEY_READ_FROM_ENV_VAR"]
@pytest.mark.skip(reason="No longer needed as strings are loaded without quotes")
def test_missing_config_read_from_env_var_invalid_python(header):
# arrange
os.environ[f"{header.upper()}_INVALID_PYTHON"] = "a string without quotes"

View File

@@ -21,6 +21,7 @@ def test_logging_directory_does_not_exist(header, shared_datadir):
assert str(logging_exception.value) == ""
# TODO: change this test so it asserts the dir exists
def test_logging_directory_does_exist(header, shared_datadir):
# arrange
config = Config(header)
@@ -32,3 +33,5 @@ def test_logging_directory_does_exist(header, shared_datadir):
# assert
assert isinstance(logger, logging.Logger)
# TODO: add tests to check that SimpleLogger, AdvancedLogger, CustomLogger work as intended

View File

View File

@@ -0,0 +1,119 @@
import pytest
from panaetius import utilities
def test_squashed_data(squashed_data, squashed_data_result):
# act
squashed_data_pre_squashed = utilities.squasher.Squash(squashed_data).as_dict
# assert
assert squashed_data_pre_squashed == squashed_data_result
@pytest.fixture
def squashed_data():
return {
"destination_addresses": [
"Washington, DC, USA",
"Philadelphia, PA, USA",
"Santa Barbara, CA, USA",
"Miami, FL, USA",
"Austin, TX, USA",
"Napa County, CA, USA",
],
"origin_addresses": ["New York, NY, USA"],
"rows": [
{
"elements": [
{
"distance": {"text": "227 mi", "value": 365468},
"duration": {
"text": "3 hours 54 mins",
"value": 14064,
},
"status": "OK",
},
{
"distance": {"text": "94.6 mi", "value": 152193},
"duration": {"text": "1 hour 44 mins", "value": 6227},
"status": "OK",
},
{
"distance": {"text": "2,878 mi", "value": 4632197},
"duration": {
"text": "1 day 18 hours",
"value": 151772,
},
"status": "OK",
},
{
"distance": {"text": "1,286 mi", "value": 2069031},
"duration": {
"text": "18 hours 43 mins",
"value": 67405,
},
"status": "OK",
},
{
"distance": {"text": "1,742 mi", "value": 2802972},
"duration": {"text": "1 day 2 hours", "value": 93070},
"status": "OK",
},
{
"distance": {"text": "2,871 mi", "value": 4620514},
"duration": {
"text": "1 day 18 hours",
"value": 152913,
},
"status": "OK",
},
]
}
],
"status": "OK",
}
@pytest.fixture
def squashed_data_result():
return {
"destination_addresses_0": "Washington, DC, USA",
"destination_addresses_1": "Philadelphia, PA, USA",
"destination_addresses_2": "Santa Barbara, CA, USA",
"destination_addresses_3": "Miami, FL, USA",
"destination_addresses_4": "Austin, TX, USA",
"destination_addresses_5": "Napa County, CA, USA",
"origin_addresses_0": "New York, NY, USA",
"rows_0_elements_0_distance_text": "227 mi",
"rows_0_elements_0_distance_value": 365468,
"rows_0_elements_0_duration_text": "3 hours 54 mins",
"rows_0_elements_0_duration_value": 14064,
"rows_0_elements_0_status": "OK",
"rows_0_elements_1_distance_text": "94.6 mi",
"rows_0_elements_1_distance_value": 152193,
"rows_0_elements_1_duration_text": "1 hour 44 mins",
"rows_0_elements_1_duration_value": 6227,
"rows_0_elements_1_status": "OK",
"rows_0_elements_2_distance_text": "2,878 mi",
"rows_0_elements_2_distance_value": 4632197,
"rows_0_elements_2_duration_text": "1 day 18 hours",
"rows_0_elements_2_duration_value": 151772,
"rows_0_elements_2_status": "OK",
"rows_0_elements_3_distance_text": "1,286 mi",
"rows_0_elements_3_distance_value": 2069031,
"rows_0_elements_3_duration_text": "18 hours 43 mins",
"rows_0_elements_3_duration_value": 67405,
"rows_0_elements_3_status": "OK",
"rows_0_elements_4_distance_text": "1,742 mi",
"rows_0_elements_4_distance_value": 2802972,
"rows_0_elements_4_duration_text": "1 day 2 hours",
"rows_0_elements_4_duration_value": 93070,
"rows_0_elements_4_status": "OK",
"rows_0_elements_5_distance_text": "2,871 mi",
"rows_0_elements_5_distance_value": 4620514,
"rows_0_elements_5_duration_text": "1 day 18 hours",
"rows_0_elements_5_duration_value": 152913,
"rows_0_elements_5_status": "OK",
"status": "OK",
}