9 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
35 changed files with 858 additions and 799 deletions

17
.vscode/settings.json vendored
View File

@@ -4,20 +4,5 @@
"python.linting.enabled": true, "python.linting.enabled": true,
"python.pythonPath": ".venv/bin/python", "python.pythonPath": ".venv/bin/python",
"restructuredtext.confPath": "${workspaceFolder}/docs/source", "restructuredtext.confPath": "${workspaceFolder}/docs/source",
"peacock.color": "#307E6A", "peacock.color": "#307E6A"
"workbench.colorCustomizations": {
"editorGroup.border": "#3ea389",
"panel.border": "#3ea389",
"sash.hoverBorder": "#3ea389",
"sideBar.border": "#3ea389",
"statusBar.background": "#307e6a",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#3ea389",
"statusBarItem.remoteBackground": "#307e6a",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#307e6a",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#307e6a99",
"titleBar.inactiveForeground": "#e7e7e799"
}
} }

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,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

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,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) 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,5 +1,5 @@
""" """
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 This module defines the `Config` class to interact and read variables from either a
`config.yml` or an environment variable. `config.yml` or an environment variable.
@@ -19,7 +19,16 @@ from panaetius.exceptions import KeyErrorTooDeepException
class Config: class Config:
"""The configuration class to access variables.""" """
A configuration class to access user variables.
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__( def __init__(
self, self,
@@ -60,17 +69,17 @@ class Config:
@property @property
def config(self) -> dict: 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: Returns:
dict: The contents of the `.yml` loaded as a python dictionary. dict: The contents of the config `.yml` loaded as a python dictionary.
""" """
if self.skip_header_init: if self.skip_header_init:
config_file_location = self.config_path / "config.yml" config_file_location = self.config_path / "config.yml"
else: else:
config_file_location = ( config_file_location = self.config_path / self.header_variable / "config.yml"
self.config_path / self.header_variable / "config.yml"
)
try: try:
with open(config_file_location, "r", encoding="utf-8") as config_file: with open(config_file_location, "r", encoding="utf-8") as config_file:
# return dict(toml.load(config_file)) # return dict(toml.load(config_file))

View File

@@ -1,13 +1,13 @@
"""Exceptions for the module.""" """Module that defines custom exceptions for Panetius."""
class KeyErrorTooDeepException(Exception): class KeyErrorTooDeepException(Exception):
pass """Raised if the keys in the config.yml are nested too deeply."""
class LoggingDirectoryDoesNotExistException(Exception): class LoggingDirectoryDoesNotExistException(Exception):
pass """Raised if the logging directory does not exist."""
class InvalidPythonException(Exception): 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 # configure file handler
if config_inst.logging_path is not None: if config_inst.logging_path is not None:
if not config_inst.skip_header_init:
logging_file = ( logging_file = (
pathlib.Path(config_inst.logging_path) pathlib.Path(config_inst.logging_path)
/ config_inst.header_variable / config_inst.header_variable
/ f"{config_inst.header_variable}.log" / f"{config_inst.header_variable}.log"
).expanduser() ).expanduser()
else:
logging_file = (
pathlib.Path(config_inst.logging_path)
/ f"{config_inst.header_variable}.log"
).expanduser()
if not logging_file.parents[0].exists(): if not logging_file.parents[0].exists():
raise LoggingDirectoryDoesNotExistException() raise LoggingDirectoryDoesNotExistException()
@@ -100,6 +106,11 @@ class LoggingData(metaclass=ABCMeta):
def format(self) -> str: def format(self) -> str:
raise NotImplementedError raise NotImplementedError
@property
@abstractmethod
def logging_level(self) -> str:
raise NotImplementedError
@abstractmethod @abstractmethod
def __init__(self, logging_level: str): def __init__(self, logging_level: str):
raise NotImplementedError raise NotImplementedError
@@ -113,8 +124,12 @@ class SimpleLogger(LoggingData):
'"%(levelname)s",\n\t"message": "%(message)s"\n}', '"%(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"): def __init__(self, logging_level: str = "INFO"):
self.logging_level = logging_level self._logging_level = logging_level
class AdvancedLogger(LoggingData): class AdvancedLogger(LoggingData):
@@ -127,8 +142,12 @@ class AdvancedLogger(LoggingData):
'"%(levelname)s",\n\t"message": "%(message)s"\n}', '"%(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"): def __init__(self, logging_level: str = "INFO"):
self.logging_level = logging_level self._logging_level = logging_level
class CustomLogger(LoggingData): class CustomLogger(LoggingData):
@@ -136,6 +155,10 @@ class CustomLogger(LoggingData):
def format(self) -> str: def format(self) -> str:
return str(self._format) return str(self._format)
@property
def logging_level(self) -> str:
return self._logging_level
def __init__(self, logging_format: str, logging_level: str = "INFO"): def __init__(self, logging_format: str, logging_level: str = "INFO"):
self.logging_level = logging_level self._logging_level = logging_level
self._format = logging_format self._format = logging_format

0
panaetius/py.typed Normal file
View File

View File

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

View File

@@ -1,4 +1,4 @@
"""Squash a json object or Python dictionary into a single level dictionary.""" """Sub-module that defines squashing json objects into a single json object."""
from __future__ import annotations from __future__ import annotations

310
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]] [[package]]
name = "astroid" name = "astroid"
version = "2.8.2" version = "2.8.2"
@@ -49,6 +64,45 @@ PyYAML = ">=5.3.1"
six = ">=1.10.0" six = ">=1.10.0"
stevedore = ">=1.20.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]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.4" version = "0.4.4"
@@ -76,6 +130,34 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "execnet" name = "execnet"
version = "1.9.0" version = "1.9.0"
@@ -87,6 +169,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras] [package.extras]
testing = ["pre-commit"] 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]] [[package]]
name = "flake8" name = "flake8"
version = "2.3.0" version = "2.3.0"
@@ -134,6 +229,14 @@ python-versions = ">=3.7"
gitdb = ">=4.0.1,<5" gitdb = ">=4.0.1,<5"
typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} 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]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "4.8.1" version = "4.8.1"
@@ -161,7 +264,7 @@ python-versions = "*"
[[package]] [[package]]
name = "isort" name = "isort"
version = "5.9.3" version = "5.10.1"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
category = "dev" category = "dev"
optional = false optional = false
@@ -173,6 +276,20 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"] colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"] 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]] [[package]]
name = "lazy-object-proxy" name = "lazy-object-proxy"
version = "1.6.0" version = "1.6.0"
@@ -181,6 +298,14 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 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]] [[package]]
name = "mccabe" name = "mccabe"
version = "0.6.1" version = "0.6.1"
@@ -315,6 +440,14 @@ with_mypy = ["mypy (>=0.600)"]
with_pyroma = ["pyroma (>=2.4)"] with_pyroma = ["pyroma (>=2.4)"]
with_vulture = ["vulture (>=1.5)"] 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]] [[package]]
name = "py" name = "py"
version = "1.10.0" version = "1.10.0"
@@ -500,6 +633,24 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" 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]] [[package]]
name = "requirements-detector" name = "requirements-detector"
version = "0.7" version = "0.7"
@@ -511,6 +662,20 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
astroid = ">=1.4" 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]] [[package]]
name = "setoptconf-tmp" name = "setoptconf-tmp"
version = "0.3.1" version = "0.3.1"
@@ -574,6 +739,14 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "types-pyyaml"
version = "6.0.1"
description = "Typing stubs for PyYAML"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "types-toml" name = "types-toml"
version = "0.10.1" version = "0.10.1"
@@ -590,6 +763,19 @@ category = "dev"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "wrapt" name = "wrapt"
version = "1.12.1" version = "1.12.1"
@@ -613,9 +799,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "f68e5ab35a155ce5ea567b670f93f4678b6c65d1335017b21431b315297a0410" content-hash = "e9da2be8225df95a41d2027f5c9cdaa7f744beff549d915d9922d7ddead8dc54"
[metadata.files] [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 = [ astroid = [
{file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"}, {file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"},
{file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"}, {file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"},
@@ -632,6 +822,22 @@ bandit = [
{file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
{file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, {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 = [ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
@@ -675,10 +881,22 @@ dodgy = [
{file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"}, {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"},
{file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"}, {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 = [ execnet = [
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, {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 = [ flake8 = [
{file = "flake8-2.3.0-py2.py3-none-any.whl", hash = "sha256:c99cc9716d6655d9c8bcb1e77632b8615bf0abd282d7abd9f5c2148cad7fc669"}, {file = "flake8-2.3.0-py2.py3-none-any.whl", hash = "sha256:c99cc9716d6655d9c8bcb1e77632b8615bf0abd282d7abd9f5c2148cad7fc669"},
{file = "flake8-2.3.0.tar.gz", hash = "sha256:5ee1a43ccd0716d6061521eec6937c983efa027793013e572712c4da55c7c83e"}, {file = "flake8-2.3.0.tar.gz", hash = "sha256:5ee1a43ccd0716d6061521eec6937c983efa027793013e572712c4da55c7c83e"},
@@ -695,6 +913,10 @@ gitpython = [
{file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
{file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, {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 = [ importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
@@ -704,8 +926,12 @@ iniconfig = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
] ]
isort = [ isort = [
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, {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 = [ lazy-object-proxy = [
{file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"},
@@ -731,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-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, {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 = [ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
@@ -792,6 +1074,10 @@ prospector = [
{file = "prospector-1.5.1-py3-none-any.whl", hash = "sha256:47f8ff3fd36ae276967eb392ca20b300a7bdea66c0d0252250a4d89a6c03ab15"}, {file = "prospector-1.5.1-py3-none-any.whl", hash = "sha256:47f8ff3fd36ae276967eb392ca20b300a7bdea66c0d0252250a4d89a6c03ab15"},
{file = "prospector-1.5.1.tar.gz", hash = "sha256:851c2892cd615cfee91fd27cfaf7a5061d14daf2853aa8f012e927b98f919578"}, {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 = [ py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
@@ -881,9 +1167,17 @@ pyyaml = [
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, {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 = [ requirements-detector = [
{file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"}, {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 = [ setoptconf-tmp = [
{file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"}, {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"},
{file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"}, {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"},
@@ -940,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-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, {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 = [ types-toml = [
{file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"}, {file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"},
{file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"}, {file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"},
@@ -949,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-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, {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 = [ wrapt = [
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
] ]

View File

@@ -1,10 +1,10 @@
[tool.poetry] [tool.poetry]
name = "panaetius" name = "panaetius"
version = "2.2.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." 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" license = "MIT"
authors = ["dtomlinson <dtomlinson@panaetius.co.uk>"] authors = ["dtomlinson <dtomlinson@panaetius.co.uk>"]
readme = "./README.rst" readme = "./README.md"
homepage = "https://github.com/dtomlinson91/panaetius" homepage = "https://github.com/dtomlinson91/panaetius"
repository = "https://github.com/dtomlinson91/panaetius" repository = "https://github.com/dtomlinson91/panaetius"
documentation = "https://panaetius.readthedocs.io/en/latest/introduction.html" documentation = "https://panaetius.readthedocs.io/en/latest/introduction.html"
@@ -34,6 +34,24 @@ pytest = "^6.2.5"
pytest-datadir = "^1.3.1" pytest-datadir = "^1.3.1"
pytest-xdist = "^2.4.0" pytest-xdist = "^2.4.0"
coverage = "^6.0.2" 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] [build-system]
requires = ["poetry>=0.12"] requires = ["poetry>=0.12"]

View File

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

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,53 +0,0 @@
Testing:
To Write:
☐ Test the Config file skipping header with `skip_header_init`
☐ Document coverage commands
`coverage run --source=./panaetius -m pytest`
`coverage report` & `coverage html` > gives ./htmlcov/index.html
☐ Document for abstract methods should raise NotImplementedError
☐ Document https://stackoverflow.com/a/9212387
Documentation:
☐ Rewrite documentation using `mkdocs` and using `.md`.
☐ Update the metadata in the `pyproject.toml`.
☐ Create a new `Readme.md` and remove the `.rst`.
☐ Document the logging strategy
CLI tools should use `logger.critical` and raise SystemExit(1)
Libraries should raise custom errors and have a `logger.critical(exec_info=1)`
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
Archive:
✘ Bump the version to release 2.0 @cancelled(21-10-23 05:36) @project(Misc)
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.No Config File)
✔ Handle if a bool is passed in as a default @done(21-10-16 05:25) @project(Coding.Config File)
✔ Create SimpleLogger, AdvancedLogger, CustomLogger classes @done(21-10-16 16:22) @project(Coding.Logging)
✔ Logging path should take by default the config path unless overwritten? @done(21-10-16 23:49) @project(Coding.Logging)
✔ Check logging path + config path are valid, if not raise error. @done(21-10-18 00:04) @project(Coding.Errors)
✔ Add tests for these. @done(21-10-18 00:04) @project(Coding.Errors)
✔ Check for a key > 2 levels, raise custom error, write test @done(21-10-17 23:30) @project(Coding.Errors)
✔ Check all functions and annotations. @done(21-10-18 01:07) @project(Coding.Linting)
✔ Write the docstrings for public functions/methods. @done(21-10-18 02:29) @project(Coding.Docstrings)
✔ When both a config file and a env var is found, use the env var. @done(21-10-18 00:38) @project(Coding.Functionality)
✔ If loading from a default, don't covert to TOML @done(21-10-17 20:33) @project(Tests.Bugfixes)
✔ Env Vars should be given as python objects @done(21-10-17 20:33) @project(Tests.Bugfixes)
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
✔ Test default config path set to "~/.config" @done(21-10-17 17:25) @project(Tests.__init__)
✔ Test config path is set when passed in @done(21-10-17 17:25) @project(Tests.__init__)
✔ Check testing config file is returned as dict @done(21-10-17 17:25) @project(Tests.config property)
✔ Check _self.missing_config and empty dict is returned @done(21-10-17 17:25) @project(Tests.config property)
✔ Arrays & tables loaded correctly from config file @done(21-10-17 20:34) @project(Tests.get_value.config_file)
✔ test when key length is 1 the value is returned @done(21-10-17 18:55) @project(Tests.get_value.config_file)
✔ test when key length is 2 the value is returned @done(21-10-17 18:55) @project(Tests.get_value.config_file)
✔ test when key not found and no env var default is loaded @done(21-10-17 19:01) @project(Tests.get_value.config_file)
✔ test bool's are properly converted @done(21-10-17 19:01) @project(Tests.get_value.config_file)
✔ test when key not found and env var is set value is loaded @done(21-10-17 20:43) @project(Tests.get_value.config_file)
✔ check if env key is missing the default is read in @done(21-10-17 20:55) @project(Tests.get_value.env_var)
✔ check if env key is present the values are read in @done(21-10-17 22:24) @project(Tests.get_value.env_var)
✔ parametrise a test to read in values form env vars and they're set correctly @done(21-10-17 22:24) @project(Tests.get_value.env_var)
✔ test that the env var is valid python @done(21-10-18 01:03) @project(Tests.get_value.env_var)
✔ test set_config works @done(21-10-17 23:29) @project(Tests.library)

View File

@@ -12,9 +12,9 @@ install_requires = \
setup_kwargs = { setup_kwargs = {
'name': 'panaetius', 'name': 'panaetius',
'version': '2.2.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.', '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': '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 ``panaetius`` the following ways:\n\nPython\n-------\n\n.. Attention:: You should install in a python virtual environment\n\nFrom pypi using pip\n~~~~~~~~~~~~~~~~~~~~\n\n.. code-block:: bash\n\n pip install panaetius\n\nFrom local wheel\n~~~~~~~~~~~~~~~~~\n\nDownload the latest verion from the `releases`_ page.\n\n.. _releases: https://github.com/dtomlinson91/panaetius/releases\n\nInstall with pip:\n\n.. code-block:: bash\n\n pip install -U panaetius-1.0.2-py3-none-any.whl\n\n\nFrom source\n~~~~~~~~~~~~\n\nClone the repo and install using ``setup.py``:\n\n.. code-block:: bash\n\n python setup.py\n', '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': 'dtomlinson',
'author_email': 'dtomlinson@panaetius.co.uk', 'author_email': 'dtomlinson@panaetius.co.uk',
'maintainer': None, 'maintainer': None,